/*
 *  Copyright 2001-present Stephen Colebourne
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.joda.beans.gen;

import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.joda.beans.Bean;
import org.joda.beans.BeanBuilder;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaBean;
import org.joda.beans.MetaProperty;
import org.joda.beans.TypedMetaBean;
import org.joda.beans.impl.BasicBeanBuilder;
import org.joda.beans.impl.direct.DirectBean;
import org.joda.beans.impl.direct.DirectBeanBuilder;
import org.joda.beans.impl.direct.DirectFieldsBeanBuilder;
import org.joda.beans.impl.direct.DirectMetaBean;
import org.joda.beans.impl.direct.DirectMetaPropertyMap;
import org.joda.beans.impl.direct.DirectPrivateBeanBuilder;
import org.joda.beans.impl.direct.MinimalMetaBean;
import org.joda.beans.impl.light.LightMetaBean;

/**
 * Code generator for a bean.
 */
class BeanGen {

    /** Constructor style for none. */
    static final int CONSTRUCTOR_NONE = 0;
    /** Constructor style for builder-based. */
    static final int CONSTRUCTOR_BY_BUILDER = 1;
    /** Constructor style for argument-based. */
    static final int CONSTRUCTOR_BY_ARGS = 2;
    /** Class name - this avoids a Class.class reference to the java.desktop module. */
    private static final String CLASS_CONSTRUCTOR_PROPERTIES = "java.beans.ConstructorProperties";
    /** Class name - this avoids a Class.class reference to the java.desktop module. */
    private static final String CLASS_PROPERTY_CHANGE_SUPPORT = "java.beans.PropertyChangeSupport";
    /** Line separator. */
    private static final String LINE_SEPARATOR = "\t//-----------------------------------------------------------------------";
    /** Line separator. */
    private static final String LINE_SEPARATOR_INDENTED = "\t\t//-----------------------------------------------------------------------";
    /** Types with primitive equals. */
    private static final Set<String> PRIMITIVE_EQUALS = new HashSet<>();
    static {
        PRIMITIVE_EQUALS.add("boolean");
        PRIMITIVE_EQUALS.add("char");
        PRIMITIVE_EQUALS.add("byte");
        PRIMITIVE_EQUALS.add("short");
        PRIMITIVE_EQUALS.add("int");
        PRIMITIVE_EQUALS.add("long");
        // not float or double, as Double.equals is not the same as double ==
    }

    /** The content to process. */
    private final File file;
    /** The content to process. */
    private final List<String> content;
    /** The config. */
    private final BeanGenConfig config;
    /** The data model of the bean. */
    private final BeanData data;
    /** The list of property generators. */
    private final List<PropertyGen> properties;
    /** The region to insert into. */
    private final List<String> insertRegion;
    /** The list of removed imports. */
    private final SortedSet<String> removedImports = new TreeSet<>();

    /**
     * Constructor used when file is not a bean.
     * @param file  the file, not null
     * @param content  the content to process, not null
     * @param config  the config to use, not null
     * @param data  the parsed data
     */
    BeanGen(File file, List<String> content, BeanGenConfig config, BeanData data) {
        this.file = file;
        this.content = content;
        this.config = config;
        this.data = data;
        this.properties = null;
        this.insertRegion = null;
    }

    /**
     * Constructor used when file is a parsed bean.
     * @param file  the file, not null
     * @param content  the content to process, not null
     * @param config  the config to use, not null
     * @param data  the parsed data
     * @param properties  the parsed properties
     * @param autoEndIndex  the start of the autogen area
     * @param autoStartIndex   the end of the autogen area
     */
    BeanGen(
            File file, List<String> content, BeanGenConfig config,
            BeanData data, List<PropertyGen> properties, int autoStartIndex, int autoEndIndex) {
        this.file = file;
        this.content = content;
        this.config = config;
        this.data = data;
        this.properties = properties;
        this.insertRegion = content.subList(autoStartIndex + 1, autoEndIndex);
    }

    //-----------------------------------------------------------------------
    void process() {
        fixImports();
        if (insertRegion != null) {
            data.ensureImport(BeanDefinition.class);
            if (!properties.isEmpty()) {
                data.ensureImport(PropertyDefinition.class);
            }
            removeOld();
            if (data.isRootClass() && data.isExtendsDirectBean()) {
                data.ensureImport(DirectBean.class);
            }
            generateMeta();
            generateSerializationVersionId();
            generatePropertyChangeSupportField();
            generateHashCodeField();
            generateFactory();
            generateImmutableBuilderMethod();
            generateArgBasedConstructor();
            generateBuilderBasedConstructor();
            generateMetaBean();
            generateGettersSetters();
            generateSeparator();
            generateImmutableToBuilder();
            generateClone();
            generateEquals();
            generateHashCode();
            generateToString();
            generateMetaClass();
            generateBuilderClass();
            resolveImports();
            resolveIndents();
        }
    }

    void processNonBean() {
        fixImports();
        resolveImports();
    }

    private void fixImports() {
        renameImport("org.joda.beans.BeanDefinition", BeanDefinition.class);
        renameImport("org.joda.beans.DerivedProperty", DerivedProperty.class);
        renameImport("org.joda.beans.ImmutableConstructor", ImmutableConstructor.class);
        renameImport("org.joda.beans.ImmutableDefaults", ImmutableDefaults.class);
        renameImport("org.joda.beans.ImmutablePreBuild", ImmutablePreBuild.class);
        renameImport("org.joda.beans.ImmutableValidator", ImmutableValidator.class);
        renameImport("org.joda.beans.PropertyDefinition", PropertyDefinition.class);
    }

    private void renameImport(String old, Class<?> cls) {
        if (data.getCurrentImports().contains(old)) {
            removedImports.add(old);
            data.ensureImport(cls);
        }
    }

    private void resolveImports() {
        if (!data.getNewImports().isEmpty()) {
            var pos = data.getImportInsertLocation() + 1;
            for (var imp : data.getNewImports()) {
                content.add(pos++, "import " + imp + ";");
            }
        }
        if (!removedImports.isEmpty()) {
            for (var it = content.listIterator(); it.hasNext(); ) {
                var line = it.next().trim();
                if (line.startsWith("import ")) {
                    var imported = line.substring(7).trim().replace(" ", "").replace(";", "");
                    if (removedImports.contains(imported)) {
                        it.remove();
                    }
                }
            }
        }
    }

    private void resolveIndents() {
        for (var it = content.listIterator(); it.hasNext(); ) {
            it.set(it.next().replace("\t", config.getIndent()));
        }
    }

    private void removeOld() {
        insertRegion.clear();
    }

    //-----------------------------------------------------------------------
    private void generateSeparator() {
        if (!insertRegion.isEmpty() && insertRegion.get(insertRegion.size() - 1).equals(LINE_SEPARATOR)) {
            return;
        }
        addLine(0, LINE_SEPARATOR);
    }

    private void generateIndentedSeparator() {
        if (!insertRegion.isEmpty() && insertRegion.get(insertRegion.size() - 1).equals(LINE_SEPARATOR_INDENTED)) {
            return;
        }
        addLine(0, LINE_SEPARATOR_INDENTED);
    }

    private void generateGenerated(int tabCount) {
        if (config.isGeneratedAnno()) {
            addLine(tabCount, "@Generated(\"org.joda.beans.gen.BeanCodeGen\")");
        }
    }

    private void generateFactory() {
        if (data.isFactoryRequired()) {
            var nonDerived = nonDerivedProperties();
            addLine(1, "/**");
            addLine(1, " * Obtains an instance.");
            if (!nonDerived.isEmpty()) {
                if (data.isTypeGeneric()) {
                    for (var j = 0; j < data.getTypeGenericCount(); j++) {
                        addLine(1, " * @param " + data.getTypeGenericName(j, true) + "  the type");
                    }
                }
                for (var propertyGen : nonDerived) {
                    var prop = propertyGen.getData();
                    addLine(1, " * @param " + prop.getPropertyName() + "  the value of the property" + prop.getNotNullJavadoc());
                }
            }
            addLine(1, " * @return the instance");
            addLine(1, " */");
            if (nonDerived.isEmpty()) {
                addLine(1, "public static " + data.getTypeNoExtends() + " " + data.getFactoryName() + "() {");
                addLine(2, "return new " + data.getTypeNoExtends() + "();");
                
            } else {
                if (data.isTypeGeneric()) {
                    addLine(1, "public static " + data.getTypeGeneric(true) + " " +
                                    data.getTypeNoExtends() + " " + data.getFactoryName() + "(");
                } else {
                    addLine(1, "public static " + data.getTypeNoExtends() + " " + data.getFactoryName() + "(");
                }
                for (var i = 0; i < nonDerived.size(); i++) {
                    var prop = nonDerived.get(i);
                    addLine(3, prop.getBuilderType() + " " + prop.getData().getPropertyName() + joinComma(i, nonDerived, ") {"));
                }
                addLine(2, "return new " + data.getTypeWithDiamond() + "(");
                for (var i = 0; i < nonDerived.size(); i++) {
                    addLine(3, nonDerived.get(i).generateBuilderFieldName() + joinComma(i, nonDerived, ");"));
                }
            }
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateImmutableBuilderMethod() {
        if (data.isConstructable() && data.isBuilderGenerated()) {
            addLine(1, "/**");
            addLine(1, " * Returns a builder used to create an instance of the bean.");
            if (data.isTypeGeneric()) {
                for (var j = 0; j < data.getTypeGenericCount(); j++) {
                    addLine(1, " * @param " + data.getTypeGenericName(j, true) + "  the type");
                }
            }
            addLine(1, " * @return the builder, not null");
            addLine(1, " */");
            if (data.isTypeGeneric()) {
                addLine(1, data.getEffectiveBuilderScope() + "static " + data.getTypeGeneric(true) +
                                " " + data.getEffectiveBeanBuilderName() + data.getTypeGenericName(true) + " builder() {");
            } else {
                addLine(1, data.getEffectiveBuilderScope() + "static " + data.getEffectiveBeanBuilderName() + " builder() {");
            }
            addLine(2, "return new " + data.getEffectiveBeanBuilderName() + data.getTypeGenericDiamond() + "();");
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateBuilderBasedConstructor() {
        if (data.getConstructorStyle() == CONSTRUCTOR_BY_BUILDER && data.getImmutableConstructor() == CONSTRUCTOR_NONE && 
                ((data.isMutable() && data.isBuilderScopeVisible()) || data.isImmutable())) {
            var nonDerived = nonDerivedProperties();
            var scope = (data.isTypeFinal() ? "private" : "protected");
            // signature
            addLine(1, "/**");
            addLine(1, " * Restricted constructor.");
            addLine(1, " * @param builder  the builder to copy from, not null");
            addLine(1, " */");
            addLine(1, scope + " " + data.getTypeRaw() + "(" + data.getEffectiveBeanBuilderName() + data.getTypeGenericName(true) + " builder) {");
            // super
            if (data.isSubClass()) {
                addLine(2, "super(builder);");
            }
            // validate
            for (var prop : properties) {
                if (prop.getData().isValidated()) {
                    addLine(2, prop.getData().getValidationMethodName() +
                            "(builder." + prop.generateBuilderFieldName() +
                            ", \"" + prop.getData().getPropertyName() + "\");");
                }
            }
            // assign
            if (data.isImmutable()) {
                // assign
                for (var propertyGen : nonDerived) {
                    addLines(propertyGen.generateConstructorAssign("builder."));
                }
            } else {
                for (var propGen : nonDerived) {
                    var prop = propGen.getData();
                    if (prop.isCollectionType()) {
                        if (prop.isNotNull()) {
                            addLine(2, "this." + prop.getPropertyName() + ".addAll(builder." + propGen.generateBuilderFieldName() + ");");
                        } else {
                            addLine(2, "this." + prop.getPropertyName() + " = builder." + propGen.generateBuilderFieldName() + ";");
                        }
                    } else if (prop.isMapType()) {
                        if (prop.isNotNull()) {
                            addLine(2, "this." + prop.getPropertyName() + ".putAll(builder." + propGen.generateBuilderFieldName() + ");");
                        } else {
                            addLine(2, "this." + prop.getPropertyName() + " = builder." + propGen.generateBuilderFieldName() + ";");
                        }
                    } else {
                        addLine(2, "this." + prop.getPropertyName() + " = builder." + propGen.generateBuilderFieldName() + ";");
                    }
                }
            }
            if (data.getImmutableValidator() != null) {
                addLine(2, data.getImmutableValidator() + "();");
            }
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateArgBasedConstructor() {
        if (isArgBasedConstructor()) {
            var scope = data.getEffectiveConstructorScope();
            var generateAnnotation = data.isConstructorPropertiesAnnotation();
            var generateJavadoc = !"private ".equals(scope);
            var nonDerived = nonDerivedProperties();
            if (nonDerived.isEmpty()) {
                if (generateJavadoc) {
                    addLine(1, "/**");
                    addLine(1, " * Creates an instance.");
                    addLine(1, " */");
                }
                if (generateAnnotation) {
                    data.ensureImport(CLASS_CONSTRUCTOR_PROPERTIES);
                    addLine(1, "@ConstructorProperties({})");
                }
                addLine(1, scope + data.getTypeRaw() + "() {");
            } else {
                // signature
                if (generateJavadoc) {
                    addLine(1, "/**");
                    addLine(1, " * Creates an instance.");
                    for (var propertyGen : nonDerived) {
                        var prop = propertyGen.getData();
                        addLine(1, " * @param " + prop.getPropertyName() + "  the value of the property" + prop.getNotNullJavadoc());
                    }
                    addLine(1, " */");
                }
                if (generateAnnotation) {
                    data.ensureImport(CLASS_CONSTRUCTOR_PROPERTIES);
                    var buf = new StringBuilder();
                    for (var i = 0; i < nonDerived.size(); i++) {
                        buf.append('"').append(nonDerived.get(i).getData().getPropertyName()).append('"');
                        buf.append(join(i, nonDerived, ", ", ""));
                    }
                    addLine(1, "@ConstructorProperties({" + buf + "})");
                }
                addLine(1, scope + data.getTypeRaw() + "(");
                for (var i = 0; i < nonDerived.size(); i++) {
                    var prop = nonDerived.get(i);
                    addLine(3, prop.getBuilderType() + " " + prop.getData().getPropertyName() + joinComma(i, nonDerived, ") {"));
                }
                // validate (mutable light beans call setters which validate)
                if (!(data.isMutable() && data.isBeanStyleLight())) {
                    for (var prop : properties) {
                        if (prop.getData().isValidated()) {
                            addLine(2, prop.getData().getValidationMethodName() +
                                    "(" + prop.getData().getPropertyName() +
                                    ", \"" + prop.getData().getPropertyName() + "\");");
                        }
                    }
                }
                // assign
                for (var prop : nonDerived) {
                    if (data.isMutable() && data.isBeanStyleLight()) {
                        var generateSetInvoke = prop.getData().getSetterGen().generateSetInvoke(
                                prop.getData(), prop.getData().getPropertyName());
                        addLine(2, generateSetInvoke + ";");
                    } else {
                        addLines(prop.generateConstructorAssign(""));
                    }
                }
            }
            if (data.getImmutableValidator() != null) {
                addLine(2, data.getImmutableValidator() + "();");
            }
            addLine(1, "}");
            addBlankLine();
        }
    }

    private boolean isArgBasedConstructor() {
        if (data.getConstructorStyle() == CONSTRUCTOR_BY_ARGS &&
                data.getImmutableConstructor() == CONSTRUCTOR_NONE &&
                ((data.isMutable() && (data.isBuilderScopeVisible() || data.isBeanStyleLight())) || data.isImmutable())) {
            return true;
        }
        return data.getConstructorStyle() == CONSTRUCTOR_BY_BUILDER &&
                data.getImmutableConstructor() == CONSTRUCTOR_NONE &&
                data.isImmutable() &&
                !data.isTypeFinal() &&
                !"smart".equals(data.getConstructorScope());
    }

    //-----------------------------------------------------------------------
    private void generateMeta() {
        if (data.isBeanStyleLightOrMinimal()) {
            addLine(1, "/**");
            addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
            addLine(1, " */");
            var genericProps = data.getProperties().stream()
                    .anyMatch(p -> p.isGeneric());
            var unchecked = data.isBeanStyleMinimal() && data.isMutable() && genericProps;
            unchecked |= data.isBeanStyleMinimal() && data.isTypeGeneric() && !data.isSkipBuilderGeneration();
            var rawtypes = data.isBeanStyleMinimal() && data.isTypeGeneric();
            if (unchecked && rawtypes) {
                addLine(1, "@SuppressWarnings({\"unchecked\", \"rawtypes\" })");
            } else if (rawtypes) {
                addLine(1, "@SuppressWarnings(\"rawtypes\")");
            } else if (unchecked) {
                addLine(1, "@SuppressWarnings(\"unchecked\")");
            }
            if (data.isTypeGeneric()) {
                data.ensureImport(MetaBean.class);
                addLine(1, "private static final MetaBean META_BEAN =");
            } else {
                data.ensureImport(TypedMetaBean.class);
                addLine(1, "private static final TypedMetaBean<" + data.getTypeNoExtends() + "> META_BEAN =");
            }
            var nonDerived = nonDerivedProperties();
            var aliases = nonDerived.stream().filter(p -> p.getData().getAlias() != null).toList();
            var hasAliases = aliases.isEmpty();
            if (data.isBeanStyleLight()) {
                // light
                data.ensureImport(LightMetaBean.class);
                data.ensureImport(MethodHandles.class);
                var specialInit = nonDerived.stream().anyMatch(p -> p.isSpecialInit());
                if (nonDerived.isEmpty()) {
                    addLine(3, "LightMetaBean.of(" + data.getTypeRaw() + ".class, MethodHandles.lookup());");
                } else {
                    addLine(3, "LightMetaBean.of(");
                    addLine(5, data.getTypeRaw() + ".class,");
                    addLine(5, "MethodHandles.lookup(),");
                    generateFieldNames(nonDerived);
                    if (specialInit) {
                        for (var i = 0; i < nonDerived.size(); i++) {
                            addLine(5, nonDerived.get(i).generateInit() +
                                    joinComma(i, nonDerived, ")" + (hasAliases ? ";" : "")));
                        }
                    } else {
                        addLine(5, "new Object[0])" + (hasAliases ? ";" : ""));
                    }
                    for (var i = 0; i < aliases.size(); i++) {
                        var prop = aliases.get(i);
                        addLine(5, ".withAlias(\"" + prop.getData().getAlias() + "\", \"" +
                                prop.getData().getPropertyName() + "\")" + join(i, aliases, "", ";"));
                    }
                }
            } else {
                // minimal
                data.ensureImport(MinimalMetaBean.class);
                addLine(3, "MinimalMetaBean.of(");
                addLine(5, data.getTypeRaw() + ".class,");
                generateFieldNames(nonDerived);
                var builderLambda = "() -> new " + data.getEffectiveBeanBuilderName() + "()";
                if (data.isSkipBuilderGeneration()) {
                    if (data.isBeanBuilderManual()) {
                        builderLambda = "() -> new " + data.getEffectiveMinimalBeanBuilderName() + "()";
                    } else {
                        data.ensureImport(BasicBeanBuilder.class);
                        builderLambda = "() -> new BasicBeanBuilder<>(new " + data.getTypeWithDiamond() + "())";
                    }
                }
                if (nonDerived.isEmpty()) {
                    if (data.isImmutable()) {
                        addLine(5, builderLambda + ");");
                    } else {
                        data.ensureImport(Collections.class);
                        data.ensureImport(Function.class);
                        data.ensureImport(BiConsumer.class);
                        addLine(5, builderLambda + ",");
                        addLine(5, "Collections.<Function<" + data.getTypeRaw() + ", Object>>emptyList(),");
                        addLine(5, "Collections.<BiConsumer<" + data.getTypeRaw() + ", Object>>emptyList());");
                    }
                } else {
                    addLine(5, builderLambda + ",");
                    if (data.isImmutable()) {
                        for (var i = 0; i < nonDerived.size(); i++) {
                            addLine(5, nonDerived.get(i).generateLambdaGetter() + joinComma(i, nonDerived, ")" +
                                    (hasAliases ? ";" : "")));
                        }
                    } else {
                        data.ensureImport(Arrays.class);
                        data.ensureImport(Function.class);
                        data.ensureImport(BiConsumer.class);
                        addLine(5, "Arrays.<Function<" + data.getTypeRaw() + ", Object>>asList(");
                        for (var i = 0; i < nonDerived.size(); i++) {
                            addLine(7, nonDerived.get(i).generateLambdaGetter() + joinComma(i, nonDerived, "),"));
                        }
                        addLine(5, "Arrays.<BiConsumer<" + data.getTypeRaw() + ", Object>>asList(");
                        for (var i = 0; i < nonDerived.size(); i++) {
                            addLine(7, nonDerived.get(i).generateLambdaSetter() +
                                    joinComma(i, nonDerived, "))" + (hasAliases ? ";" : "")));
                        }
                    }
                    for (var i = 0; i < aliases.size(); i++) {
                        var prop = aliases.get(i);
                        addLine(5, ".withAlias(\"" + prop.getData().getAlias() + "\", \"" +
                                prop.getData().getPropertyName() + "\")" + join(i, aliases, "", ";"));
                    }
                }
            }
            addBlankLine();
            addLine(1, "/**");
            addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
            addLine(1, " * @return the meta-bean, not null");
            addLine(1, " */");
            if (data.isTypeGeneric()) {
                addLine(1, "public static MetaBean meta() {");
            } else {
                addLine(1, "public static TypedMetaBean<" + data.getTypeNoExtends() + "> meta() {");
            }
            addLine(2, "return META_BEAN;");
            addLine(1, "}");
            addBlankLine();
            addLine(1, "static {");
            data.ensureImport(MetaBean.class);
            addLine(2, "MetaBean.register(META_BEAN);");
            addLine(1, "}");
            addBlankLine();
            
        } else {
            // this cannot be generified without either Eclipse or javac complaining
            // raw types forever
            addLine(1, "/**");
            addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
            addLine(1, " * @return the meta-bean, not null");
            if (data.isMetaScopePrivate()) {
                data.ensureImport(MetaBean.class);
                addLine(1, " */");
                addLine(1, "public static MetaBean meta() {");
            } else if (data.isTypeGeneric()) {
                addLine(1, " */");
                addLine(1, "@SuppressWarnings(\"rawtypes\")");
                addLine(1, "public static " + data.getTypeRaw() + ".Meta meta() {");
            } else {
                addLine(1, " */");
                addLine(1, "public static " + data.getTypeRaw() + ".Meta meta() {");
            }
            addLine(2, "return " + data.getTypeRaw() + ".Meta.INSTANCE;");
            addLine(1, "}");
            
            if (data.isTypeGeneric()) {
                generateMetaForGenericType();
            }
            
            addBlankLine();
            addLine(1, "static {");
            data.ensureImport(MetaBean.class);
            addLine(2, "MetaBean.register(" + data.getTypeRaw() + ".Meta.INSTANCE);");
            addLine(1, "}");
            addBlankLine();
        }
    }

    private void generateFieldNames(List<PropertyGen> nonDerived) {
        if (nonDerived.isEmpty()) {
            addLine(5, "new String[0],");
        } else {
            addLine(5, "new String[] {");
            for (var i = 0; i < nonDerived.size(); i++) {
                addLine(7, "\"" + nonDerived.get(i).getData().getFieldName() + join(i, nonDerived, "\",", "\"},"));
            }
        }
    }

    private void generateMetaForGenericType() {
        // this works around an Eclipse bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=397462
        // long name needed for uniqueness as static overriding is borked
        addBlankLine();
        addLine(1, "/**");
        addLine(1, " * The meta-bean for {@code " + data.getTypeRaw() + "}.");
        if (data.getTypeGenericCount() == 1) {
            addLine(1, " * @param <R>  the bean's generic type");
            addLine(1, " * @param cls  the bean's generic type");
        } else if (data.getTypeGenericCount() == 2) {
            addLine(1, " * @param <R>  the first generic type");
            addLine(1, " * @param <S>  the second generic type");
            addLine(1, " * @param cls1  the first generic type");
            addLine(1, " * @param cls2  the second generic type");
        } else if (data.getTypeGenericCount() == 3) {
            addLine(1, " * @param <R>  the first generic type");
            addLine(1, " * @param <S>  the second generic type");
            addLine(1, " * @param <T>  the second generic type");
            addLine(1, " * @param cls1  the first generic type");
            addLine(1, " * @param cls2  the second generic type");
            addLine(1, " * @param cls3  the third generic type");
        }
        addLine(1, " * @return the meta-bean, not null");
        addLine(1, " */");
        addLine(1, "@SuppressWarnings(\"unchecked\")");
        var typeNames = new String[] {"R", "S", "T"};
        if (data.getTypeGenericCount() == 1) {
            addLine(1, "public static <R" + data.getTypeGenericExtends(0, typeNames) +
                    "> " + data.getTypeRaw() + ".Meta<R> meta" + data.getTypeRaw() + "(Class<R> cls) {");
        } else if (data.getTypeGenericCount() == 2) {
            addLine(1, "public static <R" + data.getTypeGenericExtends(0, typeNames) +
                    ", S" + data.getTypeGenericExtends(1, typeNames) + "> " + data.getTypeRaw() +
                    ".Meta<R, S> meta" + data.getTypeRaw() + "(Class<R> cls1, Class<S> cls2) {");
        } else if (data.getTypeGenericCount() == 3) {
            addLine(1, "public static <R" + data.getTypeGenericExtends(0, typeNames) +
                    ", S" + data.getTypeGenericExtends(1, typeNames) +
                    ", T" + data.getTypeGenericExtends(2, typeNames) +
                    "> " + data.getTypeRaw() +
                    ".Meta<R, S, T> meta" + data.getTypeRaw() + "(Class<R> cls1, Class<S> cls2, Class<T> cls3) {");
        }
        addLine(2, "return " + data.getTypeRaw() + ".Meta.INSTANCE;");
        addLine(1, "}");
    }

    private void generateSerializationVersionId() {
        if (data.isSerializable() && !data.isManualSerializationId()) {
            addLine(1, "/**");
            addLine(1, " * The serialization version id.");
            addLine(1, " */");
            addLine(1, "private static final long serialVersionUID = 1L;");
            addBlankLine();
        }
    }

    private void generatePropertyChangeSupportField() {
        if (data.isPropertyChangeSupport()) {
            data.ensureImport(CLASS_PROPERTY_CHANGE_SUPPORT);
            addLine(1, "/**");
            addLine(1, " * The property change support field.");
            addLine(1, " */");
            addLine(1, "private final transient PropertyChangeSupport " + config.getPrefix() + "propertyChangeSupport = new PropertyChangeSupport(this);");
            addBlankLine();
        }
    }

    private void generateHashCodeField() {
        if (data.isCacheHashCode()) {
            addLine(1, "/**");
            addLine(1, " * The cached hash code, using the racy single-check idiom.");
            addLine(1, " */");
            addLine(1, "private transient int " + config.getPrefix() + "cacheHashCode;");
            addBlankLine();
        }
    }

    private void generateMetaBean() {
        if (data.isMetaScopePrivate() || data.isBeanStyleMinimal()) {
            addLine(1, "@Override");
            if (data.isBeanStyleLightOrMinimal()) {
                data.ensureImport(TypedMetaBean.class);
                if (data.isTypeGeneric()) {
                    addLine(1, "@SuppressWarnings(\"unchecked\")");
                }
                addLine(1, "public TypedMetaBean<" + data.getTypeNoExtends() + "> metaBean() {");
                if (data.isTypeGeneric()) {
                    addLine(2, "return (TypedMetaBean<" + data.getTypeNoExtends() + ">) META_BEAN;");
                } else {
                    addLine(2, "return META_BEAN;");
                }
            } else {
                data.ensureImport(MetaBean.class);
