/*
 * JProxy.java
 *
 * Copyright (C) 2002-2005 Peter Graves, Andras Simon
 * $Id$
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obligated to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */

package org.armedbear.lisp;

import static org.armedbear.lisp.Lisp.*;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

public final class JProxy
{
  static final Map<Object,Entry> table = new WeakHashMap<Object,Entry>();

  // ### %jnew-proxy interface &rest method-names-and-defs
  private static final Primitive _JNEW_PROXY =
    new Primitive("%jnew-proxy", PACKAGE_JAVA, false,
                  "interface &rest method-names-and-defs")
    {
      @Override
      public LispObject execute(LispObject[] args)
      {
        int length = args.length;
        if (length < 3 || length % 2 != 1)
          return error(new WrongNumberOfArgumentsException(this));
        Map<String,Function> lispDefinedMethods = new HashMap<String,Function>();
        for (int i = 1; i < length; i += 2)
          lispDefinedMethods.put(args[i].getStringValue(),
                                 (Function) args[i + 1]);
        Class iface = (Class) args[0].javaInstance();
        Object proxy = Proxy.newProxyInstance(iface.getClassLoader(),
                                              new Class[] { iface },
                                              new LispHandler(table));
        table.put(proxy, new Entry(iface, lispDefinedMethods));
        return new JavaObject(proxy);
      }
    };

  private static class LispHandler implements InvocationHandler
  {
    Map table;

    LispHandler (Map table)
    {
      this.table = table;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
    {
      String methodName = method.getName();

      if (methodName.equals("hashCode"))
          return Integer.valueOf(System.identityHashCode(proxy));
      if (methodName.equals("equals"))
        return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
      if (methodName.equals("toString"))
        return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode());

      if (table.containsKey(proxy))
        {
          Entry entry = (Entry) table.get(proxy);
          Function f = entry.getLispMethod(methodName);
          if (f != null)
            {
              LispObject lispArgs = NIL;
              if (args != null)
                {
                  for (int i = args.length - 1 ; 0 <= i  ; i--)
                    lispArgs = lispArgs.push(new JavaObject(args[i]));
                }
              LispObject result = evalCall(f, lispArgs, new Environment(),
                                           LispThread.currentThread());
              return (method.getReturnType() == void.class ? null : result.javaInstance());
            }
        }
      return null;
    }
  }

  private static class Entry
  {
    Class iface;
    Map lispDefinedMethods;

    public Entry (Class iface, Map lispDefinedMethods)
    {
      this.iface = iface;
      this.lispDefinedMethods = lispDefinedMethods;
    }

    public Function getLispMethod(String methodName)
    {
      if (lispDefinedMethods.containsKey(methodName))
        return (Function)lispDefinedMethods.get(methodName);
      return null;
    }
  }
  
        //NEW IMPLEMENTATION by Alessio Stalla 
  
        /**
         * A weak map associating each proxy instance with a "Lisp-this" object. 
         */
        static final Map<Object, LispObject> proxyMap = new WeakHashMap<Object, LispObject>();
  
    public static class LispInvocationHandler implements InvocationHandler {
        
        private Function function;
        private static Method hashCodeMethod;
        private static Method equalsMethod;
        private static Method toStringMethod;
        
        static {
            try {
                hashCodeMethod = Object.class.getMethod("hashCode", new Class[] {});
                equalsMethod = Object.class.getMethod("equals", new Class[] { Object.class });
                toStringMethod = Object.class.getMethod("toString", new Class[] {});
            } catch (Exception e) {
                throw new Error("Something got horribly wrong - can't get a method from Object.class", e);
            }
        }
        
        public LispInvocationHandler(Function function) {
            this.function = function;
        }
                
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if(hashCodeMethod.equals(method)) {
                return System.identityHashCode(proxy);
            }
            if(equalsMethod.equals(method)) {
                return proxy == args[0];
            }
            if(toStringMethod.equals(method)) {
                return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode());
            }
                
            if(args == null) {
                args = new Object[0];
            }
            LispObject lispArgs = NIL;
            synchronized(proxyMap) {
                lispArgs = lispArgs.push(toLispObject(proxyMap.get(proxy)));
            }
            lispArgs = lispArgs.push(new SimpleString(method.getName()));
            for(int i = 0; i < args.length; i++) {
                lispArgs = lispArgs.push(toLispObject(args[i]));
            }
            Object retVal =
                LispThread.currentThread().execute
                (Symbol.APPLY, function, lispArgs.reverse()).javaInstance();
            //(function.execute(lispArgs)).javaInstance();
            /* DOES NOT WORK due to autoboxing!
               if(retVal != null && !method.getReturnType().isAssignableFrom(retVal.getClass())) {
               return error(new TypeError(new JavaObject(retVal), new JavaObject(method.getReturnType())));
               }*/
            return retVal;
        }
    }
  
        private static final Primitive _JMAKE_INVOCATION_HANDLER =
            new Primitive("%jmake-invocation-handler", PACKAGE_JAVA, false,
                          "function") {
                
                public LispObject execute(LispObject[] args) {
                        int length = args.length;
                        if (length != 1) {
                                return error(new WrongNumberOfArgumentsException(this, 1));
                        }
                        if(!(args[0] instanceof Function)) {
                                return type_error(args[0], Symbol.FUNCTION);
                        }
                        return new JavaObject(new LispInvocationHandler((Function) args[0]));
                }
            };

    private static final Primitive _JMAKE_PROXY =
            new Primitive("%jmake-proxy", PACKAGE_JAVA, false,
                          "interfaces invocation-handler") {
                
                public LispObject execute(final LispObject[] args) {
                        int length = args.length;
                        if (length != 3) {
                                return error(new WrongNumberOfArgumentsException(this, 3));
                        }
                        if(!(args[0] instanceof Cons)) {
                            return type_error(args[0], Symbol.CONS);
                        }
                        Class[] ifaces = new Class[args[0].length()];
                        LispObject ifList = args[0];
                        for(int i = 0; i < ifaces.length; i++) {
                          ifaces[i] = (Class) ifList.car().javaInstance(Class.class);
                            ifList = ifList.cdr();
                        }
                        InvocationHandler invocationHandler = (InvocationHandler) ((JavaObject) args[1]).javaInstance(InvocationHandler.class);
                        Object proxy = Proxy.newProxyInstance(
                                        JavaClassLoader.getCurrentClassLoader(),
                                        ifaces,
                                        invocationHandler);
                        synchronized(proxyMap) {
                                proxyMap.put(proxy, args[2]);
                        }
                        return new JavaObject(proxy);
                }
            };    
            
        static LispObject toLispObject(Object obj) {
                return (obj instanceof LispObject) ? (LispObject) obj : new JavaObject(obj);
        }
            
}
