package com.github.sabomichal.immutablexjc;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;

import java.beans.Introspector;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.stream.Stream;

import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JAnnotationValue;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JFormatter;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.Outline;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.xml.sax.ErrorHandler;

/**
 * IMMUTABLE-XJC plugin implementation.
 *
 * @author <a href="mailto:[email protected]">Michal Sabo</a>
 */
public final class PluginImpl extends Plugin {

    private static final String BUILDER_OPTION_NAME = "-imm-builder";
    private static final String SIMPLEBUILDERNAME_OPTION_NAME = "-imm-simplebuildername";
    private static final String INHERIT_BUILDER_OPTION_NAME = "-imm-inheritbuilder";
    private static final String CCONSTRUCTOR_OPTION_NAME = "-imm-cc";
    private static final String WITHIFNOTNULL_OPTION_NAME = "-imm-ifnotnull";
    private static final String NOPUBLICCONSTRUCTOR_OPTION_NAME = "-imm-nopubconstructor";
    private static final String PUBLICCONSTRUCTOR_MAXARGS_OPTION_NAME = "-imm-pubconstructormaxargs";
    private static final String SKIPCOLLECTIONS_OPTION_NAME = "-imm-skipcollections";
    private static final String CONSTRUCTORDEFAULTS_OPTION_NAME = "-imm-constructordefaults";
    private static final String OPTIONAL_GETTER_OPTION_NAME = "-imm-optionalgetter";

    private static final String UNSET_PREFIX = "unset";
    private static final String SET_PREFIX = "set";
    private static final String MESSAGE_PREFIX = "IMMUTABLE-XJC";
    private static final String OPTION_NAME = "immutable";
    private static final JType[] NO_ARGS = new JType[0];

    private ResourceBundle resourceBundle = ResourceBundle.getBundle(PluginImpl.class.getCanonicalName());
    private boolean createBuilder;
    private boolean builderInheritance;
    private boolean createCConstructor;
    private boolean createWithIfNotNullMethod;
    private boolean createBuilderWithoutPublicConstructor;
    private int publicConstructorMaxArgs = Integer.MAX_VALUE;
    private boolean leaveCollectionsMutable;
    private boolean setDefaultValuesInConstructor;
    private boolean useSimpleBuilderName;
    private boolean optionalGetter;
    private Options options;

    @Override
    public boolean run(final Outline model, final Options options, final ErrorHandler errorHandler) {
        boolean success = true;
        this.options = options;

        this.log(Level.INFO, "title");

        List<? extends ClassOutline> classes = new ArrayList<ClassOutline>(model.getClasses());
        if (builderInheritance) {
            classes.sort(new Comparator<ClassOutline>() {
                @Override
                public int compare(ClassOutline o1, ClassOutline o2) {
                    return Integer.compare(getDepth(o1), getDepth(o2));
                }

                private int getDepth(ClassOutline outline) {
                    int depth = 0;
                    while ((outline = outline.getSuperClass()) != null) {
                        ++depth;
                    }
                    return depth;
                }
            });
        }
        for (ClassOutline clazz : classes) {
            JDefinedClass implClass = clazz.implClass;

            JFieldVar[] declaredFields = getDeclaredFields(implClass);
            ClassField[] superclassFieldsWithOwners = getSuperclassFields(implClass);
            JFieldVar[] superclassFields = Arrays.stream(superclassFieldsWithOwners).map(ClassField::getField).toArray(JFieldVar[]::new);

            makePropertiesPrivate(implClass);
            makePropertiesFinal(implClass, declaredFields);

            int declaredFieldsLength = declaredFields.length;
            int superclassFieldsLength = superclassFields.length;
            JMethod propertyContructor = null;
            if (declaredFieldsLength + superclassFieldsLength > 0) {
                int mod;
                if (createBuilderWithoutPublicConstructor
                        || (createBuilder && declaredFieldsLength + superclassFieldsLength > publicConstructorMaxArgs)) {
                    mod = JMod.NONE;
                } else {
                    mod = JMod.PUBLIC;
                }
                propertyContructor = addPropertyContructor(implClass, declaredFields, superclassFields, mod);
                if (propertyContructor == null) {
                    log(Level.WARNING, "couldNotAddPropertyCtor", implClass.binaryName());
                }
            }
            if (propertyContructor == null || !propertyContructor.params().isEmpty()) {
                addStandardConstructor(implClass, declaredFields, superclassFields);
            }

            makeClassFinal(implClass);
            removeSetters(implClass);
            replaceCollectionGetters(implClass, declaredFields);

            if (optionalGetter) {
                replaceOptionalGetters(implClass, declaredFields);
            }

            if (createBuilder) {
                if (!clazz.implClass.isAbstract()) {
                    JFieldVar[] unhandledSuperclassFields = getUnhandledSuperclassFields(superclassFieldsWithOwners);
                    JDefinedClass builderClass;
                    if ((builderClass = addBuilderClass(clazz, declaredFields, unhandledSuperclassFields, superclassFields)) == null) {
                        log(Level.WARNING, "couldNotAddClassBuilder", implClass.binaryName());
                    }

                    if (createCConstructor && builderClass != null) {
                        addCopyConstructor(clazz.implClass, builderClass, declaredFields, unhandledSuperclassFields);
                    }
                }
            }
        }

        // if superclass is a JAXB bound class or an abstract class, revert setting it final
        for (ClassOutline clazz : model.getClasses()) {
            if (clazz.getSuperClass() != null) {
                clazz.getSuperClass().implClass.mods().setFinal(false);
            } else if (clazz.implClass.isAbstract()) {
                clazz.implClass.mods().setFinal(false);
            }
        }

        this.options = null;

        return success;
    }

    @Override
    public String getOptionName() {
        return OPTION_NAME;
    }

    @Override
    public String getUsage() {
        final String n = System.getProperty("line.separator", "\n");
        final int maxOptionLength = PUBLICCONSTRUCTOR_MAXARGS_OPTION_NAME.length();
        StringBuilder retval = new StringBuilder();
        appendOption(retval, "-" + OPTION_NAME, getMessage("usage"), n, maxOptionLength);
        appendOption(retval, BUILDER_OPTION_NAME, getMessage("builderUsage"), n, maxOptionLength);
        appendOption(retval, SIMPLEBUILDERNAME_OPTION_NAME, getMessage("simpleBuilderNameUsage"), n, maxOptionLength);
        appendOption(retval, INHERIT_BUILDER_OPTION_NAME, getMessage("inheritBuilderUsage"), n, maxOptionLength);
        appendOption(retval, CCONSTRUCTOR_OPTION_NAME, getMessage("cConstructorUsage"), n, maxOptionLength);
        appendOption(retval, WITHIFNOTNULL_OPTION_NAME, getMessage("withIfNotNullUsage"), n, maxOptionLength);
        appendOption(retval, NOPUBLICCONSTRUCTOR_OPTION_NAME, getMessage("builderWithoutPublicConstructor"), n, maxOptionLength);
        appendOption(retval, SKIPCOLLECTIONS_OPTION_NAME, getMessage("leaveCollectionsMutable"), n, maxOptionLength);
        appendOption(retval, PUBLICCONSTRUCTOR_MAXARGS_OPTION_NAME, getMessage("publicConstructorMaxArgs"), n, maxOptionLength);
        appendOption(retval, CONSTRUCTORDEFAULTS_OPTION_NAME, getMessage("setDefaultValuesInConstructor"), n, maxOptionLength);
        appendOption(retval, OPTIONAL_GETTER_OPTION_NAME, getMessage("optionalGetterUsage"), n, maxOptionLength);
        return retval.toString();
    }

    private void appendOption(StringBuilder retval, String option, String description, String n, int optionColumnWidth) {
        retval.append("  ");
        retval.append(option);
        for (int i = option.length(); i < optionColumnWidth; i++) {
            retval.append(' ');
        }
        retval.append(" :  ");
        retval.append(description);
        retval.append(n);
    }

    @Override
    public int parseArgument(final Options opt, final String[] args, final int i) {
        if (args[i].startsWith(BUILDER_OPTION_NAME)) {
            this.createBuilder = true;
            return 1;
        }
        if (args[i].startsWith(SIMPLEBUILDERNAME_OPTION_NAME)) {
            this.useSimpleBuilderName = true;
            return 1;
        }
        if (args[i].startsWith(INHERIT_BUILDER_OPTION_NAME)) {
            this.createBuilder = true;
            this.builderInheritance = true;
            return 1;
        }
        if (args[i].startsWith(CCONSTRUCTOR_OPTION_NAME)) {
            this.createCConstructor = true;
            return 1;
        }
        if (args[i].startsWith(WITHIFNOTNULL_OPTION_NAME)) {
            this.createWithIfNotNullMethod = true;
            return 1;
        }
        if (args[i].startsWith(NOPUBLICCONSTRUCTOR_OPTION_NAME)) {
            this.createBuilderWithoutPublicConstructor = true;
            return 1;
        }
        if (args[i].startsWith(SKIPCOLLECTIONS_OPTION_NAME)) {
            this.leaveCollectionsMutable = true;
            return 1;
        }
        if (args[i].startsWith(PUBLICCONSTRUCTOR_MAXARGS_OPTION_NAME)) {
            this.publicConstructorMaxArgs = Integer.parseInt(args[i].substring(PUBLICCONSTRUCTOR_MAXARGS_OPTION_NAME.length() + 1));
            return 1;
        }
        if (args[i].startsWith(CONSTRUCTORDEFAULTS_OPTION_NAME)) {
            this.setDefaultValuesInConstructor = true;
            return 1;
        }
        if (args[i].startsWith(OPTIONAL_GETTER_OPTION_NAME)) {
            this.optionalGetter = true;
            return 1;
        }
        return 0;
    }

    private String getMessage(final String key, final Object... args) {
        return MessageFormat.format(resourceBundle.getString(key), args);
    }

    private JDefinedClass addBuilderClass(ClassOutline clazz, JFieldVar[] declaredFields, JFieldVar[] unhandledSuperclassFields, JFieldVar[] allSuperclassFields) {
        JDefinedClass builderClass = generateBuilderClass(clazz.implClass);
        if (builderClass == null) {
            return null;
        }
        addBuilderMethodsForFields(builderClass, declaredFields);
        // handle all superclass fields not handled by any superclass builder
        addBuilderMethodsForFields(builderClass, unhandledSuperclassFields);
        if (builderInheritance) {
            // re-type inherited builder methods
            for (int i = 0; i < allSuperclassFields.length - unhandledSuperclassFields.length; i++) {
                JFieldVar inheritedField = allSuperclassFields[i];
                JMethod unconditionalWithMethod = addWithMethod(builderClass, inheritedField, true);
                if (createWithIfNotNullMethod) {
                    addWithIfNotNullMethod(builderClass, inheritedField, unconditionalWithMethod, true);
                }
                if (isCollection(inheritedField)) {
                    addAddMethod(builderClass, inheritedField, true);
                }
            }
        }
        addNewBuilder(clazz, builderClass);
        if (createCConstructor) {
            addNewBuilderCc(clazz, builderClass);
        }
        addBuildMethod(clazz.implClass, builderClass, declaredFields, allSuperclassFields);
        return builderClass;
    }

    private void addBuilderMethodsForFields(JDefinedClass builderClass, JFieldVar[] declaredFields) {
        for (JFieldVar field : declaredFields) {
            addProperty(builderClass, field);
            JMethod unconditionalWithMethod = addWithMethod(builderClass, field, false);
            if (createWithIfNotNullMethod) {
                addWithIfNotNullMethod(builderClass, field, unconditionalWithMethod, false);
            }
            if (isCollection(field)) {
                addAddMethod(builderClass, field, false);
            }
        }
    }

    private JVar addProperty(JDefinedClass clazz, JFieldVar field) {
        JType jType = getJavaType(field);
        int builderFieldVisibility = builderInheritance ? JMod.PROTECTED : JMod.PRIVATE;
        if (isCollection(field)) {
            return clazz.field(builderFieldVisibility, jType, field.name(),
                    getNewCollectionExpression(field.type().owner(), jType));
        } else {
            return clazz.field(builderFieldVisibility, jType, field.name());
        }
    }

    private JMethod addBuildMethod(JDefinedClass clazz, JDefinedClass builderClass, JFieldVar[] declaredFields, JFieldVar[] superclassFields) {
        JMethod method = builderClass.method(JMod.PUBLIC, clazz, "build");
        if (hasSuperClass(builderClass)) {
            method.annotate(Override.class);
        }
        JInvocation constructorInvocation = JExpr._new(clazz);
        for (JFieldVar field : superclassFields) {
            if (mustAssign(field)) {
                constructorInvocation.arg(JExpr.ref(field.name()));
            }
        }
        for (JFieldVar field : declaredFields) {
            if (mustAssign(field)) {
                constructorInvocation.arg(JExpr.ref(field.name()));
            }
        }
        method.body()._return(constructorInvocation);
        return method;
    }

    private void addNewBuilder(ClassOutline clazz, JDefinedClass builderClass) {
        if (builderInheritance || !hasSuperClassWithSameName(clazz)) {
            String builderMethodName = generateBuilderMethodName(clazz);
            JMethod method = clazz.implClass.method(JMod.PUBLIC | JMod.STATIC, builderClass, builderMethodName);
            method.body()._return(JExpr._new(builderClass));
        }
    }

    private void addNewBuilderCc(ClassOutline clazz, JDefinedClass builderClass) {
        if (builderInheritance || !hasSuperClassWithSameName(clazz)) {
            String builderMethodName = generateBuilderMethodName(clazz);
            JMethod method = clazz.implClass.method(JMod.PUBLIC | JMod.STATIC, builderClass, builderMethodName);
            JVar param = method.param(JMod.FINAL, clazz.implClass, "o");
            method.body()._return(JExpr._new(builderClass).arg(param));
        }
    }

    private String generateBuilderMethodName(ClassOutline clazz) {
        if (isUseSimpleBuilderName()) {
            return "builder";
        }
        return Introspector.decapitalize(clazz.implClass.name()) + "Builder";
    }

    private boolean isUseSimpleBuilderName() {
        return useSimpleBuilderName;
    }

    private boolean hasSuperClassWithSameName(ClassOutline clazz) {
        ClassOutline superclass = clazz.getSuperClass();
        while (superclass != null) {
            if (superclass.implClass.name().equals(clazz.implClass.name())) {
                return true;
            }
            superclass = superclass.getSuperClass();
        }
        return false;
    }

    private JMethod addPropertyContructor(JDefinedClass clazz, JFieldVar[] declaredFields, JFieldVar[] superclassFields, int constAccess) {
        JMethod ctor = clazz.getConstructor(getFieldTypes(declaredFields, superclassFields));
        if (ctor == null) {
            ctor = this.generatePropertyConstructor(clazz, declaredFields, superclassFields, constAccess);
        } else {
            this.log(Level.WARNING, "standardCtorExists");
        }
        return ctor;
    }

    private JMethod addStandardConstructor(final JDefinedClass clazz, JFieldVar[] declaredFields, JFieldVar[] superclassFields) {
        JMethod ctor = clazz.getConstructor(NO_ARGS);
        if (ctor == null) {
            ctor = this.generateStandardConstructor(clazz, declaredFields, superclassFields);
        } else {
            this.log(Level.WARNING, "standardCtorExists");
        }
        return ctor;
    }

    private JMethod addCopyConstructor(final JDefinedClass clazz, final JDefinedClass builderClass, JFieldVar[] declaredFields, JFieldVar[] superclassFields) {
        JMethod ctor = generateCopyConstructor(clazz, builderClass, declaredFields, superclassFields);
        createConstructor(builderClass, JMod.PUBLIC);
        return ctor;
    }

    private JMethod addWithMethod(JDefinedClass builderClass, JFieldVar field, boolean inherit) {
        String fieldName = StringUtils.capitalize(field.name());
        JMethod method = builderClass.method(JMod.PUBLIC, builderClass, "with" + fieldName);
        if (inherit) {
            generateMethodParameter(method, field);
            generateSuperCall(method);
        } else {
            generatePropertyAssignment(method, field);
        }
        method.body()._return(JExpr._this());
        return method;
    }

    private JMethod addWithIfNotNullMethod(JDefinedClass builderClass, JFieldVar field, JMethod unconditionalWithMethod, boolean inherit) {
        if (field.type().isPrimitive())
            return null;
        String fieldName = StringUtils.capitalize(field.name());
        JMethod method = builderClass.method(JMod.PUBLIC, builderClass, "with" + fieldName + "IfNotNull");
        JVar param = generateMethodParameter(method, field);
        JBlock block = method.body();
        if (inherit) {
            generateSuperCall(method);
            method.body()._return(JExpr._this());
        } else {
            JConditional conditional = block._if(param.eq(JExpr._null()));
            conditional._then()._return(JExpr._this());
            conditional._else()._return(JExpr.invoke(unconditionalWithMethod).arg(param));
        }
        return method;
    }

    private JMethod addAddMethod(JDefinedClass builderClass, JFieldVar field, boolean inherit) {
        List<JClass> typeParams = ((JClass) getJavaType(field)).getTypeParameters();
        if (!typeParams.iterator().hasNext()) {
            return null;
        }
        JMethod method = builderClass.method(JMod.PUBLIC, builderClass, "add" + StringUtils.capitalize(field.name()));
        JBlock block = method.body();
        String fieldName = field.name();
        JVar param = method.param(JMod.FINAL, typeParams.iterator().next(), fieldName);
        if (inherit) {
            generateSuperCall(method);
        } else {
            JInvocation invocation = JExpr.refthis(fieldName).invoke("add").arg(param);
            block.add(invocation);
        }
        block._return(JExpr._this());
        return method;
    }

    private void generateSuperCall(JMethod method) {
        method.annotate(Override.class);
        JBlock block = method.body();
        JInvocation superInvocation = block.invoke(JExpr._super(), method);
        for (JVar param : method.params()) {
            superInvocation.arg(param);
        }
    }

    private JDefinedClass generateBuilderClass(JDefinedClass clazz) {
        JDefinedClass builderClass = null;
        String builderClassName = getBuilderClassName(clazz);
        try {
            builderClass = clazz._class(JMod.PUBLIC | JMod.STATIC, builderClassName);
            if (builderInheritance) {
                for (JClass superClass = clazz._extends(); superClass != null; superClass = superClass._extends()) {
                    JClass superClassBuilderClass = getBuilderClass(superClass);
                    if (superClassBuilderClass != null) {
                        builderClass._extends(superClassBuilderClass);
                        break;
                    }
                }
            }
        } catch (JClassAlreadyExistsException e) {
            this.log(Level.WARNING, "builderClassExists", builderClassName);
        }
        return builderClass;
    }

    private String getBuilderClassName(JClass clazz) {
        if (isUseSimpleBuilderName()) {
            return "Builder";
        }
        return clazz.name() + "Builder";
    }

    private JClass getBuilderClass(JClass clazz) {
        //Current limitation: this only works for classes from this model / outline, i.e. that are part of this generator run
        if (!createBuilder || clazz.isAbstract()) {
            return null;
        }
        String builderClassName = getBuilderClassName(clazz);
        if (clazz instanceof JDefinedClass) {
            JDefinedClass definedClass = (JDefinedClass) clazz;
            for (Iterator<JDefinedClass> i = definedClass.classes(); i.hasNext(); ) {
                JDefinedClass innerClass = i.next();
                if (builderClassName.equals(innerClass.name())) {
                    return innerClass;
                }
            }
        }
        return null;
    }

    private void replaceOptionalGetters(JDefinedClass implClass, JFieldVar[] declaredFields) {
        for (JFieldVar field : declaredFields) {
            if (isCollection(field)) {
                continue;
            }

            if (!isRequired(field)) {
                JMethod getterMethod = getGetterProperty(field, implClass);
                if (getterMethod != null) {
                    replaceOptionalGetter(implClass, field, getterMethod);
                }
            }
        }
    }

    private void replaceOptionalGetter(JDefinedClass ownerClass, JFieldVar field, final JMethod getter) {
        // remove the old getter
        ownerClass.methods().remove(getter);

        JCodeModel codeModel = field.type().owner();

        final JClass optionalWrappedReturnType = codeModel.ref(Optional.class).narrow(field.type());

        // and create a new one
        JMethod newGetter = ownerClass.method(getter.mods().getValue(), optionalWrappedReturnType, getter.name());
        JBlock block = newGetter.body();

        JVar param = generateMethodParameter(getter, field);

        block._return(getOptionalWrappedExpression(codeModel, param));

        getter.javadoc().append("Returns optional attribute/element.");
    }

    private void replaceCollectionGetters(JDefinedClass implClass, JFieldVar[] declaredFields) {
        for (JFieldVar field : declaredFields) {
            if (isCollection(field) && !leaveCollectionsMutable) {
                JMethod getterMethod = getGetterProperty(field, implClass);
                if (getterMethod != null) {
                    replaceCollectionGetter(implClass, field, getterMethod);
                }
            }
        }
    }

    private void replaceCollectionGetter(JDefinedClass ownerClass, JFieldVar field, final JMethod getter) {
        // remove the old getter
        ownerClass.methods().remove(getter);
        // and create a new one
        JMethod newGetter = ownerClass.method(getter.mods().getValue(), getter.type(), getter.name());
        JBlock block = newGetter.body();

        JVar ret = block.decl(getJavaType(field), "ret");
        JCodeModel codeModel = field.type().owner();
        JVar param = generateMethodParameter(getter, field);
        JConditional conditional = block._if(param.eq(JExpr._null()));
        conditional._then().assign(ret, getEmptyCollectionExpression(codeModel, param));
        conditional._else().assign(ret, getUnmodifiableWrappedExpression(codeModel, param));
        block._return(ret);

        getter.javadoc().append("Returns unmodifiable collection.");
    }

    private void generatePropertyAssignment(final JMethod method, JFieldVar field) {
        generatePropertyAssignment(method, field, false);
    }

    private void generatePropertyAssignment(final JMethod method, JFieldVar field, boolean wrapUnmodifiable) {
        JBlock block = method.body();
        JCodeModel codeModel = field.type().owner();
        String fieldName = field.name();
        JVar param = generateMethodParameter(method, field);
        if (isCollection(field) && !leaveCollectionsMutable && wrapUnmodifiable) {
            JConditional conditional = block._if(param.eq(JExpr._null()));
            conditional._then().assign(JExpr.refthis(fieldName), JExpr._null());
            conditional._else().assign(JExpr.refthis(fieldName),
                    getDefensiveCopyExpression(codeModel, getJavaType(field), param));
        } else {
            block.assign(JExpr.refthis(fieldName), JExpr.ref(fieldName));
        }
    }

    private JVar generateMethodParameter(final JMethod method, JFieldVar field) {
        String fieldName = field.name();
        JType javaType = getJavaType(field);
        return method.param(JMod.FINAL, javaType, fieldName);
    }

    private JExpression getDefensiveCopyExpression(JCodeModel codeModel, JType jType, JVar param) {
        List<JClass> typeParams = ((JClass) jType).getTypeParameters();
        JClass typeParameter = null;
        if (typeParams.iterator().hasNext()) {
            typeParameter = typeParams.iterator().next();
        }

        JClass newClass = null;
        if (param.type().erasure().equals(codeModel.ref(Collection.class))) {
            newClass = codeModel.ref(ArrayList.class);
        } else if (param.type().erasure().equals(codeModel.ref(List.class))) {
            newClass = codeModel.ref(ArrayList.class);
        } else if (param.type().erasure().equals(codeModel.ref(Map.class))) {
            newClass = codeModel.ref(HashMap.class);
        } else if (param.type().erasure().equals(codeModel.ref(Set.class))) {
            newClass = codeModel.ref(HashSet.class);
        } else if (param.type().erasure().equals(codeModel.ref(SortedMap.class))) {
            newClass = codeModel.ref(TreeMap.class);
        } else if (param.type().erasure().equals(codeModel.ref(SortedSet.class))) {
            newClass = codeModel.ref(TreeSet.class);
        }
        if (newClass != null && typeParameter != null) {
            newClass = newClass.narrow(typeParameter);
        }
        return newClass == null ? JExpr._null() : JExpr._new(newClass).arg(param);
    }

    private JExpression getUnmodifiableWrappedExpression(JCodeModel codeModel, JVar param) {
        if (param.type().erasure().equals(codeModel.ref(Collection.class))) {
            return codeModel.ref(Collections.class).staticInvoke("unmodifiableCollection").arg(param);
        } else if (param.type().erasure().equals(codeModel.ref(List.class))) {
            return codeModel.ref(Collections.class).staticInvoke("unmodifiableList").arg(param);
        } else if (param.type().erasure().equals(codeModel.ref(Map.class))) {
            return codeModel.ref(Collections.class).staticInvoke("unmodifiableMap").arg(param);
        } else if (param.type().erasure().equals(codeModel.ref(Set.class))) {
            return codeModel.ref(Collections.class).staticInvoke("unmodifiableSet").arg(param);
        } else if (param.type().erasure().equals(codeModel.ref(SortedMap.class))) {
            return codeModel.ref(Collections.class).staticInvoke("unmodifiableSortedMap").arg(param);
        } else if (param.type().erasure().equals(codeModel.ref(SortedSet.class))) {
            return codeModel.ref(Collections.class).staticInvoke("unmodifiableSortedSet").arg(param);
        }
        return param;
    }

    private JExpression getEmptyCollectionExpression(JCodeModel codeModel, JVar param) {
        if (param.type().erasure().equals(codeModel.ref(Collection.class))) {
            return codeModel.ref(Collections.class).staticInvoke("emptyList");
        } else if (param.type().erasure().equals(codeModel.ref(List.class))) {
            return codeModel.ref(Collections.class).staticInvoke("emptyList");
        } else if (param.type().erasure().equals(codeModel.ref(Map.class))) {
            return codeModel.ref(Collections.class).staticInvoke("emptyMap");
        } else if (param.type().erasure().equals(codeModel.ref(Set.class))) {
            return codeModel.ref(Collections.class).staticInvoke("emptySet");
        } else if (param.type().erasure().equals(codeModel.ref(SortedMap.class))) {
            return JExpr._new(codeModel.ref(TreeMap.class));
        } else if (param.type().erasure().equals(codeModel.ref(SortedSet.class))) {
            return JExpr._new(codeModel.ref(TreeSet.class));
        }
        return param;
    }

    private JExpression getNewCollectionExpression(JCodeModel codeModel, JType jType) {
        List<JClass> typeParams = ((JClass) jType).getTypeParameters();
        JClass typeParameter = null;
        if (typeParams.iterator().hasNext()) {
            typeParameter = typeParams.iterator().next();
        }

        JClass newClass = null;
        if (jType.erasure().equals(codeModel.ref(Collection.class))) {
            newClass = codeModel.ref(ArrayList.class);
        } else if (jType.erasure().equals(codeModel.ref(List.class))) {
            newClass = codeModel.ref(ArrayList.class);
        } else if (jType.erasure().equals(codeModel.ref(Map.class))) {
            newClass = codeModel.ref(HashMap.class);
        } else if (jType.erasure().equals(codeModel.ref(Set.class))) {
            newClass = codeModel.ref(HashSet.class);
        } else if (jType.erasure().equals(codeModel.ref(SortedMap.class))) {
            newClass = codeModel.ref(TreeMap.class);
        } else if (jType.erasure().equals(codeModel.ref(SortedSet.class))) {
            newClass = codeModel.ref(TreeSet.class);
        }
        if (newClass != null && typeParameter != null) {
            newClass = newClass.narrow(typeParameter);
        }

        return newClass == null ? JExpr._null() : JExpr._new(newClass);
    }

    private JExpression getOptionalWrappedExpression(JCodeModel codeModel, JVar param) {
        return codeModel.ref(Optional.class).staticInvoke("ofNullable").arg(param);
    }

    private void generateDefaultPropertyAssignment(JMethod method, JFieldVar field) {
        JBlock block = method.body();
        String propertyName = field.name();
        block.assign(JExpr.refthis(propertyName), defaultValue(field));
    }

    private JExpression defaultValue(JFieldVar field) {
        JType javaType = field.type();
        if (setDefaultValuesInConstructor) {
            Optional<JAnnotationUse> xmlElementAnnotation = getAnnotation(field.annotations(), javax.xml.bind.annotation.XmlElement.class.getCanonicalName());
            if (xmlElementAnnotation.isPresent()) {
                JAnnotationValue annotationValue = xmlElementAnnotation.get().getAnnotationMembers().get("defaultValue");
                if (annotationValue != null) {
                    StringWriter sw = new StringWriter();
                    JFormatter f = new JFormatter(sw);
                    annotationValue.generate(f);
                    return JExpr.lit(sw.toString().replaceAll("\"", ""));
                }
            }
        }
        if (javaType.isPrimitive()) {
            if (field.type().owner().BOOLEAN.equals(javaType)) {
                return JExpr.lit(false);
            } else if (javaType.owner().SHORT.equals(javaType)) {
                return JExpr.cast(javaType.owner().SHORT, JExpr.lit(0));
            } else {
                return JExpr.lit(0);
            }
        }
        return JExpr._null();
    }

    private Optional<JAnnotationUse> getAnnotation(Collection<JAnnotationUse> annotations, String clazz) {
        return annotations.stream().filter(ann -> ann.getAnnotationClass().fullName().equals(clazz)).findFirst();
    }

    private JMethod generatePropertyConstructor(JDefinedClass clazz, JFieldVar[] declaredFields, JFieldVar[] superclassFields, int constAccess) {
        final JMethod ctor = createConstructor(clazz, constAccess);
        if (superclassFields.length > 0) {
            JInvocation superInvocation = ctor.body().invoke("super");
            for (JFieldVar field : superclassFields) {
                if (mustAssign(field)) {
                    superInvocation.arg(JExpr.ref(field.name()));
                    generateMethodParameter(ctor, field);
                }
            }
        }

        for (JFieldVar field : declaredFields) {
            if (mustAssign(field)) {
                generatePropertyAssignment(ctor, field, true);
            }
        }
        return ctor;
    }

    private boolean mustAssign(JFieldVar field) {
        // we have to assign final field, except filled collection fields, since we might loose the collection type upon marshal
        return !isFinal(field) || !isCollection(field) || getInitJExpression(field) == null;
    }

    private boolean shouldAssign(JFieldVar field) {
        // we don't want to clear filled collection fields in default constructor, since we might loose the collection type upon marshal
        return !isCollection(field) || getInitJExpression(field) == null;
    }

    private JMethod generateStandardConstructor(final JDefinedClass clazz, JFieldVar[] declaredFields, JFieldVar[] superclassFields) {
        final JMethod ctor = createConstructor(clazz, JMod.PROTECTED);
        ctor.javadoc().add("Used by JAX-B");
        if (superclassFields.length > 0) {
            JInvocation superInvocation = ctor.body().invoke("super");
            for (JFieldVar field : superclassFields) {
                if (mustAssign(field)) {
                    superInvocation.arg(defaultValue(field));
                }
            }
        }
        for (JFieldVar field : declaredFields) {
            if (shouldAssign(field)) {
                generateDefaultPropertyAssignment(ctor, field);
            }
        }
        return ctor;
    }

    private JMethod generateCopyConstructor(final JDefinedClass clazz, final JDefinedClass builderClass, JFieldVar[] declaredFields, JFieldVar[] superclassFields) {
        final JMethod ctor = createConstructor(builderClass, JMod.PUBLIC);
        final JVar o = ctor.param(JMod.FINAL, clazz, "o");
        if (hasSuperClass(builderClass)) {
            ctor.body().invoke("super").arg(o);
        } else {
            String builderName = isUseSimpleBuilderName() ? String.format("%s.%s", clazz.name(), builderClass.name()) : builderClass.name();
            ctor.body()._if(o.eq(JExpr._null()))._then()
                    ._throw(JExpr._new(builderClass.owner().ref(NullPointerException.class))
                            .arg("Cannot create a copy of '" + builderName + "' from 'null'."));
        }
        JCodeModel codeModel = clazz.owner();

        for (JFieldVar field : superclassFields) {
            String propertyName = field.name();
            JType type = field.type();
            if (type instanceof JDefinedClass) {
                JMethod getter = getGetterProperty(field, clazz);
                if (isCollection(field)) {
                    JVar tmpVar = ctor.body().decl(0, getJavaType(field), "_" + propertyName, JExpr.invoke(o, getter));
                    JConditional conditional = ctor.body()._if(tmpVar.eq(JExpr._null()));
                    conditional._then().assign(JExpr.refthis(propertyName), getNewCollectionExpression(codeModel, getJavaType(field)));
                    conditional._else().assign(JExpr.refthis(propertyName), getDefensiveCopyExpression(codeModel, getJavaType(field), tmpVar));
                } else {
                    ctor.body().assign(JExpr.refthis(propertyName), JExpr.invoke(o, getter));
                }
            }
        }
        for (JFieldVar field : declaredFields) {
            String propertyName = field.name();

            if (isCollection(field)) {
                JVar tmpVar = ctor.body().decl(0, getJavaType(field), "_" + propertyName, JExpr.ref(o, propertyName));
                JConditional conditional = ctor.body()._if(tmpVar.eq(JExpr._null()));
                conditional._then().assign(JExpr.refthis(propertyName), getNewCollectionExpression(codeModel, getJavaType(field)));
                conditional._else().assign(JExpr.refthis(propertyName), getDefensiveCopyExpression(codeModel, getJavaType(field), tmpVar));
            } else {
                ctor.body().assign(JExpr.refthis(propertyName), JExpr.ref(o, propertyName));
            }
        }
        return ctor;
    }

    private boolean hasSuperClass(final JDefinedClass builderClass) {
        // we have to account for java.lang.Object, which we don't care about...
        return builderClass._extends() != null && builderClass._extends()._extends() != null;
    }

    private JMethod createConstructor(final JDefinedClass clazz, final int visibility) {
        return clazz.constructor(visibility);
    }

    private JType getJavaType(JFieldVar field) {
        return field.type();
    }

    private JType[] getFieldTypes(JFieldVar[] declaredFields, JFieldVar[] superclassFields) {
        JType[] fieldTypes = new JType[declaredFields.length + superclassFields.length];
        int i = 0;
        for (JFieldVar field : superclassFields) {
            fieldTypes[i++] = field.type();
        }
        for (JFieldVar field : declaredFields) {
            fieldTypes[i++] = field.type();
        }
        return fieldTypes;
    }

    private JMethod getGetterProperty(final JFieldVar field, final JDefinedClass clazz) {
        JMethod getter = clazz.getMethod("get" + StringUtils.capitalize(field.name()), NO_ARGS);
        if (getter == null) {
            getter = clazz.getMethod("is" + StringUtils.capitalize(field.name()), NO_ARGS);
        }

        if (getter == null) {
            List<JDefinedClass> superClasses = getSuperClasses(clazz);
            for (JDefinedClass definedClass : superClasses) {
                getter = getGetterProperty(field, definedClass);

                if (getter != null) {
                    break;
                }
            }
        }
        if (getter == null) {
            //XJC does not work conform Introspector.decapitalize when multiple upper-case letter are in field name
            Optional<JAnnotationUse> xmlElementAnnotation = getAnnotation(field.annotations(), javax.xml.bind.annotation.XmlElement.class.getCanonicalName());
            if (xmlElementAnnotation.isPresent()) {
                JAnnotationValue annotationValue = xmlElementAnnotation.get().getAnnotationMembers().get("name");
                if (annotationValue != null) {
                    StringWriter sw = new StringWriter();
                    JFormatter f = new JFormatter(sw);
                    annotationValue.generate(f);
                    getter = clazz.getMethod("get" + sw.toString().replaceAll("\"", ""), NO_ARGS);
                }
            }
        }
        return getter;
    }

    private void makeClassFinal(JDefinedClass clazz) {
        clazz.mods().setFinal(true);
    }

    private void makePropertiesPrivate(JDefinedClass clazz) {
        for (JFieldVar field : clazz.fields().values()) {
            field.mods().setPrivate();
        }
    }

    private void makePropertiesFinal(JDefinedClass clazz, JFieldVar[] declaredFields) {
        for (JFieldVar field : declaredFields) {
            String fieldName = field.name();
            clazz.fields().get(fieldName).mods().setFinal(!(leaveCollectionsMutable && isCollection(field)));
        }
    }

    private boolean isCollection(JFieldVar field) {
        if (field.type() instanceof JClass) {
            return isCollection((JClass) field.type());
        }
        return false;
    }

    private boolean isCollection(JClass clazz) {
        return clazz.owner().ref(Collection.class).isAssignableFrom(clazz) ||
                clazz.owner().ref(Map.class).isAssignableFrom(clazz);
    }

    private boolean isRequired(JFieldVar field) {
        return Stream.of(XmlElement.class, XmlAttribute.class)
                .map(annotationType ->
                        getAnnotation(field.annotations(), annotationType.getCanonicalName())
                                .map(JAnnotationUse::getAnnotationMembers)
                                .map(annotationValues -> annotationValues.get("required"))
                                .filter(annotationValue -> {
                                    StringWriter sw = new StringWriter();
                                    JFormatter f = new JFormatter(sw);
                                    annotationValue.generate(f);
                                    return sw.toString().equals("true");
                                })
                ).anyMatch(Optional::isPresent);
    }

    private void removeSetters(JDefinedClass clazz) {
        Collection<JMethod> methods = clazz.methods();
        Iterator<JMethod> it = methods.iterator();
        while (it.hasNext()) {
            JMethod method = it.next();
            String methodName = method.name();
            if (methodName.startsWith(SET_PREFIX) || methodName.startsWith(UNSET_PREFIX)) {
                it.remove();
            }
        }
    }

    private JFieldVar[] getDeclaredFields(JDefinedClass clazz) {
        return clazz.fields().values().stream().filter(f -> !(isFinal(f) && isStatic(f))).toArray(JFieldVar[]::new);
    }

    private ClassField[] getSuperclassFields(JDefinedClass clazz) {
        List<JDefinedClass> superclasses = getSuperClasses(clazz);

        // get all fields in class reverse order
        List<ClassField> superclassFields = new ArrayList<>();
        Collections.reverse(superclasses);
        for (JDefinedClass classOutline : superclasses) {
            Map<String, JFieldVar> fields = classOutline.fields();
            for (JFieldVar jFieldVar : fields.values()) {
                if (!(isStatic(jFieldVar) && isFinal(jFieldVar))) {
                    superclassFields.add(new ClassField(classOutline, jFieldVar));
                }
            }
        }
        return superclassFields.toArray(new ClassField[0]);
    }

    private List<JDefinedClass> getSuperClasses(JClass clazz) {
        // first get all superclasses
        List<JDefinedClass> superclasses = new ArrayList<>();
        JClass superclass = clazz._extends();
        while (superclass != null) {
            if (superclass instanceof JDefinedClass) {
                superclasses.add((JDefinedClass) superclass);
            }
            superclass = superclass._extends();
        }
        return superclasses;
    }

    public boolean isStatic(JFieldVar var) {
        return (var.mods().getValue() & JMod.STATIC) != 0;
    }

    public boolean isFinal(JFieldVar var) {
        return (var.mods().getValue() & JMod.FINAL) != 0;
    }

    private JFieldVar[] getUnhandledSuperclassFields(ClassField[] superclassFieldsWithOwners) {
        JFieldVar[] superclassFields = Arrays.stream(superclassFieldsWithOwners).map(ClassField::getField).toArray(JFieldVar[]::new);
        if (!builderInheritance) {
            //we want to handle all inherited field
            return superclassFields;
        }

        // we only need fields whose classes don't have a builder themselves...
        // superclassFields are in class reverse order, i.e. root class first, direct superclass last, cf. #getSuperclassFields(ClassOutline)
        for (int i = superclassFields.length - 1; i >= 0; i--) {
            JDefinedClass type = superclassFieldsWithOwners[i].getClazz();
            if (type != null && getBuilderClass(type) != null) {
                // this class has its own builder, so we can stop here...
                if (i == superclassFields.length - 1) {
                    return new JFieldVar[0];
                }
                JFieldVar[] handledSuperclassFields = new JFieldVar[superclassFields.length - i - 1];
                System.arraycopy(superclassFields, i + 1, handledSuperclassFields, 0, handledSuperclassFields.length);
                return handledSuperclassFields;
            }
        }
        // no superclass with a builder, so we actually need them all...
        return superclassFields;
    }

    // init field is private :-( , we really need this
    private JExpression getInitJExpression(JFieldVar jFieldVar) {
        try {
            return (JExpression) FieldUtils.readField(jFieldVar, "init", true);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private void log(final Level level, final String key, final Object... args) {
        final String message = "[" + MESSAGE_PREFIX + "] [" + level.getLocalizedName() + "] " + getMessage(key, args);

        int logLevel = Level.WARNING.intValue();
        if (this.options != null && !this.options.quiet) {
            if (this.options.verbose) {
                logLevel = Level.INFO.intValue();
            }
            if (this.options.debugMode) {
                logLevel = Level.ALL.intValue();
            }
        }

        if (level.intValue() >= logLevel) {
            if (level.intValue() <= Level.INFO.intValue()) {
                System.out.println(message);
            } else {
                System.err.println(message);
            }
        }
    }

    private static class ClassField {

        private JDefinedClass clazz;
        private JFieldVar field;

        public ClassField(JDefinedClass clazz, JFieldVar field) {
            this.clazz = clazz;
            this.field = field;
        }

        public JDefinedClass getClazz() {
            return clazz;
        }

        public JFieldVar getField() {
            return field;
        }
    }
}