/*
 * 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
 *
 *   https://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.bcel.generic;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import org.apache.bcel.Const;
import org.apache.bcel.classfile.Annotations;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantObject;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantValue;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.util.BCELComparator;

/**
 * Template class for building up a field. The only extraordinary thing one can do is to add a constant value attribute
 * to a field (which must of course be compatible with to the declared type).
 *
 * @see Field
 */
public class FieldGen extends FieldGenOrMethodGen {

    private static BCELComparator<FieldGen> bcelComparator = new BCELComparator<FieldGen>() {

        @Override
        public boolean equals(final FieldGen a, final FieldGen b) {
            return a == b || a != null && b != null && Objects.equals(a.getName(), b.getName()) && Objects.equals(a.getSignature(), b.getSignature());
        }

        @Override
        public int hashCode(final FieldGen o) {
            return o != null ? Objects.hash(o.getSignature(), o.getName()) : 0;
        }
    };

    /**
     * Gets the comparison strategy object.
     *
     * @return Comparison strategy object.
     */
    public static BCELComparator<FieldGen> getComparator() {
        return bcelComparator;
    }

    /**
     * Sets the comparison strategy object.
     *
     * @param comparator Comparison strategy object.
     */
    public static void setComparator(final BCELComparator<FieldGen> comparator) {
        bcelComparator = comparator;
    }

    private Object value;

    private List<FieldObserver> observers;

    /**
     * Instantiate from existing field.
     *
     * @param field Field object.
     * @param cp constant pool (must contain the same entries as the field's constant pool).
     */
    public FieldGen(final Field field, final ConstantPoolGen cp) {
        this(field.getAccessFlags(), Type.getType(field.getSignature()), field.getName(), cp);
        final Attribute[] attrs = field.getAttributes();
        for (final Attribute attr : attrs) {
            if (attr instanceof ConstantValue) {
                setValue(((ConstantValue) attr).getConstantValueIndex());
            } else if (attr instanceof Annotations) {
                final Annotations runtimeAnnotations = (Annotations) attr;
                runtimeAnnotations.forEach(element -> addAnnotationEntry(new AnnotationEntryGen(element, cp, false)));
            } else {
                addAttribute(attr);
            }
        }
    }

    /**
     * Declare a field. If it is static (isStatic() == true) and has a basic type like int or String it may have an initial
     * value associated with it as defined by setInitValue().
     *
     * @param accessFlags access qualifiers.
     * @param type field type.
     * @param name field name.
     * @param cp constant pool.
     */
    public FieldGen(final int accessFlags, final Type type, final String name, final ConstantPoolGen cp) {
        super(accessFlags);
        setType(type);
        setName(name);
        setConstantPool(cp);
    }

    private void addAnnotationsAsAttribute(final ConstantPoolGen cp) {
        Stream.of(AnnotationEntryGen.getAnnotationAttributes(cp, super.getAnnotationEntries())).forEach(this::addAttribute);
    }

    private int addConstant() {
        switch (super.getType().getType()) { // sic
        case Const.T_INT:
        case Const.T_CHAR:
        case Const.T_BYTE:
        case Const.T_BOOLEAN:
        case Const.T_SHORT:
            return super.getConstantPool().addInteger(((Integer) value).intValue());
        case Const.T_FLOAT:
            return super.getConstantPool().addFloat(((Float) value).floatValue());
        case Const.T_DOUBLE:
            return super.getConstantPool().addDouble(((Double) value).doubleValue());
        case Const.T_LONG:
            return super.getConstantPool().addLong(((Long) value).longValue());
        case Const.T_REFERENCE:
            return super.getConstantPool().addString((String) value);
        default:
            throw new IllegalStateException("Unhandled : " + super.getType().getType()); // sic
        }
    }

    /**
     * Add observer for this object.
     *
     * @param o the observer to add.
     */
    public void addObserver(final FieldObserver o) {
        if (observers == null) {
            observers = new ArrayList<>();
        }
        observers.add(o);
    }

    /**
     * Remove any initial value.
     */
    public void cancelInitValue() {
        value = null;
    }

    private void checkType(final Type atype) {
        final Type superType = super.getType();
        if (superType == null) {
            throw new ClassGenException("You haven't defined the type of the field yet");
        }
        if (!isFinal()) {
            throw new ClassGenException("Only final fields may have an initial value!");
        }
        if (!superType.equals(atype)) {
            throw new ClassGenException("Types are not compatible: " + superType + " vs. " + atype);
        }
    }

    /**
     * Creates a deep copy of this field.
     *
     * @param cp the constant pool.
     * @return deep copy of this field.
     */
    public FieldGen copy(final ConstantPoolGen cp) {
        final FieldGen fg = (FieldGen) clone();
        fg.setConstantPool(cp);
        return fg;
    }

    /**
     * Return value as defined by given BCELComparator strategy. By default two FieldGen objects are said to be equal when
     * their names and signatures are equal.
     *
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals(final Object obj) {
        return obj instanceof FieldGen && bcelComparator.equals(this, (FieldGen) obj);
    }

    /**
     * Gets field object after having set up all necessary values.
     *
     * @return the field object.
     */
    public Field getField() {
        final String signature = getSignature();
        final int nameIndex = super.getConstantPool().addUtf8(super.getName());
        final int signatureIndex = super.getConstantPool().addUtf8(signature);
        if (value != null) {
            checkType(super.getType());
            final int index = addConstant();
            addAttribute(new ConstantValue(super.getConstantPool().addUtf8("ConstantValue"), 2, index, super.getConstantPool().getConstantPool())); // sic
        }
        addAnnotationsAsAttribute(super.getConstantPool());
        return new Field(super.getAccessFlags(), nameIndex, signatureIndex, getAttributes(), super.getConstantPool().getConstantPool()); // sic
    }

    /**
     * Gets the initial value.
     *
     * @return the initial value.
     */
    public String getInitValue() {
        return Objects.toString(value, null);
    }

    @Override
    public String getSignature() {
        return super.getType().getSignature();
    }

    /**
     * Return value as defined by given BCELComparator strategy. By default return the hash code of the field's name XOR
     * signature.
     *
     * @see Object#hashCode()
     */
    @Override
    public int hashCode() {
        return bcelComparator.hashCode(this);
    }

    /**
     * Remove observer for this object.
     *
     * @param o the observer to remove.
     */
    public void removeObserver(final FieldObserver o) {
        if (observers != null) {
            observers.remove(o);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param b the boolean value.
     */
    public void setInitValue(final boolean b) {
        checkType(Type.BOOLEAN);
        if (b) {
            value = Integer.valueOf(1);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param b the byte value.
     */
    public void setInitValue(final byte b) {
        checkType(Type.BYTE);
        if (b != 0) {
            value = Integer.valueOf(b);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param c the char value.
     */
    public void setInitValue(final char c) {
        checkType(Type.CHAR);
        if (c != 0) {
            value = Integer.valueOf(c);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param d the double value.
     */
    public void setInitValue(final double d) {
        checkType(Type.DOUBLE);
        if (d != 0.0) {
            value = Double.valueOf(d);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param f the float value.
     */
    public void setInitValue(final float f) {
        checkType(Type.FLOAT);
        if (f != 0.0) {
            value = Float.valueOf(f);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param i the int value.
     */
    public void setInitValue(final int i) {
        checkType(Type.INT);
        if (i != 0) {
            value = Integer.valueOf(i);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param l the long value.
     */
    public void setInitValue(final long l) {
        checkType(Type.LONG);
        if (l != 0L) {
            value = Long.valueOf(l);
        }
    }

    /**
     * Sets the initial value.
     *
     * @param s the short value.
     */
    public void setInitValue(final short s) {
        checkType(Type.SHORT);
        if (s != 0) {
            value = Integer.valueOf(s);
        }
    }

    /**
     * Sets (optional) initial value of field, otherwise it will be set to null/0/false by the JVM automatically.
     *
     * @param str the string value.
     */
    public void setInitValue(final String str) {
        checkType(ObjectType.getInstance("java.lang.String"));
        if (str != null) {
            value = str;
        }
    }

    private void setValue(final int index) {
        final ConstantPool cp = super.getConstantPool().getConstantPool();
        final Constant c = cp.getConstant(index);
        value = ((ConstantObject) c).getConstantValue(cp);
    }

    /**
     * Return string representation close to declaration format, for example: 'public static final short MAX = 100'.
     *
     * @return String representation of field.
     */
    @Override
    public final String toString() {
        // Short cuts to constant pool
        String access = Utility.accessToString(super.getAccessFlags());
        access = access.isEmpty() ? "" : access + " ";
        final String signature = super.getType().toString();
        final String name = getName();
        final StringBuilder buf = new StringBuilder(32); // CHECKSTYLE IGNORE MagicNumber
        buf.append(access).append(signature).append(" ").append(name);
        final String value = getInitValue();
        if (value != null) {
            buf.append(" = ").append(value);
        }
        return buf.toString();
    }

    /**
     * Call notify() method on all observers. This method is not called automatically whenever the state has changed, but
     * has to be called by the user after they have finished editing the object.
     */
    public void update() {
        if (observers != null) {
            for (final FieldObserver observer : observers) {
                observer.notify(this);
            }
        }
    }
}
