package org.mvel2.ast;

import org.mvel2.CompileException;
import org.mvel2.MVEL;
import org.mvel2.Operator;
import org.mvel2.ParserContext;
import org.mvel2.integration.VariableResolver;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.util.ExecutionStack;
import org.mvel2.util.ParseTools;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Mike Brock <cbrock@redhat.com>
 */
public class Stacklang extends BlockNode {
  List<Instruction> instructionList;
  ParserContext pCtx;

  public Stacklang(char[] expr, int blockStart, int blockOffset, int fields, ParserContext pCtx) {
    super(pCtx);
    this.expr = expr;
    this.blockStart = blockStart;
    this.blockOffset = blockOffset;
    this.fields = fields | ASTNode.STACKLANG;

    String[] instructions = new String(expr, blockStart, blockOffset).split(";");

    instructionList = new ArrayList<Instruction>(instructions.length);
    for (String s : instructions) {
      instructionList.add(parseInstruction(s.trim()));
    }

    this.pCtx = pCtx;
  }

  @Override
  public Object getReducedValueAccelerated(Object ctx, Object thisValue, VariableResolverFactory factory) {
    ExecutionStack stk = new ExecutionStack();
    stk.push(getReducedValue(stk, thisValue, factory));
    if (stk.isReduceable()) {
      while (true) {
        stk.op();
        if (stk.isReduceable()) {
          stk.xswap();
        }
        else {
          break;
        }
      }
    }
    return stk.peek();
  }

  @Override
  public Object getReducedValue(Object ctx, Object thisValue, VariableResolverFactory factory) {
    ExecutionStack stack = (ExecutionStack) ctx;

    for (int i1 = 0, instructionListSize = instructionList.size(); i1 < instructionListSize; i1++) {
      Instruction instruction = instructionList.get(i1);

      System.out.println(stack.toString() + " >> " + instruction.opcode + ":" + instruction.expr);


      switch (instruction.opcode) {
        case Operator.STORE:
          if (instruction.cache == null) {
            instruction.cache = factory.createVariable(instruction.expr, stack.peek());
          }
          else {
            ((VariableResolver) instruction.cache).setValue(stack.peek());
          }
          break;
        case Operator.LOAD:
          if (instruction.cache == null) {
            instruction.cache = factory.getVariableResolver(instruction.expr);
          }
          stack.push(((VariableResolver) instruction.cache).getValue());
          break;
        case Operator.GETFIELD:
          try {
            if (stack.isEmpty() || !(stack.peek() instanceof Class)) {
              throw new CompileException("getfield without class", expr, blockStart);
            }

            Field field;
            if (instruction.cache == null) {
              instruction.cache = field = ((Class) stack.pop()).getField(instruction.expr);
            }
            else {
              stack.discard();
              field = (Field) instruction.cache;
            }

            stack.push(field.get(stack.pop()));
          }
          catch (Exception e) {
            throw new CompileException("field access error", expr, blockStart, e);
          }
          break;
        case Operator.STOREFIELD:
          try {
            if (stack.isEmpty() || !(stack.peek() instanceof Class)) {
              throw new CompileException("storefield without class", expr, blockStart);
            }

            Class cls = (Class) stack.pop();
            Object val = stack.pop();
            cls.getField(instruction.expr).set(stack.pop(), val);
            stack.push(val);
          }
          catch (Exception e) {
            throw new CompileException("field access error", expr, blockStart, e);
          }
          break;

        case Operator.LDTYPE:
          try {
            if (instruction.cache == null) {
              instruction.cache = ParseTools.createClass(instruction.expr, pCtx);
            }
            stack.push(instruction.cache);
          }
          catch (ClassNotFoundException e) {
            throw new CompileException("error", expr, blockStart, e);
          }
          break;

        case Operator.INVOKE:
          Object[] parms;
          ExecutionStack call = new ExecutionStack();
          while (!stack.isEmpty() && !(stack.peek() instanceof Class)) {
            call.push(stack.pop());
          }
          if (stack.isEmpty()) {
            throw new CompileException("invoke without class", expr, blockStart);
          }

          parms = new Object[call.size()];
          for (int i = 0; !call.isEmpty(); i++) parms[i] = call.pop();

          if ("<init>".equals(instruction.expr)) {
            Constructor c;
            if (instruction.cache == null) {
              instruction.cache = c = ParseTools.getBestConstructorCandidate(parms, (Class) stack.pop(), false);
            }
            else {
              c = (Constructor) instruction.cache;
            }

            try {
              stack.push(c.newInstance(parms));
            }
            catch (Exception e) {
              throw new CompileException("instantiation error", expr, blockStart, e);
            }
          }
          else {
            Method m;
            if (instruction.cache == null) {
              Class cls = (Class) stack.pop();

              instruction.cache = m = ParseTools.getBestCandidate(parms, instruction.expr, cls,
                  cls.getDeclaredMethods(), false);
            }
            else {
              stack.discard();
              m = (Method) instruction.cache;
            }

            try {
              stack.push(m.invoke(stack.isEmpty() ? null : stack.pop(), parms));
            }
            catch (Exception e) {
              throw new CompileException("invokation error", expr, blockStart, e);
            }
          }
          break;
        case Operator.PUSH:
          if (instruction.cache == null) {
            instruction.cache = MVEL.eval(instruction.expr, ctx, factory);
          }
          stack.push(instruction.cache);
          break;
        case Operator.POP:
          stack.pop();
          break;
        case Operator.DUP:
          stack.dup();
          break;
        case Operator.LABEL:
          break;
        case Operator.JUMPIF:
          if (!stack.popBoolean()) continue;

        case Operator.JUMP:
          if (instruction.cache != null) {
            i1 = (Integer) instruction.cache;
          }
          else {
            for (int i2 = 0; i2 < instructionList.size(); i2++) {
              Instruction ins = instructionList.get(i2);
              if (ins.opcode == Operator.LABEL &&
                  instruction.expr.equals(ins.expr)) {
                instruction.cache = i1 = i2;
                break;
              }
            }
          }
          break;
        case Operator.EQUAL:
          stack.push(stack.pop().equals(stack.pop()));
          break;
        case Operator.NEQUAL:
          stack.push(!stack.pop().equals(stack.pop()));
          break;
        case Operator.REDUCE:
          stack.op();
          break;
        case Operator.XSWAP:
          stack.xswap2();
          break;
        case Operator.SWAP:
          Object o = stack.pop();
          Object o2 = stack.pop();
          stack.push(o);
          stack.push(o2);
          break;

      }
    }

    return stack.pop();
  }

  private static class Instruction {
    int opcode;
    String expr;
    Object cache;
  }

  private static Instruction parseInstruction(String s) {
    int split = s.indexOf(' ');

    Instruction instruction = new Instruction();

    String keyword = split == -1 ? s : s.substring(0, split);

    if (opcodes.containsKey(keyword)) {
      instruction.opcode = opcodes.get(keyword);
    }

    //noinspection StringEquality
    if (keyword != s) {
      instruction.expr = s.substring(split + 1);
    }

    return instruction;
  }

  static final Map<String, Integer> opcodes = new HashMap<String, Integer>();

  static {
    opcodes.put("push", Operator.PUSH);
    opcodes.put("pop", Operator.POP);
    opcodes.put("load", Operator.LOAD);
    opcodes.put("ldtype", Operator.LDTYPE);
    opcodes.put("invoke", Operator.INVOKE);
    opcodes.put("store", Operator.STORE);
    opcodes.put("getfield", Operator.GETFIELD);
    opcodes.put("storefield", Operator.STOREFIELD);
    opcodes.put("dup", Operator.DUP);
    opcodes.put("jump", Operator.JUMP);
    opcodes.put("jumpif", Operator.JUMPIF);
    opcodes.put("label", Operator.LABEL);
    opcodes.put("eq", Operator.EQUAL);
    opcodes.put("ne", Operator.NEQUAL);
    opcodes.put("reduce", Operator.REDUCE);
    opcodes.put("xswap", Operator.XSWAP);
    opcodes.put("swap", Operator.SWAP);
  }
}
