package jxl.biff.formula;

import common.Logger;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Stack;
import jxl.WorkbookSettings;
import jxl.biff.WorkbookMethods;

class StringFormulaParser implements Parser {
  private static Logger logger = Logger.getLogger(StringFormulaParser.class);
  
  private String formula;
  
  private String parsedFormula;
  
  private ParseItem root;
  
  private Stack arguments;
  
  private WorkbookSettings settings;
  
  private ExternalSheet externalSheet;
  
  private WorkbookMethods nameTable;
  
  public StringFormulaParser(String f, ExternalSheet es, WorkbookMethods nt, WorkbookSettings ws) {
    this.formula = f;
    this.settings = ws;
    this.externalSheet = es;
    this.nameTable = nt;
  }
  
  public void parse() throws FormulaException {
    ArrayList tokens = getTokens();
    Iterator i = tokens.iterator();
    this.root = parseCurrent(i);
  }
  
  private ParseItem parseCurrent(Iterator i) throws FormulaException {
    Stack stack = new Stack();
    Stack operators = new Stack();
    Stack args = null;
    boolean parenthesesClosed = false;
    ParseItem lastParseItem = null;
    while (i.hasNext() && !parenthesesClosed) {
      ParseItem pi = i.next();
      if (pi instanceof Operand) {
        handleOperand((Operand)pi, stack);
      } else if (pi instanceof StringFunction) {
        handleFunction((StringFunction)pi, i, stack);
      } else if (pi instanceof Operator) {
        Operator op = (Operator)pi;
        if (op instanceof StringOperator) {
          StringOperator sop = (StringOperator)op;
          if (stack.isEmpty() || lastParseItem instanceof Operator) {
            op = sop.getUnaryOperator();
          } else {
            op = sop.getBinaryOperator();
          } 
        } 
        if (operators.empty()) {
          operators.push(op);
        } else {
          Operator operator = operators.peek();
          if (op.getPrecedence() < operator.getPrecedence()) {
            operators.push(op);
          } else {
            operators.pop();
            operator.getOperands(stack);
            stack.push(operator);
            operators.push(op);
          } 
        } 
      } else if (pi instanceof ArgumentSeparator) {
        while (!operators.isEmpty()) {
          Operator o = operators.pop();
          o.getOperands(stack);
          stack.push(o);
        } 
        if (args == null)
          args = new Stack(); 
        args.push(stack.pop());
        stack.clear();
      } else if (pi instanceof OpenParentheses) {
        ParseItem pi2 = parseCurrent(i);
        Parenthesis p = new Parenthesis();
        pi2.setParent(p);
        p.add(pi2);
        stack.push(p);
      } else if (pi instanceof CloseParentheses) {
        parenthesesClosed = true;
      } 
      lastParseItem = pi;
    } 
    while (!operators.isEmpty()) {
      Operator o = operators.pop();
      o.getOperands(stack);
      stack.push(o);
    } 
    ParseItem rt = !stack.empty() ? stack.pop() : null;
    if (args != null && rt != null)
      args.push(rt); 
    this.arguments = args;
    if (!stack.empty() || !operators.empty())
      logger.warn("Formula " + this.formula + " has a non-empty parse stack"); 
    return rt;
  }
  
  private ArrayList getTokens() throws FormulaException {
    ArrayList tokens = new ArrayList();
    StringReader sr = new StringReader(this.formula);
    Yylex lex = new Yylex(sr);
    lex.setExternalSheet(this.externalSheet);
    lex.setNameTable(this.nameTable);
    try {
      ParseItem pi = lex.yylex();
      while (pi != null) {
        tokens.add(pi);
        pi = lex.yylex();
      } 
    } catch (IOException e) {
      logger.warn(e.toString());
    } catch (Error e) {
      throw new FormulaException(FormulaException.lexicalError, this.formula + " at char  " + lex.getPos());
    } 
    return tokens;
  }
  
  public String getFormula() {
    if (this.parsedFormula == null) {
      StringBuffer sb = new StringBuffer();
      this.root.getString(sb);
      this.parsedFormula = sb.toString();
    } 
    return this.parsedFormula;
  }
  
  public byte[] getBytes() {
    byte[] bytes = this.root.getBytes();
    if (this.root.isVolatile()) {
      byte[] newBytes = new byte[bytes.length + 4];
      System.arraycopy(bytes, 0, newBytes, 4, bytes.length);
      newBytes[0] = Token.ATTRIBUTE.getCode();
      newBytes[1] = 1;
      bytes = newBytes;
    } 
    return bytes;
  }
  
  private void handleFunction(StringFunction sf, Iterator i, Stack stack) throws FormulaException {
    ParseItem pi2 = parseCurrent(i);
    if (sf.getFunction(this.settings) == Function.UNKNOWN)
      throw new FormulaException(FormulaException.unrecognizedFunction); 
    if (sf.getFunction(this.settings) == Function.SUM && this.arguments == null) {
      Attribute a = new Attribute(sf, this.settings);
      a.add(pi2);
      stack.push(a);
      return;
    } 
    if (sf.getFunction(this.settings) == Function.IF) {
      Attribute a = new Attribute(sf, this.settings);
      VariableArgFunction vaf = new VariableArgFunction(this.settings);
      int k = this.arguments.size();
      for (int j = 0; j < k; j++) {
        ParseItem pi3 = this.arguments.get(j);
        vaf.add(pi3);
      } 
      a.setIfConditions(vaf);
      stack.push(a);
      return;
    } 
    if (sf.getFunction(this.settings).getNumArgs() == 255) {
      if (this.arguments == null) {
        int numArgs = (pi2 != null) ? 1 : 0;
        VariableArgFunction vaf = new VariableArgFunction(sf.getFunction(this.settings), numArgs, this.settings);
        if (pi2 != null)
          vaf.add(pi2); 
        stack.push(vaf);
      } else {
        int k = this.arguments.size();
        VariableArgFunction vaf = new VariableArgFunction(sf.getFunction(this.settings), k, this.settings);
        ParseItem[] args = new ParseItem[k];
        int j;
        for (j = 0; j < k; j++) {
          ParseItem pi3 = this.arguments.pop();
          args[k - j - 1] = pi3;
        } 
        for (j = 0; j < args.length; j++)
          vaf.add(args[j]); 
        stack.push(vaf);
        this.arguments.clear();
        this.arguments = null;
      } 
      return;
    } 
    BuiltInFunction bif = new BuiltInFunction(sf.getFunction(this.settings), this.settings);
    int numargs = sf.getFunction(this.settings).getNumArgs();
    if (numargs == 1) {
      bif.add(pi2);
    } else {
      if ((this.arguments == null && numargs != 0) || (this.arguments != null && numargs != this.arguments.size()))
        throw new FormulaException(FormulaException.incorrectArguments); 
      for (int j = 0; j < numargs; j++) {
        ParseItem pi3 = this.arguments.get(j);
        bif.add(pi3);
      } 
    } 
    stack.push(bif);
  }
  
  public void adjustRelativeCellReferences(int colAdjust, int rowAdjust) {
    this.root.adjustRelativeCellReferences(colAdjust, rowAdjust);
  }
  
  public void columnInserted(int sheetIndex, int col, boolean currentSheet) {
    this.root.columnInserted(sheetIndex, col, currentSheet);
  }
  
  public void columnRemoved(int sheetIndex, int col, boolean currentSheet) {
    this.root.columnRemoved(sheetIndex, col, currentSheet);
  }
  
  public void rowInserted(int sheetIndex, int row, boolean currentSheet) {
    this.root.rowInserted(sheetIndex, row, currentSheet);
  }
  
  public void rowRemoved(int sheetIndex, int row, boolean currentSheet) {
    this.root.rowRemoved(sheetIndex, row, currentSheet);
  }
  
  private void handleOperand(Operand o, Stack stack) {
    if (!(o instanceof IntegerValue)) {
      stack.push(o);
      return;
    } 
    if (o instanceof IntegerValue) {
      IntegerValue iv = (IntegerValue)o;
      if (!iv.isOutOfRange()) {
        stack.push(iv);
      } else {
        DoubleValue dv = new DoubleValue(iv.getValue());
        stack.push(dv);
      } 
    } 
  }
}
