package com.bytezone.diskbrowser.visicalc;

import java.util.ArrayList;
import java.util.List;

class Expression extends AbstractValue
{
  private final List<String> operators = new ArrayList<String> ();
  private final List<String> signs = new ArrayList<String> ();

  public Expression (Cell cell, String text)
  {
    super (cell, text);

    String line = balanceBrackets (text);   // add trailing right brackets if necessary

    if (Condition.isCondition (text))
    {
      values.add (new Condition (cell, text));
      signs.add ("(+)");                        // reduce() needs this
      return;
    }

    int ptr = 0;
    while (ptr < line.length ())
    {
      // check for optional leading + or -
      char ch = line.charAt (ptr);
      if (ch == '-')
      {
        signs.add ("(-)");
        ch = line.charAt (++ptr);
      }
      else
      {
        signs.add ("(+)");
        if (ch == '+')
          ch = line.charAt (++ptr);
      }

      // check for mandatory function/sub-expression/number/cell reference
      switch (ch)
      {
        case '@':                                           // function
          String functionText = getFunctionCall (line.substring (ptr));
          ptr += functionText.length ();
          values.add (cell.getFunction (functionText));
          break;

        case '(':                                           // parentheses block
          String bracketText = getBalancedText (line.substring (ptr));
          ptr += bracketText.length ();
          values.add (new Expression (cell,
              bracketText.substring (1, bracketText.length () - 1)));
          break;

        case '#':                                           // no idea
          System.out.printf ("Hash character [%s] in [%s]%n", ch, line);
          ptr++;
          break;

        default:
          if (ch == '.' || (ch >= '0' && ch <= '9'))        // number
          {
            String numberText = getNumberText (line.substring (ptr));
            ptr += numberText.length ();
            values.add (new Number (cell, numberText));
          }
          else if (ch >= 'A' && ch <= 'Z')                  // cell address
          {
            String addressText = getAddressText (line.substring (ptr));
            ptr += addressText.length ();
            values.add (cell.getCell (addressText));
          }
          else
          {
            System.out.printf ("Unexpected character [%s] in [%s]%n", ch, line);
            return;
          }
      }

      // check for possible continuation operator
      if (ptr < line.length ())
      {
        ch = line.charAt (ptr);
        if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '^')
          operators.add (line.substring (ptr, ++ptr));
        else
        {
          System.out.printf ("Unknown operator [%s] in [%s]%n", ch, line);
          return;
        }
      }
    }

    assert values.size () > 0;
    valueType = values.get (0).getValueType ();
  }

  Value reduce ()
  {
    return values.size () == 1 && signs.get (0).equals ("(+)") ? values.get (0) : this;
  }

  Value get (int index)
  {
    if (index < 0 || index >= values.size ())
      throw new IllegalArgumentException ();
    return values.get (index);
  }

  @Override
  public String getType ()
  {
    return "Expression";
  }

  @Override
  public void calculate ()
  {
    assert values.size () > 0;

    try
    {
      Value thisValue = values.get (0);
      thisValue.calculate ();

      if (valueType == ValueType.NUMBER)
        value = thisValue.getDouble ();
      else
        bool = thisValue.getBoolean ();

      if (!thisValue.isValid ())            // ERROR / NA
      {
        valueType = thisValue.getValueType ();
        return;
      }

      String sign = signs.get (0);
      if (sign.equals ("(-)"))
        value *= -1;

      for (int i = 1; i < values.size (); i++)      // only NUMBER will enter here
      {
        thisValue = values.get (i);
        thisValue.calculate ();

        if (!thisValue.isValid ())
        {
          valueType = thisValue.getValueType ();
          return;
        }

        double nextValue = thisValue.getDouble ();

        sign = signs.get (i);
        if (sign.equals ("(-)"))
          nextValue *= -1;

        String operator = operators.get (i - 1);
        if (operator.equals ("+"))
          value += nextValue;
        else if (operator.equals ("-"))
          value -= nextValue;
        else if (operator.equals ("*"))
          value *= nextValue;
        else if (operator.equals ("/"))
        {
          if (nextValue == 0)
          {
            valueResult = ValueResult.ERROR;
            return;
          }
          value /= nextValue;
        }
        else if (operator.equals ("^"))
          value = Math.pow (value, nextValue);

        if (Double.isNaN (value))
        {
          valueResult = ValueResult.ERROR;
          return;
        }
      }
    }
    catch (Exception e)
    {
      valueResult = ValueResult.ERROR;
      e.printStackTrace ();
      return;
    }
  }

  private String balanceBrackets (String input)
  {
    String line = input.trim ();

    int leftBracket = 0;
    int rightBracket = 0;

    for (char c : line.toCharArray ())
      if (c == '(')
        leftBracket++;
      else if (c == ')')
        rightBracket++;

    if (leftBracket != rightBracket)
    {
      if (rightBracket > leftBracket)
      {
        System.out.printf ("**** Unbalanced brackets: left:%d, right:%d  ****%n",
            leftBracket, rightBracket);
        System.out.println (input);
        return "@ERROR";
      }

      while (rightBracket < leftBracket)
      {
        line = line + ")";
        rightBracket++;
      }
    }
    return line;
  }

  // called for functions and expressions
  private String getBalancedText (String text)
  {
    int ptr = text.indexOf ('(');         // find first left parenthesis
    if (ptr < 0)
      return text;
    int depth = 1;

    while (++ptr < text.length ())        // find matching right parenthesis
    {
      if (text.charAt (ptr) == ')')
      {
        --depth;
        if (depth == 0)
          break;
      }
      else if (text.charAt (ptr) == '(')
        ++depth;
    }

    return text.substring (0, ptr + 1);   // include closing parenthesis
  }

  // reads text up to the next comma that is not part of a function
  // text does not include the outer brackets or calling function name
  static String getParameter (String text)
  {
    int depth = 0;
    int ptr = 0;

    while (ptr < text.length ())
    {
      char c = text.charAt (ptr);
      if (c == '(')
        ++depth;
      else if (c == ')')
        --depth;
      else if (c == ',' && depth == 0)
        break;
      ++ptr;
    }

    return text.substring (0, ptr);
  }

  // receives a string starting with the function name
  private String getFunctionCall (String text)
  {
    if (text.charAt (0) != '@')
      throw new IllegalArgumentException ("Bad function name: " + text);

    for (String functionName : Function.functionList)
      if (text.startsWith (functionName))
      {
        if (functionName.endsWith ("("))          // if function has parameters
          return getBalancedText (text);          //   return full function call
        return functionName;                      // return function name only
      }

    throw new IllegalArgumentException ("Bad function name: " + text);
  }

  private String getNumberText (String text)
  {
    int ptr = 0;
    while (++ptr < text.length ())
    {
      char c = text.charAt (ptr);
      if (c != '.' && (c < '0' || c > '9'))
        break;
    }
    return text.substring (0, ptr);
  }

  private String getAddressText (String text)
  {
    int ptr = 0;
    while (++ptr < text.length ())
    {
      char c = text.charAt (ptr);
      if ((c < '0' || c > '9') && (c < 'A' || c > 'Z'))
        break;
    }
    return text.substring (0, ptr);
  }

  public String fullText ()
  {
    StringBuilder text = new StringBuilder ();

    int ptr = 0;
    for (Value value : values)
    {
      assert value != null;
      text.append (signs.get (ptr));
      text.append (value.getDouble ());
      if (ptr < operators.size ())
        text.append (operators.get (ptr++));
    }

    return text.toString ();
  }

  @Override
  public String toString ()
  {
    StringBuilder text = new StringBuilder ();
    text.append (String.format ("%s%n", LINE));
    text.append (String.format (FMT4, "Exprssion", getFullText (), valueType,
        getValueText (this)));
    int index = 0;
    for (Value value : values)
    {
      String sign = signs.get (index);
      if (!"(+)".equals (sign))
        text.append (String.format (FMT2, "sign", sign));
      text.append (String.format (FMT4, value.getType (), value.getFullText (),
          value.getValueType (), getValueText (value)));
      if (index < operators.size ())
        text.append (String.format (FMT2, "operator", operators.get (index)));
      ++index;
    }
    return text.toString ();
  }
}