/*
 * MIT License
 *
 * Copyright (c) 2014 Klemm Software Consulting, Mirko Klemm
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.kscs.util.plugins.xjc;

import com.kscs.util.jaxb.Buildable;
import com.kscs.util.jaxb.PropertyTree;
import com.kscs.util.plugins.xjc.codemodel.ClassName;
import com.kscs.util.plugins.xjc.codemodel.GenerifiedClass;
import com.kscs.util.plugins.xjc.outline.DefinedInterfaceOutline;
import com.kscs.util.plugins.xjc.outline.DefinedPropertyOutline;
import com.kscs.util.plugins.xjc.outline.DefinedTypeOutline;
import com.kscs.util.plugins.xjc.outline.PropertyOutline;
import com.kscs.util.plugins.xjc.outline.ReferencedClassOutline;
import com.kscs.util.plugins.xjc.outline.TypeOutline;
import com.sun.codemodel.JAssignmentTarget;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldRef;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JForEach;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JType;
import com.sun.codemodel.JTypeVar;
import com.sun.codemodel.JVar;
import com.sun.tools.xjc.model.CClassInfo;
import com.sun.tools.xjc.model.CElementInfo;
import com.sun.tools.xjc.model.CNonElement;
import com.sun.tools.xjc.model.nav.NClass;
import com.sun.tools.xjc.model.nav.NType;
import com.sun.tools.xjc.outline.Aspect;
import com.sun.tools.xjc.outline.Outline;
import com.sun.xml.bind.v2.model.core.TypeInfo;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.namespace.QName;
import java.beans.Introspector;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;

import static com.kscs.util.plugins.xjc.base.PluginUtil.nullSafe;

/**
 * Helper class to generate fluent builder classes in two steps
 */
class BuilderGenerator {
	public static final String PRODUCT_VAR_NAME = "_product";
	public static final String PARENT_BUILDER_TYPE_PARAMETER_NAME = "_B";
	public static final String PRODUCT_TYPE_PARAMETER_NAME = "_P";
	public static final String OTHER_PARAM_NAME = "_other";
	public static final String OTHER_VAR_NAME = "_my";
	public static final String PARENT_BUILDER_PARAM_NAME = "_parentBuilder";
	public static final String STORED_VALUE_PARAM_NAME = "_storedValue";
	public static final String NEW_BUILDER_VAR_NAME = "_newBuilder";
	public static final String COPY_FLAG_PARAM_NAME = "_copy";
	private static final String ITEM_VAR_NAME = "_item";
	private final PluginContext pluginContext;
	private final JDefinedClass definedClass;
	private final GenerifiedClass builderClass;
	private final DefinedTypeOutline typeOutline;
	private final Map<String, BuilderOutline> builderOutlines;
	private final JFieldVar parentBuilderField;
	private final JAssignmentTarget storedValueField;
	private final boolean implement;
	private final BuilderGeneratorSettings settings;
	private final ResourceBundle resources;

	BuilderGenerator(final PluginContext pluginContext, final Map<String, BuilderOutline> builderOutlines, final BuilderOutline builderOutline, final BuilderGeneratorSettings settings) {
		this.pluginContext = pluginContext;
		this.settings = settings;
		this.builderOutlines = builderOutlines;
		this.typeOutline = (DefinedTypeOutline)builderOutline.getClassOutline();
		this.definedClass = this.typeOutline.getImplClass();
		this.builderClass = new GenerifiedClass(builderOutline.getDefinedBuilderClass(), BuilderGenerator.PARENT_BUILDER_TYPE_PARAMETER_NAME);
		this.resources = ResourceBundle.getBundle(BuilderGenerator.class.getName());
		this.implement = !this.builderClass.raw.isInterface();
		if (builderOutline.getClassOutline().getSuperClass() == null) {
			final JMethod endMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.typeParam, this.settings.getEndMethodName());
			if (this.implement) {
				this.parentBuilderField = this.builderClass.raw.field(JMod.PROTECTED | JMod.FINAL, this.builderClass.typeParam, BuilderGenerator.PARENT_BUILDER_PARAM_NAME);
				endMethod.body()._return(JExpr._this().ref(this.parentBuilderField));
				this.storedValueField = this.settings.isCopyAlways() ? null : this.builderClass.raw.field(JMod.PROTECTED | JMod.FINAL, this.definedClass, BuilderGenerator.STORED_VALUE_PARAM_NAME);
			} else {
				this.parentBuilderField = null;
				this.storedValueField = null;
			}
		} else {
			this.parentBuilderField = null;
			this.storedValueField = this.implement ? JExpr.ref(BuilderGenerator.STORED_VALUE_PARAM_NAME) : null;
		}
		if (this.implement) {
			generateCopyConstructor(false);
			if (this.settings.isGeneratingPartialCopy()) {
				generateCopyConstructor(true);
			}
		}
	}

	void generateBuilderMember(final PropertyOutline propertyOutline, final JBlock initBody, final JVar productParam) {
		final JType fieldType = propertyOutline.getRawType();
		if (propertyOutline.isCollection()) {
			if (propertyOutline.getRawType().isArray()) {
				generateArrayProperty(initBody, productParam, propertyOutline, fieldType.elementType(), this.builderClass.type);
			} else {
				final List<JClass> typeParameters = ((JClass)fieldType).getTypeParameters();
				final JClass elementType = typeParameters.get(0);
				generateCollectionProperty(initBody, productParam, propertyOutline, elementType);
				if (propertyOutline.getChoiceProperties().size() > 1) {
					generateCollectionChoiceProperty(propertyOutline);
				}
			}
		} else {
			if (propertyOutline.getChoiceProperties().size() > 1) {
				//throw new UnsupportedOperationException("Singular Properties with multiple references not currently supported.");
				generateSingularChoiceProperty(initBody, productParam, propertyOutline);
			} else {
				generateSingularProperty(initBody, productParam, propertyOutline);
			}
		}
	}
	private JType getTagRefType(final PropertyOutline.TagRef tagRef, final Outline outline, final Aspect aspect) {
		final TypeInfo<NType, NClass> typeInfo = tagRef.getTypeInfo();
		final JType type;
		if (typeInfo instanceof CClassInfo) {
			type = ((CClassInfo) typeInfo).toType(outline, aspect);
		} else if (typeInfo instanceof CElementInfo) {
			final List<CNonElement> refs = ((CElementInfo) typeInfo).getProperty().ref();
			// This feels dirty but am not sure what we do if we get multiple refs
			if (refs.size() == 1) {
				try {
					type = ((CClassInfo) refs.get(0)).toType(outline, aspect);
				} catch (Exception e) {
					throw new RuntimeException(String.format("Unexpected type %s for tagRef %s",
							refs.get(0).getClass().getCanonicalName(),
							tagRef.getTagName()));
				}
			} else {
				throw new RuntimeException(String.format("Expecting one ref type for tagRef %s, found %s",
						tagRef.getTagName(),
						refs.size()));
			}
		} else {
			throw new RuntimeException(String.format("Unexpected type %s for tagRef %s",
					typeInfo.getClass().getCanonicalName(),
					tagRef.getTagName()));
		}
		return type;
	}

	private void generateSingularChoiceProperty(final JBlock initBody, final JVar productParam, final PropertyOutline propertyOutline) {
		// First create the builder field, init and withXXX methods for the supertype of the choices
	    JFieldVar superTypeBuilderField = generateSingularChoiceSuperTypeProperty(initBody, productParam, propertyOutline)
				.orElseThrow(() -> new RuntimeException(String.format(
				        "Expecting to have a builderField for property %s", propertyOutline.getFieldName())));

		// Now create the withXXX methods for each choice type
		for (final PropertyOutline.TagRef typeInfo : propertyOutline.getChoiceProperties()) {
			final QName elementName = typeInfo.getTagName();
			final JType elementType = getTagRefType(typeInfo, this.pluginContext.outline, Aspect.EXPOSED);
			final String fieldName = this.pluginContext.toVariableName(elementName.getLocalPart());
			final String propertyName = this.pluginContext.toPropertyName(elementName.getLocalPart());
			final BuilderOutline childBuilderOutline = getBuilderDeclaration(elementType);
			if (childBuilderOutline == null) {
			    // TODO not sure when we will come in here so throw ex to highlight in testing
				throw new RuntimeException(String.format(
						"Don't think we should ever come in here, fieldName: %s", propertyOutline.getFieldName()));
			} else {
				final JClass builderFieldElementType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
				final JClass builderWithMethodReturnType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
				final JMethod withValueMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
				final JVar param = withValueMethod.param(JMod.FINAL, elementType, fieldName);
				generateWithMethodJavadoc(
						withValueMethod, param, propertyOutline.getSchemaAnnotationText(typeInfo).orElse(null));
				final JMethod withBuilderMethod = this.builderClass.raw.method(JMod.PUBLIC, builderWithMethodReturnType, PluginContext.WITH_METHOD_PREFIX + propertyName);
				generateBuilderMethodJavadoc(
				        withBuilderMethod,
						"with",
						fieldName,
						propertyOutline.getSchemaAnnotationText(typeInfo).orElse(null));
				if (this.implement) {
					// Generate the withXXX method that takes a value and returns the parent builder
					withValueMethod.body().assign(
							JExpr._this().ref(superTypeBuilderField),
							nullSafe(param, JExpr._new(builderFieldElementType).arg(JExpr._this()).arg(param).arg(JExpr.FALSE)));
					withValueMethod.body()._return(JExpr._this());

					// Generate the withXXX method that takes no args and returns a new child builder for that type
					JVar childBuilder = withBuilderMethod.body().decl(
					        JMod.FINAL,
							builderFieldElementType,
							fieldName + this.settings.getBuilderFieldSuffix(),
							JExpr._new(builderFieldElementType)
									.arg(JExpr._this())
									.arg(JExpr._null())
									.arg(JExpr.FALSE));

					withBuilderMethod.body().assign(
							JExpr._this().ref(superTypeBuilderField),
							childBuilder);
					withBuilderMethod.body()._return(childBuilder);
				}
			}
		}
	}

	private void generateCollectionChoiceProperty(final PropertyOutline propertyOutline) {
		for (final PropertyOutline.TagRef tagRef : propertyOutline.getChoiceProperties()) {
			final TypeInfo<NType,NClass> typeInfo = tagRef.getTypeInfo();
			final QName elementName = tagRef.getTagName();
			final JType elementType = typeInfo.getType().toType(this.pluginContext.outline, Aspect.EXPOSED);
			generateAddMethods(
					propertyOutline,
					elementName,
					elementType,
					propertyOutline.getSchemaAnnotationText(tagRef).orElse(null));
		}
	}

	private void generateAddMethods(final PropertyOutline propertyOutline,
	                                final QName elementName, final JType jType,
									final String schemaAnnotation) {
		final JClass elementType = jType.boxify();
		final JClass iterableType = this.pluginContext.iterableClass.narrow(elementType.wildcard());
		final String fieldName = this.pluginContext.toVariableName(elementName.getLocalPart());
		final String propertyName = this.pluginContext.toPropertyName(elementName.getLocalPart());
		final JMethod addIterableMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + propertyName);
		final JVar addIterableParam = addIterableMethod.param(JMod.FINAL, iterableType, fieldName + "_");
		generateAddMethodJavadoc(addIterableMethod, addIterableParam, schemaAnnotation);
		final JMethod addVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + propertyName);
		final JVar addVarargsParam = addVarargsMethod.varParam(elementType, fieldName + "_");
		generateAddMethodJavadoc(addVarargsMethod, addVarargsParam, schemaAnnotation);
		final BuilderOutline childBuilderOutline = getBuilderDeclaration(elementType);
		final JMethod addMethod;
		if (childBuilderOutline != null && !childBuilderOutline.getClassOutline().getImplClass().isAbstract()) {
			final JClass builderWithMethodReturnType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
			addMethod = this.builderClass.raw.method(JMod.PUBLIC, builderWithMethodReturnType, PluginContext.ADD_METHOD_PREFIX + propertyName);
			generateBuilderMethodJavadoc(addMethod, "add", fieldName, schemaAnnotation);
		} else {
			addMethod = null;
		}
		if (this.implement) {
			final BuilderOutline choiceChildBuilderOutline = getBuilderDeclaration(propertyOutline.getElementType());
			final JClass childBuilderType = childBuilderOutline == null ? this.pluginContext.buildableInterface : childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
			final JClass builderFieldElementType = choiceChildBuilderOutline == null ? this.pluginContext.buildableInterface : choiceChildBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
			final JClass builderArrayListClass = this.pluginContext.arrayListClass.narrow(builderFieldElementType);
			final JFieldVar builderField = this.builderClass.raw.fields().get(propertyOutline.getFieldName());
			addVarargsMethod.body()._return(JExpr.invoke(addIterableMethod).arg(this.pluginContext.asList(addVarargsParam)));
			if (addMethod == null) {
				addIterableMethod.body()._return(JExpr.invoke(PluginContext.ADD_METHOD_PREFIX + propertyOutline.getBaseName()).arg(addIterableParam));
			} else {
				final JConditional addIterableIfParamNull = addIterableMethod.body()._if(addIterableParam.ne(JExpr._null()));
				final JConditional addIterableIfNull = addIterableIfParamNull._then()._if(JExpr._this().ref(builderField).eq(JExpr._null()));
				addIterableIfNull._then().assign(JExpr._this().ref(builderField), JExpr._new(builderArrayListClass));
				final JForEach addIterableForEach = addIterableIfParamNull._then().forEach(elementType, BuilderGenerator.ITEM_VAR_NAME, addIterableParam);
				final JExpression builderCreationExpression = JExpr._new(childBuilderType).arg(JExpr._this()).arg(addIterableForEach.var()).arg(this.settings.isCopyAlways() ? JExpr.TRUE : JExpr.FALSE);
				addIterableForEach.body().add(JExpr._this().ref(builderField).invoke("add").arg(builderCreationExpression));
				addIterableMethod.body()._return(JExpr._this());

				final JConditional addIfNull = addMethod.body()._if(JExpr._this().ref(builderField).eq(JExpr._null()));
				addIfNull._then().assign(JExpr._this().ref(builderField), JExpr._new(builderArrayListClass));
				final JVar childBuilderVar = addMethod.body().decl(JMod.FINAL, childBuilderType, fieldName + this.settings.getBuilderFieldSuffix(), JExpr._new(childBuilderType).arg(JExpr._this()).arg(JExpr._null()).arg(JExpr.FALSE));
				addMethod.body().add(JExpr._this().ref(builderField).invoke("add").arg(childBuilderVar));
				addMethod.body()._return(childBuilderVar);
			}
		}
	}


	private void overrideAddMethods(final PropertyOutline propertyOutline,
	                                final QName elementName, final JType elementType) {
		final JClass iterableType = this.pluginContext.iterableClass.narrow(elementType instanceof JClass ? ((JClass)elementType).wildcard() : elementType);
		final String fieldName = this.pluginContext.toVariableName(elementName.getLocalPart());
		final String propertyName = this.pluginContext.toPropertyName(elementName.getLocalPart());
		final JMethod addIterableMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + propertyName);
		addIterableMethod.annotate(Override.class);
		final JVar addIterableParam = addIterableMethod.param(JMod.FINAL, iterableType, fieldName + "_");
		generateAddMethodJavadoc(addIterableMethod, addIterableParam, propertyOutline.getSchemaAnnotationText().orElse(null));
		final JMethod addVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + propertyName);
		addVarargsMethod.annotate(Override.class);
		final JVar addVarargsParam = addVarargsMethod.varParam(elementType, fieldName + "_");
		generateAddMethodJavadoc(addVarargsMethod, addVarargsParam, propertyOutline.getSchemaAnnotationText().orElse(null));
		final BuilderOutline childBuilderOutline = getBuilderDeclaration(elementType);
		final JMethod addMethod;
		if (childBuilderOutline != null && !childBuilderOutline.getClassOutline().getImplClass().isAbstract()) {
			addMethod = this.builderClass.raw.method(JMod.PUBLIC, childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard()), PluginContext.ADD_METHOD_PREFIX + propertyName);
			addMethod.annotate(Override.class);
			generateBuilderMethodJavadoc(addMethod, "add", fieldName, propertyOutline.getSchemaAnnotationText().orElse(null));
		} else {
			addMethod = null;
		}
		if (this.implement) {
			addVarargsMethod.body().add(JExpr._super().invoke(addVarargsMethod).arg(addVarargsParam));
			addVarargsMethod.body()._return(JExpr._this());
			addIterableMethod.body().add(JExpr._super().invoke(addIterableMethod).arg(addIterableParam));
			addIterableMethod.body()._return(JExpr._this());
			if (addMethod != null) {
				addMethod.body()._return(JExpr.cast(childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard()), JExpr._super().invoke(addMethod)));
			}
		}
	}

	private void generateCollectionProperty(final JBlock initBody, final JVar productParam, final PropertyOutline propertyOutline, final JClass elementType) {
		final String fieldName = propertyOutline.getFieldName();
		final String propertyName = propertyOutline.getBaseName();
		final JClass iterableType = this.pluginContext.iterableClass.narrow(elementType.wildcard());
		final JMethod addIterableMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + propertyName);
		final JVar addIterableParam = addIterableMethod.param(JMod.FINAL, iterableType, fieldName);
		generateAddMethodJavadoc(addIterableMethod, addIterableParam, propertyOutline.getSchemaAnnotationText().orElse(null));
		final JMethod withIterableMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
		final JVar withIterableParam = withIterableMethod.param(JMod.FINAL, iterableType, fieldName);
		generateWithMethodJavadoc(withIterableMethod, withIterableParam, propertyOutline.getSchemaAnnotationText().orElse(null));
		final JMethod addVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + propertyName);
		final JVar addVarargsParam = addVarargsMethod.varParam(elementType, fieldName);
		generateAddMethodJavadoc(addVarargsMethod, addVarargsParam, propertyOutline.getSchemaAnnotationText().orElse(null));
		final JMethod withVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
		final JVar withVarargsParam = withVarargsMethod.varParam(elementType, fieldName);
		generateWithMethodJavadoc(withVarargsMethod, withVarargsParam, propertyOutline.getSchemaAnnotationText().orElse(null));
		final BuilderOutline childBuilderOutline = getBuilderDeclaration(elementType);
		final JMethod addMethod;
		if (childBuilderOutline != null && !childBuilderOutline.getClassOutline().getImplClass().isAbstract()) {
			final JClass builderWithMethodReturnType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
			addMethod = this.builderClass.raw.method(JMod.PUBLIC, builderWithMethodReturnType, PluginContext.ADD_METHOD_PREFIX + propertyName);
			generateBuilderMethodJavadoc(addMethod, "add", propertyName, propertyOutline.getSchemaAnnotationText().orElse(null));
		} else {
			addMethod = null;
		}
		if (this.implement) {
			final JClass childBuilderType = childBuilderOutline == null ? this.pluginContext.buildableInterface : childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
			final JClass builderArrayListClass = this.pluginContext.arrayListClass.narrow(childBuilderType);
			final JClass builderListClass = this.pluginContext.listClass.narrow(childBuilderType);
			final JFieldVar builderField = this.builderClass.raw.field(JMod.PRIVATE, builderListClass, fieldName);
			addVarargsMethod.body().invoke(addIterableMethod).arg(this.pluginContext.asList(addVarargsParam));
			addVarargsMethod.body()._return(JExpr._this());
			withVarargsMethod.body().invoke(withIterableMethod).arg(this.pluginContext.asList(withVarargsParam));
			withVarargsMethod.body()._return(JExpr._this());
			final JConditional addIterableIfParamNull = addIterableMethod.body()._if(addIterableParam.ne(JExpr._null()));
			final JConditional addIterableIfNull = addIterableIfParamNull._then()._if(JExpr._this().ref(builderField).eq(JExpr._null()));
			addIterableIfNull._then().assign(JExpr._this().ref(builderField), JExpr._new(builderArrayListClass));
			final JForEach jForEach = addIterableIfParamNull._then().forEach(elementType, BuilderGenerator.ITEM_VAR_NAME, addIterableParam);
			final JExpression builderCreationExpression = childBuilderOutline == null
					? JExpr._new(this.pluginContext.buildableClass).arg(jForEach.var())
					: JExpr._new(childBuilderType).arg(JExpr._this()).arg(jForEach.var()).arg(this.settings.isCopyAlways() ? JExpr.TRUE : JExpr.FALSE);
			jForEach.body().add(JExpr._this().ref(builderField).invoke("add").arg(builderCreationExpression));
			addIterableMethod.body()._return(JExpr._this());
			final JConditional withIterableIfNull = withIterableMethod.body()._if(JExpr._this().ref(builderField).ne(JExpr._null()));
			withIterableIfNull._then().add(JExpr._this().ref(builderField).invoke("clear"));
			withIterableMethod.body()._return(JExpr.invoke(addIterableMethod).arg(withIterableParam));
			final JConditional ifNull = initBody._if(JExpr._this().ref(builderField).ne(JExpr._null()));
			final JVar collectionVar = ifNull._then().decl(JMod.FINAL, this.pluginContext.listClass.narrow(elementType), fieldName, JExpr._new(this.pluginContext.arrayListClass.narrow(elementType)).arg(JExpr._this().ref(builderField).invoke("size")));
			final JForEach initForEach = ifNull._then().forEach(childBuilderType, BuilderGenerator.ITEM_VAR_NAME, JExpr._this().ref(builderField));
			final JInvocation buildMethodInvocation = initForEach.var().invoke(this.settings.getBuildMethodName());
			final JExpression buildExpression = childBuilderOutline == null ? JExpr.cast(elementType, buildMethodInvocation) : buildMethodInvocation;
			initForEach.body().add(collectionVar.invoke("add").arg(buildExpression));
			ifNull._then().assign(productParam.ref(fieldName), collectionVar);
			if (addMethod != null) {
				final JConditional addIfNull = addMethod.body()._if(JExpr._this().ref(builderField).eq(JExpr._null()));
				addIfNull._then().assign(JExpr._this().ref(builderField), JExpr._new(builderArrayListClass));
				final JVar childBuilderVar = addMethod.body().decl(JMod.FINAL, childBuilderType, fieldName + this.settings.getBuilderFieldSuffix(), JExpr._new(childBuilderType).arg(JExpr._this()).arg(JExpr._null()).arg(JExpr.FALSE));
				addMethod.body().add(JExpr._this().ref(builderField).invoke("add").arg(childBuilderVar));
				addMethod.body()._return(childBuilderVar);
			}
			this.pluginContext.generateImmutableFieldInit(initBody, productParam, propertyOutline);
		}
	}

	private void generateSingularProperty(final JBlock initBody, final JVar productParam, final PropertyOutline propertyOutline) {
		final String propertyName = propertyOutline.getBaseName();
		final String fieldName = propertyOutline.getFieldName();
		final JType fieldType = propertyOutline.getRawType();
		final BuilderOutline childBuilderOutline = getBuilderDeclaration(fieldType);
		if (childBuilderOutline == null) {
			final JMethod withMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
			final JVar param = withMethod.param(JMod.FINAL, fieldType, fieldName);
			generateWithMethodJavadoc(withMethod, param, propertyOutline.getSchemaAnnotationText().orElse(null));
			final JFieldVar builderField;
			if (this.implement) {
				builderField = this.builderClass.raw.field(JMod.PRIVATE, fieldType, fieldName);
				withMethod.body().assign(JExpr._this().ref(builderField), param);
				withMethod.body()._return(JExpr._this());
				initBody.assign(productParam.ref(fieldName), JExpr._this().ref(builderField));
			}
		} else {
			final JClass elementType = (JClass)fieldType;
			final JClass builderFieldElementType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
			final JClass builderWithMethodReturnType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
			final JMethod withValueMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
			final JVar param = withValueMethod.param(JMod.FINAL, elementType, fieldName);
			generateWithMethodJavadoc(withValueMethod, param, propertyOutline.getSchemaAnnotationText().orElse(null));
			final JMethod withBuilderMethod = childBuilderOutline.getClassOutline().getImplClass().isAbstract() ? null : this.builderClass.raw.method(JMod.PUBLIC, builderWithMethodReturnType, PluginContext.WITH_METHOD_PREFIX + propertyName);
			if (withBuilderMethod != null) {
				generateBuilderMethodJavadoc(withBuilderMethod, "with", fieldName, propertyOutline.getSchemaAnnotationText().orElse(null));
			}
			if (this.implement) {
				final JFieldVar builderField = this.builderClass.raw.field(JMod.PRIVATE, builderFieldElementType, fieldName);
				withValueMethod.body().assign(JExpr._this().ref(builderField), nullSafe(param, JExpr._new(builderFieldElementType).arg(JExpr._this()).arg(param).arg(this.settings.isCopyAlways() ? JExpr.TRUE : JExpr.FALSE)));
				withValueMethod.body()._return(JExpr._this());
				if (withBuilderMethod != null) {
					withBuilderMethod.body()._if(JExpr._this().ref(builderField).ne(JExpr._null()))._then()._return(JExpr._this().ref(builderField));
					withBuilderMethod.body()._return(JExpr._this().ref(builderField).assign(JExpr._new(builderFieldElementType).arg(JExpr._this()).arg(JExpr._null()).arg(JExpr.FALSE)));
				}
				initBody.assign(productParam.ref(fieldName), nullSafe(JExpr._this().ref(builderField), JExpr._this().ref(builderField).invoke(this.settings.getBuildMethodName())));
			}
		}
	}

	private Optional<JFieldVar> generateSingularChoiceSuperTypeProperty(
			final JBlock initBody,
			final JVar productParam,
			final PropertyOutline propertyOutline) {

		final String propertyName = propertyOutline.getBaseName();
		final String fieldName = propertyOutline.getFieldName();
		final JType fieldType = propertyOutline.getRawType();
		final BuilderOutline childBuilderOutline = getBuilderDeclaration(fieldType);
		JFieldVar builderField = null;
		if (childBuilderOutline == null) {
		    // No child builder so the items extend some class that is outside of the xjc generated code, e.g.
			// an added interface or a class in the JDK, such as Object.
			final JMethod withMethod = this.builderClass.raw.method(
					JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
			final JVar param = withMethod.param(JMod.FINAL, fieldType, fieldName);
			generateWithMethodJavadoc(withMethod, param, propertyOutline.getSchemaAnnotationText().orElse(null));
			if (this.implement) {

				// TODO what do we do about a choice between multiple things of the same type, e.g. a choice
				// of xs:string or a choice where all options are the same complex type

				// We have a singular prop that represents a choice so it needs to be of type Buildable
				builderField = this.builderClass.raw.field(JMod.PRIVATE, this.pluginContext.buildableInterface, fieldName);
				// Produce a withXXX method for the supertype of the choices (or Object if it doesn't have one)
				// The method will create a primitive builder, seeded with the passed choice item.
				withMethod.body().assign(
						JExpr._this().ref(builderField),
						nullSafe(param, JExpr._new(this.pluginContext.buildableClass).arg(param)));
				withMethod.body()._return(JExpr._this());

				// In init(), call the build() method of the builder of the chosen choice
				initBody.assign(
						productParam.ref(fieldName),
						nullSafe(
								JExpr._this().ref(builderField),
								this.pluginContext.castOnDemand(
										fieldType,
										JExpr._this().ref(builderField).invoke(this.settings.getBuildMethodName()))));
			}
		} else {
			// The singular choice items extend a class that was also generated by xjc so it has a builder too. This is
			// the case if the inheritance is modelled in the schema with the choice items extending a super-type
			// and the bindings file forcing the baseType of the property to the super-type.
			final JClass elementType = (JClass)fieldType;
			final JClass builderFieldElementType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
			final JClass builderWithMethodReturnType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
			final JMethod withValueMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + propertyName);
			final JVar param = withValueMethod.param(JMod.FINAL, elementType, fieldName);
			generateWithMethodJavadoc(withValueMethod, param, propertyOutline.getSchemaAnnotationText().orElse(null));
			final JMethod withBuilderMethod = childBuilderOutline.getClassOutline().getImplClass().isAbstract() ? null : this.builderClass.raw.method(JMod.PUBLIC, builderWithMethodReturnType, PluginContext.WITH_METHOD_PREFIX + propertyName);
			if (withBuilderMethod != null) {
				generateBuilderMethodJavadoc(withBuilderMethod, "with", fieldName, propertyOutline.getSchemaAnnotationText().orElse(null));
			}
			if (this.implement) {
				builderField = this.builderClass.raw.field(JMod.PRIVATE, builderFieldElementType, fieldName);
				withValueMethod.body().assign(
						JExpr._this().ref(builderField),
						nullSafe(
								param,
								JExpr._new(builderFieldElementType)
										.arg(JExpr._this())
										.arg(param)
										.arg(this.settings.isCopyAlways() ? JExpr.TRUE : JExpr.FALSE)));
				withValueMethod.body()._return(JExpr._this());

				if (withBuilderMethod != null) {
					withBuilderMethod.body()
							._if(JExpr._this()
									.ref(builderField)
									.ne(JExpr._null()))
							._then()
							._return(JExpr._this().ref(builderField));
					withBuilderMethod.body()
							._return(JExpr._this()
									.ref(builderField)
									.assign(JExpr._new(builderFieldElementType)
											.arg(JExpr._this())
											.arg(JExpr._null())
											.arg(JExpr.FALSE)));
				}
				initBody.assign(
						productParam.ref(fieldName),
						nullSafe(
						        JExpr._this().ref(builderField),
                                JExpr._this().ref(builderField).invoke(this.settings.getBuildMethodName())));
			}
		}
		return Optional.of(builderField);
	}

	void generateBuilderMemberOverride(final PropertyOutline superPropertyOutline, final PropertyOutline propertyOutline, final String superPropertyName) throws SAXException {
		final JType fieldType = propertyOutline.getRawType();
		final String fieldName = propertyOutline.getFieldName();
		if (superPropertyOutline.isCollection()) {
			if (!fieldType.isArray()) {
				if (superPropertyOutline.getChoiceProperties().size() > 1) {
					for (final PropertyOutline.TagRef tagRef : propertyOutline.getChoiceProperties()) {
						final QName elementName = tagRef.getTagName();
						final JType elementType;
						try {
							elementType = getTagRefType(tagRef, this.pluginContext.outline, Aspect.EXPOSED);
							overrideAddMethods(superPropertyOutline, elementName, elementType);
						} catch (Exception e) {
							pluginContext.errorHandler.warning(
							        new SAXParseException("Encountered unsupported child type \""+tagRef.getTypeInfo()+"\" in choice children collection. Unable to generate choice expansion.",
											this.pluginContext.outline.getModel().getLocator(),
											e));
						}
					}
				}
				final JClass elementType = ((JClass)fieldType).getTypeParameters().get(0);
				final JClass iterableType = this.pluginContext.iterableClass.narrow(elementType.wildcard());
				final JClass collectionType = this.pluginContext.collectionClass.narrow(elementType.wildcard());
				final JMethod addIterableMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + superPropertyName);
				final JVar addIterableParam = addIterableMethod.param(JMod.FINAL, iterableType, fieldName);
				generateAddMethodJavadoc(addIterableMethod, addIterableParam, propertyOutline.getSchemaAnnotationText().orElse(null));
				final JMethod addVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.ADD_METHOD_PREFIX + superPropertyName);
				final JVar addVarargsParam = addVarargsMethod.varParam(elementType, fieldName);
				generateAddMethodJavadoc(addVarargsMethod, addVarargsParam, propertyOutline.getSchemaAnnotationText().orElse(null));
				final JMethod withIterableMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + superPropertyName);
				final JVar withIterableParam = withIterableMethod.param(JMod.FINAL, iterableType, fieldName);
				generateWithMethodJavadoc(withIterableMethod, withIterableParam, propertyOutline.getSchemaAnnotationText().orElse(null));
				final JMethod withVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + superPropertyName);
				final JVar withVarargsParam = withVarargsMethod.varParam(elementType, fieldName);
				generateWithMethodJavadoc(withVarargsMethod, withVarargsParam, propertyOutline.getSchemaAnnotationText().orElse(null));
				if (this.implement) {
					addIterableMethod.annotate(Override.class);
					addIterableMethod.body().invoke(JExpr._super(), PluginContext.ADD_METHOD_PREFIX + superPropertyName).arg(addIterableParam);
					addIterableMethod.body()._return(JExpr._this());
					addVarargsMethod.annotate(Override.class);
					addVarargsMethod.body().invoke(JExpr._super(), PluginContext.ADD_METHOD_PREFIX + superPropertyName).arg(addVarargsParam);
					addVarargsMethod.body()._return(JExpr._this());
					withIterableMethod.annotate(Override.class);
					withIterableMethod.body().invoke(JExpr._super(), PluginContext.WITH_METHOD_PREFIX + superPropertyName).arg(withIterableParam);
					withIterableMethod.body()._return(JExpr._this());
					withVarargsMethod.annotate(Override.class);
					withVarargsMethod.body().invoke(JExpr._super(), PluginContext.WITH_METHOD_PREFIX + superPropertyName).arg(withVarargsParam);
					withVarargsMethod.body()._return(JExpr._this());
				}
				final BuilderOutline childBuilderOutline = getBuilderDeclaration(elementType);
				if (childBuilderOutline != null && !childBuilderOutline.getClassOutline().getImplClass().isAbstract()) {
					final JClass builderFieldElementType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
					final JMethod addMethod = this.builderClass.raw.method(JMod.PUBLIC, builderFieldElementType, PluginContext.ADD_METHOD_PREFIX + superPropertyName);
					generateBuilderMethodJavadoc(addMethod, "add", superPropertyOutline.getFieldName(), propertyOutline.getSchemaAnnotationText().orElse(null));
					if (this.implement) {
						addMethod.annotate(Override.class);
						addMethod.body()._return(JExpr.cast(builderFieldElementType, JExpr._super().invoke(addMethod)));
					}
				}
			} else {
				final JType elementType = fieldType.elementType();
				final JClass iterableType = this.pluginContext.iterableClass.narrow(elementType);
				final JMethod withVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + superPropertyName);
				final JVar withVarargsParam = withVarargsMethod.varParam(((JClass)fieldType).getTypeParameters().get(0), fieldName);
				generateWithMethodJavadoc(withVarargsMethod, withVarargsParam, propertyOutline.getSchemaAnnotationText().orElse(null));
				if (this.implement) {
					withVarargsMethod.annotate(Override.class);
					withVarargsMethod.body().invoke(JExpr._super(), PluginContext.WITH_METHOD_PREFIX + superPropertyName).arg(withVarargsParam);
					withVarargsMethod.body()._return(JExpr._this());
				}
			}
		} else {
			final JMethod withMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.WITH_METHOD_PREFIX + superPropertyName);
			final JVar param = withMethod.param(JMod.FINAL, superPropertyOutline.getRawType(), fieldName);
			generateWithMethodJavadoc(withMethod, param, propertyOutline.getSchemaAnnotationText().orElse(null));
			if (this.implement) {
				withMethod.annotate(Override.class);
				withMethod.body().invoke(JExpr._super(), PluginContext.WITH_METHOD_PREFIX + superPropertyName).arg(param);
				withMethod.body()._return(JExpr._this());
			}
			final BuilderOutline childBuilderOutline = getBuilderDeclaration(fieldType);
			if (childBuilderOutline != null && !childBuilderOutline.getClassOutline().getImplClass().isAbstract()) {
				final JClass builderFieldElementType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type.wildcard());
				final JMethod addMethod = this.builderClass.raw.method(JMod.PUBLIC, builderFieldElementType, PluginContext.WITH_METHOD_PREFIX + superPropertyName);
				generateBuilderMethodJavadoc(addMethod, "with", superPropertyOutline.getFieldName(), propertyOutline.getSchemaAnnotationText().orElse(null));
				if (this.implement) {
					addMethod.body()._return(JExpr.cast(builderFieldElementType, JExpr._super().invoke(addMethod)));
				}
			}
		}
	}

	JDefinedClass generateExtendsClause(final BuilderOutline superClassBuilder) {
		return this.builderClass.raw._extends(superClassBuilder.getBuilderClass().narrow(this.builderClass.typeParam));
	}

	void generateImplementsClause() throws SAXException {
		if (this.typeOutline.isLocal()) {
			final GroupInterfacePlugin groupInterfacePlugin = this.pluginContext.findPlugin(GroupInterfacePlugin.class);
			if (groupInterfacePlugin != null) {
				for (final TypeOutline interfaceOutline : groupInterfacePlugin.getGroupInterfacesForClass(this.pluginContext, this.typeOutline.getImplClass().fullName())) {
					final JClass parentClass = interfaceOutline.getImplClass();
					this.builderClass.raw._implements(getBuilderInterface(parentClass).narrow(this.builderClass.typeParam));
				}
			}
			this.builderClass.raw._implements(Buildable.class);
		}
	}

	private JClass getBuilderInterface(final JClass parentClass) {
		return this.pluginContext.ref(parentClass, PluginContext.BUILDER_INTERFACE_NAME, true, false, this.pluginContext.codeModel.ref(Object.class));
	}

	JMethod generateBuildMethod(final JMethod initMethod) {
		final JMethod buildMethod = this.builderClass.raw.method(JMod.PUBLIC, this.definedClass, this.settings.getBuildMethodName());
		if (!(this.builderClass.type._extends() == null || this.builderClass.type._extends().name().equals("java.lang.Object"))) {
			buildMethod.annotate(Override.class);
		}
		if (this.implement) {
			final JExpression buildExpression = JExpr._this().invoke(initMethod).arg(JExpr._new(this.definedClass));
			if (this.settings.isCopyAlways()) {
				buildMethod.body()._return(buildExpression);
			} else if (this.definedClass.isAbstract()) {
				buildMethod.body()._return(JExpr.cast(this.definedClass, this.storedValueField));
			} else {
				final JConditional jConditional = buildMethod.body()._if(this.storedValueField.eq(JExpr._null()));
				jConditional._then()._return(buildExpression);
				jConditional._else()._return(JExpr.cast(this.definedClass, this.storedValueField));
			}
		}
		return buildMethod;
	}

	JMethod generateNewBuilderMethod() {
		final JMethod builderMethod = this.definedClass.method(JMod.PUBLIC | JMod.STATIC, this.builderClass.raw.narrow(Void.class), this.settings.getNewBuilderMethodName());
		builderMethod.body()._return(JExpr._new(this.builderClass.raw.narrow(Void.class)).arg(JExpr._null()).arg(JExpr._null()).arg(JExpr.FALSE));
		return builderMethod;
	}

	JMethod generateCopyOfMethod(final TypeOutline paramType, final boolean partial) {
		if (paramType.getSuperClass() != null) {
			generateCopyOfMethod(paramType.getSuperClass(), partial);
		}
		final JMethod copyOfMethod = this.definedClass.method(JMod.PUBLIC | JMod.STATIC, this.builderClass.raw.narrow(Void.class), this.pluginContext.buildCopyMethodName);
		final JTypeVar copyOfMethodTypeParam = copyOfMethod.generify(BuilderGenerator.PARENT_BUILDER_TYPE_PARAMETER_NAME);
		copyOfMethod.type(this.builderClass.raw.narrow(copyOfMethodTypeParam));
		final JVar otherParam = copyOfMethod.param(JMod.FINAL, paramType.getImplClass(), BuilderGenerator.OTHER_PARAM_NAME);
		final CopyGenerator copyGenerator = this.pluginContext.createCopyGenerator(copyOfMethod, partial);
		final JVar newBuilderVar = copyOfMethod.body().decl(JMod.FINAL, copyOfMethod.type(), BuilderGenerator.NEW_BUILDER_VAR_NAME, JExpr._new(copyOfMethod.type()).arg(JExpr._null()).arg(JExpr._null()).arg(JExpr.FALSE));
		copyOfMethod.body().add(copyGenerator.generatePartialArgs(this.pluginContext.invoke(otherParam, this.settings.getCopyToMethodName()).arg(newBuilderVar)));
		copyOfMethod.body()._return(newBuilderVar);
		return copyOfMethod;
	}

	JMethod generateNewCopyBuilderMethod(final boolean partial) {
		final JDefinedClass typeDefinition = this.typeOutline.isInterface() && ((DefinedInterfaceOutline)this.typeOutline).getSupportInterface() != null ? ((DefinedInterfaceOutline)this.typeOutline).getSupportInterface() : this.definedClass;
		final int mods = this.implement ? this.definedClass.isAbstract() ? JMod.PUBLIC | JMod.ABSTRACT : JMod.PUBLIC : JMod.NONE;
		final JMethod copyBuilderMethod = typeDefinition.method(mods, this.builderClass.raw, this.settings.getNewCopyBuilderMethodName());
		final JTypeVar copyBuilderMethodTypeParam = copyBuilderMethod.generify(BuilderGenerator.PARENT_BUILDER_TYPE_PARAMETER_NAME);
		final JVar parentBuilderParam = copyBuilderMethod.param(JMod.FINAL, copyBuilderMethodTypeParam, BuilderGenerator.PARENT_BUILDER_PARAM_NAME);
		final CopyGenerator copyGenerator = this.pluginContext.createCopyGenerator(copyBuilderMethod, partial);
		copyBuilderMethod.type(this.builderClass.raw.narrow(copyBuilderMethodTypeParam));
		final JMethod copyBuilderConvenienceMethod = typeDefinition.method(mods, this.builderClass.raw.narrow(this.pluginContext.voidClass), this.settings.getNewCopyBuilderMethodName());
		final CopyGenerator copyConvenienceGenerator = this.pluginContext.createCopyGenerator(copyBuilderConvenienceMethod, partial);
		if (this.implement && !this.definedClass.isAbstract()) {
			copyBuilderMethod.body()._return(copyGenerator.generatePartialArgs(this.pluginContext._new((JClass)copyBuilderMethod.type()).arg(parentBuilderParam).arg(JExpr._this()).arg(JExpr.TRUE)));
			copyBuilderConvenienceMethod.body()._return(copyConvenienceGenerator.generatePartialArgs(this.pluginContext.invoke(this.settings.getNewCopyBuilderMethodName()).arg(JExpr._null())));
		}
		if (this.typeOutline.getSuperClass() != null) {
			copyBuilderMethod.annotate(Override.class);
			copyBuilderConvenienceMethod.annotate(Override.class);
		}
		return copyBuilderMethod;
	}

	private JMethod generateConveniencePartialCopyMethod(final TypeOutline paramType, final JMethod partialCopyOfMethod, final String methodName, final JExpression propertyTreeUseArg) {
		if (paramType.getSuperClass() != null) {
			generateConveniencePartialCopyMethod(paramType.getSuperClass(), partialCopyOfMethod, methodName, propertyTreeUseArg);
		}
		final JMethod conveniencePartialCopyMethod = this.definedClass.method(JMod.PUBLIC | JMod.STATIC, this.builderClass.raw.narrow(Void.class), methodName);
		final JVar partialOtherParam = conveniencePartialCopyMethod.param(JMod.FINAL, paramType.getImplClass(), BuilderGenerator.OTHER_PARAM_NAME);
		final JVar propertyPathParam = conveniencePartialCopyMethod.param(JMod.FINAL, PropertyTree.class, PartialCopyGenerator.PROPERTY_TREE_PARAM_NAME);
		conveniencePartialCopyMethod.body()._return(JExpr.invoke(partialCopyOfMethod).arg(partialOtherParam).arg(propertyPathParam).arg(propertyTreeUseArg));
		return conveniencePartialCopyMethod;
	}

	final void generateCopyOfBuilderMethods() {
		if (this.implement) {
			final JMethod builderCopyOfValueMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.BUILD_COPY_METHOD_NAME);
			JVar paramOtherValue = builderCopyOfValueMethod.param(JMod.FINAL, this.definedClass, BuilderGenerator.OTHER_PARAM_NAME);
			builderCopyOfValueMethod.body()
				.add(JExpr.invoke(paramOtherValue, PluginContext.COPY_TO_METHOD_NAME).arg(JExpr._this()))
				._return(JExpr._this());

			final JMethod builderCopyOfBuilderMethod = this.builderClass.raw.method(JMod.PUBLIC, this.builderClass.type, PluginContext.BUILD_COPY_METHOD_NAME);
			JVar paramOtherBuilder = builderCopyOfBuilderMethod.param(JMod.FINAL, this.builderClass.raw, BuilderGenerator.OTHER_PARAM_NAME);
			builderCopyOfBuilderMethod.body()._return(JExpr.invoke(builderCopyOfValueMethod).arg(JExpr.invoke(paramOtherBuilder, PluginContext.BUILD_METHOD_NAME)));
		}
	}

	final void generateCopyToMethod(final boolean partial) {
		if (this.implement) {
			final JDefinedClass typeDefinition = this.typeOutline.isInterface() && ((DefinedInterfaceOutline)this.typeOutline).getSupportInterface() != null ? ((DefinedInterfaceOutline)this.typeOutline).getSupportInterface() : this.definedClass;
			final JMethod copyToMethod = typeDefinition.method(JMod.PUBLIC, this.pluginContext.voidType, this.settings.getCopyToMethodName());
			final JTypeVar typeVar = copyToMethod.generify(BuilderGenerator.PARENT_BUILDER_TYPE_PARAMETER_NAME);
			final JVar otherParam = copyToMethod.param(JMod.FINAL, this.builderClass.raw.narrow(typeVar), BuilderGenerator.OTHER_PARAM_NAME);
			final CopyGenerator cloneGenerator = this.pluginContext.createCopyGenerator(copyToMethod, partial);
			final JBlock body = copyToMethod.body();
			final JVar otherRef;
			if (this.typeOutline.getSuperClass() != null) {
				body.add(cloneGenerator.generatePartialArgs(this.pluginContext.invoke(JExpr._super(), copyToMethod.name()).arg(otherParam)));
			}
			otherRef = otherParam;
			generateFieldCopyExpressions(cloneGenerator, body, otherRef, JExpr._this());
			copyToMethod.javadoc().append(JavadocUtils.hardWrapTextForJavadoc(getMessage("javadoc.method.copyTo")));
			copyToMethod.javadoc().addParam(otherParam).append(JavadocUtils.hardWrapTextForJavadoc(getMessage("javadoc.method.copyTo.param.other")));
		}
	}

	final void generateCopyConstructor(final boolean partial) {
		final JMethod constructor = this.builderClass.raw.constructor(this.builderClass.raw.isAbstract() ? JMod.PROTECTED : JMod.PUBLIC);
		final JVar parentBuilderParam = constructor.param(JMod.FINAL, this.builderClass.typeParam, BuilderGenerator.PARENT_BUILDER_PARAM_NAME);
		final JVar otherParam = constructor.param(JMod.FINAL, this.typeOutline.getImplClass(), BuilderGenerator.OTHER_PARAM_NAME);
		final JVar copyParam = constructor.param(JMod.FINAL, this.pluginContext.codeModel.BOOLEAN, BuilderGenerator.COPY_FLAG_PARAM_NAME);
		final CopyGenerator cloneGenerator = this.pluginContext.createCopyGenerator(constructor, partial);
		if (this.typeOutline.getSuperClass() != null) {
			constructor.body().add(cloneGenerator.generatePartialArgs(this.pluginContext._super().arg(parentBuilderParam).arg(otherParam).arg(copyParam)));
		} else {
			constructor.body().assign(JExpr._this().ref(this.parentBuilderField), parentBuilderParam);
		}
		final JConditional ifNullStmt = constructor.body()._if(otherParam.ne(JExpr._null()));
		final JBlock body;
		if (!this.settings.isCopyAlways() && this.typeOutline.getSuperClass() == null) {
			final JConditional ifCopyStmt = ifNullStmt._then()._if(copyParam);
			ifCopyStmt._else().assign(this.storedValueField, otherParam);
			ifNullStmt._else().assign(this.storedValueField, JExpr._null());
			body = ifCopyStmt._then();
			body.assign(this.storedValueField, JExpr._null());
		} else {
			body = ifNullStmt._then();
		}
		generateFieldCopyExpressions(cloneGenerator, body, JExpr._this(), otherParam);
	}

	private void generateFieldCopyExpressions(final CopyGenerator cloneGenerator, final JBlock body, final JExpression targetObject, final JExpression sourceObject) {
		for (final DefinedPropertyOutline fieldOutline : this.typeOutline.getDeclaredFields()) {
			final JFieldVar field = fieldOutline.getFieldVar();
			if (field != null) {
				if ((field.mods().getValue() & (JMod.FINAL | JMod.STATIC)) == 0) {
					final JFieldRef targetField = targetObject.ref(field.name());
					final JFieldRef sourceRef = sourceObject.ref(field.name());
					final PropertyTreeVarGenerator treeVarGenerator = cloneGenerator.createPropertyTreeVarGenerator(body, fieldOutline.getFieldName());
					final JType fieldType = fieldOutline.getRawType();
					final JBlock currentBlock = treeVarGenerator.generateEnclosingBlock(body);
					if (fieldType.isReference()) {
						final JClass fieldClass = (JClass)fieldType;
						if (this.pluginContext.collectionClass.isAssignableFrom(fieldClass)) {
							final JClass elementType = fieldClass.getTypeParameters().get(0);
							final BuilderOutline childBuilderOutline = getBuilderDeclaration(elementType);
							if (this.settings.isGeneratingNarrowCopy() && this.pluginContext.canInstantiate(elementType)) {
								final JClass childBuilderType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
								final JForEach forLoop = loop(currentBlock, sourceRef, elementType, targetField, childBuilderType);
								forLoop.body().invoke(targetField, "add").arg(nullSafe(forLoop.var(), treeVarGenerator.generatePartialArgs(this.pluginContext.invoke(elementType, this.pluginContext.buildCopyMethodName).narrow(this.builderClass.type).arg(forLoop.var()))));
							} else if (childBuilderOutline != null) {
								final JClass childBuilderType = childBuilderOutline.getBuilderClass().narrow(this.builderClass.type);
								final JForEach forLoop = loop(currentBlock, sourceRef, elementType, targetField, childBuilderType);
								forLoop.body().invoke(targetField, "add").arg(nullSafe(forLoop.var(), treeVarGenerator.generatePartialArgs(this.pluginContext.invoke(forLoop.var(), this.settings.getNewCopyBuilderMethodName()).arg(targetObject))));
							} else if (this.pluginContext.partialCopyableInterface.isAssignableFrom(elementType)) {
								final JForEach forLoop = loop(currentBlock, sourceRef, elementType, targetField, elementType);
								forLoop.body().invoke(targetField, "add").arg(nullSafe(forLoop.var(), JExpr._new(this.pluginContext.buildableClass).arg(treeVarGenerator.generatePartialArgs(forLoop.var().invoke(this.pluginContext.copyMethodName)))));
							} else if (this.pluginContext.copyableInterface.isAssignableFrom(elementType)) {
								final JForEach forLoop = loop(currentBlock, sourceRef, elementType, targetField, elementType);
								forLoop.body().invoke(targetField, "add").arg(nullSafe(forLoop.var(), JExpr._new(this.pluginContext.buildableClass).arg(forLoop.var().invoke(this.pluginContext.copyMethodName))));
							} else if (this.pluginContext.cloneableInterface.isAssignableFrom(elementType)) {
								final JBlock maybeTryBlock = this.pluginContext.catchCloneNotSupported(currentBlock, elementType);
								final JForEach forLoop = loop(maybeTryBlock, sourceRef, elementType, targetField, this.pluginContext.buildableInterface);
								forLoop.body().invoke(targetField, "add").arg(nullSafe(forLoop.var(), JExpr._new(this.pluginContext.buildableClass).arg(forLoop.var().invoke(this.pluginContext.cloneMethodName))));
							} else {
								final JForEach forLoop = loop(currentBlock, sourceRef, elementType, targetField, this.pluginContext.buildableInterface);
								forLoop.body().invoke(targetField, "add").arg(nullSafe(forLoop.var(), JExpr._new(this.pluginContext.buildableClass).arg(forLoop.var())));
							}
						} else {
							final BuilderOutline childBuilderOutline = getBuilderDeclaration(fieldType);
							if (this.settings.isGeneratingNarrowCopy() && this.pluginContext.canInstantiate(fieldType)) {
								currentBlock.assign(targetField, nullSafe(sourceRef, treeVarGenerator.generatePartialArgs(this.pluginContext.invoke(fieldType, this.pluginContext.buildCopyMethodName).narrow(this.builderClass.type).arg(sourceRef))));
							} else if (childBuilderOutline != null) {
								currentBlock.assign(targetField, nullSafe(sourceRef, treeVarGenerator.generatePartialArgs(this.pluginContext.invoke(sourceRef, this.settings.getNewCopyBuilderMethodName()).arg(targetObject))));
							} else if (this.pluginContext.partialCopyableInterface.isAssignableFrom(fieldClass)) {
								currentBlock.assign(targetField, nullSafe(sourceRef, this.pluginContext.castOnDemand(fieldType, treeVarGenerator.generatePartialArgs(sourceRef.invoke(this.pluginContext.copyMethodName)))));
							} else if (this.pluginContext.copyableInterface.isAssignableFrom(fieldClass)) {
								currentBlock.assign(targetField, nullSafe(sourceRef, this.pluginContext.castOnDemand(fieldType, sourceRef.invoke(this.pluginContext.copyMethodName))));
							} else if (this.pluginContext.cloneableInterface.isAssignableFrom(fieldClass)) {
								final JBlock maybeTryBlock = this.pluginContext.catchCloneNotSupported(currentBlock, fieldClass);
								maybeTryBlock.assign(targetField, nullSafe(sourceRef, this.pluginContext.castOnDemand(fieldType, sourceRef.invoke(this.pluginContext.cloneMethodName))));
							} else if (this.pluginContext.buildableInterface.isAssignableFrom(fieldClass)) {
								currentBlock.assign(
										targetField,
										nullSafe(sourceRef, JExpr._new(this.pluginContext.buildableClass).arg(sourceRef)));
							} else if (fieldOutline.getChoiceProperties().size() > 1) {
							    // TODO don't think this will work if choice items have no builder
								// Handle single choice properties
								currentBlock.assign(
										targetField,
										nullSafe(sourceRef, JExpr._new(this.pluginContext.buildableClass).arg(sourceRef)));
							} else {
								currentBlock.assign(targetField, sourceRef);
							}
						}
					} else {
						currentBlock.assign(targetField, sourceRef);
					}
				}
			}
		}
	}

	public void buildProperties() throws SAXException {
		final TypeOutline superClass = this.typeOutline.getSuperClass();
		final JMethod initMethod;
		final JVar productParam;
		final JBlock initBody;
		if (this.implement) {
			initMethod = this.builderClass.raw.method(JMod.PROTECTED, this.definedClass, PluginContext.INIT_METHOD_NAME);
			final JTypeVar typeVar = initMethod.generify(BuilderGenerator.PRODUCT_TYPE_PARAMETER_NAME, this.definedClass);
			initMethod.type(typeVar);
			productParam = initMethod.param(JMod.FINAL, typeVar, BuilderGenerator.PRODUCT_VAR_NAME);
			initBody = initMethod.body();
		} else {
			initMethod = null;
			initBody = null;
			productParam = null;
		}
		generateDefinedClassJavadoc();

		if (this.typeOutline.getDeclaredFields() != null) {
			for (final PropertyOutline fieldOutline : this.typeOutline.getDeclaredFields()) {
				if (fieldOutline.hasGetter()) {
					generateBuilderMember(fieldOutline, initBody, productParam);
				}
			}
		}
		if (superClass != null) {
			generateExtendsClause(getBuilderDeclaration(superClass.getImplClass()));
			if (this.implement) initBody._return(JExpr._super().invoke(initMethod).arg(productParam));
			generateBuilderMemberOverrides(superClass);
		} else if (this.implement) {
			initBody._return(productParam);
		}
		generateImplementsClause();
		generateBuildMethod(initMethod);
		generateCopyToMethod(false);
		generateNewCopyBuilderMethod(false);
		if (this.implement && !this.definedClass.isAbstract()) {
			generateNewBuilderMethod();
			generateCopyOfMethod(this.typeOutline, false);
		}
		if (this.settings.isGeneratingPartialCopy()) {
			generateCopyToMethod(true);
			generateNewCopyBuilderMethod(true);
			if (this.implement && !this.definedClass.isAbstract()) {
				final JMethod partialCopyOfMethod = generateCopyOfMethod(this.typeOutline, true);
				generateConveniencePartialCopyMethod(this.typeOutline, partialCopyOfMethod, this.pluginContext.copyExceptMethodName, this.pluginContext.excludeConst);
				generateConveniencePartialCopyMethod(this.typeOutline, partialCopyOfMethod, this.pluginContext.copyOnlyMethodName, this.pluginContext.includeConst);
			}
		}
		generateCopyOfBuilderMethods();
	}

	private void generateDefinedClassJavadoc() {
		if (settings.isGeneratingJavadocFromAnnotations()) {

			// xjc seems to add annotations at the class level for named and anonymous complex types
			// but doesn't wrap them to a sensible width. If we hard wrap the existing javadoc then
            // it messes up all the <pre> blocks. It is not worth the effort/risk to try and wrap everything
			// this is not a pre block.
//		    JavadocUtils.hardWrapCommentText(this.definedClass.javadoc());

			// Add our field level annotations to the getters/setters on the defined class
			this.typeOutline.getImplClass().methods().forEach(jMethod -> {
				final String fieldName = getCorrespondingFieldName(jMethod);
				this.typeOutline.getDeclaredFields().stream()
						.filter(typeOutline ->
                                // need to allow for reserved word renaming
                                pluginContext.areVariableNamesEqual(typeOutline.getFieldName(), fieldName))
						.findAny()
						.flatMap(DefinedPropertyOutline::getSchemaAnnotationText)
						.ifPresent(schemaAnnotation -> {
							if (settings.isGeneratingJavadocFromAnnotations()) {
								JavadocUtils.appendJavadocParagraph(jMethod, schemaAnnotation);
							}
						});
			});
		}
	}

	private String getCorrespondingFieldName(final JMethod jMethod) {
		String methodName = jMethod.name();
		return Introspector.decapitalize(methodName.substring(methodName.startsWith("is") ? 2 : 3));
	}

	private void generateBuilderMemberOverrides(final TypeOutline superClass) throws SAXException {
		if (superClass.getDeclaredFields() != null) {
			for (final PropertyOutline superFieldOutline : superClass.getDeclaredFields()) {
				if (superFieldOutline.hasGetter()) {
					final String superPropertyName = superFieldOutline.getBaseName();
					generateBuilderMemberOverride(superFieldOutline, superFieldOutline, superPropertyName);
				}
			}
		}
		if (superClass.getSuperClass() != null) {
			generateBuilderMemberOverrides(superClass.getSuperClass());
		}
	}

	BuilderOutline getBuilderDeclaration(final JType type) {
		BuilderOutline builderOutline = this.builderOutlines.get(type.fullName());
		if (builderOutline == null) {
			builderOutline = getReferencedBuilderOutline(type);
		}
		return builderOutline;
	}

	void generateArrayProperty(final JBlock initBody, final JVar productParam, final PropertyOutline fieldOutline, final JType elementType, final JType builderType) {
		final String fieldName = fieldOutline.getFieldName();
		final String propertyName = fieldOutline.getBaseName();
		final JType fieldType = fieldOutline.getRawType();
		final JMethod withVarargsMethod = this.builderClass.raw.method(JMod.PUBLIC, builderType, PluginContext.WITH_METHOD_PREFIX + propertyName);
		final JVar withVarargsParam = withVarargsMethod.varParam(elementType, fieldName);
		if (this.implement) {
			final JFieldVar builderField = this.builderClass.raw.field(JMod.PRIVATE, fieldType, fieldName, JExpr._null());
			withVarargsMethod.body().assign(JExpr._this().ref(builderField), withVarargsParam);
			withVarargsMethod.body()._return(JExpr._this());
			initBody.assign(productParam.ref(fieldName), JExpr._this().ref(builderField));
		}
	}

	JForEach loop(final JBlock block, final JExpression source, final JType sourceElementType, final JAssignmentTarget target, final JType targetElementType) {
		final JConditional ifNull = block._if(source.eq(JExpr._null()));
		ifNull._then().assign(target, JExpr._null());
		ifNull._else().assign(target, JExpr._new(this.pluginContext.arrayListClass.narrow(targetElementType)));
		return ifNull._else().forEach(sourceElementType, BuilderGenerator.ITEM_VAR_NAME, source);
	}

	private void generateAddMethodJavadoc(final JMethod method, final JVar param, final String schemaAnnotation) {
		final String propertyName = param.name();
		JavadocUtils.appendJavadocCommentParagraphs(
					method.javadoc(),
					settings.isGeneratingJavadocFromAnnotations() ? schemaAnnotation : null,
					MessageFormat.format(this.resources.getString("comment.addMethod"), propertyName))
				.addParam(param)
				.append(JavadocUtils.hardWrapTextForJavadoc(MessageFormat.format(
						this.resources.getString("comment.addMethod.param"),
						propertyName)));
	}

	private void generateWithMethodJavadoc(final JMethod method, final JVar param, final String schemaAnnotation) {
		final String propertyName = param.name();
		JavadocUtils.appendJavadocCommentParagraphs(
					method.javadoc(),
					settings.isGeneratingJavadocFromAnnotations() ? schemaAnnotation : null,
					MessageFormat.format(this.resources.getString("comment.withMethod"), propertyName))
				.addParam(param)
				.append(JavadocUtils.hardWrapTextForJavadoc(MessageFormat.format(
						this.resources.getString("comment.withMethod.param"),
						propertyName)));
	}

	private void generateBuilderMethodJavadoc(
			final JMethod method,
			final String methodPrefix,
			final String propertyName,
            final String schemaAnnotation) {
		final String endMethodClassName = method.type().erasure().fullName();
		JavadocUtils.appendJavadocCommentParagraphs(
					method.javadoc(),
					settings.isGeneratingJavadocFromAnnotations() ? schemaAnnotation : null,
					MessageFormat.format(
							this.resources.getString("comment." + methodPrefix + "BuilderMethod"),
							propertyName,
							endMethodClassName))
				.addReturn()
				.append(JavadocUtils.hardWrapTextForJavadoc(MessageFormat.format(
						this.resources.getString("comment." + methodPrefix + "BuilderMethod.return"),
						propertyName,
						endMethodClassName)));
	}


	private BuilderOutline getReferencedBuilderOutline(final JType type) {
		BuilderOutline builderOutline = null;
		if (this.pluginContext.getClassOutline(type) == null && this.pluginContext.getEnumOutline(type) == null && type.isReference() && !type.isPrimitive() && !type.isArray() && type.fullName().contains(".")) {
			final Class<?> runtimeParentClass;
			try {
				runtimeParentClass = Class.forName(type.binaryName());
			} catch (final ClassNotFoundException e) {
				return null;
			}
			final JClass builderClass = reflectRuntimeInnerClass(runtimeParentClass, this.settings.getBuilderClassName());
			if (builderClass != null) {
				final ReferencedClassOutline referencedClassOutline = new ReferencedClassOutline(this.pluginContext.codeModel, runtimeParentClass);
				builderOutline = new BuilderOutline(referencedClassOutline, builderClass);
			}
		}
		return builderOutline;
	}

	private JClass reflectRuntimeInnerClass(final Class<?> runtimeParentClass, final ClassName className) {
		final JClass parentClass = this.pluginContext.codeModel.ref(runtimeParentClass);
		final String innerClassName = className.getName(runtimeParentClass.isInterface());
		final Class<?> runtimeInnerClass = PluginContext.findInnerClass(runtimeParentClass, innerClassName);
		if (runtimeInnerClass != null) {
			final JClass innerSuperClass = runtimeParentClass.getSuperclass() != null ? this.pluginContext.codeModel.ref(runtimeInnerClass.getSuperclass()) : null;
			return this.pluginContext.ref(parentClass, innerClassName, runtimeInnerClass.isInterface(), Modifier.isAbstract(runtimeInnerClass.getModifiers()), innerSuperClass);
		} else {
			return null;
		}
	}

	private String getMessage(final String resourceKey, final Object... args) {
		return MessageFormat.format(this.resources.getString(resourceKey), args);
	}

}