
package com.orientechnologies.orient.core.sql.methods;

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.parser.OBaseParser;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.command.OCommandExecutorNotFoundException;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.sql.OCommandSQL;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.OSQLHelper;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterItemAbstract;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterItemField;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterItemVariable;
import com.orientechnologies.orient.core.sql.filter.OSQLPredicate;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionRuntime;
import com.orientechnologies.orient.core.sql.method.OSQLMethod;

import java.util.List;

/**
 * Wraps function managing the binding of parameters.
 * 
 * @author Luca Garulli (l.garulli--at--orientechnologies.com)
 * 
 */
public class OSQLMethodRuntime extends OSQLFilterItemAbstract implements Comparable<OSQLMethodRuntime> {

  public OSQLMethod method;
  public Object[]   configuredParameters;
  public Object[]   runtimeParameters;

  public OSQLMethodRuntime(final OBaseParser iQueryToParse, final String iText) {
    super(iQueryToParse, iText);
  }

  public OSQLMethodRuntime(final OSQLMethod iFunction) {
    method = iFunction;
  }

  /**
   * Execute a method.
   * 
   * @param iCurrentRecord
   *          Current record
   * @param iCurrentResult
   *          TODO
   * @param iContext
   * @return
   */
  public Object execute(final Object iThis, final OIdentifiable iCurrentRecord, final Object iCurrentResult,
      final OCommandContext iContext) {
    if (iThis == null)
      return null;

    if (configuredParameters != null) {
      // RESOLVE VALUES USING THE CURRENT RECORD
      for (int i = 0; i < configuredParameters.length; ++i) {
        runtimeParameters[i] = configuredParameters[i];

        if (method.evaluateParameters()) {
          if (configuredParameters[i] instanceof OSQLFilterItemField) {
            runtimeParameters[i] = ((OSQLFilterItemField) configuredParameters[i]).getValue(iCurrentRecord, iCurrentResult,
                iContext);
            if (runtimeParameters[i] == null && iCurrentResult instanceof OIdentifiable)
              // LOOK INTO THE CURRENT RESULT
              runtimeParameters[i] = ((OSQLFilterItemField) configuredParameters[i]).getValue((OIdentifiable) iCurrentResult,
                  iCurrentResult, iContext);
          } else if (configuredParameters[i] instanceof OSQLMethodRuntime)
            runtimeParameters[i] = ((OSQLMethodRuntime) configuredParameters[i]).execute(iThis, iCurrentRecord, iCurrentResult,
                                                                                          iContext);
          else if (configuredParameters[i] instanceof OSQLFunctionRuntime)
            runtimeParameters[i] = ((OSQLFunctionRuntime) configuredParameters[i]).execute(iThis, iCurrentRecord, iCurrentResult,
                iContext);
          else if (configuredParameters[i] instanceof OSQLFilterItemVariable) {
            runtimeParameters[i] = ((OSQLFilterItemVariable) configuredParameters[i]).getValue(iCurrentRecord, iCurrentResult,
                iContext);
            if (runtimeParameters[i] == null && iCurrentResult instanceof OIdentifiable)
              // LOOK INTO THE CURRENT RESULT
              runtimeParameters[i] = ((OSQLFilterItemVariable) configuredParameters[i]).getValue((OIdentifiable) iCurrentResult,
                  iCurrentResult, iContext);
          } else if (configuredParameters[i] instanceof OCommandSQL) {
            try {
              runtimeParameters[i] = ((OCommandSQL) configuredParameters[i]).setContext(iContext).execute();
            } catch (OCommandExecutorNotFoundException e) {
              // TRY WITH SIMPLE CONDITION
              final String text = ((OCommandSQL) configuredParameters[i]).getText();
              final OSQLPredicate pred = new OSQLPredicate(text);
              runtimeParameters[i] = pred.evaluate(iCurrentRecord instanceof ORecord ? (ORecord) iCurrentRecord : null,
                  (ODocument) iCurrentResult, iContext);
              // REPLACE ORIGINAL PARAM
              configuredParameters[i] = pred;

            }
          } else if (configuredParameters[i] instanceof OSQLPredicate)
            runtimeParameters[i] = ((OSQLPredicate) configuredParameters[i]).evaluate(iCurrentRecord.getRecord(),
                (iCurrentRecord instanceof ODocument ? (ODocument) iCurrentResult : null), iContext);
          else if (configuredParameters[i] instanceof String) {
            if (configuredParameters[i].toString().startsWith("\"") || configuredParameters[i].toString().startsWith("'"))
              runtimeParameters[i] = OStringSerializerHelper.getStringContent(configuredParameters[i]);
          }
        }
      }

      if (method.getMaxParams() == -1 || method.getMaxParams() > 0) {
        if (runtimeParameters.length < method.getMinParams()
            || (method.getMaxParams() > -1 && runtimeParameters.length > method.getMaxParams()))
          throw new OCommandExecutionException("Syntax error: function '"
              + method.getName()
              + "' needs "
              + (method.getMinParams() == method.getMaxParams() ? method.getMinParams() : method.getMinParams() + "-"
                  + method.getMaxParams()) + " argument(s) while has been received " + runtimeParameters.length);
      }
    }

    final Object functionResult = method.execute(iThis, iCurrentRecord, iContext, iCurrentResult, runtimeParameters);

    return transformValue(iCurrentRecord, iContext, functionResult);
  }

  @Override
  public Object getValue(final OIdentifiable iRecord, Object iCurrentResult, OCommandContext iContext) {
    final ODocument current = iRecord != null ? (ODocument) iRecord.getRecord() : null;
    return execute(current, current, null, iContext);
  }

  @Override
  public String getRoot() {
    return method.getName();
  }

  @Override
  protected void setRoot(final OBaseParser iQueryToParse, final String iText) {
    final int beginParenthesis = iText.indexOf('(');

    // SEARCH FOR THE FUNCTION
    final String funcName = iText.substring(0, beginParenthesis);

    final List<String> funcParamsText = OStringSerializerHelper.getParameters(iText);

    method = OSQLEngine.getInstance().getMethod(funcName);
    if (method == null)
      throw new OCommandSQLParsingException("Unknown method " + funcName + "()");

    // PARSE PARAMETERS
    this.configuredParameters = new Object[funcParamsText.size()];
    for (int i = 0; i < funcParamsText.size(); ++i)
      this.configuredParameters[i] = funcParamsText.get(i);

    setParameters(configuredParameters, true);
  }

  public OSQLMethodRuntime setParameters(final Object[] iParameters, final boolean iEvaluate) {
    if (iParameters != null) {
      this.configuredParameters = new Object[iParameters.length];
      for (int i = 0; i < iParameters.length; ++i) {
        this.configuredParameters[i] = iParameters[i];

        if (iParameters[i] != null) {
          if (iParameters[i] instanceof String && !iParameters[i].toString().startsWith("[")) {
            final Object v = OSQLHelper.parseValue(null, null, iParameters[i].toString(), null);
            if (v == OSQLHelper.VALUE_NOT_PARSED
                || (v != null && OMultiValue.isMultiValue(v) && OMultiValue.getFirstValue(v) == OSQLHelper.VALUE_NOT_PARSED))
              continue;

            configuredParameters[i] = v;
          }
        } else
          this.configuredParameters[i] = null;
      }

      // COPY STATIC VALUES
      this.runtimeParameters = new Object[configuredParameters.length];
      for (int i = 0; i < configuredParameters.length; ++i) {
        if (!(configuredParameters[i] instanceof OSQLFilterItemField) && !(configuredParameters[i] instanceof OSQLMethodRuntime))
          runtimeParameters[i] = configuredParameters[i];
      }
    }

    return this;
  }

  public OSQLMethod getMethod() {
    return method;
  }

  public Object[] getConfiguredParameters() {
    return configuredParameters;
  }

  public Object[] getRuntimeParameters() {
    return runtimeParameters;
  }

  @Override
  public int compareTo(final OSQLMethodRuntime o) {
    return method.compareTo(o.getMethod());
  }
}
