/*
 * $Id: LuaObject.java,v 1.6 2006/12/22 14:06:40 thiago Exp $
 * Copyright (C) 2003-2007 Kepler Project.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package org.keplerproject.luajava;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.StringTokenizer;

/**
 * This class represents a Lua object of any type. A LuaObject is constructed by
 * a {@link LuaState} object using one of the four methods:
 * <ul>
 * <li>{@link LuaState#getLuaObject(String globalName)}</li>
 * <li>{@link LuaState#getLuaObject(LuaObject parent, String name)}</li>
 * <li>{@link LuaState#getLuaObject(LuaObject parent, Number name)}</li>
 * <li>{@link LuaState#getLuaObject(LuaObject parent, LuaObject name)}</li>
 * <li>{@link LuaState#getLuaObject(int index)}</li>
 * </ul>
 * The LuaObject will represent only the object itself, not a variable or a
 * stack index, so when you change a string, remember that strings are immutable
 * objects in Lua, and the LuaObject you have will represent the old one.
 * <p>
 * <h2>Proxies</h2>
 * <p>
 * LuaJava allows you to implement a class in Lua, like said before. If you want
 * to create this proxy from Java, you should have a LuaObject representing the
 * table that has the functions that implement the interface. From this
 * LuaObject you can call the <code>createProxy(String implements)</code>. This
 * method receives the string with the name of the interfaces implemented by the
 * object separated by comma.
 *
 * @author Rizzato
 * @author Thiago Ponte
 */
public class LuaObject {
    protected Integer ref;

    protected LuaState L;

    /**
     * This constructor creates a LuaObject from a table that is indexed by a
     * LuaObject.
     *
     * @param parent The Lua Table or Userdata that contains the Field.
     * @param name   The name (LuaObject) that index the field
     * @throws LuaException When the parent object isn't a Table or Userdata
     */
    protected LuaObject(LuaObject parent, LuaObject name) throws LuaException {
        if (parent.getLuaState() != name.getLuaState())
            throw new LuaException("LuaStates must be the same!");
        synchronized (parent.getLuaState()) {
            if (!parent.isTable() && !parent.isUserdata())
                throw new LuaException(
                        "Object parent should be a table or userdata .");

            this.L = parent.getLuaState();

            parent.push();
            name.push();
            L.getTable(-2);
            L.remove(-2);
            registerValue(-1);
            L.pop(1);
        }
    }

    /**
     * This constructor creates a LuaObject from a table that is indexed by a
     * number.
     *
     * @param parent The Lua Table or Userdata that contains the Field.
     * @param name   The name (number) that index the field
     * @throws LuaException When the parent object isn't a Table or Userdata
     */
    protected LuaObject(LuaObject parent, Number name) throws LuaException {
        synchronized (parent.getLuaState()) {
            this.L = parent.getLuaState();
            if (!parent.isTable() && !parent.isUserdata())
                throw new LuaException(
                        "Object parent should be a table or userdata .");

            parent.push();
            L.pushNumber(name.doubleValue());
            L.getTable(-2);
            L.remove(-2);
            registerValue(-1);
            L.pop(1);
        }
    }

    /**
     * Creates a reference to an object inside another object
     *
     * @param parent The Lua Table or Userdata that contains the Field.
     * @param name   The name that index the field
     */
    protected LuaObject(LuaObject parent, String name) throws LuaException {
        synchronized (parent.getLuaState()) {
            this.L = parent.getLuaState();

            if (!parent.isTable() && !parent.isUserdata()) {
                throw new LuaException(
                        "Object parent should be a table or userdata .");
            }

            parent.push();
            L.pushString(name);
            L.getTable(-2);
            L.remove(-2);
            registerValue(-1);
            L.pop(1);
        }
    }

    /**
     * Creates a reference to an object in the given index of the stack
     *
     * @param L
     * @param index of the object on the lua stack
     */
    protected LuaObject(LuaState L, int index) {
        synchronized (L) {
            this.L = L;

            registerValue(index);
        }
    }

    /**
     * Creates a reference to an object in the variable globalName
     *
     * @param L
     * @param globalName
     */
    protected LuaObject(LuaState L, String globalName) {
        synchronized (L) {
            this.L = L;
            L.getGlobal(globalName);
            registerValue(-1);
            L.pop(1);
        }
    }

    /**
     * Calls the object represented by <code>this</code> using Lua function
     * pcall. Returns 1 object
     *
     * @param args - Call arguments
     * @return Object - Returned Object
     * @throws LuaException
     */
    public Object call(Object[] args) throws LuaException {
        return call(args, 1)[0];
    }

    /**
     * Calls the object represented by <code>this</code> using Lua function
     * pcall.
     *
     * @param args - Call arguments
     * @param nres - Number of objects returned
     * @return Object[] - Returned Objects
     * @throws LuaException
     */
    public Object[] call(Object[] args, int nres) throws LuaException {
        synchronized (L) {
            if (!isFunction() && !isTable() && !isUserdata())
                throw new LuaException(
                        "Invalid object. Not a function, table or userdata .");

            int top = L.getTop();
            push();
            int nargs;
            if (args != null) {
                nargs = args.length;
                for (int i = 0; i < nargs; i++) {
                    Object obj = args[i];
                    L.pushObjectValue(obj);
                }
            } else
                nargs = 0;

            int err = L.pcall(nargs, nres, 0);

            if (err != 0) {
                String str;
                if (L.isString(-1)) {
                    str = L.toString(-1);
                    L.pop(1);
                } else
                    str = "";

                if (err == LuaState.LUA_ERRRUN.intValue()) {
                    str = "Runtime error. " + str;
                } else if (err == LuaState.LUA_ERRMEM.intValue()) {
                    str = "Memory allocation error. " + str;
                } else if (err == LuaState.LUA_ERRERR.intValue()) {
                    str = "Error while running the error handler function. "
                            + str;
                } else {
                    str = "Lua Error code " + err + ". " + str;
                }

                throw new LuaException(str);
            }

            if (nres == LuaState.LUA_MULTRET.intValue())
                nres = L.getTop() - top;
            if (L.getTop() - top < nres) {
                throw new LuaException("Invalid Number of Results .");
            }

            Object[] res = new Object[nres];

            for (int i = nres; i > 0; i--) {
                res[i - 1] = L.toJavaObject(-1);
                L.pop(1);
            }
            return res;
        }
    }

    /**
     * Function that creates a java proxy to the object represented by
     * <code>this</code>
     *
     * @param implem Interfaces that are implemented, separated by <code>,</code>
     */
    public Object createProxy(String implem) throws ClassNotFoundException,
            LuaException {
        synchronized (L) {
            if (!isTable())
                throw new LuaException("Invalid Object. Must be Table.");

            StringTokenizer st = new StringTokenizer(implem, ",");
            Class[] interfaces = new Class[st.countTokens()];
            for (int i = 0; st.hasMoreTokens(); i++)
                interfaces[i] = Class.forName(st.nextToken());

            InvocationHandler handler = new LuaInvocationHandler(this);

            return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                    interfaces, handler);
        }
    }

    @Override
    protected void finalize() {
        try {
            synchronized (L) {
                if (L.getCPtrPeer() != 0)
                    L.LunRef(LuaState.LUA_REGISTRYINDEX.intValue(),
                            ref.intValue());
            }
        } catch (Exception e) {
            System.err.println("Unable to release object " + ref);
        }
    }

    public boolean getBoolean() {
        synchronized (L) {
            push();
            boolean bool = L.toBoolean(-1);
            L.pop(1);
            return bool;
        }
    }

    /**
     * If <code>this<code> is a table or userdata tries to set
     * a field value.
     */
    public LuaObject getField(String field) throws LuaException {
        return L.getLuaObject(this, field);
    }

    /**
     * Gets the Object's State
     */
    public LuaState getLuaState() {
        return L;
    }

    public double getNumber() {
        synchronized (L) {
            push();
            double db = L.toNumber(-1);
            L.pop(1);
            return db;
        }
    }

    public Object getObject() throws LuaException {
        synchronized (L) {
            push();
            Object obj = L.getObjectFromUserdata(-1);
            L.pop(1);
            return obj;
        }
    }

    public String getString() {
        synchronized (L) {
            push();
            String str = L.toString(-1);
            L.pop(1);
            return str;
        }
    }

    public boolean isBoolean() {
        synchronized (L) {
            push();
            boolean bool = L.isBoolean(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isFunction() {
        synchronized (L) {
            push();
            boolean bool = L.isFunction(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isJavaFunction() {
        synchronized (L) {
            push();
            boolean bool = L.isJavaFunction(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isJavaObject() {
        synchronized (L) {
            push();
            boolean bool = L.isObject(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isNil() {
        synchronized (L) {
            push();
            boolean bool = L.isNil(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isNumber() {
        synchronized (L) {
            push();
            boolean bool = L.isNumber(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isString() {
        synchronized (L) {
            push();
            boolean bool = L.isString(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isTable() {
        synchronized (L) {
            push();
            boolean bool = L.isTable(-1);
            L.pop(1);
            return bool;
        }
    }

    public boolean isUserdata() {
        synchronized (L) {
            push();
            boolean bool = L.isUserdata(-1);
            L.pop(1);
            return bool;
        }
    }

    /**
     * Pushes the object represented by <code>this<code> into L's stack
     */
    public void push() {
        L.rawGetI(LuaState.LUA_REGISTRYINDEX.intValue(), ref.intValue());
    }

    /**
     * Creates the reference to the object in the registry table
     *
     * @param index of the object on the lua stack
     */
    private void registerValue(int index) {
        synchronized (L) {
            L.pushValue(index);
            int key = L.Lref(LuaState.LUA_REGISTRYINDEX.intValue());
            ref = new Integer(key);
        }
    }

    @Override
    public String toString() {
        synchronized (L) {
            try {
                if (isNil())
                    return "nil";
                else if (isBoolean())
                    return String.valueOf(getBoolean());
                else if (isNumber())
                    return String.valueOf(getNumber());
                else if (isString())
                    return getString();
                else if (isFunction())
                    return "Lua Function";
                else if (isJavaObject())
                    return getObject().toString();
                else if (isUserdata())
                    return "Userdata";
                else if (isTable())
                    return "Lua Table";
                else if (isJavaFunction())
                    return "Java Function";
                else
                    return null;
            } catch (LuaException e) {
                return null;
            }
        }
    }

    public int type() {
        synchronized (L) {
            push();
            int type = L.type(-1);
            L.pop(1);
            return type;
        }
    }
}
