package org.mvel2.util;

import org.mvel2.CompileException;
import org.mvel2.ParserContext;
import org.mvel2.ast.ASTNode;
import org.mvel2.ast.EndOfStatement;
import org.mvel2.ast.Proto;
import org.mvel2.compiler.ExecutableStatement;

import java.util.*;

import static org.mvel2.util.ParseTools.*;

public class ProtoParser {
  private char[] expr;
  private ParserContext pCtx;
  private int endOffset;

  private int cursor;
  private String protoName;

  String tk1 = null;
  String tk2 = null;

  private Class type;
  private String name;
  private String deferredName;

  private boolean interpreted = false;

  private ExecutionStack splitAccumulator;

  private static ThreadLocal<Queue<DeferredTypeResolve>> deferred = new ThreadLocal<Queue<DeferredTypeResolve>>();

  public ProtoParser(char[] expr, int offset, int offsetEnd, String protoName, ParserContext pCtx, int fields,
                     ExecutionStack splitAccumulator) {
    this.expr = expr;

    this.cursor = offset;
    this.endOffset = offsetEnd;

    this.protoName = protoName;
    this.pCtx = pCtx;

    this.interpreted = (ASTNode.COMPILE_IMMEDIATE & fields) == 0;

    this.splitAccumulator = splitAccumulator;
  }

  public Proto parse() {
    Proto proto = new Proto(protoName);

    Mainloop:
    while (cursor < endOffset) {
      cursor = ParseTools.skipWhitespace(expr, cursor);

      int start = cursor;

      if (tk2 == null) {
        while (cursor < endOffset && isIdentifierPart(expr[cursor])) cursor++;

        if (cursor > start) {
          tk1 = new String(expr, start, cursor - start);

          if ("def".equals(tk1) || "function".equals(tk1)) {
            cursor++;
            cursor = ParseTools.skipWhitespace(expr, cursor);
            start = cursor;
            while (cursor < endOffset && isIdentifierPart(expr[cursor])) cursor++;

            if (start == cursor) {
              throw new CompileException("attempt to declare an anonymous function as a prototype member",
                  expr, start);
            }

            FunctionParser parser =
                new FunctionParser(new String(expr, start, cursor - start),
                    cursor, endOffset, expr, 0, pCtx, null);

            proto.declareReceiver(parser.getName(), parser.parse());
            cursor = parser.getCursor() + 1;

            tk1 = null;
            continue;
          }
        }

        cursor = ParseTools.skipWhitespace(expr, cursor);
      }

      if (cursor > endOffset) {
        throw new CompileException("unexpected end of statement in proto declaration: " + protoName,
            expr, start);
      }

      switch (expr[cursor]) {
        case ';':
          cursor++;
          calculateDecl();

          if (interpreted && type == DeferredTypeResolve.class) {
            /**
             * If this type could not be immediately resolved, it may be a look-ahead case, so
             * we defer resolution of the type until later and place it in the wait queue.
             */
            enqueueReceiverForLateResolution(deferredName,
                proto.declareReceiver(name, Proto.ReceiverType.DEFERRED, null), null);
          }
          else {
            proto.declareReceiver(name, type, null);
          }
          break;

        case '=':
          cursor++;
          cursor = ParseTools.skipWhitespace(expr, cursor);
          start = cursor;

          Loop:
          while (cursor < endOffset) {
            switch (expr[cursor]) {
              case '{':
              case '[':
              case '(':
              case '\'':
              case '"':
                cursor = balancedCaptureWithLineAccounting(expr, cursor, endOffset, expr[cursor], pCtx);
                break;

              case ';':
                break Loop;
            }
            cursor++;
          }

          calculateDecl();

          String initString = new String(expr, start, cursor++ - start);

          if (interpreted && type == DeferredTypeResolve.class) {
            enqueueReceiverForLateResolution(deferredName,
                proto.declareReceiver(name, Proto.ReceiverType.DEFERRED, null), initString);
          }
          else {
            proto.declareReceiver(name, type, (ExecutableStatement)
                subCompileExpression(initString, pCtx));
          }
          break;

        default:
          start = cursor;
          while (cursor < endOffset && isIdentifierPart(expr[cursor])) cursor++;
          if (cursor > start) {
            tk2 = new String(expr, start, cursor - start);
          }
      }
    }

    cursor++;

    /**
     * Check if the function is manually terminated.
     */
    if (splitAccumulator != null && ParseTools.isStatementNotManuallyTerminated(expr, cursor)) {
      /**
       * Add an EndOfStatement to the split accumulator in the parser.
       */
      splitAccumulator.add(new EndOfStatement());
    }

    return proto;
  }

  private void calculateDecl() {
    if (tk2 != null) {
      try {
        if (pCtx.hasProtoImport(tk1)) {
          type = Proto.class;

        }
        else {
          type = ParseTools.findClass(null, tk1, pCtx);
        }
        name = tk2;

      }
      catch (ClassNotFoundException e) {
        if (interpreted) {
          type = DeferredTypeResolve.class;
          deferredName = tk1;
          name = tk2;
        }
        else {
          throw new CompileException("could not resolve class: " + tk1, expr, cursor, e);
        }
      }
    }
    else {
      type = Object.class;
      name = tk1;
    }

    tk1 = null;
    tk2 = null;
  }

  private interface DeferredTypeResolve {
    public boolean isWaitingFor(Proto proto);

    public String getName();

  }


  private void enqueueReceiverForLateResolution(final String name, final Proto.Receiver receiver, final String initializer) {
    Queue<DeferredTypeResolve> recv = deferred.get();
    if (recv == null) {
      deferred.set(recv = new LinkedList<DeferredTypeResolve>());
    }

    recv.add(new DeferredTypeResolve() {
      public boolean isWaitingFor(Proto proto) {
        if (name.equals(proto.getName())) {
          receiver.setType(Proto.ReceiverType.PROPERTY);
          receiver.setInitValue((ExecutableStatement) subCompileExpression(initializer, pCtx));
          return true;
        }
        return false;
      }

      public String getName() {
        return name;
      }
    });
  }

  public static void notifyForLateResolution(final Proto proto) {
    if (deferred.get() != null) {
      Queue<DeferredTypeResolve> recv = deferred.get();
      Set<DeferredTypeResolve> remove = new HashSet<DeferredTypeResolve>();
      for (DeferredTypeResolve r : recv) {
        if (r.isWaitingFor(proto)) {
          remove.add(r);
        }
      }

      for (DeferredTypeResolve r : remove) {
        recv.remove(r);
      }
    }
  }

  public int getCursor() {
    return cursor;
  }

  /**
   * This is such a horrible hack, but it's more performant than any other horrible hack I can think of
   * right now.
   *
   * @param expr
   * @param cursor
   * @param pCtx
   */
  public static void checkForPossibleUnresolvedViolations(char[] expr, int cursor, ParserContext pCtx) {
    if (isUnresolvedWaiting()) {
      LinkedHashMap<String, Object> imports =
          (LinkedHashMap<String, Object>) pCtx.getParserConfiguration().getImports();

      Object o = imports.values().toArray()[imports.size() - 1];

      if (o instanceof Proto) {
        Proto proto = (Proto) o;

        int last = proto.getCursorEnd();
        cursor--;

        /**
         * We walk backwards to ensure that the last valid statement was a proto declaration.
         */

        while (cursor > last && ParseTools.isWhitespace(expr[cursor])) cursor--;
        while (cursor > last && ParseTools.isIdentifierPart(expr[cursor])) cursor--;
        while (cursor > last && (ParseTools.isWhitespace(expr[cursor]) || expr[cursor] == ';')) cursor--;

        if (cursor != last) {
          throw new CompileException("unresolved reference (possible illegal forward-reference?): " +
              ProtoParser.getNextUnresolvedWaiting(), expr, proto.getCursorStart());
        }
      }
    }
  }

  public static boolean isUnresolvedWaiting() {
    return deferred.get() != null && !deferred.get().isEmpty();
  }

  public static String getNextUnresolvedWaiting() {
    if (deferred.get() != null && !deferred.get().isEmpty()) {
      return deferred.get().poll().getName();
    }
    return null;
  }

}
