/*
 *  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 libcore.reflect;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.AnnotationTypeMismatchException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * This class represents member element of an annotation.
 * It consists of name and value, supplemented with element
 * definition information (such as declared type of element).
 * <br>The value may be one of the following types:
 * <ul>
 * <li> boxed primitive
 * <li> Class
 * <li> enum constant
 * <li> annotation (nested)
 * <li> one-dimensional array of the above
 * <li> Throwable
 * </ul>
 * The last type is specific for this implementation; a Throwable value
 * means that the error occured during parsing or resolution of corresponding
 * class-data structures and throwing is delayed until the element
 * is requested for value.
 *
 * @see AnnotationFactory
 *
 * @author Alexey V. Varlamov, Serguei S. Zapreyev
 * @version $Revision$
 */
@SuppressWarnings({"serial"})
public final class AnnotationMember implements Serializable {

    /**
     * Tag description of a Throwable value type.
     */
    protected static final char ERROR = '!';

    /**
     * Tag description of an array value type.
     */
    protected static final char ARRAY = '[';

    /**
     * Tag description of all value types except arrays and Throwables.
     */
    protected static final char OTHER = '*';

//    public static final char INT = 'I';
//    public static final char CHAR = 'C';
//    public static final char DOUBLE = 'D';
//    public static final char FLOAT = 'F';
//    public static final char BYTE = 'B';
//    public static final char LONG = 'J';
//    public static final char SHORT = 'S';
//    public static final char BOOL = 'Z';
//    public static final char CLASS = 'c';
//    public static final char ENUM = 'e';
//    public static final char ANTN = '@';

    private enum DefaultValues {NO_VALUE}

    /**
     * Singleton representing missing element value.
     */
    protected static final Object NO_VALUE = DefaultValues.NO_VALUE;

    protected final String name;
    protected final Object value; // a primitive value is wrapped to the corresponding wrapper class
    protected final char tag;
    // no sense to serialize definition info as it can be changed arbitrarily
    protected transient Class<?> elementType;
    protected transient Method definingMethod;


    /**
     * Creates a new element with specified name and value.
     * Definition info will be provided later when this
     * element becomes actual annotation member.
     * @param name element name, must not be null
     * @param val element value, should be of addmissible type,
     * as specified in the description of this class
     *
     * @see #setDefinition(AnnotationMember)
     */
    public AnnotationMember(String name, Object val) {
        this.name = name;
        value = val == null ? NO_VALUE : val;
        if (value instanceof Throwable) {
            tag = ERROR;
        } else if (value.getClass().isArray()) {
            tag = ARRAY;
        } else {
            tag = OTHER;
        }
    }

    /**
     * Creates the completely defined element.
     * @param name element name, must not be null
     * @param value element value, should be of addmissible type,
     * as specified in the description of this class
     * @param m element-defining method, reflected on the annotation type
     * @param type declared type of this element
     * (return type of the defining method)
     */
    public AnnotationMember(String name, Object val, Class type, Method m) {
        this(name, val);

        definingMethod = m;

        if (type == int.class) {
            elementType = Integer.class;
        } else if (type == boolean.class) {
            elementType = Boolean.class;
        } else if (type == char.class) {
            elementType = Character.class;
        } else if (type == float.class) {
            elementType = Float.class;
        } else if (type == double.class) {
            elementType = Double.class;
        } else if (type == long.class) {
            elementType = Long.class;
        } else if (type == short.class) {
            elementType = Short.class;
        } else if (type == byte.class) {
            elementType = Byte.class;
        } else {
            elementType = type;
        }
    }

    /**
     * Fills in element's definition info and returns this.
     */
    protected AnnotationMember setDefinition(AnnotationMember copy) {
        definingMethod = copy.definingMethod;
        elementType = copy.elementType;
        return this;
    }

    /**
     * Returns readable description of this annotation value.
     */
    public String toString() {
        if (tag == ARRAY) {
            StringBuilder sb = new StringBuilder(80);
            sb.append(name).append("=[");
            int len = Array.getLength(value);
            for (int i = 0; i < len; i++) {
                if (i != 0) sb.append(", ");
                sb.append(Array.get(value, i));
            }
            return sb.append("]").toString();
        } else {
            return name+ "=" +value;
        }
    }

    /**
     * Returns true if the specified object represents equal element
     * (equivalent name-value pair).
     * <br> A special case is the contained Throwable value; it is considered
     * transcendent so no other element would be equal.
     * @return true if passed object is equivalent element representation,
     * false otherwise
     * @see #equalArrayValue(Object)
     * @see java.lang.annotation.Annotation#equals(Object)
     */
    public boolean equals(Object obj) {
        if (obj == this) {
            // not a mere optimization,
            // this is needed for consistency with hashCode()
            return true;
        }
        if (obj instanceof AnnotationMember) {
            AnnotationMember that = (AnnotationMember)obj;
            if (name.equals(that.name) && tag == that.tag) {
                if (tag == ARRAY) {
                    return equalArrayValue(that.value);
                } else if (tag == ERROR) {
                    // undefined value is incomparable (transcendent)
                    return false;
                } else {
                    return value.equals(that.value);
                }
            }
        }
        return false;
    }

    /**
     * Returns true if the contained value and a passed object are equal arrays,
     * false otherwise. Appropriate overloaded method of Arrays.equals()
     * is used for equality testing.
     * @see java.util.Arrays#equals(java.lang.Object[], java.lang.Object[])
     * @return true if the value is array and is equal to specified object,
     * false otherwise
     */
    public boolean equalArrayValue(Object otherValue) {
        if (value instanceof Object[] && otherValue instanceof Object[]) {
            return Arrays.equals((Object[])value, (Object[])otherValue);
        }
        Class type = value.getClass();
        if (type != otherValue.getClass()) {
            return false;
        }
        if (type == int[].class) {
            return Arrays.equals((int[])value, (int[])otherValue);
        } else if (type == byte[].class) {
            return Arrays.equals((byte[])value, (byte[])otherValue);
        } else if (type == short[].class) {
            return Arrays.equals((short[])value, (short[])otherValue);
        } else if (type == long[].class) {
            return Arrays.equals((long[])value, (long[])otherValue);
        } else if (type == char[].class) {
            return Arrays.equals((char[])value, (char[])otherValue);
        } else if (type == boolean[].class) {
            return Arrays.equals((boolean[])value, (boolean[])otherValue);
        } else if (type == float[].class) {
            return Arrays.equals((float[])value, (float[])otherValue);
        } else if (type == double[].class) {
            return Arrays.equals((double[])value, (double[])otherValue);
        }
        return false;
    }

    /**
     * Computes hash code of this element. The formula is as follows:
     * <code> (name.hashCode() * 127) ^ value.hashCode() </code>
     * <br>If value is an array, one of overloaded Arrays.hashCode()
     * methods is used.
     * @return the hash code
     * @see java.util.Arrays#hashCode(java.lang.Object[])
     * @see java.lang.annotation.Annotation#hashCode()
     */
    public int hashCode() {
        int hash = name.hashCode() * 127;
        if (tag == ARRAY) {
            Class type = value.getClass();
            if (type == int[].class) {
                return hash ^ Arrays.hashCode((int[])value);
            } else if (type == byte[].class) {
                return hash ^ Arrays.hashCode((byte[])value);
            } else if (type == short[].class) {
                return hash ^ Arrays.hashCode((short[])value);
            } else if (type == long[].class) {
                return hash ^ Arrays.hashCode((long[])value);
            } else if (type == char[].class) {
                return hash ^ Arrays.hashCode((char[])value);
            } else if (type == boolean[].class) {
                return hash ^ Arrays.hashCode((boolean[])value);
            } else if (type == float[].class) {
                return hash ^ Arrays.hashCode((float[])value);
            } else if (type == double[].class) {
                return hash ^ Arrays.hashCode((double[])value);
            }
            return hash ^ Arrays.hashCode((Object[])value);
        } else {
            return hash ^ value.hashCode();
        }
    }

    /**
     * Throws contained error (if any) with a renewed stack trace.
     */
    public void rethrowError() throws Throwable {
        if (tag == ERROR) {
            // need to throw cloned exception for thread safety
            // besides it is better to provide actual stack trace
            // rather than recorded during parsing

            // first check for expected types
            if (value instanceof TypeNotPresentException) {
                TypeNotPresentException tnpe = (TypeNotPresentException)value;
                throw new TypeNotPresentException(tnpe.typeName(), tnpe.getCause());
            } else if (value instanceof EnumConstantNotPresentException) {
                EnumConstantNotPresentException ecnpe = (EnumConstantNotPresentException)value;
                throw new EnumConstantNotPresentException(ecnpe.enumType(), ecnpe.constantName());
            } else if (value instanceof ArrayStoreException) {
                ArrayStoreException ase = (ArrayStoreException)value;
                throw new ArrayStoreException(ase.getMessage());
            }
            // got some other error, have to go with deep cloning
            // via serialization mechanism
            Throwable error = (Throwable)value;
            StackTraceElement[] ste = error.getStackTrace();
            ByteArrayOutputStream bos = new ByteArrayOutputStream(
                    ste == null ? 512 : (ste.length + 1) * 80);
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(error);
            oos.flush();
            oos.close();
            ByteArrayInputStream bis = new ByteArrayInputStream(bos
                    .toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            error = (Throwable)ois.readObject();
            ois.close();

            throw error;
        }
    }

    /**
     * Validates contained value against its member definition
     * and if ok returns the value.
     * Otherwise, if the value type mismatches definition
     * or the value itself describes an error,
     * throws appropriate exception.
     * <br> Note, this method may return null if this element was constructed
     * with such value.
     *
     * @see #rethrowError()
     * @see #copyValue()
     * @return actual valid value or null if no value
     */
    public Object validateValue() throws Throwable {
        if (tag == ERROR) {
            rethrowError();
        }
        if (value == NO_VALUE) {
            return null;
        }
        if (elementType == value.getClass()
                || elementType.isInstance(value)) { // nested annotation value
            return copyValue();
        } else {
            throw new AnnotationTypeMismatchException(definingMethod,
                    value.getClass().getName());
        }

    }


    /**
     * Provides mutation-safe access to contained value. That is, caller is free
     * to modify the returned value, it will not affect the contained data value.
     * @return cloned value if it is mutable or the original immutable value
     */
    public Object copyValue() throws Throwable
    {
        if (tag != ARRAY || Array.getLength(value) == 0) {
            return value;
        }
        Class type = value.getClass();
        if (type == int[].class) {
            return ((int[])value).clone();
        } else if (type == byte[].class) {
            return ((byte[])value).clone();
        } else if (type == short[].class) {
            return ((short[])value).clone();
        } else if (type == long[].class) {
            return ((long[])value).clone();
        } else if (type == char[].class) {
            return ((char[])value).clone();
        } else if (type == boolean[].class) {
            return ((boolean[])value).clone();
        } else if (type == float[].class) {
            return ((float[])value).clone();
        } else if (type == double[].class) {
            return ((double[])value).clone();
        }
        return ((Object[])value).clone();
    }
}
