/*
 * Copyright 2019, TeamDev. All rights reserved.
 *
 * Redistribution and use in source and/or binary forms, with or without
 * modification, must retain the above copyright notice and the following
 * disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.spine.tools.validate;

import com.google.gson.reflect.TypeToken;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import io.spine.code.java.PackageName;
import io.spine.code.proto.FieldDeclaration;
import io.spine.tools.validate.code.Expression;
import io.spine.tools.validate.field.FieldValidatorFactories;
import io.spine.tools.validate.field.FieldValidatorFactory;
import io.spine.type.MessageType;
import io.spine.validate.ConstraintViolation;
import io.spine.validate.ValidationException;

import java.util.ArrayList;
import java.util.Optional;
import java.util.function.Function;

import static com.squareup.javapoet.ClassName.bestGuess;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

public final class MessageValidatorFactory {

    private static final String VALIDATE_METHOD = "validate";
    private static final String MESSAGE_PARAMETER = "msg";
    private static final String MESSAGE_VARIABLE = "msg";
    private static final TypeToken<ArrayList<ConstraintViolation>> listOfViolations =
            new TypeToken<ArrayList<ConstraintViolation>>() {};

    private final MessageType type;
    private final PackageName packageName;
    private final String validatorSimpleName;
    private final String messageSimpleName;

    public MessageValidatorFactory(MessageType type) {
        this.type = type;
        this.packageName = type.javaPackage();
        this.validatorSimpleName = type.nestedSimpleName()
                                       .joinWithUnderscore() + "_Validator";
        this.messageSimpleName = type.nestedSimpleName()
                                     .toString();
    }

    public JavaFile generateClass() {
        MethodSpec validateMethod = MethodSpec
                .methodBuilder(VALIDATE_METHOD)
                .addModifiers(STATIC)
                .returns(void.class)
                .addParameter(bestGuess(messageSimpleName), MESSAGE_PARAMETER)
                .addCode(validator())
                .build();
        MethodSpec ctor = MethodSpec
                .constructorBuilder()
                .addModifiers(PRIVATE)
                .addJavadoc("Prevents validator class instantiation.")
                .build();
        TypeSpec type = TypeSpec
                .classBuilder(validatorSimpleName)
                .addModifiers(FINAL)
                .addMethod(ctor)
                .addMethod(validateMethod)
                .build();
        return JavaFile.builder(packageName.value(), type)
                       .build();
    }

    private CodeBlock validator() {
        CodeBlock.Builder body = CodeBlock
                .builder()
                .addStatement("$1T violations = new $1T()", listOfViolations.getType());
        Function<ViolationTemplate, Expression> violationAccumulator =
                violation -> Expression.of("violations.add(" + violation + ')');
        Expression msg = Expression.of(MESSAGE_PARAMETER);
        FieldValidatorFactories factories = new FieldValidatorFactories(msg);
        for (FieldDeclaration field : type.fields()) {
            FieldValidatorFactory factory = factories.forField(field);
            Optional<CodeBlock> fieldValidation = factory.generate(violationAccumulator);
            fieldValidation.ifPresent(body::add);
        }
        return body.build();
    }

    public MethodSpec generateValidate() {
        CodeBlock body = CodeBlock.of("$T.validate(this);", bestGuess(validatorSimpleName));
        return MethodSpec
                .methodBuilder(VALIDATE_METHOD)
                .addModifiers(PUBLIC)
                .returns(void.class)
                .addException(ValidationException.class)
                .addCode(body)
                .build();
    }

    public MethodSpec generateVBuild() {
        CodeBlock body = CodeBlock
                .builder()
                .addStatement("$T $N = build()",
                              bestGuess(messageSimpleName), MESSAGE_VARIABLE)
                .addStatement("$T." + VALIDATE_METHOD + "($N)",
                              bestGuess(validatorSimpleName), MESSAGE_VARIABLE)
                .addStatement("return $N", MESSAGE_VARIABLE)
                .build();
        return MethodSpec
                .methodBuilder("vBuild")
                .addAnnotation(Override.class)
                .addModifiers(PUBLIC)
                .returns(bestGuess(messageSimpleName))
                .addException(ValidationException.class)
                .addCode(body)
                .build();
    }
}

@@ -1082,16 +1082,61 @@
 eToken;%0A
+import com.squareup.javapoet.AnnotationSpec;%0A
 import c
@@ -1282,24 +1282,72 @@
 t.TypeSpec;%0A
+import io.spine.code.gen.java.GeneratedBySpine;%0A
 import io.sp
@@ -1711,24 +1711,59 @@
 Exception;%0A%0A
+import javax.annotation.Generated;%0A
 import java.
@@ -3602,32 +3602,283 @@
       .build();%0A
+        GeneratedBySpine bySpine = GeneratedBySpine.instance();%0A        AnnotationSpec generated = AnnotationSpec%0A                .builder(Generated.class)%0A                .addMember(bySpine.fieldName(), bySpine.codeBlock())%0A                .build();%0A
         TypeSpec
@@ -3945,16 +3945,58 @@
 leName)%0A
+                .addAnnotation(generated)%0A
         

Annotate generated code