/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.openjpa.enhance;

import java.io.Externalizable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamException;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLDecoder;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.meta.ClassArgParser;
import org.apache.openjpa.util.asm.BytecodeWriter;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.lib.util.Services;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.lib.util.git.GitUtils;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataModes;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.BigDecimalId;
import org.apache.openjpa.util.BigIntegerId;
import org.apache.openjpa.util.ByteId;
import org.apache.openjpa.util.CharId;
import org.apache.openjpa.util.DateId;
import org.apache.openjpa.util.DoubleId;
import org.apache.openjpa.util.FloatId;
import org.apache.openjpa.util.GeneralException;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.IntId;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.LongId;
import org.apache.openjpa.util.ObjectId;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.ShortId;
import org.apache.openjpa.util.StringId;
import org.apache.openjpa.util.UserException;
import org.apache.openjpa.util.asm.AsmHelper;
import org.apache.openjpa.util.asm.ClassNodeTracker;
import org.apache.openjpa.util.asm.EnhancementProject;
import org.apache.openjpa.util.asm.RedefinedAttribute;
import org.apache.xbean.asm9.Opcodes;
import org.apache.xbean.asm9.Type;
import org.apache.xbean.asm9.tree.*;


/**
 * Bytecode enhancer used to enhance persistent classes from metadata. The
 * enhancer must be invoked on all persistence-capable and persistence aware
 * classes.
 *
 * @author Abe White
 * @author Mark Struberg
 */
public class PCEnhancer {
    // Designates a version for maintaining compatbility when PCEnhancer
    // modifies enhancement that can break serialization or other contracts
    // Each enhanced class will return the value of this field via
    // public int getEnhancementContractVersion()
    public static final int ENHANCER_VERSION;

    public static final int ENHANCE_NONE = 0;
    public static final int ENHANCE_AWARE = 2 << 0;
    public static final int ENHANCE_INTERFACE = 2 << 1;
    public static final int ENHANCE_PC = 2 << 2;

    public static final String PRE = "pc";
    public static final String ISDETACHEDSTATEDEFINITIVE = PRE + "isDetachedStateDefinitive";

    private static final Class<?> PCTYPE = PersistenceCapable.class;
    private static final Type TYPE_PCTYPE = Type.getType(PersistenceCapable.class);
    private static final String SM = PRE + "StateManager";
    private static final Class<?> SMTYPE = StateManager.class;
    private static final String INHERIT = PRE + "InheritedFieldCount";
    private static final String CONTEXTNAME = "GenericContext";
    private static final Class<?> USEREXCEP = UserException.class;
    private static final Class<?> INTERNEXCEP = InternalException.class;
    private static final Class<?> HELPERTYPE = PCRegistry.class;
    private static final String SUPER = PRE + "PCSuperclass";
    private static final Class OIDFSTYPE = FieldSupplier.class;
    private static final Class<?> OIDFCTYPE = FieldConsumer.class;

    private static final String VERSION_INIT_STR = PRE + "VersionInit";

    private static final Localizer _loc = Localizer.forPackage(PCEnhancer.class);

    private static final AuxiliaryEnhancer[] _auxEnhancers;

    private static final Method LONG_VALUE_OF = Stream.of(Long.class.getDeclaredMethods())
            .filter(m -> "valueOf".equals(m.getName()) && long.class == m.getParameterTypes()[0])
            .findAny().get();

    static {
        Class[] classes = Services.getImplementorClasses(
                AuxiliaryEnhancer.class,
                AccessController.doPrivileged(
                        J2DoPrivHelper.getClassLoaderAction(AuxiliaryEnhancer.class)));
        List auxEnhancers = new ArrayList(classes.length);
        for (Class aClass : classes) {
            try {
                auxEnhancers.add(AccessController.doPrivileged(
                        J2DoPrivHelper.newInstanceAction(aClass)));
            }
            catch (Throwable t) {
                // aux enhancer may rely on non-existant spec classes, etc
            }
        }
        _auxEnhancers = (AuxiliaryEnhancer[]) auxEnhancers.toArray
                (new AuxiliaryEnhancer[0]);

        int rev = 0;
        Properties revisionProps = new Properties();
        try {
            InputStream in = PCEnhancer.class.getResourceAsStream("/META-INF/org.apache.openjpa.revision.properties");
            if (in != null) {
                try (in) {
                    revisionProps.load(in);
                }
            }
            rev = GitUtils.convertGitInfoToPCEnhancerVersion(revisionProps.getProperty("openjpa.enhancer.revision"));
        }
        catch (Exception e) {
        }
        if (rev > 0) {
            ENHANCER_VERSION = rev;
        }
        else {
            // Something bad happened and we couldn't load from the properties file. We need to default to using the
            // value of 2 because that is the value that was the value as of rev.511998.
            ENHANCER_VERSION = 2;
        }
    }

    private final MetaDataRepository _repos;
    private final ClassMetaData _meta;
    private final Log _log;

    boolean _addVersionInitFlag = true;


    private final EnhancementProject project;

    /**
     * represents the managed type.
     */
    private final ClassNodeTracker managedType;

    /**
     * represents the persistent class.
     * This might be the same as {@link #managedType}
     * but also a subclass.
     */
    private ClassNodeTracker pc;

    private boolean _defCons = true;
    private boolean _redefine = false;
    private boolean _subclass = false;
    private boolean _fail = false;
    private Set _violations = null;
    private File _dir = null;
    private BytecodeWriter _writer = null;
    private Map<String, String> _backingFields = null; // map of set / get names => field names
    private Map<String, String> _attrsToFields = null; // map of attr names => field names
    private Map<String, String> _fieldsToAttrs = null; // map of field names => attr names
    private boolean _isAlreadyRedefined = false;
    private boolean _isAlreadySubclassed = false;
    private boolean _bcsConfigured = false;

    private boolean _optimizeIdCopy = false; // whether to attempt optimizing id copy

    /**
     * Constructor. Supply configuration and type to enhance. This will look
     * up the metadata for <code>type</code> from <code>conf</code>'s
     * repository.
     */
    public PCEnhancer(OpenJPAConfiguration conf, Class<?> type) {
        this(conf, new EnhancementProject().loadClass(type), (MetaDataRepository) null);
    }

    /**
     * Constructor. Supply configuration and type to enhance. This will look
     * up the metadata for <code>meta</code> by converting back to a class
     * and then loading from <code>conf</code>'s repository.
     */
    public PCEnhancer(OpenJPAConfiguration conf, ClassMetaData meta) {
        this(conf, new EnhancementProject().loadClass(meta.getDescribedType()), meta.getRepository());
    }

    /**
     * Constructor. Supply configuration.
     *
     * @param type  the bytecode representation fo the type to
     *              enhance; this can be created from any stream or file
     * @param repos a metadata repository to use for metadata access,
     *              or null to create a new reporitory; the repository
     *              from the given configuration isn't used by default
     *              because the configuration might be an
     *              implementation-specific subclass whose metadata
     *              required more than just base metadata files
     * @deprecated use {@link #PCEnhancer(OpenJPAConfiguration, ClassNodeTracker,
     * MetaDataRepository, ClassLoader)} instead.
     */
    @Deprecated
    public PCEnhancer(OpenJPAConfiguration conf, ClassNodeTracker type, MetaDataRepository repos) {
        this(conf, type, repos, null);
    }

    /**
     * Constructor. Supply configuration.
     *
     * @param type   the bytecode representation fo the type to
     *               enhance; this can be created from any stream or file
     * @param repos  a metadata repository to use for metadata access,
     *               or null to create a new reporitory; the repository
     *               from the given configuration isn't used by default
     *               because the configuration might be an
     *               implementation-specific subclass whose metadata
     *               required more than just base metadata files
     * @param loader the environment classloader to use for loading
     *               classes and resources.
     */
    public PCEnhancer(OpenJPAConfiguration conf, ClassNodeTracker type, MetaDataRepository repos, ClassLoader loader) {

        // we assume that the original class and the enhanced class is the same
        project = type.getProject();
        managedType = type;
        pc = managedType;

        _log = conf.getLog(OpenJPAConfiguration.LOG_ENHANCE);

        if (repos == null) {
            _repos = conf.newMetaDataRepositoryInstance();
            _repos.setSourceMode(MetaDataModes.MODE_META);
        }
        else {
            _repos = repos;
        }

        _meta = _repos.getMetaData(type.getType(), loader, false);

        configureOptimizeIdCopy();
    }

    /**
     * Constructor. Supply repository. The repository's configuration will
     * be used, and the metadata passed in will be used as-is without doing
     * any additional lookups. This is useful when running the enhancer
     * during metadata load.
     *
     * @param repos a metadata repository to use for metadata access,
     *              or null to create a new reporitory; the repository
     *              from the given configuration isn't used by default
     *              because the configuration might be an
     *              implementation-specific subclass whose metadata
     *              required more than just base metadata files
     * @param type  the bytecode representation fo the type to
     *              enhance; this can be created from any stream or file
     * @param meta  the metadata to use for processing this type.
     * @since 1.1.0
     */
    public PCEnhancer(MetaDataRepository repos, ClassNodeTracker type, ClassMetaData meta) {
        // we assume that the original class and the enhanced class is the same
        project = type.getProject();
        managedType = type;
        pc = managedType;

        _log = repos.getConfiguration()
                .getLog(OpenJPAConfiguration.LOG_ENHANCE);

        _repos = repos;
        _meta = meta;
    }

    static String toPCSubclassName(ClassNodeTracker cnt) {
        return ClassUtil.getPackageName(PCEnhancer.class) + "."
                + cnt.getClassNode().name.replace('/', '$') + "$pcsubclass";
    }

    @Deprecated
    static String toPCSubclassName(Class cls) {
        return ClassUtil.getPackageName(PCEnhancer.class) + "."
                + cls.getName().replace('.', '$') + "$pcsubclass";
    }

    /**
     * Whether <code>className</code> is the name for a
     * dynamically-created persistence-capable subclass.
     *
     * @since 1.1.0
     */
    public static boolean isPCSubclassName(String className) {
        return className.startsWith(ClassUtil.getPackageName(PCEnhancer.class))
                && className.endsWith("$pcsubclass");
    }

    /**
     * If <code>className</code> is a dynamically-created persistence-capable
     * subclass name, returns the name of the class that it subclasses.
     * Otherwise, returns <code>className</code>.
     *
     * @since 1.1.0
     */
    public static String toManagedTypeName(String className) {
        if (isPCSubclassName(className)) {
            className = className.substring(
                    ClassUtil.getPackageName(PCEnhancer.class).length() + 1);
            className = className.substring(0, className.lastIndexOf("$"));
            // this is not correct for nested PCs
            className = className.replace('$', '.');
        }

        return className;
    }

    /**
     * Return the bytecode representation of the persistence-capable class
     * being manipulated.
     */
    public ClassNodeTracker getPCBytecode() {
        return pc;
    }

    /**
     * Return the bytecode representation of the managed class being
     * manipulated. This is usually the same as {@link #getPCBytecode},
     * except when running the enhancer to redefine and subclass
     * existing persistent types.
     */
    public ClassNodeTracker getManagedTypeBytecode() {
        return managedType;
    }

    /**
     * Return the metadata for the class being manipulated, or null if not
     * a persistent type.
     */
    public ClassMetaData getMetaData() {
        return _meta;
    }

    /**
     * A boolean indicating whether the enhancer should add a no-args
     * constructor if one is not already present in the class. OpenJPA
     * requires that a no-arg constructor (whether created by the compiler
     * or by the user) be present in a PC.
     */
    public boolean getAddDefaultConstructor() {
        return _defCons;
    }

    /**
     * A boolean indicating whether the enhancer should add a no-args
     * constructor if one is not already present in the class. OpenJPA
     * requires that a no-arg constructor (whether created by the compiler
     * or by the user) be present in a PC.
     */
    public void setAddDefaultConstructor(boolean addDefaultConstructor) {
        _defCons = addDefaultConstructor;
    }

    /**
     * Whether the enhancer should mutate its arguments, or just run validation
     * and optional subclassing logic on them. Usually used in conjunction with
     * <code>setCreateSubclass(true)</code>.
     *
     * @since 1.0.0
     */
    public boolean getRedefine() {
        return _redefine;
    }

    /**
     * Whether the enhancer should mutate its arguments, or just run validation
     * and optional subclassing logic on them. Usually used in conjunction with
     * <code>setCreateSubclass(true)</code>.
     *
     * @since 1.0.0
     */
    public void setRedefine(boolean redefine) {
        _redefine = redefine;
    }

    /**
     * Whether the type that this instance is enhancing has already been
     * redefined.
     *
     * @since 1.0.0
     */
    public boolean isAlreadyRedefined() {
        return _isAlreadyRedefined;
    }

    /**
     * Whether the type that this instance is enhancing has already been
     * subclassed in this instance's environment classloader.
     *
     * @since 1.0.0
     */
    public boolean isAlreadySubclassed() {
        return _isAlreadySubclassed;
    }

    /**
     * Whether the enhancer should make its arguments persistence-capable,
     * or generate a persistence-capable subclass.
     *
     * @since 1.0.0
     */
    public boolean getCreateSubclass() {
        return _subclass;
    }

    /**
     * Whether the enhancer should make its arguments persistence-capable,
     * or generate a persistence-capable subclass.
     *
     * @since 1.0.0
     */
    public void setCreateSubclass(boolean subclass) {
        _subclass = subclass;
        _addVersionInitFlag = false;
    }

    /**
     * Whether to fail if the persistent type uses property access and
     * bytecode analysis shows that it may be violating OpenJPA's property
     * access restrictions.
     */
    public boolean getEnforcePropertyRestrictions() {
        return _fail;
    }

    /**
     * Whether to fail if the persistent type uses property access and
     * bytecode analysis shows that it may be violating OpenJPA's property
     * access restrictions.
     */
    public void setEnforcePropertyRestrictions(boolean fail) {
        _fail = fail;
    }

    /**
     * The base build directory to generate code to. The proper package
     * structure will be created beneath this directory. Defaults to
     * overwriting the existing class file if null.
     */
    public File getDirectory() {
        return _dir;
    }

    /**
     * The base build directory to generate code to. The proper package
     * structure will be creaed beneath this directory. Defaults to
     * overwriting the existing class file if null.
     */
    public void setDirectory(File dir) {
        _dir = dir;
    }

    /**
     * Return the current {@link BytecodeWriter} to write to or null if none.
     */
    public BytecodeWriter getBytecodeWriter() {
        return _writer;
    }

    /**
     * Set the {@link BytecodeWriter} to write the bytecode to or null if none.
     */
    public void setBytecodeWriter(BytecodeWriter writer) {
        _writer = writer;
    }

    /**
     * Perform bytecode enhancements.
     *
     * @return <code>ENHANCE_*</code> constant
     */
    public int run() {
        try {
            // if enum, skip, no need of any meta
            if ((managedType.getClassNode().access & Opcodes.ACC_ENUM) > 0) {
                return ENHANCE_NONE;
            }

            // if managed interface, skip
            if ((managedType.getClassNode().access & Opcodes.ACC_INTERFACE) > 0) {
                return ENHANCE_INTERFACE;
            }

            // check if already enhanced
            // we cannot simply use instanceof or isAssignableFrom as we have a temp ClassLoader inbetween
            ClassLoader loader = managedType.getClassLoader();
            for (String iface : managedType.getClassNode().interfaces) {
                final String pctypeInternalName = TYPE_PCTYPE.getInternalName();
                if (iface.equals(pctypeInternalName)) {
                    if (_log.isTraceEnabled()) {
                        _log.trace(_loc.get("pc-type", managedType.getClassNode().name, loader));
                    }
                    return ENHANCE_NONE;
                }
            }

            if (_log.isTraceEnabled()) {
                _log.trace(_loc.get("enhance-start", managedType.getClassNode().name));
            }


            configureBCs();

            // validate properties before replacing field access so that
            // we build up a record of backing fields, etc
            if (isPropertyAccess(_meta)) {
                validateProperties();
                if (getCreateSubclass()) {
                    addAttributeTranslation();
                }
            }
            replaceAndValidateFieldAccess();
            processViolations();

            if (_meta != null) {
                enhanceClass(pc);
                addFields(pc);
                addStaticInitializer(pc);
                addPCMethods();
                addAccessors(pc);
                addAttachDetachCode();
                addSerializationCode();
                addCloningCode();
                runAuxiliaryEnhancers();

                return ENHANCE_PC;
            }
            return ENHANCE_AWARE;
        }
        catch (OpenJPAException ke) {
            throw ke;
        }
        catch (Exception e) {
            throw new GeneralException(_loc.get("enhance-error",
                                                managedType.getClassNode().name, e.getMessage()), e);
        }
    }

    private void configureBCs() {
        if (!_bcsConfigured) {
            if (getRedefine()) {
                final boolean isRedefined = managedType.getClassNode().attrs != null &&
                        managedType.getClassNode().attrs.stream().anyMatch(a -> a.isUnknown() && a.type.equals(RedefinedAttribute.ATTR_TYPE));

                if (!isRedefined) {
                    if (managedType.getClassNode().attrs == null) {
                        managedType.getClassNode().attrs = new ArrayList<>();
                    }
                    managedType.getClassNode().attrs.add(new RedefinedAttribute());
                }
                else {
                    _isAlreadyRedefined = true;
                }
            }

            if (getCreateSubclass()) {
                PCSubclassValidator val = new PCSubclassValidator(_meta, managedType.getClassNode(), _log, _fail);
                val.assertCanSubclass();
                pc = project.loadClass(toPCSubclassName(managedType));
                if (pc.getClassNode().superName.equals("java/lang/Object")) {
                    // set the parent class
                    pc.getClassNode().superName = managedType.getClassNode().name;
                    if ((managedType.getClassNode().access & Opcodes.ACC_ABSTRACT) > 0) {
                        pc.getClassNode().access |= Opcodes.ACC_ABSTRACT;
                    }

                    pc.declareInterface(DynamicPersistenceCapable.class);
                }
                else {
                    _isAlreadySubclassed = true;
                }
            }

            _bcsConfigured = true;
        }
    }

    /**
     * Write the generated bytecode.
     */
    public void record() throws IOException {
        if (managedType != pc && getRedefine()) {
            record(managedType);
        }

        record(pc);
    }

    /**
     * Write the given class.
     */
    private void record(ClassNodeTracker cnt)
            throws IOException {
        if (_writer != null) {
            _writer.write(cnt);
        }
        else if (_dir == null) {
            String name = cnt.getClassNode().name.replace(".", "/");
            ClassLoader cl = cnt.getClassLoader();
            if (cl == null) {
                cl = Thread.currentThread().getContextClassLoader();
            }
            final URL resource = cl.getResource(name + ".class");
            try (OutputStream out = new FileOutputStream(URLDecoder.decode(resource.getFile()))) {
                out.write(AsmHelper.toByteArray(cnt));
                out.flush();
            }
        }
        else {
            String name = cnt.getClassNode().name.replace(".", "/") + ".class";
            File targetFile = new File(_dir, name);
            if (!targetFile.getParentFile().exists()) {
                targetFile.getParentFile().mkdirs();
            }
            java.nio.file.Files.write(targetFile.toPath(), AsmHelper.toByteArray(cnt));
        }
    }

    /**
     * Validate that the methods that use a property-access instance are
     * written correctly. This method also gathers information on each
     * property's backing field.
     */
    private void validateProperties() {
        final ClassNode classNode = managedType.getClassNode();
        FieldMetaData[] fmds;
        if (getCreateSubclass()) {
            fmds = _meta.getFields();
        }
        else {
            fmds = _meta.getDeclaredFields();
        }

        Method getter, setter;
        Field returned, assigned = null;

        for (FieldMetaData fmd : fmds) {
            if (!(fmd.getBackingMember() instanceof Method)) {
                // If not mixed access is not defined, flag the field members,
                // otherwise do not process them because they are valid
                // persistent attributes.
                if (!_meta.isMixedAccess()) {
                    addViolation("property-bad-member",
                                 new Object[]{fmd, fmd.getBackingMember()},
                                 true);
                }
                continue;
            }

            getter = (Method) fmd.getBackingMember();

            if (getter == null) {
                addViolation("property-no-getter", new Object[]{fmd},
                             true);
                continue;
            }
            returned = getReturnedField(classNode, getter);


            if (returned != null) {
                registerBackingFieldInfo(fmd, getter, returned);
            }

            setter = getMethod(getter.getDeclaringClass(), getSetterName(fmd), fmd.getDeclaredType());

            if (setter == null) {
                if (returned == null) {
                    addViolation("property-no-setter",
                                 new Object[]{fmd}, true);
                    continue;
                }
                else if (!getRedefine()) {
                    // create synthetic setter
                    MethodNode setterNode = new MethodNode(Opcodes.ACC_PRIVATE,
                                                           getSetterName(fmd),
                                                           Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(fmd.getDeclaredType())),
                                                           null, null);
                    //X TODO: pc or managedType?
                    pc.getClassNode().methods.add(setterNode);
                    InsnList instructions = setterNode.instructions;
                    instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // this
                    instructions.add(new VarInsnNode(AsmHelper.getLoadInsn(fmd.getDeclaredType()), 1)); // param1
                    instructions.add(new FieldInsnNode(Opcodes.PUTFIELD,
                                                       Type.getInternalName(returned.getDeclaringClass()),
                                                       returned.getName(),
                                                       Type.getDescriptor(fmd.getDeclaredType())));
                    instructions.add(new InsnNode(Opcodes.RETURN));
                }
            }

            if (setter != null) {
                assigned = getAssignedField(classNode, getMethod(fmd.getDeclaringType(), fmd.getSetterName(), new Class[]{fmd.getDeclaredType()}));
            }

            if (assigned != null) {
                if (setter != null) {
                    registerBackingFieldInfo(fmd, setter, assigned);
                }

                if (!assigned.equals(returned)) {
                    addViolation("property-setter-getter-mismatch", new Object[]
                            {fmd, assigned.getName(), (returned == null)
                                    ? null : returned.getName()}, false);
                }
            }
        }
    }

    private void registerBackingFieldInfo(FieldMetaData fmd, Method method, Field field) {
        if (_backingFields == null) {
            _backingFields = new HashMap();
        }
        _backingFields.put(method.getName(), field.getName());

        if (_attrsToFields == null) {
            _attrsToFields = new HashMap();
        }
        _attrsToFields.put(fmd.getName(), field.getName());

        if (_fieldsToAttrs == null) {
            _fieldsToAttrs = new HashMap();
        }
        _fieldsToAttrs.put(field.getName(), fmd.getName());
    }


    private void addAttributeTranslation() {

        // Get all field metadata
        ArrayList<Integer> propFmds = new ArrayList<>();
        FieldMetaData[] fmds = _meta.getFields();

        if (_meta.isMixedAccess()) {
            // Stores indexes of property access fields to be used in
            //
            propFmds = new ArrayList<>();

            // Determine which fields have property access and save their
            // indexes
            for (int i = 0; i < fmds.length; i++) {
                if (isPropertyAccess(fmds[i])) {
                    propFmds.add(i);
                }
            }

            // if no fields have property access do not do attribute translation
            if (propFmds.size() == 0) {
                return;
            }
        }

        ClassNode classNode = pc.getClassNode();
        classNode.interfaces.add(Type.getInternalName(AttributeTranslator.class));

        MethodNode attrIdxMeth = new MethodNode(Opcodes.ACC_PUBLIC,
                                                PRE + "AttributeIndexToFieldName",
                                                Type.getMethodDescriptor(Type.getType(String.class), Type.INT_TYPE),
                                                null, null);
        classNode.methods.add(attrIdxMeth);

        InsnList instructions = attrIdxMeth.instructions;

        // switch (val)
        instructions.add(new VarInsnNode(Opcodes.ILOAD, 1)); // int param of the method
        if (!_meta.isMixedAccess()) {
            // if not mixed access use a table switch on all property-based fmd.
            // a table switch is more efficient with +1 incremental operations

            LabelNode defLbl = new LabelNode();
            TableSwitchInsnNode switchNd = new TableSwitchInsnNode(0, fmds.length - 1, defLbl);
            instructions.add(switchNd);

            // case i:
            //     return <_attrsToFields.get(fmds[i].getName())>
            for (FieldMetaData fmd : fmds) {
                LabelNode caseLabel = new LabelNode();
                switchNd.labels.add(caseLabel);
                instructions.add(caseLabel);
                instructions.add(AsmHelper.getLoadConstantInsn(_attrsToFields.get(fmd.getName())));
                instructions.add(new InsnNode(Opcodes.ARETURN));
            }

            // default: throw new IllegalArgumentException ()
            instructions.add(defLbl);
            instructions.add(AsmHelper.throwException(IllegalArgumentException.class));
        }
        else {
            // In mixed access mode, property indexes are not +1 incremental
            // a lookup switch must be used to do indexed lookup.
            LabelNode defLbl = new LabelNode();
            LookupSwitchInsnNode switchNd = new LookupSwitchInsnNode(defLbl, null, null);
            instructions.add(switchNd);
            for (Integer i : propFmds) {
                LabelNode caseLabel = new LabelNode();
                instructions.add(caseLabel);
                switchNd.labels.add(caseLabel);
                switchNd.keys.add(propFmds.get(i));
                instructions.add(AsmHelper.getLoadConstantInsn(_attrsToFields.get(fmds[i].getName())));
                instructions.add(new InsnNode(Opcodes.ARETURN));
