/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source 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.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.quercus.env;

import java.io.IOException;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;

import com.caucho.quercus.Quercus;
import com.caucho.quercus.QuercusException;
import com.caucho.quercus.expr.DefaultExpr;
import com.caucho.quercus.expr.RequiredExpr;
import com.caucho.quercus.expr.Expr;
import com.caucho.quercus.expr.NullLiteralExpr;
import com.caucho.quercus.gen.PhpWriter;
import com.caucho.quercus.module.*;
import com.caucho.quercus.parser.QuercusParser;
import com.caucho.quercus.program.AbstractFunction;
import com.caucho.util.L10N;

/**
 * Represents the introspected static function information.
 */
abstract public class JavaInvoker
  extends AbstractJavaMethod
{
  private static final L10N L = new L10N(JavaInvoker.class);

  private static final Object [] NULL_ARGS = new Object[0];
  private static final Value [] NULL_VALUES = new Value[0];

  private final ModuleContext _moduleContext;
  private final String _name;
  private final Class [] _param;
  private final Class _retType;
  private final Annotation [][] _paramAnn;
  private final Annotation []_methodAnn;

  private volatile boolean _isInit;
  
  private boolean _hasEnv;
  private boolean _hasThis;
  private Expr [] _defaultExprs;
  private Marshall [] _marshallArgs;
  private boolean _hasRestArgs;
  private Marshall _unmarshallReturn;

  private boolean _isRestReference;

  private boolean _isCallUsesVariableArgs;
  private boolean _isCallUsesSymbolTable;

  /**
   * Creates the statically introspected function.
   */
  public JavaInvoker(ModuleContext moduleContext,
                     String name,
                     Class []param,
                     Annotation [][]paramAnn,
                     Annotation []methodAnn,
                     Class retType)
  {
    _moduleContext = moduleContext;
    _name = name;
    _param = param;
    _paramAnn = paramAnn;
    _methodAnn = methodAnn;
    _retType = retType;
    
    // init();
  }

  public synchronized void init()
  {
    if (_isInit)
      return;

    try {
      boolean callUsesVariableArgs = false;
      boolean callUsesSymbolTable = false;
      boolean returnNullAsFalse = false;

      for (Annotation ann : _methodAnn) {
	if (VariableArguments.class.isAssignableFrom(ann.annotationType())) {
	  callUsesVariableArgs = true;
	}

	if (UsesSymbolTable.class.isAssignableFrom(ann.annotationType())) {
	  callUsesSymbolTable = true;
	}

	if (ReturnNullAsFalse.class.isAssignableFrom(ann.annotationType())) {
	  returnNullAsFalse = true;
	}
      }

      _isCallUsesVariableArgs = callUsesVariableArgs;
      _isCallUsesSymbolTable = callUsesSymbolTable;

      _hasEnv = _param.length > 0 && _param[0].equals(Env.class);
      int envOffset = _hasEnv ? 1 : 0;

      if (envOffset < _param.length)
	_hasThis = hasThis(_param[envOffset], _paramAnn[envOffset]);
      else
	_hasThis = false;

      if (_hasThis)
	envOffset++;

      boolean hasRestArgs = false;
      boolean isRestReference = false;

      if (_param.length > 0 &&
	  (_param[_param.length - 1].equals(Value[].class) ||
	   _param[_param.length - 1].equals(Object[].class))) {
	hasRestArgs = true;

	for (Annotation ann : _paramAnn[_param.length - 1]) {
	  if (Reference.class.isAssignableFrom(ann.annotationType()))
	    isRestReference = true;
	}
      }

      _hasRestArgs = hasRestArgs;
      _isRestReference = isRestReference;

      int argLength = _param.length;

      if (_hasRestArgs)
	argLength -= 1;

      _defaultExprs = new Expr[argLength - envOffset];

      _marshallArgs = new Marshall[argLength - envOffset];

      for (int i = 0; i < argLength - envOffset; i++) {
	boolean isReference = false;
	boolean isNotNull = false;

	for (Annotation ann : _paramAnn[i + envOffset]) {
	  if (Optional.class.isAssignableFrom(ann.annotationType())) {
	    Optional opt = (Optional) ann;

	    if (! opt.value().equals("")) {
	      Expr expr = QuercusParser.parseDefault(opt.value());

	      _defaultExprs[i] = expr;
	    } else
	      _defaultExprs[i] = new DefaultExpr(getLocation());
	  } else if (Reference.class.isAssignableFrom(ann.annotationType())) {
	    isReference = true;
	  } else if (NotNull.class.isAssignableFrom(ann.annotationType())) {
	    isNotNull = true;
	  }
	}

	Class argType = _param[i + envOffset];

	if (isReference)
	  _marshallArgs[i] = Marshall.MARSHALL_REFERENCE;
	else
	  _marshallArgs[i] = Marshall.create(_moduleContext,
					     argType,
					     isNotNull);
      }

      _unmarshallReturn = Marshall.create(_moduleContext, _retType, false,
					  returnNullAsFalse);
    } finally {
      _isInit = true;
    }
  }

  /**
   * Returns true if the environment is an argument.
   */
  public boolean getHasEnv()
  {
    if (! _isInit)
      init();
    
    return _hasEnv;
  }

  /**
   * Returns true if the environment has rest-style arguments.
   */
  public boolean getHasRestArgs()
  {
    if (! _isInit)
      init();
    
    return _hasRestArgs;
  }

  /**
   * Returns true if the rest argument is a reference.
   */
  public boolean isRestReference()
  {
    if (! _isInit)
      init();
    
    return _isRestReference;
  }

  /**
   * Returns the unmarshaller for the return
   */
  public Marshall getUnmarshallReturn()
  {
    if (! _isInit)
      init();
    
    return _unmarshallReturn;
  }

  /**
   * Returns true if the call uses variable arguments.
   */
  @Override
  public boolean isCallUsesVariableArgs()
  {
    if (! _isInit)
      init();
    
    return _isCallUsesVariableArgs;
  }

  /**
   * Returns true if the call uses the symbol table
   */
  @Override
  public boolean isCallUsesSymbolTable()
  {
    if (! _isInit)
      init();
    
    return _isCallUsesSymbolTable;
  }

  /**
   * Returns true if the result is a boolean.
   */
  public boolean isBoolean()
  {
    if (! _isInit)
      init();
    
    return _unmarshallReturn.isBoolean();
  }

  /**
   * Returns true if the result is a string.
   */
  public boolean isString()
  {
    if (! _isInit)
      init();
    
    return _unmarshallReturn.isString();
  }

  /**
   * Returns true if the result is a long.
   */
  public boolean isLong()
  {
    if (! _isInit)
      init();
    
    return _unmarshallReturn.isLong();
  }

  /**
   * Returns true if the result is a double.
   */
  public boolean isDouble()
  {
    if (! _isInit)
      init();
    
    return _unmarshallReturn.isDouble();
  }

  /**
   * Returns the marshall arguments.
   */
  protected Marshall []getMarshallArgs()
  {
    if (! _isInit)
      init();
    
    return _marshallArgs;
  }

  /**
   * Returns the parameter annotations.
   */
  protected Annotation [][]getParamAnn()
  {
    if (! _isInit)
      init();
    
    return _paramAnn;
  }

  /**
   * Returns the default expressions.
   */
  protected Expr []getDefaultExprs()
  {
    if (! _isInit)
      init();
    
    return _defaultExprs;
  }

  /**
   * Evaluates a function's argument, handling ref vs non-ref
   */
  @Override
  public Value []evalArguments(Env env, Expr fun, Expr []args)
  {
    if (! _isInit)
      init();
    
    Value []values = new Value[args.length];

    for (int i = 0; i < args.length; i++) {
      Marshall arg = null;

      if (i < _marshallArgs.length)
        arg = _marshallArgs[i];
      else if (_isRestReference) {
        values[i] = args[i].evalRef(env);
	continue;
      }
      else {
        values[i] = args[i].eval(env);
	continue;
      }

      if (arg == null)
        values[i] = args[i].eval(env).copy();
      else if (arg.isReference())
        values[i] = args[i].evalRef(env);
      else {
        // php/0d04
        values[i] = args[i].eval(env);
      }
    }

    return values;
  }

  public Value call(Env env, Value []value)
  {
    return call(env, env.getThis(), value);
  }

  /**
   * Evaluates the function, returning a copy.  Java methods don't
   * need the copy.
   */
  public Value callCopy(Env env, Value []args)
  {
    return call(env, args);
  }

  public Value call(Env env, Value obj, Value []value)
  {
    return call(env, obj.toJavaObject(), value);
  }

  public Value call(Env env, Object obj, Expr []exprs)
  {
    if (! _isInit)
      init();
    
    int len = (_defaultExprs.length +
	       (_hasEnv ? 1 : 0) +
	       (_hasThis ? 1 : 0) +
	       (_hasRestArgs ? 1 : 0));

    Object []values = new Object[len];

    int k = 0;

    if (_hasEnv)
      values[k++] = env;
    if (_hasThis) {
      values[k++] = (ObjectValue) obj;
      obj = null;
    }

    for (int i = 0; i < _marshallArgs.length; i++) {
      Expr expr;

      if (i < exprs.length && exprs[i] != null)
        expr = exprs[i];
      else {
        expr = _defaultExprs[i];

        if (expr == null)
          expr = new RequiredExpr(getLocation());
      }

      values[k] = _marshallArgs[i].marshall(env, expr, _param[k]);

      k++;
    }

    if (_hasRestArgs) {
      Value []rest;

      int restLen = exprs.length - _marshallArgs.length;

      if (restLen <= 0)
        rest = NULL_VALUES;
      else {
        rest = new Value[restLen];

        for (int i = _marshallArgs.length; i < exprs.length; i++) {
          if (_isRestReference)
            rest[i - _marshallArgs.length] = exprs[i].evalRef(env);
          else
            rest[i - _marshallArgs.length] = exprs[i].eval(env);
        }
      }

      values[values.length - 1] = rest;
    }

    Object result = invoke(obj, values);
    
    return _unmarshallReturn.unmarshall(env, result);
  }

  public Value call(Env env, Object obj, Value []args)
  {
    if (! _isInit)
      init();
    
    int len = _param.length;

    Object []javaArgs = new Object[len];

    int k = 0;

    if (_hasEnv)
      javaArgs[k++] = env;
    if (_hasThis) {
      javaArgs[k++] = (ObjectValue) obj;
      obj = null;
    }

    for (int i = 0; i < _marshallArgs.length; i++) {
      Value value;

      if (i < args.length && args[i] != null)
        javaArgs[k] = _marshallArgs[i].marshall(env, args[i], _param[k]);
      else if (_defaultExprs[i] != null) {
        javaArgs[k] = _marshallArgs[i].marshall(env,
                                              _defaultExprs[i],
                                              _param[k]);
      } else {
	env.warning(L.l("function '{0}' has {1} required arguments, but only {2} were provided",
			_name, _marshallArgs.length, args.length));
	
        javaArgs[k] = _marshallArgs[i].marshall(env,
						NullValue.NULL,
						_param[k]);
      }

      k++;
    }

    if (_hasRestArgs) {
      Value []rest;

      int restLen = args.length - _marshallArgs.length;

      if (restLen <= 0)
        rest = NULL_VALUES;
      else {
        rest = new Value[restLen];

        for (int i = _marshallArgs.length; i < args.length; i++) {
          if (_isRestReference)
            rest[i - _marshallArgs.length] = args[i];
          else
            rest[i - _marshallArgs.length] = args[i].toValue();
        }
      }

      javaArgs[k++] = rest;
    }

    Object result = invoke(obj, javaArgs);

    return _unmarshallReturn.unmarshall(env, result);
  }

  public Value call(Env env, Object obj)
  {
    return call(env, obj, new Value[0]);
  }

  public Value call(Env env, Object obj, Value a1)
  {
    return call(env, obj, new Value[]{a1});
  }

  public Value call(Env env, Object obj, Value a1, Value a2)
  {
    return call(env, obj, new Value[]{a1, a2});
  }

  public Value call(Env env, Object obj, Value a1, Value a2, Value a3)
  {
    return call(env, obj, new Value[]{a1, a2, a3});
  }

  public Value call(Env env, Object obj,
                    Value a1, Value a2, Value a3, Value a4)
  {
    return call(env, obj, new Value[]{a1, a2, a3, a4});
  }

  public Value call(Env env, Object obj,
                    Value a1, Value a2, Value a3, Value a4, Value a5)
  {
    return call(env, obj, new Value[]{a1, a2, a3, a4, a5});
  }

  public void generate(PhpWriter out, Expr funExpr, Expr []expr)
    throws IOException
  {
    throw new UnsupportedOperationException();
  }

  abstract public Object invoke(Object obj, Object []args);

  //
  // Utility methods
  //
  private boolean hasThis(Class param, Annotation[]ann)
  {
    if (! param.isAssignableFrom(ObjectValue.class))
      return false;

    for (int i = 0; i < ann.length; i++) {
      if (This.class.isAssignableFrom(ann[i].annotationType()))
	return true;
    }

    return false;
  }
}
