package de.uni_hamburg.fs;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import de.renew.util.Null;
import de.renew.util.Types;
import de.renew.util.Value;


/**
 * A JavaArrayType wraps a java array, {@link Enumeration} or
 * {@link Iterator} object as a Type.
 * It builds a List of the component type containing all elements of
 * the array, enumeration or iterator.
 **/
public class JavaArrayType extends ListType implements JavaType {
    /**
     * The wrapped Java array containing the elements of this type.
     */
    Object[] _javaArray;

    /**
     * Cache for hashcode to avoid recalculation.
     */
    private int _hashCode;

    /**
     * The root node of the list structure built from the array elements.
     */
    private AbstractNode _root = null;

    /**
     * Construct a new wrapper for the given Java Array.
     */
    JavaArrayType(Object javaArray) {
        super(
            TypeSystem.instance().getType(javaArray.getClass().getComponentType()),
            length(javaArray) == 0 ? ELIST : NELIST);
        this._javaArray = makeObjectArray(javaArray);
    }

    /**
     * Construct a new wrapper for the given Enumeration.
     */
    JavaArrayType(Enumeration<?> enumeration) {
        super(
            TypeSystem.instance().getType(Object.class),
            enumeration.hasMoreElements() ? NELIST : ELIST);
        _javaArray = makeObjectArray(enumeration);
    }

    /**
     * Construct a new wrapper for the given Iterator.
     */
    JavaArrayType(Iterator<?> it) {
        super(TypeSystem.instance().getType(Object.class), it.hasNext() ? NELIST : ELIST);
        _javaArray = makeObjectArray(it);
    }

    /**
     * Converts various input types (Enumeration, Iterator, or array) into an Object array.
     * For primitive arrays, the elements are wrapped into their corresponding object types.
     *
     * @param array the input object to convert (Enumeration, Iterator or array)
     * @return an Object array containing all elements from the input
     * @throws IllegalArgumentException if the input is not an array, Enumeration or Iterator
     */
    public static Object[] makeObjectArray(Object array) {
        if (array instanceof Enumeration) {
            Enumeration<?> enumeration = (Enumeration<?>) array;
            Vector<Object> elements = new Vector<Object>();
            while (enumeration.hasMoreElements()) {
                elements.addElement(enumeration.nextElement());
            }
            Object[] objectArray = new Object[elements.size()];
            elements.copyInto(objectArray);
            return objectArray;
        } else if (array instanceof Iterator) {
            Iterator<?> it = (Iterator<?>) array;
            List<Object> elements = new ArrayList<Object>();
            while (it.hasNext()) {
                elements.add(it.next());
            }
            Object[] objectArray = elements.toArray();
            return objectArray;
        }

        // "array" must be array type:
        if (array == null || !array.getClass().isArray()) {
            throw new IllegalArgumentException();
        }
        Class<?> clazz = array.getClass().getComponentType();
        if (clazz.isPrimitive()) {
            // copy components and wrap primitive types as Values:
            Value[] objectArray = new Value[length(array)];
            for (int i = 0; i < objectArray.length; ++i) {
                Object val;
                if (clazz == Integer.TYPE) {
                    val = Integer.valueOf(((int[]) array)[i]);
                } else if (clazz == Character.TYPE) {
                    val = Character.valueOf(((char[]) array)[i]);
                } else if (clazz == Double.TYPE) {
                    val = Double.valueOf(((double[]) array)[i]);
                } else if (clazz == Float.TYPE) {
                    val = Float.valueOf(((float[]) array)[i]);
                } else if (clazz == Long.TYPE) {
                    val = Long.valueOf(((long[]) array)[i]);
                } else if (clazz == Byte.TYPE) {
                    val = Byte.valueOf(((byte[]) array)[i]);
                } else if (clazz == Boolean.TYPE) {
                    val = Boolean.valueOf(((boolean[]) array)[i]);
                } else {
                    throw new RuntimeException("Unknown primitive class: " + clazz);
                }
                objectArray[i] = new Value(val);
            }
            return objectArray;
        } else {
            return (Object[]) array;
        }
    }

    /**
     * Converts an Object array back to a primitive array if needed.
     * If the input array contains Value objects wrapping primitive types,
     * they are unwrapped into a new primitive array.
     *
     * @param objectArray the Object array to convert
     * @return either the original array or a new primitive array with unwrapped values
     * @throws RuntimeException if an unknown primitive type is encountered
     */
    public static Object makeArray(Object[] objectArray) {
        if (objectArray.length > 0 && objectArray.getClass().getComponentType() == Value.class) {
            Class<?> clazz = Types.typify(((Value) objectArray[0]).value.getClass());

            // copy components and unwrap Values to primitive types:
            Object array = Array.newInstance(clazz, objectArray.length);
            for (int i = 0; i < objectArray.length; ++i) {
                Object val = ((Value) objectArray[i]).value;
                if (clazz == Integer.TYPE) {
                    ((int[]) array)[i] = ((Integer) val).intValue();
                } else if (clazz == Character.TYPE) {
                    ((char[]) array)[i] = ((Character) val).charValue();
                } else if (clazz == Double.TYPE) {
                    ((double[]) array)[i] = ((Double) val).doubleValue();
                } else if (clazz == Float.TYPE) {
                    ((float[]) array)[i] = ((Float) val).floatValue();
                } else if (clazz == Long.TYPE) {
                    ((long[]) array)[i] = ((Long) val).longValue();
                } else if (clazz == Byte.TYPE) {
                    ((byte[]) array)[i] = ((Byte) val).byteValue();
                } else if (clazz == Boolean.TYPE) {
                    ((boolean[]) array)[i] = ((Boolean) val).booleanValue();
                } else {
                    throw new RuntimeException("Unknown primitive class: " + clazz);
                }
            }
            return array;
        } else {
            return objectArray;
        }
    }

    /**
     * Returns the length of the given array, handling both primitive and object arrays.
     *
     * @param array the array to get the length from
     * @return the length of the array
     */
    public static int length(Object array) {
        Class<?> clazz = array.getClass().getComponentType();
        if (clazz.isPrimitive()) {
            // cast to the right primitve array type
            if (clazz == Integer.TYPE) {
                return ((int[]) array).length;
            } else if (clazz == Character.TYPE) {
                return ((char[]) array).length;
            } else if (clazz == Double.TYPE) {
                return ((double[]) array).length;
            } else if (clazz == Float.TYPE) {
                return ((float[]) array).length;
            } else if (clazz == Long.TYPE) {
                return ((long[]) array).length;
            } else if (clazz == Byte.TYPE) {
                return ((byte[]) array).length;
            } else if (clazz == Boolean.TYPE) {
                return ((boolean[]) array).length;
            }
        }
        return ((Object[]) array).length;
    }

    /**
     * Compares two objects that may be arrays, handling null values.
     * If both objects are arrays, their contents are compared.
     *
     * @param o1 the first object to compare
     * @param o2 the second object to compare
     * @return true if the objects are equal or both arrays with equal contents
     */
    public static boolean equals(Object o1, Object o2) {
        if (Null.nullAwareEquals(o1, o2)) {
            return true;
        }
        try {
            return equals(makeObjectArray(o1), makeObjectArray(o2));
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    /**
     * Compares two Object arrays for equality by checking their length,
     * component type and elements.
     *
     * @param a1 the first array to compare
     * @param a2 the second array to compare
     * @return true if both arrays have same length, component type and equal elements
     */
    public static boolean equals(Object[] a1, Object[] a2) {
        if (a1.length == a2.length) {
            if (a1.getClass().getComponentType() == a2.getClass().getComponentType()) {
                for (int i = 0; i < a1.length; ++i) {
                    if (!Null.nullAwareEquals(a1[i], a2[i])) {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean equals(Object that) {
        if (this == that) {
            return true;
        }
        if (that instanceof JavaArrayType) {
            JavaArrayType thatJAT = (JavaArrayType) that;
            return equals(_javaArray, thatJAT._javaArray);
        }
        return false;
    }

    /**
     * Return whether this Type represents an instance.
     */
    @Override
    public boolean isInstanceType() {
        return true;
    }

    @Override
    public Object getJavaObject() {
        return _javaArray;
    }

    @Override
    public int hashCode() {
        buildList();
        return _hashCode;
    }

    @Override
    public String toString() {
        return BasicType.objToString(_javaArray);
    }

    /**
     * Return whether this Type subsumes {@literal <that>} Type.
     * In other words, return wheter this Type is more general than {@literal <that>} Type.
     */
    @Override
    public boolean subsumes(Type that) {
        return equals(that);
    }

    /**
     * Return the unification of this Type and {@literal <that} Type.
     * this Type is not modified!
     */
    @Override
    public Type unify(Type that) throws UnificationFailure {
        if (that instanceof JavaArrayType) {
            if (equals(that)) {
                return this;
            }
        } else if (that instanceof ConjunctiveType) {
            return that.unify(this);
        }
        throw new UnificationFailure();
    }

    /**
     * Return whether this Type and {@literal <that>} Type are compatible.
     */
    @Override
    public boolean canUnify(Type that) {
        try {
            unify(that);
            return true;
        } catch (UnificationFailure uff) {
            return false;
        }
    }

    /**
     * Look for the most general common extensional supertype of this and {@literal <that>}.
     */
    @Override
    public Type mostGeneralExtensionalSupertype(Type that) {
        // TODO
        return null;
    }

    private void buildList() {
        if (_root == null) {
            // build the actual list:
            _root = (AbstractNode) ListType.getEList(getBaseType()).newNode();
            _hashCode = 0;
            ListType listType = ListType.getNEList(getBaseType());
            for (int i = _javaArray.length - 1; i >= 0; --i) {
                _hashCode += Null.nullAwareHashCode(_javaArray[i]);
                //root=new JavaArrayNode(this,javaArray[i],root,i,hashCode);
                Node tail = _root;
                _root = new ListNode(listType);
                _root.setFeature(ListType.HEAD, JavaObject.getJavaType(_javaArray[i]).newNode());
                _root.setFeature(ListType.TAIL, tail);
            }
            _root._nodetype = this;
            _hashCode += 3 * getBaseType().hashCode();
        }
    }

    @Override
    public Node newNode() {
        buildList();
        return _root; // always return the same node!
    }
}