/*
 * Copyright (c) 2014, Victor Nazarov <asviraspossible@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation and/or
 *     other materials provided with the distribution.
 *
 *  3. Neither the name of the copyright holder nor the names of its contributors
 *     may be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 *  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 HOLDER 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 com.github.sviperll.adt4j.model;

import com.github.sviperll.adt4j.Caching;
import com.github.sviperll.adt4j.model.config.FieldConfiguration;
import com.github.sviperll.adt4j.model.config.PredicateConfigutation;
import com.github.sviperll.adt4j.model.config.Serialization;
import com.github.sviperll.adt4j.model.config.VisitorDefinition;
import com.github.sviperll.adt4j.model.config.VariableDeclaration;
import com.github.sviperll.adt4j.model.config.VisitorDefinition.MethodUsage;
import com.github.sviperll.adt4j.model.util.Source;
import com.github.sviperll.adt4j.model.util.Types;
import com.github.sviperll.adt4j.model.util.VariableNameSource;
import com.helger.jcodemodel.AbstractJClass;
import com.helger.jcodemodel.AbstractJType;
import com.helger.jcodemodel.EClassType;
import com.helger.jcodemodel.IJExpression;
import com.helger.jcodemodel.JAnnotationArrayMember;
import com.helger.jcodemodel.JAnnotationUse;
import com.helger.jcodemodel.JBlock;
import com.helger.jcodemodel.JClassAlreadyExistsException;
import com.helger.jcodemodel.JConditional;
import com.helger.jcodemodel.JDefinedClass;
import com.helger.jcodemodel.JExpr;
import com.helger.jcodemodel.JFieldRef;
import com.helger.jcodemodel.JFieldVar;
import com.helger.jcodemodel.JInvocation;
import com.helger.jcodemodel.JMethod;
import com.helger.jcodemodel.JMod;
import com.helger.jcodemodel.JOp;
import com.helger.jcodemodel.JSynchronizedBlock;
import com.helger.jcodemodel.JTypeVar;
import com.helger.jcodemodel.JVar;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

public class FinalValueClassModel {
    static FinalValueClassModel createErrorModel(FinalValueClassModelEnvironment environment, Types types) {
        return new FinalValueClassModel(environment, types, true);
    }

    static FinalValueClassModel createModel(FinalValueClassModelEnvironment environment, Types types) {
        return new FinalValueClassModel(environment, types, false);
    }

    private final FinalValueClassModelEnvironment environment;
    private final Types types;
    private final boolean isError;

    private FinalValueClassModel(FinalValueClassModelEnvironment environment, Types modelTypes, boolean isError) {
        this.types = modelTypes;
        this.isError = isError;
        this.environment = environment;
    }

    private String hashCodeAcceptorMethodName() {
        return Source.decapitalize(environment.valueClassName()) + "HashCode";
    }

    MethodBuilder createMethodBuilder(Serialization serialization) {
        if (isError)
            return new MethodBuilder(null, null);
        else {
            JFieldVar acceptorField = buildAcceptorField();
            Map<String, JDefinedClass> caseClasses;
            try {
                caseClasses = buildCaseClasses(serialization);
            } catch (JClassAlreadyExistsException ex) {
                throw new RuntimeException("Unexpected exception :)", ex);
            }
            Caching hashCode = environment.hashCodeCaching();
            if (!hashCode.enabled())
                return new MethodBuilder(caseClasses, acceptorField);
            else {
                JFieldVar hashCodeField = buildHashCodeCachedValueField(serialization);
                return new MethodBuilder(caseClasses, acceptorField, hashCodeField);
            }
        }
    }

    void buildSerialVersionUID() {
        if (environment.isValueClassSerializable())
            environment.buildValueClassField(JMod.PRIVATE | JMod.FINAL | JMod.STATIC, types._long, "serialVersionUID", JExpr.lit(environment.serialVersionUIDForGeneratedCode()));
    }

    private JFieldVar buildAcceptorField() {
        return environment.buildValueClassField(JMod.PRIVATE | JMod.FINAL, environment.acceptingInterfaceTypeInsideValueClass(), "acceptor");
    }

    JMethod buildFactory(Map<String, JMethod> constructorMethods) throws JClassAlreadyExistsException {
        JDefinedClass factory = buildFactoryClass(constructorMethods);

        JFieldVar factoryField = environment.buildValueClassField(JMod.PRIVATE | JMod.STATIC | JMod.FINAL, factory, "FACTORY");
        JAnnotationUse fieldAnnotationUse = factoryField.annotate(SuppressWarnings.class);
        JAnnotationArrayMember paramArray = fieldAnnotationUse.paramArray("value");
        paramArray.param("unchecked");
        paramArray.param("rawtypes");

        factoryField.init(JExpr._new(factory));
        JMethod factoryMethod = environment.buildValueClassMethod(Source.toJMod(environment.factoryMethodAccessLevel()) | JMod.STATIC, "factory");
        Source.annotateNonnull(factoryMethod);
        JAnnotationUse methodAnnotationUse = factoryMethod.annotate(SuppressWarnings.class);
        methodAnnotationUse.param("value", "unchecked");
        for (JTypeVar visitorTypeParameter: environment.getValueTypeParameters()) {
            JTypeVar typeParameter = factoryMethod.generify(visitorTypeParameter.name());
            typeParameter.boundLike(visitorTypeParameter);
        }
        AbstractJClass usedValueClassType = environment.wrappedValueClassType(factoryMethod.typeParams());
        factoryMethod.type(environment.visitor(usedValueClassType, usedValueClassType, types._RuntimeException).getVisitorType());
        factoryMethod.body()._return(factoryField);
        return factoryMethod;
    }

    private JDefinedClass buildFactoryClass(Map<String, JMethod> constructorMethods) throws JClassAlreadyExistsException {
        JDefinedClass factoryClass = environment.buildValueClassInnerClass(JMod.PRIVATE | JMod.STATIC, environment.valueClassName() + "Factory", EClassType.CLASS);
        for (JTypeVar visitorTypeParameter: environment.getValueTypeParameters()) {
            JTypeVar typeParameter = factoryClass.generify(visitorTypeParameter.name());
            typeParameter.boundLike(visitorTypeParameter);
        }
        AbstractJClass usedValueClassType = environment.wrappedValueClassType(factoryClass.typeParams());
        VisitorDefinition.VisitorUsage usedVisitor = environment.visitor(usedValueClassType, usedValueClassType, types._RuntimeException);
        factoryClass._implements(usedVisitor.getVisitorType());
        for (MethodUsage interfaceMethod: usedVisitor.methods()) {
            JMethod factoryMethod = factoryClass.method(interfaceMethod.mods().getValue() & ~JMod.ABSTRACT, usedValueClassType, interfaceMethod.name());
            for (JTypeVar visitorMethodTypeParameter: interfaceMethod.typeParams()) {
                JTypeVar typeParameter = factoryMethod.generify(visitorMethodTypeParameter.name());
                typeParameter.boundLike(visitorMethodTypeParameter);
            }
            MethodUsage usedInterfaceMethod = interfaceMethod.narrow(factoryMethod.typeParams());
            Source.annotateNonnull(factoryMethod);
            factoryMethod.annotate(Override.class);

            List<AbstractJClass> typeArguments = new ArrayList<>();
            typeArguments.addAll(factoryClass.typeParamList());
            typeArguments.addAll(factoryMethod.typeParamList());
            JMethod constructorMethod = constructorMethods.get(interfaceMethod.name());
            JInvocation staticInvoke = environment.invokeValueClassStaticMethod(constructorMethod, typeArguments.toArray(new AbstractJClass[typeArguments.size()]));
            for (VariableDeclaration param: usedInterfaceMethod.params()) {
                JVar argument = factoryMethod.param(param.mods().getValue(), param.type().declarable(), param.name());
                staticInvoke.arg(argument);
            }
            VariableDeclaration param = usedInterfaceMethod.varParam();
            if (param != null) {
                JVar argument = factoryMethod.varParam(param.mods().getValue(), param.type().elementType().declarable(), param.name());
                staticInvoke.arg(argument);
            }
            factoryMethod.body()._return(staticInvoke);
        }
        return factoryClass;
    }

    private Map<String, JDefinedClass> buildCaseClasses(Serialization serialization) throws JClassAlreadyExistsException {
        Map<String, JDefinedClass> caseClasses = new TreeMap<>();
        for (JMethod interfaceMethod: environment.visitorDefinition().methodDefinitions()) {
            JDefinedClass caseClass = buildCaseClass(interfaceMethod.name(), serialization);
            caseClasses.put(interfaceMethod.name(), caseClass);
        }
        return caseClasses;
    }

    private JDefinedClass buildCaseClass(String interfaceMethodName, Serialization serialization) throws JClassAlreadyExistsException {
        JDefinedClass caseClass = environment.buildValueClassInnerClass(JMod.PRIVATE | JMod.STATIC, Source.capitalize(interfaceMethodName) + "Case" + environment.acceptingInterfaceName(), EClassType.CLASS);
        for (JTypeVar visitorTypeParameter: environment.getValueTypeParameters()) {
            JTypeVar typeParameter = caseClass.generify(visitorTypeParameter.name());
            typeParameter.boundLike(visitorTypeParameter);
        }

        AbstractJClass usedAcceptingInterfaceType = environment.acceptingInterfaceType(caseClass.typeParams());
        AbstractJClass usedValueClassType = environment.wrappedValueClassType(caseClass.typeParams());
        VisitorDefinition.VisitorUsage usedVisitor = environment.visitor(usedValueClassType, usedValueClassType, types._RuntimeException);
        MethodUsage interfaceMethod = usedVisitor.findMethod(interfaceMethodName);
        if (interfaceMethod == null)
            throw new IllegalStateException("Method with given name not found: " + interfaceMethodName);

        JTypeVar[] methodArguments = new JTypeVar[interfaceMethod.typeParams().length];
        for (int i = 0; i < methodArguments.length; i++) {
            JTypeVar visitorMethodTypeParameter = interfaceMethod.typeParams()[i];
            JTypeVar typeParameter = caseClass.generify(visitorMethodTypeParameter.name());
            typeParameter.boundLike(visitorMethodTypeParameter);
            methodArguments[i] = typeParameter;
        }
        MethodUsage usedInterfaceMethod = interfaceMethod.narrow(methodArguments);

        caseClass._implements(usedAcceptingInterfaceType);

        if (serialization.isSerializable()) {
            caseClass._implements(types._Serializable);
            caseClass.field(JMod.PRIVATE | JMod.FINAL | JMod.STATIC, types._long, "serialVersionUID", JExpr.lit(serialization.serialVersionUIDForGeneratedCode()));
        }

        JMethod constructor = caseClass.constructor(JMod.NONE);
        for (VariableDeclaration param: usedInterfaceMethod.params()) {
            AbstractJType paramType = param.type().declarable();
            JFieldVar field = caseClass.field(JMod.PRIVATE | JMod.FINAL, paramType, param.name());
            JVar argument = constructor.param(paramType, param.name());
            constructor.body().assign(JExpr._this().ref(field), argument);
        }
        VariableDeclaration param = usedInterfaceMethod.varParam();
        if (param != null) {
            AbstractJType paramType = param.type().elementType().declarable();
            JFieldVar field = caseClass.field(JMod.PRIVATE | JMod.FINAL, paramType.array(), param.name());
            JVar argument = constructor.varParam(paramType, param.name());
            constructor.body().assign(JExpr._this().ref(field), argument);
        }

        JMethod acceptMethod = declareAcceptMethod(caseClass, usedValueClassType);
        JInvocation invocation = JExpr.invoke(acceptMethod.params().get(0), usedInterfaceMethod.name());
        for (AbstractJClass argument: methodArguments) {
            invocation.narrow(argument);
        }
        for (VariableDeclaration param1: usedInterfaceMethod.params()) {
            invocation.arg(JExpr._this().ref(param1.name()));
        }
        VariableDeclaration param1 = usedInterfaceMethod.varParam();
        if (param1 != null) {
            invocation.arg(JExpr._this().ref(param1.name()));
        }
        acceptMethod.body()._return(invocation);

        return caseClass;
    }

    private JMethod declareAcceptMethod(JDefinedClass caseClass, AbstractJClass usedValueClassType) {
        JMethod acceptMethod = caseClass.method(JMod.PUBLIC, types._void, environment.acceptMethodName());
        acceptMethod.annotate(Override.class);
        JTypeVar visitorResultTypeParameter = environment.visitorDefinition().getResultTypeParameter();
        AbstractJClass resultType;
        if (visitorResultTypeParameter == null)
            resultType = types._Object;
        else {
            JTypeVar resultTypeVar = acceptMethod.generify(visitorResultTypeParameter.name());
            resultTypeVar.boundLike(visitorResultTypeParameter);
            resultType = resultTypeVar;
        }
        acceptMethod.type(resultType);
        JTypeVar visitorExceptionTypeParameter = environment.visitorDefinition().getExceptionTypeParameter();
        JTypeVar exceptionType = null;
        if (visitorExceptionTypeParameter != null) {
            JTypeVar exceptionTypeParameter = acceptMethod.generify(visitorExceptionTypeParameter.name());
            exceptionTypeParameter.boundLike(visitorExceptionTypeParameter);
            exceptionType = exceptionTypeParameter;
            acceptMethod._throws(exceptionType);
        }
        VisitorDefinition.VisitorUsage usedVisitorType = environment.visitor(usedValueClassType, resultType, exceptionType);
        acceptMethod.param(usedVisitorType.getVisitorType(), "visitor");
        return acceptMethod;
    }

    private JFieldVar buildHashCodeCachedValueField(Serialization serialization) {
        if (!environment.hashCodeCaching().enabled())
            throw new IllegalStateException("Unsupported method evaluation to cache hash code: " + environment.hashCodeCaching());
        else {
            boolean isSerializable = serialization.isSerializable();
            boolean precomputes = environment.hashCodeCaching() == Caching.PRECOMPUTE;
            int mods = JMod.PRIVATE;
            mods = !isSerializable ? mods : mods | JMod.TRANSIENT;
            if (!precomputes)
                return environment.buildValueClassField(mods, types._int, "hashCodeCachedValue", JExpr.lit(0));
            else {
                mods = isSerializable ? mods : mods | JMod.FINAL;
                return environment.buildValueClassField(mods, types._int, "hashCodeCachedValue");
            }
        }
    }


    class MethodBuilder {
        private final Map<String, JDefinedClass> caseClasses;
        private final JFieldVar acceptorField;
        private final JFieldVar hashCodeCachedValueField;

        private MethodBuilder(Map<String, JDefinedClass> caseClasses, JFieldVar acceptorField, JFieldVar hashCodeCachedValueField) {
            this.caseClasses = caseClasses;
            this.acceptorField = acceptorField;
            this.hashCodeCachedValueField = hashCodeCachedValueField;
        }

        private MethodBuilder(Map<String, JDefinedClass> caseClasses, JFieldVar acceptorField) {
            this(caseClasses, acceptorField, null);
        }

        void buildPrivateConstructor() {
            if (!isError) {
                JMethod constructor = environment.buildValueClassConstructor(JMod.PRIVATE);
                JVar acceptorParam = constructor.param(acceptorField.type(), acceptorField.name());
                if (environment.hashCodeCaching() == Caching.PRECOMPUTE) {
                    JInvocation invocation = acceptorParam.invoke(hashCodeAcceptorMethodName());
                    constructor.body().assign(JExpr.refthis(hashCodeCachedValueField), invocation);
                }
                constructor.body().assign(JExpr.refthis(acceptorField.name()), acceptorParam);
            }
        }

        void buildProtectedConstructor(Serialization serialization) {
            JMethod constructor = environment.buildValueClassConstructor(JMod.PROTECTED);
            JAnnotationUse annotation = constructor.annotate(SuppressWarnings.class);
            annotation.paramArray("value", "null");
            AbstractJClass unwrappedUsedValueClassType = environment.unwrappedValueClassTypeInsideValueClass();
            JVar param = constructor.param(unwrappedUsedValueClassType, "implementation");
            Source.annotateNonnull(param);
            if (isError) {
                constructor.body()._throw(JExpr._new(types._UnsupportedOperationException));
            } else {
                JConditional nullCheck = constructor.body()._if(JExpr.ref("implementation").eq(JExpr._null()));
                JInvocation nullPointerExceptionConstruction = JExpr._new(types._NullPointerException);
                nullPointerExceptionConstruction.arg(JExpr.lit("Argument shouldn't be null: 'implementation' argument in class constructor invocation: " + environment.valueClassQualifiedName()));
                nullCheck._then()._throw(nullPointerExceptionConstruction);

                if (environment.hashCodeCaching().enabled())
                    constructor.body().assign(JExpr.refthis(hashCodeCachedValueField), param.ref(hashCodeCachedValueField));
                constructor.body().assign(JExpr.refthis(acceptorField), param.ref(acceptorField));
            }
        }

        void buildAcceptMethod() {
            JMethod acceptMethod = environment.buildValueClassMethod(Source.toJMod(environment.acceptMethodAccessLevel()) | JMod.FINAL, environment.acceptMethodName());

            JTypeVar visitorResultTypeParameter = environment.visitorDefinition().getResultTypeParameter();
            AbstractJClass resultType;
            if (visitorResultTypeParameter == null)
                resultType = types._Object;
            else {
                JTypeVar resultTypeVar = acceptMethod.generify(visitorResultTypeParameter.name());
                resultTypeVar.boundLike(visitorResultTypeParameter);
                resultType = resultTypeVar;
            }
            acceptMethod.type(resultType);

            JTypeVar visitorExceptionTypeParameter = environment.visitorDefinition().getExceptionTypeParameter();
            JTypeVar exceptionType = null;
            if (visitorExceptionTypeParameter != null) {
                JTypeVar exceptionTypeVar = acceptMethod.generify(visitorExceptionTypeParameter.name());
                exceptionTypeVar.boundLike(visitorExceptionTypeParameter);
                exceptionType = exceptionTypeVar;
                acceptMethod._throws(exceptionType);
            }

            AbstractJClass usedValueClassType = environment.wrappedValueClassTypeInsideValueClass();
            VisitorDefinition.VisitorUsage usedVisitorType = environment.visitor(usedValueClassType, resultType, exceptionType);
            acceptMethod.param(usedVisitorType.getVisitorType(), "visitor");
            if (isError) {
                acceptMethod.body()._throw(JExpr._new(types._UnsupportedOperationException));
            } else {
                JInvocation invocation = acceptorField.invoke(environment.acceptMethodName());
                invocation.arg(JExpr.ref("visitor"));
                acceptMethod.body()._return(invocation);
            }
        }

        Map<String, JMethod> buildConstructorMethods(Serialization serialization) {
            Map<String, JMethod> constructorMethods = new TreeMap<>();
            for (JMethod interfaceMethodDefinition: environment.visitorDefinition().methodDefinitions()) {
                JMethod constructorMethod = environment.buildValueClassMethod(Source.toJMod(environment.factoryMethodAccessLevel()) | JMod.STATIC, interfaceMethodDefinition.name());
                Source.annotateNonnull(constructorMethod);
                for (JTypeVar visitorTypeParameter: environment.getValueTypeParameters()) {
                    JTypeVar typeParameter = constructorMethod.generify(visitorTypeParameter.name());
                    typeParameter.boundLike(visitorTypeParameter);
                }
                AbstractJClass unwrappedUsedValueClassType = environment.unwrappedValueClassType(constructorMethod.typeParams());
                AbstractJClass usedValueClassType = environment.wrappedValueClassType(constructorMethod.typeParams());
                VisitorDefinition.VisitorUsage usedVisitor = environment.visitor(usedValueClassType, usedValueClassType, types._RuntimeException);

                MethodUsage genericInterfaceMethod = usedVisitor.findMethod(interfaceMethodDefinition.name());
                if (genericInterfaceMethod == null)
                    throw new IllegalStateException("Method with given name not found: " + interfaceMethodDefinition.name());
                List<AbstractJClass> methodTypeArguments = new ArrayList<>();
                for (JTypeVar visitorTypeParameter: genericInterfaceMethod.typeParams()) {
                    JTypeVar typeParameter = constructorMethod.generify(visitorTypeParameter.name());
                    typeParameter.boundLike(visitorTypeParameter);
                    methodTypeArguments.add(typeParameter);
                }
                constructorMethod.type(usedValueClassType);
                VisitorDefinition.MethodUsage usedInterfaceMethod = genericInterfaceMethod.narrow(methodTypeArguments.toArray(new AbstractJClass[methodTypeArguments.size()]));
                for (VariableDeclaration param: usedInterfaceMethod.params()) {
                    JVar constructorMethodParam = constructorMethod.param(param.mods().getValue(), param.type().declarable(), param.name());
                    if (param.type().isReference()) {
                        if (Source.isNullable(param))
                            Source.annotateNullable(constructorMethodParam);
                        else
                            Source.annotateNonnull(constructorMethodParam);
                    }
                }
                VariableDeclaration param = usedInterfaceMethod.varParam();
                if (param != null) {
                    AbstractJType paramType = param.type().elementType().declarable();
                    JVar constructorMethodParam = constructorMethod.varParam(param.mods().getValue(), paramType, param.name());
                    if (param.type().isReference())
                        if (Source.isNullable(param))
                            Source.annotateNullable(constructorMethodParam);
                        else
                            Source.annotateNonnull(constructorMethodParam);
                }

                if (isError) {
                    constructorMethod.body()._throw(JExpr._new(types._UnsupportedOperationException));
                } else {
                    AbstractJClass usedCaseClassType = Source.narrowType(caseClasses.get(usedInterfaceMethod.name()), constructorMethod.typeParams());
                    if (!usedInterfaceMethod.params().isEmpty() || usedInterfaceMethod.hasVarArgs()) {
                        boolean hasNullChecks = false;
                        for (VariableDeclaration param1: usedInterfaceMethod.params()) {
                            if (param1.type().isReference() && !Source.isNullable(param1)) {
                                JConditional nullCheck = constructorMethod.body()._if(JExpr.ref(param1.name()).eq(JExpr._null()));
                                JInvocation nullPointerExceptionConstruction = JExpr._new(types._NullPointerException);
                                nullPointerExceptionConstruction.arg(JExpr.lit(MessageFormat.format("Argument shouldn''t be null: ''{0}'' argument in static method invocation: ''{1}'' in class {2}",
                                                                                                    param1.name(),
                                                                                                    constructorMethod.name(),
                                                                                                    environment.valueClassQualifiedName())));
                                nullCheck._then()._throw(nullPointerExceptionConstruction);
                                hasNullChecks = true;
                            }
                        }
                        VariableDeclaration param1 = usedInterfaceMethod.varParam();
                        if (param1 != null) {
                            if (param1.type().isReference() && !Source.isNullable(param1)) {
                                JConditional nullCheck = constructorMethod.body()._if(JExpr.ref(param1.name()).eq(JExpr._null()));
                                JInvocation nullPointerExceptionConstruction = JExpr._new(types._NullPointerException);
                                nullPointerExceptionConstruction.arg(JExpr.lit(MessageFormat.format("Argument shouldn''t be null: ''{0}'' argument in static method invocation: ''{1}'' in class {2}",
                                                                                                    param1.name(),
                                                                                                    constructorMethod.name(),
                                                                                                    environment.valueClassQualifiedName())));
                                nullCheck._then()._throw(nullPointerExceptionConstruction);
                                hasNullChecks = true;
                            }
                        }
                        if (hasNullChecks) {
                            JAnnotationUse annotation = constructorMethod.annotate(SuppressWarnings.class);
                            annotation.paramArray("value", "null");
                        }

                        JInvocation caseClassConstructorInvocation = JExpr._new(usedCaseClassType);
                        for (VariableDeclaration param2: usedInterfaceMethod.params()) {
                            caseClassConstructorInvocation.arg(JExpr.ref(param2.name()));
                        }
                        VariableDeclaration param2 = usedInterfaceMethod.varParam();
                        if (param2 != null) {
                            caseClassConstructorInvocation.arg(JExpr.ref(param2.name()));
                        }
                        JInvocation constructorInvocation = JExpr._new(unwrappedUsedValueClassType);
                        constructorInvocation.arg(caseClassConstructorInvocation);
                        constructorMethod.body()._return(environment.wrappedValue(usedValueClassType, constructorInvocation));
                    } else {
                        JInvocation caseClassConstructorInvocation = JExpr._new(usedCaseClassType.erasure());
                        JInvocation initializer = JExpr._new(unwrappedUsedValueClassType.erasure());
                        initializer.arg(caseClassConstructorInvocation);
                        JFieldVar singletonInstanceField = environment.buildValueClassField(JMod.PRIVATE | JMod.STATIC | JMod.FINAL,
                                                                            usedValueClassType.erasure(),
                                                                            usedInterfaceMethod.name().toUpperCase(Locale.US),
                                                                            environment.wrappedValue(usedValueClassType.erasure(), initializer));
                        JAnnotationUse fieldAnnotationUse = singletonInstanceField.annotate(SuppressWarnings.class);
                        JAnnotationArrayMember paramArray = fieldAnnotationUse.paramArray("value");
                        paramArray.param("unchecked");
                        paramArray.param("rawtypes");

                        JAnnotationUse methodAnnotationUse = constructorMethod.annotate(SuppressWarnings.class);
                        methodAnnotationUse.param("value", "unchecked");
                        constructorMethod.body()._return(singletonInstanceField);
                    }
                }
                constructorMethods.put(usedInterfaceMethod.name(), constructorMethod);
            }
            return constructorMethods;
        }

        void buildHashCodeMethod(int hashCodeBase) {
            if (!isError) {
                String hashCodeMethodName = hashCodeAcceptorMethodName();
                JMethod hashCodeMethod = environment.buildValueClassMethod(JMod.PUBLIC | JMod.FINAL, "hashCode");
                hashCodeMethod.type(types._int);
                hashCodeMethod.annotate(Override.class);

                if (environment.hashCodeCaching() == Caching.NONE) {
                    JInvocation invocation = JExpr.refthis(acceptorField).invoke(hashCodeMethodName);
                    hashCodeMethod.body()._return(invocation);
                } else if (environment.hashCodeCaching() == Caching.PRECOMPUTE) {
                    hashCodeMethod.body()._return(hashCodeCachedValueField);
                } else if (environment.hashCodeCaching() == Caching.SIMPLE) {
                    VariableNameSource nameSource = new VariableNameSource();
                    JFieldRef hashCodeField = JExpr.refthis(hashCodeCachedValueField);
                    JVar code = hashCodeMethod.body().decl(types._int, nameSource.get("code"), hashCodeField);
                    JConditional _if = hashCodeMethod.body()._if(code.eq0());
                    JInvocation invocation = JExpr.refthis(acceptorField).invoke(hashCodeMethodName);
                    _if._then().assign(code, invocation);
                    _if._then().assign(code, JOp.cond(code.ne0(), code, JExpr.lit(Integer.MIN_VALUE)));
                    _if._then().assign(hashCodeField, code);
                    hashCodeMethod.body()._return(code);
                } else if (environment.hashCodeCaching() == Caching.SYNCRONIZED) {
                    VariableNameSource nameSource = new VariableNameSource();
                    JFieldRef hashCodeField = JExpr.refthis(hashCodeCachedValueField);
                    JFieldRef lockField = JExpr.refthis(acceptorField);
                    JVar code = hashCodeMethod.body().decl(types._int, nameSource.get("code"), hashCodeField);
                    JConditional _if1 = hashCodeMethod.body()._if(code.eq0());
                    JSynchronizedBlock synchronizedBlock = _if1._then().synchronizedBlock(lockField);
                    synchronizedBlock.body().assign(code, hashCodeField);
                    JConditional _if2 = synchronizedBlock.body()._if(code.eq0());
                    JInvocation invocation = JExpr.refthis(acceptorField).invoke(hashCodeMethodName);
                    _if2._then().assign(code, invocation);
                    _if2._then().assign(code, JOp.cond(code.ne0(), code, JExpr.lit(Integer.MIN_VALUE)));
                    _if2._then().assign(hashCodeField, code);
                    hashCodeMethod.body()._return(code);
                } else
                    throw new IllegalStateException("Unsupported hashCodeCaching: " + environment.hashCodeCaching());

