/*
 * Copyright (c) 1998-2012 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 SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.es.parser;

import com.caucho.es.ESId;
import com.caucho.util.CharBuffer;
import com.caucho.util.IntMap;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Function is an intermediate form representing an expression.
 */
class Function {
  static ESId PROTOTYPE = ESId.intern("prototype");

  Parser parser;
  ParseClass cl;
  private Function parent;

  int funDepth;
  private int lambdaCount = 0;

  ArrayList formals;
  Expr returnType;
  
  ArrayList variables = new ArrayList();

  // child closures 
  ArrayList functions;
  private IntMap funMap;
  

  boolean isClass;
  ESId classProto; // null for non-classes
  Function constructor;

  String name;
  ESId id;
  int num;
  boolean isSilent;
  boolean hasCall;
  boolean hasThis;
  boolean allowLocals;
  boolean allowJavaLocals;
  boolean needsScope;
  
  ArrayList data = new ArrayList();
  private HashMap vars = new HashMap();
  CharBuffer tail;
  private int iterCount;
  private int tempCount;
  private int stmtCount;
  private int stmtTop;
  private HashMap usedVars;
  private boolean isGlobal;
  // True if an arguments variable needs to be created.  eval, closures,
  // the arguments variable and "with" will set this.
  private boolean needsArguments;
  private boolean needsStatementResults;
  // True if any variable can be used even if not explicitly referenced
  // e.g. if the "arguments" variable is used.
  private boolean useAllVariables;
  // True if the function contains an eval
  private boolean isEval;
  // True if the function contains a switch
  private boolean hasSwitch;

  Function(ParseClass cl, Function parent,
           String name, ESId id, boolean isClass)
  {
    this.id = id;
    this.name = name;
    this.parent = parent;
    this.cl = cl;
    
    if (parent == null || isClass)
      funDepth = 0;
    else
      funDepth = parent.funDepth + 1;

    num = -1;

    isGlobal = parent == null;
    allowLocals = funDepth >= 1 || isClass;
    allowJavaLocals = allowLocals;
    needsStatementResults = parent == null;
  }

  void setFast(boolean isFast)
  {
    if (isFast) {
      allowLocals = true;
      allowJavaLocals = true;
    }
  }
  
  Function getParent()
  {
    return parent;
  }

  boolean isGlobalScope()
  {
    return ! needsScope && (getFunctionDepth() == 0 ||
                            getFunctionDepth() == 1 && ! needsArguments());
  }

  boolean needsStatementResults()
  {
    return needsStatementResults;
  }

  void setNeedsResults()
  {
    needsStatementResults = true;
  }

  void setEval()
  {
    setArguments();
    needsStatementResults = true;
    isEval = true;
  }

  boolean useAllVariables()
  {
    return isEval || useAllVariables;
  }

  void setUseAllVariables()
  {
    useAllVariables = true;
  }

  String getStatementVar()
  {
    if (! needsStatementResults())
      return null;
    else
      return "_val" + stmtCount;
  }

  void pushStatementLoop()
  {
    if (! needsStatementResults())
      return;

    stmtCount++;
    if (stmtCount > stmtTop)
      stmtTop = stmtCount;
  }

  void popStatementLoop()
  {
    if (! needsStatementResults())
      return;

    stmtCount--;
  }

  void setCall()
  {
    this.hasCall = true;
  }

  void setThis()
  {
    this.hasThis = true;
  }

  /**
   * Force all unassigned variables to become object vars.
   */
  void setVars()
  {
    Iterator iter = vars.values().iterator();
    while (iter.hasNext()) {
      Variable var = (Variable) iter.next();
      var.getType();
    }
  }

  boolean isGlobal()
  {
    return isGlobal;
  }

  int getFunctionDepth()
  {
    return funDepth;
  }

  void disableGlobal()
  {
    isGlobal = false;
  }

  void disallowLocal()
  {
    if (parent != null) {
      allowJavaLocals = false;
      allowLocals = false;
      needsScope = true;
    }
  }

  void disallowJavaLocal()
  {
    allowJavaLocals = false;
  }

  void setNeedsScope()
  {
    if (parent != null)
      needsScope = true;
  }

  void setArguments()
  {
    needsArguments = true;
    setNeedsScope();
    disallowLocal();
  }

  boolean needsArguments()
  {
    return needsArguments;
  }
  
  boolean allowLocals()
  {
    return allowLocals;
  }
  
  boolean allowJavaLocals()
  {
    return allowLocals && allowJavaLocals;
  }

  void useClosureVar(ESId name)
  {
    for (Function fun = parent; fun != null; fun = fun.parent) {
      Variable var = (Variable) fun.vars.get(name);

      fun.needsArguments = true;
      
      if (var != null)
        var.setUsedByClosure();
      else {
        if (fun.usedVars == null)
          fun.usedVars = new HashMap();
        fun.usedVars.put(name, name);
      }
    }
  }

  /**
   * Returns true if the variable is declared.
   */
  boolean hasVar(ESId name)
  {
    return vars.get(name) != null;
  }
  
  /**
   * Returns a new variable.
   */
  IdExpr newVar(Block block, ESId name)
  {
    return newVar(block, name, null);
  }
  
  /**
   * Returns a new variable.
   */
  IdExpr newVar(Block block, ESId name, Expr type)
  {
    Variable var = (Variable) vars.get(name);

    if (var == null && type == null) {
      var = cl.getVariable(name);
      if (var != null) {
        return new IdExpr(block, var);
      }
    }
    
    if (var == null) {
        
      var = new Variable(block, name, type, false);
      vars.put(name, var);
      
      if (usedVars != null && usedVars.get(name) != null)
        var.setUsedByClosure();
    }

    useClosureVar(name);

    return new IdExpr(block, var);
  }

  /**
   * Add a variable to the function.  If the function is the global
   * function, add it to the class.
   */
  void addVariable(Block block, ESId id, Expr type)
  {
    if (variables == null)
      variables = new ArrayList();

    Variable var = (Variable) vars.get(id);
    if (var == null) {
      var = new Variable(block, id, type, allowLocals);
      vars.put(id, var);
      
      if (usedVars != null && usedVars.get(id) != null)
        var.setUsedByClosure();

      // Only add global variables if they're declared, not if defined
      // by use.
      if (parent == null && type != null)
        cl.addVariable(id, var);
    }
    else if (parent != null)
      var.setLocal();

    if (! variables.contains(var) &&
        (formals == null || ! formals.contains(var)))
      variables.add(var);
    
    useClosureVar(id);
  }

  int getIter()
  {
    return iterCount++;
  }

  String getTemp()
  {
    return "temp" + tempCount++;
  }

  void setConstructor(Function constructor)
  {
    isClass = true;
    this.constructor = constructor;
  }

  void setClassProto(ESId classProto)
  {
    isClass = true;
    this.classProto = classProto;
  }

  void setCodeNumber(int num)
  {
    this.num = num;
  }

  void addFormal(Block block, ESId id, Expr type)
  {
    if (formals == null)
      formals = new ArrayList();

    Variable var = new Variable(block, id, type, true);
    var.setUsed();
    formals.add(var);
    vars.put(id, var);
  }

  int getFormalSize()
  {
    return formals == null ? 0 : formals.size();
  }

  Variable getFormal(int j)
  {
    return (Variable) formals.get(j);
  }

  Expr getReturnType()
  {
    return returnType;
  }

  void setReturnType(Expr type)
  {
    returnType = type;
  }

  int getVariableSize()
  {
    return variables == null ? 0 : variables.size();
  }

  void addFunction(Function function)
  {
    if (functions == null) {
      functions = new ArrayList();
      funMap = new IntMap();
    }
    int pos = funMap.get(function.id);

    if (pos < 0) {
      funMap.put(function.id, functions.size());
      functions.add(function);
    }
    else
      functions.set(pos, function);
  }

  int getFunctionSize()
  {
    return functions == null ? 0 : functions.size();
  }

  Function getFunction(int i)
  {
    return (Function) functions.get(i);
  }

  Function getFunction(ESId id)
  {
    if (funMap == null)
      return null;

    int index = funMap.get(id);

    if (index >= 0) {
      return (Function) functions.get(index);
    }
    else
      return null;
  }

  void print(Object value)
  {
    if (tail == null)
      tail = CharBuffer.allocate();
    tail.append(String.valueOf(value));
  }

  void println(String value)
  {
    if (tail == null)
      tail = CharBuffer.allocate();
    tail.append(String.valueOf(value));
    tail.append('\n');
  }

  void addExpr(Expr expr)
  {
    if (tail != null)
      data.add(tail);
    tail = null;
    data.add(expr);
    expr.setUsed();
  }

  void addBoolean(Expr expr)
  {
    if (tail != null)
      data.add(tail);
    tail = null;

    data.add(expr.setBoolean());
    expr.setUsed();
  }

  Object getTop()
  {
    if (tail != null)
      return tail;
    else if (data.size() > 0)
      return data.get(data.size() - 1);
    else {
      tail = new CharBuffer();
      return tail;
    }
  }

  Object getSwitch()
  {
    if (tail != null)
      data.add(tail);
    tail = null;

    hasSwitch = true;
    
    return data.get(data.size() - 1);
  }
  
  int mark()
  {
    if (tail != null)
      data.add(tail);
    tail = null;
    
    return data.size();
  }

  void moveChunk(Object topObject, int mark)
  {
    if (tail != null)
      data.add(tail);
    tail = null;

    int top;
    for (top = 0; top < mark; top++) {
      if (data.get(top) == topObject)
        break;
    }
    top++;

    int here = data.size();

    for (int i = 0; i < here - mark; i++) {
      Object chunk = data.remove(data.size() - 1);
      data.add(top, chunk);
    }
  }

  void writeCode(ParseClass cl) throws IOException
  {
    cl.print("public ");

    if (returnType == null)
      cl.print("ESBase ");
    else if (returnType.getType() == Expr.TYPE_INTEGER)
      cl.print("int ");
    else
      cl.print("ESBase ");
    
    cl.print(name + "(Call _env, int _length");

    for (int i = 0; i < getFormalSize(); i++) {
      Variable formal = (Variable) formals.get(i);
      
      if (formal.getTypeExpr() instanceof TypeExpr) {
        TypeExpr type = (TypeExpr) formal.getTypeExpr();

        if (type.getTypeName() != null)
          cl.print(", " + type.getTypeName() + " " + formal.getId());
      }
    }
    cl.println(")");
    
    cl.println("throws Throwable");
    cl.println("{");
    cl.pushDepth();
    
    if (hasCall)
      cl.println("Call _call = _env.getCall();");

    if (hasThis)
      cl.println("ESObject _this = _env.getThis();");

    if (parent != null && functions != null && functions.size() > 0) {
      needsArguments = true;
      setNeedsScope();
    }

    // Eval just uses the calling scope
    if (isEval) {
      cl.println("ESObject _arg = _env.getEval();");
    }
    else {
      if (needsScope && parent != null)
        cl.println("_env.fillScope();");

      if (needsArguments && parent != null) {
        cl.print("ESObject _arg = _env.createArg(");
        if (getFormalSize() > 0)
          cl.print("_js._a_" + num);
        else
          cl.print("_js._a_null");
        cl.println(", _length);");
      }
    }

    printFormals();
    printLocalVariables();

    // do closures
    for (int i = 0;
         needsArguments && functions != null && i < functions.size();
         i++) {
      if (i == 0)
        cl.println("ESClosure _closure;");
        
      Function fun = (Function) functions.get(i);
      Variable var = (Variable) vars.get(fun.id);

      if (! isGlobal && ! isEval &&
          var != null && ! var.isUsed() && ! useAllVariables) {
        continue;
      }
      
      cl.print("_closure = new ESClosure(");
      cl.printLiteral(fun.id);
      cl.println(", _js, null, " + fun.num + ", _js._a_null, null);");
      cl.println("_closure.closure(_env);");
        
      if (! isEval && allowLocals &&
          var != null && var.isUsed() && var.isJavaLocal()) {
        cl.println("ESBase " + fun.id + " = _closure;");
        continue;
      }
      else if (! isEval && parent != null)
        cl.print("_arg.put(");
      else
        cl.print("_env.global.put(");
      cl.printLiteral(fun.id);
      cl.print(", _closure, ");
      if (! isEval)
        cl.print("ESBase.DONT_ENUM|ESBase.DONT_DELETE");
      else
        cl.print("0");
      cl.println(");");
    }
    
    for (int i = 0; i < iterCount; i++)
      cl.println("java.util.Iterator iter" + i + ";");
    for (int i = 0; i < tempCount; i++)
      cl.println("ESBase temp" + i + ";");
    if (needsStatementResults())
      cl.println("ESBase _val0 = ESBase.esUndefined;");
    for (int i = 1; i <= stmtTop; i++)
      cl.println("ESBase _val" + i + " = ESBase.esUndefined;");
    if (hasSwitch) {
      cl.println("int _switchcode;");
      cl.println("ESBase _switchtemp;");
    }

    for (int i = 0; i < data.size(); i++) {
      Object d = data.get(i);
      if (d instanceof CharBuffer)
        cl.print((CharBuffer) d);
      else if (d instanceof Expr) {
        Expr expr = (Expr) d;
        //cl.setLine(expr.getFilename(), expr.getLine());
        expr.printExpr();
      }
    }

    if (tail != null)
      cl.print(tail);

    cl.popDepth();
    cl.println("}");
  }

  void printFormals()
    throws IOException
  {
    formal:
    for (int i = 0; formals != null && i < formals.size(); i++) {
      Variable formal = (Variable) formals.get(i);
      
      // Functions have priority
      if (funMap != null && funMap.get(formal.getId()) >= 0)
        continue formal;

      // Duplicate formals use the last one
      for (int j = i + 1; j < formals.size(); j++) {
        if (formal.getId() == ((Variable) formals.get(j)).getId())
          continue formal;
      }

      if (! allowLocals) {
      }
      else if (formal.getTypeExpr() instanceof JavaTypeExpr) {
      }
      else if (formal.getType() != Expr.TYPE_INTEGER) {
        cl.print("ESBase " + formal.getId());
        cl.println(" = _env.getArg(" + i + ", _length);");
      }
    }
  }

  /**
   * Initializes the local variables for a function.
   */
  void printLocalVariables()
    throws IOException
  {
    for (int i = 0; i < variables.size(); i++) {
      Variable var = (Variable) variables.get(i);

      if (funMap != null && funMap.get(var.getId()) >= 0) {
        var.killLocal();
        continue;
      }

      if (var.isJavaGlobal()) {
        // already initialized
      }
      else if ((! allowLocals || ! var.isJavaLocal()) && isGlobal()) {
        cl.print("_env.global.setProperty(");
        cl.printLiteral(var.getId());
        cl.println(", ESBase.esUndefined);");
      }
      else if (! var.isUsed() && ! useAllVariables()) {
      }
      else if (! allowLocals || ! var.isJavaLocal()) {
        if (isEval) {
          cl.print("if (_arg.getProperty(");
          cl.printLiteral(var.getId());
          cl.println(") == ESBase.esEmpty)");
          cl.print("  ");
        }
        if (! var.hasInit()) {
          cl.print("_arg.put(");
          cl.printLiteral(var.getId());
          cl.print(", ESBase.esUndefined, ");
          if (! isEval)
            cl.print("ESBase.DONT_ENUM|ESBase.DONT_DELETE");
          else
            cl.print("0");
          cl.println(");");
        }
      }
      else if (var.getType() == Expr.TYPE_BOOLEAN) {
        cl.println("boolean " + var.getId() + ";");
        if (! var.hasInit())
          cl.println(var.getId() + " = false;");
      }
      else if (var.getType() == Expr.TYPE_INTEGER) {
        cl.println("int " + var.getId() + ";");
        if (! var.hasInit())
          cl.println(var.getId() + " = 0;");
      }
      else if (var.getType() == Expr.TYPE_NUMBER) {
        cl.println("double " + var.getId() + ";");
        if (! var.hasInit())
          cl.println(var.getId() + " = Double.NaN;");
      }
      else if (var.getType() == Expr.TYPE_STRING) {
        cl.println("ESString " + var.getId() + ";");
        if (! var.hasInit())
          cl.println(var.getId() + " = null;");
      }
      else if (var.getType() == Expr.TYPE_JAVA &&
               var.getTypeExpr() != null) {
        TypeExpr type = (TypeExpr) var.getTypeExpr();
        
        cl.println(type.getTypeName() + " " + var.getId() + " = null;");
      }
      else if (var.isLocal()) {
        if (! var.hasInit())
          cl.println("ESBase " + var.getId() + " = ESBase.esUndefined;");
        else
          cl.println("ESBase " + var.getId() + ";");
      }
      else {
        cl.print("  _env.global.setProperty(");
        cl.printLiteral(var.getId());
        cl.println(", ESBase.esUndefined);");
      }
    }
  }
}
