package de.uni_hamburg.fs;

import java.util.Enumeration;
import java.util.Iterator;

import collections.CollectionEnumeration;
import collections.HashedMap;
import collections.UpdatableMap;

import de.renew.util.Value;


/**
 * A JavaObject wraps a non-primitive java object as a Type and
 * at the same time as a node.
 * It reflects the public (bean) attributes of the java object as features.
 * For wrapping primitive java objects as Types/Nodes, use
 * BasicObjectType/Node.
 **/
public class JavaObject extends JavaClassType implements Node {
    /**
     * The wrapped Java object
     */
    Object _javaObject;

    /**
     * The concept representing the type of the Java object
     */
    JavaConcept _concept;

    /**
     * Cache for feature values to avoid repeated reflection
     */
    UpdatableMap _featureCache = new HashedMap();

    /**
     * Constructs a new wrapper for the given Java Object.
     * @param javaObject the Java object to wrap
     * @throws RuntimeException if the object is null or of an unsupported type
     */
    JavaObject(Object javaObject) {
        if (javaObject == null || javaObject instanceof String || javaObject instanceof Value
            || javaObject.getClass().isArray() || javaObject instanceof Enumeration
            || javaObject instanceof Iterator) {
            throw new RuntimeException(
                "Someone tried to build a JavaObject for " + javaObject
                    + (javaObject == null ? "" : "(" + javaObject.getClass().getName() + ")"));
        }

        this._javaObject = javaObject;
        Class<?> clazz = javaObject.getClass();
        this._concept = TypeSystem.instance().getJavaConcept(clazz);
        setJavaClass(clazz);
    }

    /**
     * Returns a JavaType for the given Java object.
     * Different JavaType implementations are used based on the object type:
     * - null -> NullObject
     * - String or Value -> BasicType
     * - Arrays -> JavaArrayType
     * - Enumerations/Iterators -> JavaArrayType
     * - Other objects -> JavaObject
     * @param javaObject the Java object to wrap
     * @return a JavaType appropriate for the given object
     */
    public static JavaType getJavaType(Object javaObject) {
        if (javaObject == null) {
            return NullObject.INSTANCE;
        }


        //      if (javaObject instanceof Class) {
        //        if (javaObject==String.class || javaObject==Value.class) {
        //      return new BasicType((Class)javaObject);
        //        } else {
        //      return new ConjunctiveType(TypeSystem.instance().getJavaConcept((Class)javaObject));
        //        }
        //      }
        if (javaObject instanceof String || javaObject instanceof Value) {
            return new BasicType(javaObject);
        }
        if (javaObject.getClass().isArray()) {
            return new JavaArrayType(javaObject);
        }
        if (javaObject instanceof Enumeration) {
            return new JavaArrayType((Enumeration<?>) javaObject);
        }
        if (javaObject instanceof Iterator) {
            return new JavaArrayType((Iterator<?>) javaObject);
        }
        return new JavaObject(javaObject);
    }

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

    /**
     * Return the instantiated version of this Type.
     */
    @Override
    public Type getInstanceType() {
        return this;
    }

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

    @Override
    public int hashCode() {
        // workaround for jdk1.1 bug in hashCode() of java.awt.Dimension:
        if (_javaObject instanceof java.awt.Dimension) {
            java.awt.Dimension dim = (java.awt.Dimension) _javaObject;
            return dim.width * 3 + dim.height;
        }
        return _javaObject.hashCode();
    }

    @Override
    public boolean equals(Object that) {
        if (that instanceof JavaObject) {
            JavaObject thatJO = (JavaObject) that;
            return _javaObject.equals(thatJO._javaObject);
        }
        return false;
    }

    /**
     * Return the name of this Type.
     */
    @Override
    public String getName() {
        //return BasicType.objToString(javaObject);
        return _concept.getName();
    }

    /**
     * Return the fully qualified name of this Type.
     */
    @Override
    public String getFullName() {
        return _concept.getFullName();
    }

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

    @Override
    public boolean isApprop(Name featureName) {
        return _concept.isApprop(featureName);
    }

    @Override
    public CollectionEnumeration appropFeatureNames() {
        return _concept.appropFeatureNames();
    }

    @Override
    public Type appropType(Name featureName) {
        return _concept.appropType(featureName);
    }

    /**
     * Return whether this Type is extensional.
     */
    @Override
    public boolean isExtensional() {
        return _concept.isExtensional();
    }

    /**
     * Return whether this Type subsumes {@literal <that>} Type.
     * In other words, return whether this Type is more general than {@literal <that>} Type.
     */
    @Override
    public boolean subsumes(Type that) {
        if (that instanceof JavaObject) {
            JavaObject thatJO = (JavaObject) that;
            return _javaObject.equals(thatJO._javaObject);
        }
        return false;
    }

    /**
     * Return the unification of this Type and {@literal <that>} Type.
     * this Type is not modified!
     */
    @Override
    public Type unify(Type that) throws UnificationFailure {
        // special case for other JavaObject:
        if (that instanceof JavaType) {
            JavaType thatJT = (JavaType) that;
            if (_javaObject.equals(thatJT.getJavaObject())) {
                return this;
            }
            throw new UnificationFailure();
        }
        return that.unify(this);
    }

    /**
     * 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;
    }

    @Override
    public Type getType() {
        return this;
    }

    @Override
    public Node newNode() {
        return this;
    }

    @Override
    public CollectionEnumeration featureNames() {
        return appropFeatureNames();
    }

    @Override
    public boolean hasFeature(Name featureName) {
        return isApprop(featureName);
    }

    @Override
    public Node delta(Name featureName) {
        // read the feature value from the object:
        if (_featureCache.includesKey(featureName)) {
            return (Node) _featureCache.at(featureName);
        }
        Object value = _concept.getJavaFeature(featureName).getValue(_javaObject);
        Node valueNode;
        if (value == _javaObject) {
            valueNode = this;
        } else {
            valueNode = getJavaType(value).newNode();
        }
        _featureCache.putAt(featureName, valueNode);
        return valueNode;
    }

    /**
     * Returns the Node at the given Path.
     * The empty path returns the Node itself.
     * The exception is thrown if at any point, the feature given
     * by the path does not exist in the current Node.
     */
    @Override
    public Node delta(Path path) throws NoSuchFeatureException {
        Enumeration<?> featenumeration = path.features();
        Node curr = this;
        while (featenumeration.hasMoreElements()) {
            curr = curr.delta((Name) featenumeration.nextElement());
        }
        return curr;
    }

    /**
     * Sets the value of the feature with the given name.
     * This method should only be called during construction of
     * a Node and with a value of the correct type.
     */
    @Override
    public void setFeature(Name featureName, Node value) {
        // update the real java object:
        JavaFeature feature = _concept.getJavaFeature(featureName);
        Object val = ((JavaType) value.getType()).getJavaObject();


        // logger.debug("Setting feature "+featureName+" of object "+javaObject+" to "+val);
        feature.setValue(_javaObject, val);
    }

    /**
     * Sets the value of the feature with the given name using a raw Java object.
     * This method updates the underlying Java object's property directly without
     * wrapping the value in a Node.
     * @param featureName the name of the feature to set
     * @param val         the Java object to set as the feature value
     */
    public void setFeature(Name featureName, Object val) {
        // update the real java object:
        JavaFeature feature = _concept.getJavaFeature(featureName);


        // logger.debug("Setting feature "+featureName+" of object "+javaObject+" to "+val);
        feature.setValue(_javaObject, val);
    }

    @Override
    public Node duplicate() {
        return this; // better clone the object (how?)
    }
}