/**
 * MVEL 2.0
 * Copyright (C) 2007 The Codehaus
 * Mike Brock, Dhanji Prasanna, John Graham, Mark Proctor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mvel2.ast;

import org.mvel2.CompileException;
import org.mvel2.MVEL;
import org.mvel2.Operator;
import org.mvel2.ParserContext;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.util.ErrorUtil;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

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

/**
 * @author Christopher Brock
 */
public class WithNode extends BlockNode implements NestedStatement {
  protected String nestParm;
  //   protected ExecutableStatement nestedStatement;
  protected ParmValuePair[] withExpressions;

  public WithNode(char[] expr, int start, int offset, int blockStart, int blockOffset, int fields,
                  ParserContext pCtx) {
    nestParm = createStringTrimmed(this.expr = expr, this.start = start, this.offset = offset);
    this.blockStart = blockStart;
    this.blockOffset = blockOffset;

    if ((fields & COMPILE_IMMEDIATE) != 0) {
      pCtx.setBlockSymbols(true);

      egressType = (compiledBlock = (ExecutableStatement)
          subCompileExpression(expr, start, offset, pCtx)).getKnownEgressType();

      withExpressions = compileWithExpressions(expr, blockStart, blockOffset, nestParm, egressType, pCtx);

      pCtx.setBlockSymbols(false);
    }
  }

  public Object getReducedValueAccelerated(Object ctx, Object thisValue, VariableResolverFactory factory) {
    Object ctxObject = compiledBlock.getValue(ctx, thisValue, factory);
    if (ctxObject == null) throw new CompileException("with-block against null pointer", expr, start);

    for (ParmValuePair pvp : withExpressions) {
      pvp.eval(ctxObject, factory);
    }

    return ctxObject;
  }


  public Object getReducedValue(Object ctx, Object thisValue, VariableResolverFactory factory) {
    parseWithExpressions(nestParm, expr, blockStart, blockOffset, ctx = MVEL.eval(expr, start, offset, ctx, factory), factory);
    return ctx;
  }

  public static ParmValuePair[] compileWithExpressions(char[] block, int start, int offset, String nestParm, Class egressType, ParserContext pCtx) {
    /**
     *
     * MAINTENANCE NOTE: AN INTERPRETED MODE VERSION OF THIS CODE IS DUPLICATED IN: ParseTools
     *
     */

    List<ParmValuePair> parms = new ArrayList<ParmValuePair>();
    String parm = "";

    int end = start + offset;

    int _st = start;
    int _end = -1;

    int oper = -1;
    for (int i = start; i < end; i++) {
      switch (block[i]) {
        case '{':
        case '[':
        case '(':
        case '\'':
        case '"':
          i = balancedCapture(block, i, end, block[i]);
          continue;

        case '/':
          if (i < end && block[i + 1] == '/') {
            while (i < end && block[i] != '\n') block[i++] = ' ';
            if (parm == null) _st = i;
          }
          else if (i < end && block[i + 1] == '*') {
            int len = end - 1;
            while (i < len && !(block[i] == '*' && block[i + 1] == '/')) {
              block[i++] = ' ';
            }
            block[i++] = ' ';
            block[i++] = ' ';

            if (parm == null) _st = i;
          }
          else if (i < end && block[i + 1] == '=') {
            oper = Operator.DIV;
          }
          continue;

        case '%':
        case '*':
        case '-':
        case '+':
          if (i + 1 < end && block[i + 1] == '=') {
            oper = opLookup(block[i]);
          }
          continue;

        case '=':
          parm = createStringTrimmed(block, _st, i - _st - (oper != -1 ? 1 : 0));
          _st = i + 1;
          continue;

        case ',':
          if (_end == -1) _end = i;

          if (parm == null || parm.length() == 0) {
            try {
              String expr;
              if (nestParm == null) {
                expr = new String(block, _st, _end - _st);
              }
              else {
                expr = new StringBuilder(nestParm).append('.')
                    .append(new String(block, _st, _end - _st)).toString();
              }

              parms.add(
                  new ParmValuePair(null, (ExecutableStatement)
                      subCompileExpression(expr, pCtx),
                          egressType, pCtx)
              );

            }
            catch (CompileException e) {
              e.setCursor(_st + (e.getCursor() - (e.getExpr().length - offset)));
              e.setExpr(block);
              throw e;
            }

            oper = -1;
            _st = ++i;
          }
          else {
            if (nestParm == null) {
              throw new CompileException("operative assignment not possible here", block, start);
            }

            try {
              parms.add(new ParmValuePair(
                  parm,
                  oper != -1 ?
                      (ExecutableStatement) subCompileExpression(
                          createShortFormOperativeAssignment(nestParm + "." + parm,
                              block, _st, _end - _st, oper), pCtx)
                      //or
                      : (ExecutableStatement) subCompileExpression(block, _st, _end - _st, pCtx)
                  , egressType, pCtx));
            }
            catch (CompileException e) {
              e.setCursor(_st + (e.getCursor() - (e.getExpr().length - offset)));
              e.setExpr(block);
              throw e;
            }

            parm = null;
            oper = -1;
            _st = ++i;
          }

          _end = -1;

          break;
      }
    }

    if (_st != (_end = end)) {
      try {
        if (parm == null || "".equals(parm)) {
          String expr;
          if (nestParm == null) {
            expr = new String(block, _st, _end - _st);
          }
          else {
            expr = new StringBuilder(nestParm).append('.')
                .append(new String(block, _st, _end - _st)).toString();
          }

          parms.add(
              new ParmValuePair(null, (ExecutableStatement)
                  subCompileExpression(expr, pCtx),
                      egressType, pCtx)
          );
        }
        else {
          if (nestParm == null) {
            throw new CompileException("operative assignment not possible here", block, start);
          }

          parms.add(new ParmValuePair(
              parm,
              oper != -1 ?
                  (ExecutableStatement) subCompileExpression(
                      createShortFormOperativeAssignment(nestParm + "." + parm, block, _st, _end - _st, oper),
                      pCtx)
                  //or
                  : (ExecutableStatement) subCompileExpression(block, _st, _end - _st, pCtx)
              , egressType, pCtx));
        }
      }
      catch (CompileException e) {
        throw ErrorUtil.rewriteIfNeeded(e, block, _st);
      }
    }

    ParmValuePair[] withExpressions;
    parms.toArray(withExpressions = new ParmValuePair[parms.size()]);
    return withExpressions;
  }


  public ExecutableStatement getNestedStatement() {
    return compiledBlock;
  }

//  public ParmValuePair[] getWithExpressions() {
//    return withExpressions;
//  }

  public static final class ParmValuePair implements Serializable {
    private Serializable setExpression;
    private ExecutableStatement statement;

    public ParmValuePair(String parameter, ExecutableStatement statement, Class ingressType, ParserContext pCtx) {
      if (parameter != null && parameter.length() != 0) {
        this.setExpression = MVEL.compileSetExpression(parameter,
            ingressType != null ? getReturnType(ingressType, parameter, pCtx) : Object.class
            , pCtx);
      }
      this.statement = statement;
    }

    public Serializable getSetExpression() {
      return setExpression;
    }

    public ExecutableStatement getStatement() {
      return statement;
    }

    public void eval(Object ctx, VariableResolverFactory factory) {
      if (setExpression == null) {
        this.statement.getValue(ctx, factory);
      }
      else {
        MVEL.executeSetExpression(setExpression, ctx, factory, this.statement.getValue(ctx, factory));
      }
    }
  }
}
