/*
 * Copyright (c) 1998-2008 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.el;

import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;

import javax.el.ELContext;
import javax.el.FunctionMapper;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;

/**
 * Parses the expression.
 */
public class ELParser
{
  private static final Logger log = Logger.getLogger(ELParser.class.getName());
  private static final L10N L = new L10N(ELParser.class);

  // The expression string
  private String _string;
  // Current parse index into the string
  private int _index;
  // The peek token
  private int _peek = -1;
  // The current lexeme
  private String _lexeme;
  // Temporary buffer
  private CharBuffer _cb = new CharBuffer();

  protected final ELContext _elContext;

  private boolean _checkEscape = true;

  public ELParser(ELContext elContext, String string)
  {
    if (elContext == null)
      throw new NullPointerException();
    
    _elContext = elContext;
    _string = string;
  }

  protected ELParser create(String string)
  {
    ELParser parser = new ELParser(_elContext, string);

    copyTo(parser);

    return parser;
  }

  /**
   * Copy to the dest parser.
   */
  protected void copyTo(ELParser parser)
  {
  }

  /**
   * Set true if escapes are checked.
   */
  public void setCheckEscape(boolean checkEscape)
  {
    _checkEscape = checkEscape;
  }

  /**
   * Parses the expression string.
   */
  public Expr parse()
    throws ELParseException
  {
    return parseInterpolate();
  }

  /**
   * Parses interpolated code.
   */
  public Expr parseInterpolate()
    throws ELParseException
  {
    CharBuffer text = CharBuffer.allocate();
    CharBuffer exprString = CharBuffer.allocate();
    Expr expr = null;
    int ch;

    while ((ch = read()) >= 0) {
      if (_checkEscape && ch == '\\') {
	ch = read();

	if (ch == '$' || ch == '#' || ch == '\\')
	  text.append((char) ch);
	else {
	  text.append('\\');
	  unread();
	}
      }
      else if (ch == '$' || ch == '#') {
	int origChar = ch;
	
        ch = read();
        
        if (ch == '{') {
          if (text.length() > 0) {
            StringLiteral right = new StringLiteral(text.toString());

            if (expr == null)
              expr = right;
            else
              expr = new InterpolateExpr(expr, right);

            text.clear();
          }

          exprString.clear();

          for (ch = read(); ch > 0 && ch != '}'; ch = read()) {
            exprString.append((char) ch);
	    
	    if (ch == '\'' || ch == '"') {
	      int end = ch;

	      for (ch = read(); ch > 0 && ch != end; ch = read()) {
		exprString.append((char) ch);

		if (ch == '\\') {
		  ch = read();
		  if (ch > 0)
		    exprString.append((char) ch);
		}
	      }

	      if (ch > 0)
		exprString.append((char) ch);
	    }
	  }

	  if (ch != '}')
	    throw error(L.l("expected '}' at end of EL expression",
			    exprString));

          Expr right = create(exprString.toString()).parseExpr();

          if (expr == null)
            expr = right;
          else
            expr = new InterpolateExpr(expr, right);
        }
        else {
          text.append((char) origChar);
          unread();
        }
      }
      else
        text.append((char) ch);
    }

    if (text.length() > 0) {
      StringLiteral right = new StringLiteral(text.toString());

      if (expr == null)
        expr = right;
      else
        expr = new InterpolateExpr(expr, right);
    }

    if (expr == null)
      expr = new StringLiteral("");

    return expr;
  }

  /**
   * expr ::= term
   */
  private Expr parseExpr()
    throws ELParseException
  {
    Expr left = parseTerm();

    while (true) {
      int token = scanToken();
      
      switch (token) {
      case '?':
	{
	  Expr trueExpr = parseExpr();
	  token = scanToken();
	  if (token != ':')
	    throw error(L.l("Expected ':' at {0}.  Conditional syntax is 'expr ? expr : expr'.", badChar(token)));
	  Expr falseExpr = parseExpr();

	  left = new ConditionalExpr(left, trueExpr, falseExpr);
	}
	break;
	
      case Expr.OR:
	left = parseOrExpr(token, left, parseTerm());
	break;

      case Expr.AND:
	left = parseAndExpr(token, left, parseTerm());
	break;

      case Expr.EQ: case Expr.NE: case Expr.LT:
      case Expr.LE: case Expr.GT: case Expr.GE:
	left = parseCmpExpr(token, left, parseTerm());
	break;
        
      case Expr.ADD: case Expr.SUB:
	left = parseAddExpr(token, left, parseTerm());
	break;

      case Expr.MUL: case Expr.DIV: case Expr.MOD:
	left = parseMulExpr(token, left, parseTerm());
	break;

      default:
        _peek = token;
	return left;
      }
    }
  }

  /**
   * or-expr ::= or-expr 'or' expr
   *         ::= and-expr
   */
  private Expr parseOrExpr(int code, Expr left, Expr right)
    throws ELParseException
  {
    while (true) {
      int token = scanToken();
      switch (token) {
      case Expr.OR:
        left = new BooleanExpr(code, left, right);
	code = token;
	right = parseTerm();
	break;
        
      case Expr.AND:
	right = parseAndExpr(token, right, parseTerm());
	break;
        
      case Expr.EQ: case Expr.NE:
      case Expr.LT: case Expr.GT:
      case Expr.LE: case Expr.GE:
	right = parseCmpExpr(token, right, parseTerm());
	break;
        
      case Expr.ADD: case Expr.SUB:
	right = parseAddExpr(token, right, parseTerm());
	break;

      case Expr.MUL: case Expr.DIV: case Expr.MOD:
	right = parseMulExpr(token, right, parseTerm());
	break;

      default:
	_peek = token;
        return new BooleanExpr(code, left, right);
      }
    }
  }

  /**
   * and-expr ::= and-expr 'and' expr
   *          ::= cmp-expr
   */
  private Expr parseAndExpr(int code, Expr left, Expr right)
    throws ELParseException
  {
    while (true) {
      int token = scanToken();
      switch (token) {
      case Expr.AND:
        left = new BooleanExpr(code, left, right);
	code = token;
	right = parseTerm();
	break;
        
      case Expr.EQ: case Expr.NE:
      case Expr.LT: case Expr.GT:
      case Expr.LE: case Expr.GE:
	right = parseCmpExpr(token, right, parseTerm());
	break;
        
      case Expr.ADD: case Expr.SUB:
	right = parseAddExpr(token, right, parseTerm());
	break;

      case Expr.MUL: case Expr.DIV: case Expr.MOD:
	right = parseMulExpr(token, right, parseTerm());
	break;

      default:
	_peek = token;
        return new BooleanExpr(code, left, right);
      }
    }
  }

  /**
   * cmp-expr ::= cmp-expr '=' expr
   *          ::= add-expr
   */
  private Expr parseCmpExpr(int code, Expr left, Expr right)
    throws ELParseException
  {
    while (true) {
      int token = scanToken();
      switch (token) {
      case Expr.EQ: case Expr.NE:
      case Expr.GT: case Expr.LT:
      case Expr.LE: case Expr.GE:
	left = CmpExpr.create(code, left, right);
          
	code = token;
	right = parseTerm();
	break;
        
      case Expr.ADD: case Expr.SUB:
	right = parseAddExpr(token, right, parseTerm());
	break;

      case Expr.MUL: case Expr.DIV: case Expr.MOD:
	right = parseMulExpr(token, right, parseTerm());
	break;

      default:
	_peek = token;
	return CmpExpr.create(code, left, right);
      }
    }
  }

  /**
   * add-expr ::= add-expr '+' expr
   *          ::= mul-expr
   */
  private Expr parseAddExpr(int code, Expr left, Expr right)
    throws ELParseException
  {
    while (true) {
      int token = scanToken();
      switch (token) {
      case Expr.ADD: case Expr.SUB:
	left = BinaryExpr.create(code, left, right);
	code = token;
	right = parseTerm();
	break;

      case Expr.MUL: case Expr.DIV: case Expr.MOD:
	right = parseMulExpr(token, right, parseTerm());
	break;

      default:
	_peek = token;
	return BinaryExpr.create(code, left, right);
      }
    }
  }

  /**
   * mul-expr ::= mul-expr '*' expr
   *          ::= expr
   */
  private Expr parseMulExpr(int code, Expr left, Expr right)
    throws ELParseException
  {
    while (true) {
      int token = scanToken();
      switch (token) {
      case Expr.MUL: case Expr.DIV: case Expr.MOD:
	left = BinaryExpr.create(code, left, right);
	right = parseTerm();
	code = token;
	break;

      default:
	_peek = token;
	return BinaryExpr.create(code, left, right);
      }
    }
  }

  /**
   * term ::= simple-term
   *      ::= term '[' expr ']'
   *      ::= term . identifier
   */
  private Expr parseTerm()
    throws ELParseException
  {
    Expr term = parseSimpleTerm();
    
    while (true) {
      int token = scanToken();

      switch (token) {
      case '[':
      {
	Expr expr = parseExpr();
        token = scanToken();
	if (token != ']')
	  throw error(L.l("Expected `]' at {0}.  All open array braces must have matching closing brace.", badChar(token)));

	term = term.createField(expr);
        break;
      }
      
      case '(':
      {
        ArrayList<Expr> argList = new ArrayList<Expr>();

        int ch = skipWhitespace(read());

        while (ch > 0 && ch != ')') {
          unread();
          argList.add(parseExpr());
          
          token = scanToken();
          
          if (token != ',') {
            ch = token;
            break;
          }
          
          ch = skipWhitespace(read());
        }
        
	if (ch != ')')
	  throw error(L.l("Expected `)' at {0}.  All functions must have matching closing parenthesis.", badChar(ch)));

        // token = scanToken();

        Expr []args = (Expr []) argList.toArray(new Expr[argList.size()]);

	Expr expr = term.createMethod(args);
        if (expr == null)
	  throw error(L.l("Method call not supported in this context `{0}'.",
                          term));
        term = expr;
        break;
      }

      case '.':
      {
        int ch = skipWhitespace(read());
        
        if (! Character.isJavaIdentifierStart((char) ch))
	  throw error(L.l("Expected `]' at {0}.  Field references must be identifiers.", badChar(ch)));

        String field = readName(ch);

        term = term.createField(field);
        break;
      }
        
      default:
        _peek = token;
        return term;
      }
    }
  }

  /**
   * simple-term ::= number
   *             ::= '(' expr ')'
   *             ::= variable
   *             ::= '"' string '"'
   *             ::= true | false | null
   */
  private Expr parseSimpleTerm()
    throws ELParseException
  {
    int ch = read();
    
    ch = skipWhitespace(ch);

    switch (ch) {
    case '.':
    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
      {
	long value = 0;
	double exp = 1;
        int digits = 0;

	for (; ch >= '0' && ch <= '9'; ch = read())
	  value = 10 * value + ch - '0';

        if (ch != '.' && ch != 'e' && ch != 'E') {
          unread();
          return new LongLiteral(value);
        }
        
	if (ch == '.') {
	  for (ch = read(); ch >= '0' && ch <= '9'; ch = read()) {
	    value = 10 * value + ch - '0';
	    exp *= 10;
            digits--;
	  }
	}

        if (ch == 'e' || ch == 'E') {
          int sign = 1;
          int expValue = 0;
          
          ch = read();
          if (ch == '-') {
            sign = -1;
            ch = read();
          }
          else if (ch == '+')
            ch = read();
          
          for (; ch >= '0' && ch <= '9'; ch = read())
            expValue = 10 * expValue + ch - '0';

          exp = Math.pow(10, digits + sign * expValue);

          unread();
          
          return new DoubleLiteral((double) value * (double) exp);
        }
        
	unread();
	return new DoubleLiteral((double) value / (double) exp);
      }

    case '-':
      return new MinusExpr(parseTerm());

    case '!':
      return UnaryExpr.create(Expr.NOT, parseTerm());

    case '+':
      return parseTerm();

    case '(':
      {
	Expr expr = parseExpr();
	if ((ch = scanToken()) != ')')
	  throw error(L.l("Expected `)' at {0}.  All open parentheses must have matching closing parentheses.", badChar(ch)));
	
	return expr;
      }

    case '\'': case '"':
      {
	int end = ch;
	CharBuffer cb = new CharBuffer();
        for (ch = read(); ch >= 0; ch = read()) {
          if (ch == '\\')
            cb.append((char) read());
          else if (ch != end)
            cb.append((char) ch);
          else if ((ch = read()) == end)
            cb.append((char) ch);
          else {
            unread();
            break;
          }
        }

	return new StringLiteral(cb.toString());
      }

    default:
      if (! Character.isJavaIdentifierStart((char) ch) && ch != ':')
	throw error(L.l("Unexpected character at {0}.", badChar(ch)));
      
      CharBuffer cb = CharBuffer.allocate();
      for (;
           Character.isJavaIdentifierPart((char) ch) || ch == ':';
           ch = read())
        cb.append((char) ch);

      unread();

      String name = cb.close();

      if (name.equals("null"))
        return new NullLiteral();
      else if (name.equals("true"))
        return new BooleanLiteral(true);
      else if (name.equals("false"))
        return new BooleanLiteral(false);
      else if (name.equals("not"))
        return UnaryExpr.create(Expr.NOT, parseTerm());
      else if (name.equals("empty"))
        return UnaryExpr.create(Expr.EMPTY, parseTerm());
      else {
	VariableMapper varMapper = _elContext.getVariableMapper();
	
	ValueExpression valueExpr = null;

	if (varMapper != null)
	  valueExpr = varMapper.resolveVariable(name);

	if (valueExpr != null)
	  return new ValueExpr(name, valueExpr);
	
        Expr expr = createImplicitObjectExpr(name);

        if (expr != null)
          return expr;

	try {
	  Method method = getStaticMethod(name);

	  if (method != null)
	    return new StaticMethodExpr(method);
	  else
	    return new IdExpr(name);
	} catch (Exception e) {
	  log.log(Level.FINEST, e.toString(), e);
	  
	  return new IdExpr(name);
	}
      }
    }
  }

  /**
   * Creates the implicit object for the name.
   */
  protected Expr createImplicitObjectExpr(String name)
  {
    return null;
  }

  /**
   * Creates the implicit object for the name.
   */
  protected Method getStaticMethod(String name)
    throws ELParseException
  {
    Method method = null;

    FunctionMapper funMapper = _elContext.getFunctionMapper();

    if (funMapper != null) {
      String prefix = "";
      String localName = name;

      int p = name.indexOf(':');
      if (p > 0) {
	prefix = name.substring(0, p);
	localName = name.substring(p + 1);
      }
      
      method = funMapper.resolveFunction(prefix, localName);
    }
    
    return method;
  }

  /**
   * Scans the next token.
   *
   * @return token code, expressed as an Expr enumeration.
   */
  private int scanToken()
    throws ELParseException
  {
    if (_peek >= 0) {
      int value = _peek;
      _peek = -1;
      return value;
    }

    int ch = skipWhitespace(read());

    switch (ch) {
    case '+': return Expr.ADD;
    case '-': return Expr.SUB;
    case '*': return Expr.MUL;
    case '/': return Expr.DIV;
    case '%': return Expr.MOD;

    case '!':
      ch = read();
      if (ch == '=')
	return Expr.NE;
      else
        return Expr.NOT;
      
    case '=':
      ch = read();
      if (ch == '=')
	return Expr.EQ;
      else
	throw error(L.l("expected '==' at '={0}'", badChar(ch)));
      
    case '&':
      ch = read();
      if (ch == '&')
	return Expr.AND;
      else
	throw error(L.l("expected '&&' at '&{0}'", badChar(ch)));
      
    case '|':
      ch = read();
      if (ch == '|')
	return Expr.OR;
      else
	throw error(L.l("expected '||' at '|{0}'", badChar(ch)));

    case '<':
      ch = read();
      if (ch == '=')
	return Expr.LE;
      else {
	unread();
	return Expr.LT;
      }

    case '>':
      ch = read();
      if (ch == '=')
	return Expr.GE;
      else {
	unread();
	return Expr.GT;
      }

    case '[':
      return '[';
      
    case ']':
      return ']';
      
    case ')':
      return ')';
      
    case '(':
      return '(';
      
    case '.':
      return '.';
      
    case ',':
      return ',';
      
    case '?':
    case ':':
      return ch;

    default:
      if (Character.isJavaIdentifierStart((char) ch)) {
	String name = readName(ch);

	if (name.equals("div"))
	  return Expr.DIV;
	else if (name.equals("mod"))
	  return Expr.MOD;
	else if (name.equals("eq"))
	  return Expr.EQ;
	else if (name.equals("ne"))
	  return Expr.NE;
	else if (name.equals("lt"))
	  return Expr.LT;
	else if (name.equals("le"))
	  return Expr.LE;
	else if (name.equals("gt"))
	  return Expr.GT;
	else if (name.equals("ge"))
	  return Expr.GE;
	else if (name.equals("and"))
	  return Expr.AND;
	else if (name.equals("or"))
	  return Expr.OR;
	else
	  throw error(L.l("expected binary operation at `{0}'", name));
      }

      unread();
      return -1;
    }
  }

  private String readName(int ch)
  {
    CharBuffer cb = CharBuffer.allocate();

    for (; Character.isJavaIdentifierPart((char) ch); ch = read())
      cb.append((char) ch);

    unread();

    return cb.toString();
  }

  /**
   * Skips whitespace, returning the next meaningful character.
   */
  private int skipWhitespace(int ch)
    throws ELParseException
  {
    for (; ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; ch = read()) {
    }

    return ch;
  }

  /**
   * Reads the next character, returning -1 on end of file.
   */
  private int read()
  {
    if (_index < _string.length())
      return _string.charAt(_index++);
    else {
      _index++;
      return -1;
    }
  }

  /**
   * Unread the last character.
   */
  private void unread()
  {
    _index--;
  }

  /**
   * Returns a readable version of the character.
   */
  private String badChar(int ch)
  {
    if (ch < 0)
      return L.l("end of file");
    else if (ch == '\n')
      return L.l("end of line");
    else
      return "`" + (char) ch + "'";
  }

  /**
   * Returns an new exception.
   */
  private ELParseException error(String message)
  {
    return new ELParseException(message + " in " + _string);
  }
}
