/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile$
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.scripting.fesi;

import FESI.Data.*;
import FESI.Exceptions.*;
import FESI.Interpreter.*;
import helma.framework.core.*;
import helma.objectmodel.*;
import helma.objectmodel.db.*;
import helma.util.*;
import java.io.*;
import java.util.*;

/**
 * An EcmaScript wrapper around a Node object. This is the basic
 * HopObject that can be stored in the internal or external databases.
 */
public class ESNode extends ObjectPrototype {
    // the INode object wrapped by this ESObject
    INode node;

    // the cache node - persistent nodes have a transient property as a commodity to
    // store temporary stuff.
    INode cache;

    // the ecmascript wrapper for the cache.
    ESNode cacheWrapper;

    // The handle of the wrapped Node. Makes ESNodes comparable without accessing the wrapped node.
    NodeHandle handle;
    DbMapping dbmap;
    Throwable lastError = null;
    FesiEngine engine;

    /**
     * Constructor used to create transient cache nodes
     */
    public ESNode(INode node, FesiEngine engine) {
        super(engine.getPrototype("hopobject"), engine.getEvaluator());
        this.engine = engine;
        this.node = node;
        cache = null;
        cacheWrapper = null;

        // this is a transient node, set node handle to null
        handle = null;
    }

    /**
     * Creates a new ESNode object.
     *
     * @param prototype ...
     * @param evaluator ...
     * @param obj ...
     * @param engine ...
     */
    public ESNode(ESObject prototype, Evaluator evaluator, Object obj, FesiEngine engine) {
        super(prototype, evaluator);

        // eval.app.logEvent ("in ESNode constructor: "+o.getClass ());
        this.engine = engine;

        if (obj == null) {
            node = new TransientNode(null);
        } else if (obj instanceof ESWrapper) {
            node = (INode) ((ESWrapper) obj).getJavaObject();
        } else if (obj instanceof INode) {
            node = (INode) obj;
        } else {
            node = new TransientNode(obj.toString());
        }

        // set node handle to wrapped node
        if (node instanceof Node) {
            handle = ((Node) node).getHandle();
        } else {
            handle = null;
        }

        //  cache Node is initialized on demend when it is needed
        cache = null;
        cacheWrapper = null;
    }

    /**
     * Check if the node has been invalidated. If so, it has to be re-fetched
     * from the db via the app's node manager.
     */
    protected void checkNode() {
        if (node.getState() == INode.INVALID) {
            try {
                node = handle.getNode(engine.app.getWrappedNodeManager());
            } catch (Exception nx) {
            }
        }
    }

    /**
     *
     *
     * @return ...
     */
    public INode getNode() {
        checkNode();

        return node;
    }

    /**
     *
     *
     * @param protoName ...
     */
    public void setPrototype(String protoName) {
        checkNode();
        node.setPrototype(protoName);
    }

    /**
     *
     *
     * @return ...
     */
    public String getPrototypeName() {
        return node.getPrototype();
    }

    /**
     *
     *
     * @return ...
     */
    public String getESClassName() {
        return "HopObject";
    }

    /**
     *
     *
     * @return ...
     */
    public String toString() {
        if (node == null) {
            return "<null>";
        }

        return node.toString();
    }

    /**
     *
     *
     * @return ...
     */
    public String toDetailString() {
        return "ES:[Object: builtin " + this.getClass().getName() + ":" +
               ((node == null) ? "null" : node.toString()) + "]";
    }

    protected void setError(Throwable e) {
        lastError = e;
    }

    /**
     *
     *
     * @param what ...
     *
     * @return ...
     */
    public boolean add(ESValue[] what) {
        checkNode();

        for (int i = 0; i < what.length; i++)
            if (what[i] instanceof ESNode) {
                ESNode esn = (ESNode) what[i];
                INode added = node.addNode(esn.getNode());
            }

        return true;
    }

    /**
     *
     *
     * @return ...
     */
    public ESValue list() {
        checkNode();

        int l = node.numberOfNodes();
        Enumeration e = node.getSubnodes();
        ESObject ap = evaluator.getArrayPrototype();
        ArrayPrototype theArray = new ArrayPrototype(ap, evaluator);

        if (e != null) {
            theArray.setSize(l);

            for (int i = 0; i < l; i++) {
                theArray.setElementAt(engine.getNodeWrapper((INode) e.nextElement()), i);
            }
        } else {
            theArray.setSize(0);
        }

        return theArray;
    }

    /**
     *
     *
     * @param what ...
     *
     * @return ...
     *
     * @throws EcmaScriptException ...
     */
    public boolean addAt(ESValue[] what) throws EcmaScriptException {
        checkNode();

        if (what.length < 2) {
            throw new EcmaScriptException("Wrong number of arguments");
        }

        if (!(what[1] instanceof ESNode)) {
            throw new EcmaScriptException("Can only add Node objects as subnodes");
        }

        ESNode esn = (ESNode) what[1];
        INode added = node.addNode(esn.getNode(), (int) what[0].toInt32());

        return true;
    }

    /**
     *  Remove node itself or one or more subnodes.
     */
    public boolean remove(ESValue[] args) {
        checkNode();

        // semantics: if called without arguments, remove self.
        // otherwise, remove given subnodes.
        if (args.length == 0) {
            return node.remove();
        } else {
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof ESNode) {
                    ESNode esn = (ESNode) args[i];

                    node.removeNode(esn.getNode());
                }
            }
        }

        return true;
    }

    /**
     *  Invalidate the node itself or a subnode
     */
    public boolean invalidate(ESValue[] args) {
        if (node instanceof helma.objectmodel.db.Node) {
            if (args.length == 0) {
                ((helma.objectmodel.db.Node) node).invalidate();
            } else {
                ((helma.objectmodel.db.Node) node).invalidateNode(args[0].toString());
            }
        }

        return true;
    }

    /**
     *  Check if node is contained in subnodes
     */
    public int contains(ESValue[] args) {
        checkNode();

        if ((args.length == 1) && args[0] instanceof ESNode) {
            ESNode esn = (ESNode) args[0];

            return node.contains(esn.getNode());
        }

        return -1;
    }

    /**
     *  Prefetch child objects from (relational) database.
     */
    public void prefetchChildren(ESValue[] args) throws Exception {
        checkNode();

        if (!(node instanceof Node)) {
            return;
        }

        int start = 0;
        int length = 0;

        try {
            if (args.length == 0) {
                start = 0;
                length = 1000;
            } else if (args.length == 2) {
                if (args[0].isNumberValue()) {
                    start = args[0].toInt32();
                } else {
                    throw new RuntimeException("Illegal argument in prefetchChildren: " +
                                               args[0]);
                }

                if (args[1].isNumberValue()) {
                    length = args[1].toInt32();
                } else {
                    throw new RuntimeException("Illegal argument in prefetchChildren: " +
                                               args[1]);
                }
            } else {
                throw new RuntimeException("Wrong number of arguments in prefetchChildren");
            }
        } catch (Exception x) {
            throw new IllegalArgumentException(x.getMessage());
        }

        ((Node) node).prefetchChildren(start, length);
    }


    /**
     *
     *
     * @param pval ...
     *
     * @return ...
     */
    public boolean setParent(ESValue[] pval) {
        // do a couple of checks: both nodes need to be persistent in order for  setParent to make sense.
        if (!(node instanceof helma.objectmodel.db.Node)) {
            return false;
        }

        if ((pval == null) || (pval.length < 1) || (pval.length > 2)) {
            return false;
        }

        if (!(pval[0] instanceof ESNode)) {
            return false;
        }

        ESNode esn = (ESNode) pval[0];
        INode pn = esn.getNode();

        if (!(pn instanceof helma.objectmodel.db.Node)) {
            return false;
        }

        // check if there is an additional string element - if so, it's the property name by which the node is
        // accessed, otherwise it will be accessed as anonymous subnode via its id
        String propname = null;

        if ((pval.length == 2) && (pval[1] != null) && !(pval[1] instanceof ESNull)) {
            propname = pval[1].toString();
        }

        helma.objectmodel.db.Node n = (helma.objectmodel.db.Node) node;

        n.setParent((helma.objectmodel.db.Node) pn, propname);

        return true;
    }

    /**
     *
     *
     * @param propertyName ...
     * @param propertyValue ...
     * @param hash ...
     *
     * @throws EcmaScriptException ...
     */
    public void putProperty(String propertyName, ESValue propertyValue, int hash)
                     throws EcmaScriptException {
        checkNode();

        // engine.app.logEvent ("put property called: "+propertyName+", "+propertyValue.getClass());
        if ("cache".equalsIgnoreCase(propertyName)) {
            throw new EcmaScriptException("Can't modify read-only property \"" +
                                          propertyName + "\".");
        }

        if ("subnodeRelation".equalsIgnoreCase(propertyName)) {
            node.setSubnodeRelation((propertyValue instanceof ESNull) ? null
                                                                      : propertyValue.toString());

            return;
        }

        // check if the property is write protected, i.e. the type.property file describes it as [readonly]
        DbMapping dbm = node.getDbMapping();

        if (dbm != null) {
            Relation rel = dbm.getPropertyRelation(propertyName);

            if ((rel != null) && rel.isReadonly()) {
                return;
            }
        }

        if (propertyValue instanceof ESNull || propertyValue instanceof ESUndefined) {
            node.unset(propertyName);
        } else if (propertyValue instanceof ESString) {
            node.setString(propertyName, propertyValue.toString());
        } else if (propertyValue instanceof ESBoolean) {
            node.setBoolean(propertyName, propertyValue.booleanValue());
        } else if (propertyValue instanceof ESNumber) {
            node.setFloat(propertyName, propertyValue.doubleValue());
        } else if (propertyValue instanceof DatePrototype) {
            node.setDate(propertyName, (Date) propertyValue.toJavaObject());
        } else if (propertyValue instanceof ESNode) {
            // long now = System.currentTimeMillis ();
            ESNode esn = (ESNode) propertyValue;

            node.setNode(propertyName, esn.getNode());

            // engine.app.logEvent ("*** spent "+(System.currentTimeMillis () - now)+" ms to set property "+propertyName);
        } else {
            // engine.app.logEvent ("got "+propertyValue.getClass ());
            // A persistent node can't store anything other than the types above, so throw an exception
            // throw new EcmaScriptException ("Can't set a JavaScript Object or Array as property of "+node);
            node.setJavaObject(propertyName, propertyValue.toJavaObject());
        }
    }

    /**
     *
     *
     * @param propertyName ...
     * @param hash ...
     *
     * @return ...
     *
     * @throws EcmaScriptException ...
     */
    public boolean deleteProperty(String propertyName, int hash)
                           throws EcmaScriptException {
        checkNode();

        // engine.app.logEvent ("delete property called: "+propertyName);
        if (node.get(propertyName) != null) {
            node.unset(propertyName);

            return true;
        }

        return super.deleteProperty(propertyName, hash);
    }

    /**
     *
     *
     * @param i ...
     *
     * @return ...
     *
     * @throws EcmaScriptException ...
     */
    public ESValue getProperty(int i) throws EcmaScriptException {
        checkNode();

        INode n = node.getSubnodeAt(i);

        if (n == null) {
            return ESNull.theNull;
        }

        return engine.getNodeWrapper(n);
    }

    /**
     *
     *
     * @param index ...
     * @param propertyValue ...
     *
     * @throws EcmaScriptException ...
     */
    public void putProperty(int index, ESValue propertyValue)
                     throws EcmaScriptException {
        checkNode();

        if (propertyValue instanceof ESNode) {
            ESNode n = (ESNode) propertyValue;

            node.addNode(n.getNode(), index);
        } else {
            throw new EcmaScriptException("Can only add Nodes to Node arrays");
        }
    }

    /**
     *  Retrieve a property from the node object or the underlying EcmaScript prototype.
     *  Normally we would first check the node object and then the prototype, but since
     *  node properties are potentially expensive to look up because they require database
     *  queries, we do the prototype lookup first. This usually doesn't cause any confusion
     *  because generally things are divided cleanly between prototype and object - the
     *  first holds the functions, the latter the mapped data properties.
     */
    public ESValue getProperty(String propertyName, int hash)
                        throws EcmaScriptException {
        checkNode();

        // engine.app.logEvent ("get property called: "+propertyName);
        ESValue retval = super.getProperty(propertyName, hash);

        if (!(retval instanceof ESUndefined)) {
            return retval;
        }

        return getNodeProperty(propertyName);
    }

    /**
     *  Retrieve a property only from the node itself, not the underlying EcmaScript prototype object.
     *  This is called directly when we call get(x) on a hopobject, since we don't want to return
     *  the prototype functions in that case.
     */
    public ESValue getNodeProperty(String propertyName)
                            throws EcmaScriptException {
        // check for null
        if (propertyName == null) {
            return ESNull.theNull;
        }

        // persistent or persistent capable nodes have a cache property that's a transient node.
        // it it hasn't requested before, initialize it now
        if ("cache".equalsIgnoreCase(propertyName) && node instanceof Node) {
            cache = node.getCacheNode();

            if (cacheWrapper == null) {
                cacheWrapper = new ESNode(cache, engine);
            } else {
                cacheWrapper.node = cache;
            }

            return cacheWrapper;
        }

        if ("subnodeRelation".equalsIgnoreCase(propertyName)) {
            String rel = node.getSubnodeRelation();

            return (rel == null) ? (ESValue) ESNull.theNull : new ESString(rel);
        }

        // Everything starting with an underscore is interpreted as internal property
        if (propertyName.startsWith("_")) {
            return getInternalProperty(propertyName);
        }

        // this _may_ do a relational query if properties are mapped to a relational type.
        IProperty p = node.get(propertyName);

        if (p != null) {
            if (p.getType() == IProperty.STRING) {
                String str = p.getStringValue();

                if (str == null) {
                    return ESNull.theNull;
                } else {
                    return new ESString(str);
                }
            }

            if (p.getType() == IProperty.BOOLEAN) {
                return ESBoolean.makeBoolean(p.getBooleanValue());
            }

            if (p.getType() == IProperty.DATE) {
                return new DatePrototype(evaluator, p.getDateValue());
            }

            if (p.getType() == IProperty.INTEGER) {
                return new ESNumber((double) p.getIntegerValue());
            }

            if (p.getType() == IProperty.FLOAT) {
                return new ESNumber(p.getFloatValue());
            }

            if (p.getType() == IProperty.NODE) {
                INode nd = p.getNodeValue();

                if (nd == null) {
                    return ESNull.theNull;
                } else {
                    return engine.getNodeWrapper(nd);
                }
            }

            if (p.getType() == IProperty.JAVAOBJECT) {
                return ESLoader.normalizeObject(p.getJavaObjectValue(), evaluator);
            }
        }

        // as last resort, try to get property as anonymous subnode
        INode anon = (INode) node.getChildElement(propertyName);

        if (anon != null) {
            return engine.getNodeWrapper(anon);
        }

        return ESNull.theNull;
    }

    /**
     *  Some internal properties defined for every Node object. These are most commonly
     *  used for debugging Helma applications.
     */
    private ESValue getInternalProperty(String propertyName)
                                 throws EcmaScriptException {
        if ("__id__".equals(propertyName) || "_id".equals(propertyName)) {
            return new ESString(node.getID());
        }

        if ("__prototype__".equals(propertyName) || "_prototype".equals(propertyName)) {
            String p = node.getPrototype();

            if (p == null) {
                return ESNull.theNull;
            } else {
                return new ESString(node.getPrototype());
            }
        }

        if ("__parent__".equals(propertyName) || "_parent".equals(propertyName)) {
            INode n = node.getParent();

            if (n == null) {
                return ESNull.theNull;
            } else {
                return engine.getNodeWrapper(n);
            }
        }

        // some more internal properties
        if ("__name__".equals(propertyName)) {
            return new ESString(node.getName());
        }

        if ("__fullname__".equals(propertyName)) {
            return new ESString(node.getFullName());
        }

        if ("__hash__".equals(propertyName)) {
            return new ESString("" + node.hashCode());
        }

        if ("__node__".equals(propertyName)) {
            return new ESWrapper(node, evaluator);
        }

        if ("__created__".equalsIgnoreCase(propertyName)) {
            return new DatePrototype(evaluator, node.created());
        }

        if ("__lastmodified__".equalsIgnoreCase(propertyName)) {
            return new DatePrototype(evaluator, node.lastModified());
        }

        return ESNull.theNull;
    }

    /**
     *
     *
     * @return ...
     */
    public boolean clearCache() {
        checkNode();
        node.clearCacheNode();

        return true;
    }

    /**
     *
     *
     * @return ...
     */
    public Enumeration getAllProperties() {
        return getProperties();
    }

    /**
     *
     *
     * @return ...
     */
    public Enumeration getProperties() {
        checkNode();

        return node.properties();
    }

    /**
     *
     *
     * @return ...
     */
    public String error() {
        if (lastError == null) {
            return "";
        } else {
            String exceptionName = lastError.getClass().getName();
            int l = exceptionName.lastIndexOf(".");

            if (l > 0) {
                exceptionName = exceptionName.substring(l + 1);
            }

            return exceptionName + ": " + lastError.getMessage();
        }
    }

    /**
     *
     */
    public void clearError() {
        lastError = null;
    }

    /**
     *
     *
     * @return ...
     */
    public Object toJavaObject() {
        return getNode();
    }

    /**
     * An ESNode equals another object if it is an ESNode that wraps the same INode
     * or the wrapped INode itself. FIXME: doesen't check dbmapping/type!
     */
    public boolean equals(Object what) {
        if (what == null) {
            return false;
        }

        if (what == this) {
            return true;
        }

        if (what instanceof ESNode) {
            ESNode other = (ESNode) what;

            if (handle != null) {
                return handle.equals(other.handle);
            } else {
                return (node == other.node);
            }
        }

        return false;
    }

    /**
     *
     *
     * @param hint ...
     *
     * @return ...
     *
     * @throws EcmaScriptException ...
     */
    public ESValue getDefaultValue(int hint) throws EcmaScriptException {
        return new ESString(this.toString());
    }
}
 // class ESNode
