/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 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 Lesser General Public License for more details.
 *
 * Copyright (c) 2006 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.formula.lvalues;

import java.util.ArrayList;

import org.pentaho.reporting.libraries.formula.EvaluationException;
import org.pentaho.reporting.libraries.formula.FormulaContext;
import org.pentaho.reporting.libraries.formula.operators.InfixOperator;

/**
 * An term is a list of LValues connected by operators. For the sake of efficiency, this is not stored as tree. We store
 * the term as a list in the following format: (headValue)(OP value)* ...
 *
 * @author Thomas Morgner
 */
public class Term extends AbstractLValue
{
  private static final LValue[] EMPTY_L_VALUE = new LValue[0];
  private static final InfixOperator[] EMPTY_OPERATOR = new InfixOperator[0];
  private static final long serialVersionUID = -1854082494425470979L;

  private LValue optimizedHeadValue;
  private LValue headValue;
  private ArrayList operators;
  private ArrayList operands;
  private InfixOperator[] operatorArray;
  private LValue[] operandsArray;
  private boolean initialized;

  public Term(final LValue headValue)
  {
    if (headValue == null)
    {
      throw new NullPointerException();
    }

    this.headValue = headValue;
  }

  public TypeValuePair evaluate() throws EvaluationException
  {
    TypeValuePair result = optimizedHeadValue.evaluate();
    for (int i = 0; i < operandsArray.length; i++)
    {
      final LValue value = operandsArray[i];
      final InfixOperator op = operatorArray[i];
      result = op.evaluate(getContext(), result, value.evaluate());
    }
    return result;
  }

  public void add(final InfixOperator operator, final LValue operand)
  {
    if (operator == null)
    {
      throw new NullPointerException();
    }
    if (operand == null)
    {
      throw new NullPointerException();
    }

    if (operands == null || operators == null)
    {
      this.operands = new ArrayList();
      this.operators = new ArrayList();
    }

    operands.add(operand);
    operators.add(operator);
    initialized = false;
  }

  public void initialize(final FormulaContext context) throws EvaluationException
  {
    super.initialize(context);
    if (operands == null || operators == null)
    {
      this.optimizedHeadValue = headValue;
      this.optimizedHeadValue.initialize(context);
      this.operandsArray = EMPTY_L_VALUE;
      this.operatorArray = EMPTY_OPERATOR;
      return;
    }

    if (initialized)
    {
      optimizedHeadValue.initialize(context);
      for (int i = 0; i < operandsArray.length; i++)
      {
        final LValue lValue = operandsArray[i];
        lValue.initialize(context);
      }
      return;
    }

    optimize();
    this.optimizedHeadValue.initialize(context);
    for (int i = 0; i < operandsArray.length; i++)
    {
      final LValue value = operandsArray[i];
      value.initialize(context);
    }
    initialized = true;
  }

  private void optimize()
  {
    if (operands == null || operators == null)
    {
      this.optimizedHeadValue = headValue;
      this.operandsArray = EMPTY_L_VALUE;
      this.operatorArray = EMPTY_OPERATOR;
      return;
    }
    final ArrayList operators = (ArrayList) this.operators.clone();
    final ArrayList operands = (ArrayList) this.operands.clone();
    this.optimizedHeadValue = headValue;

    while (true)
    {
      // now start to optimize everything.
      // first, search the operator with the highest priority..
      final InfixOperator op = (InfixOperator) operators.get(0);
      int level = op.getLevel();
      boolean moreThanOne = false;
      for (int i = 1; i < operators.size(); i++)
      {
        final InfixOperator operator = (InfixOperator) operators.get(i);
        final int opLevel = operator.getLevel();
        if (opLevel != level)
        {
          moreThanOne = true;
          level = Math.min(opLevel, level);
        }
      }

      if (moreThanOne == false)
      {
        // No need to optimize the operators ..
        break;
      }

      // There are at least two op-levels in this term.
      Term subTerm = null;
      for (int i = 0; i < operators.size(); i++)
      {
        final InfixOperator operator = (InfixOperator) operators.get(i);
        if (operator.getLevel() != level)
        {
          subTerm = null;
          continue;
        }

        if (subTerm == null)
        {
          if (i == 0)
          {
            subTerm = new Term(optimizedHeadValue);
            optimizedHeadValue = subTerm;
          }
          else
          {
            final LValue lval = (LValue) operands.get(i - 1);
            subTerm = new Term(lval);
            operands.set(i - 1, subTerm);
          }
        }

        // OK, now a term exists, and we should join it.
        final LValue operand = (LValue) operands.get(i);
        subTerm.add(operator, operand);
        operands.remove(i);
        operators.remove(i);
        // Rollback the current index ..
        //noinspection AssignmentToForLoopParameter
        i -= 1;
      }
    }

    this.operatorArray = (InfixOperator[])
        operators.toArray(new InfixOperator[operators.size()]);
    this.operandsArray = (LValue[])
        operands.toArray(new LValue[operands.size()]);
  }

  /**
   * Returns any dependent lvalues (parameters and operands, mostly).
   *
   * @return
   */
  public LValue[] getChildValues()
  {
    if (operandsArray == null)
    {
      optimize();
    }
    final LValue[] values = new LValue[operandsArray.length + 1];
    values[0] = headValue;
    System.arraycopy(operandsArray, 0, values, 1, operandsArray.length);
    return values;
  }


  public String toString()
  {
    final StringBuffer b = new StringBuffer(100);

    b.append('(');
    b.append(headValue);
    if (operands != null && operators != null)
    {
      for (int i = 0; i < operands.size(); i++)
      {
        final InfixOperator op = (InfixOperator) operators.get(i);
        final LValue value = (LValue) operands.get(i);
        b.append(op);
        b.append(value);
      }
    }
    b.append(')');
//
//    b.append(";OPTIMIZED(");
//    b.append(optimizedHeadValue);
//    if (operandsArray != null && operatorArray != null)
//    {
//      for (int i = 0; i < operandsArray.length; i++)
//      {
//        final InfixOperator op = operatorArray[i];
//        final LValue value = operandsArray[i];
//        b.append(op);
//        b.append(value);
//      }
//    }
//    b.append(")");

    return b.toString();
  }

  /**
   * Checks whether the LValue is constant. Constant lvalues always return the same value.
   *
   * @return
   */
  public boolean isConstant()
  {
    if (headValue.isConstant() == false)
    {
      return false;
    }

    for (int i = 0; i < operands.size(); i++)
    {
      final LValue value = (LValue) operands.get(i);
      if (value.isConstant() == false)
      {
        return false;
      }
    }
    return true;
  }

  public Object clone() throws CloneNotSupportedException
  {
    final Term o = (Term) super.clone();
    if (operands != null)
    {
      o.operands = (ArrayList) operands.clone();
    }
    if (operators != null)
    {
      o.operators = (ArrayList) operators.clone();
    }
    o.headValue = (LValue) headValue.clone();
    o.optimizedHeadValue = null;
    o.operandsArray = null;
    o.operatorArray = null;
    o.initialized = false;
    return o;
  }

  public LValue[] getOperands()
  {
    return (LValue[]) operands.toArray(new LValue[operands.size()]);
  }

  public InfixOperator[] getOperators()
  {
    return (InfixOperator[]) operators.toArray(new InfixOperator[operators.size()]);
  }

  public LValue getHeadValue()
  {
    return headValue;
  }

  /**
   * Allows access to the post optimized head value note that without the optimization, it's difficult to traverse
   * libformula's object model.
   *
   * @return optimized head value
   */
  public LValue getOptimizedHeadValue()
  {
    return optimizedHeadValue;
  }
//
//  /**
//   * Allows access to the post optimized operator array
//   *
//   * @return optimized operator array
//   */
//  public InfixOperator[] getOptimizedOperators()
//  {
//    return operatorArray;
//  }
//
//  /**
//   * Allows access to the post optimized operand array
//   *
//   * @return optimized operand array
//   */
//  public LValue[] getOptimizedOperands()
//  {
//    return operandsArray;
//  }

  public ParsePosition getParsePosition()
  {
    final ParsePosition parsePosition = super.getParsePosition();
    if (parsePosition == null)
    {
      final int startColumn = headValue.getParsePosition().getStartColumn();
      final int startLine = headValue.getParsePosition().getStartLine();
      final ParsePosition lastParsePos =
          operandsArray[operandsArray.length - 1].getParsePosition();
      final int endColumn = lastParsePos.getEndColumn();
      final int endLine = lastParsePos.getEndLine();
      setParsePosition(new ParsePosition(startLine, startColumn, endLine, endColumn));
    }
    return super.getParsePosition();
  }
}
