/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.drill.exec.store.mapr.db.json;

import org.apache.commons.codec.binary.Base64;

import org.apache.drill.common.expression.FunctionCall;
import org.apache.drill.common.expression.LogicalExpression;
import org.apache.drill.common.expression.SchemaPath;
import org.apache.drill.common.expression.ValueExpressions.IntExpression;
import org.apache.drill.common.expression.ValueExpressions.LongExpression;
import org.apache.drill.common.expression.ValueExpressions.QuotedString;
import org.apache.drill.common.expression.visitors.AbstractExprVisitor;
import com.google.common.collect.ImmutableMap;
import org.ojai.Value;
import org.ojai.store.QueryCondition;

import com.mapr.db.impl.ConditionImpl;
import com.mapr.db.impl.MapRDBImpl;

import java.nio.ByteBuffer;

class OjaiFunctionsProcessor extends AbstractExprVisitor<Void, Void, RuntimeException> {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(OjaiFunctionsProcessor.class);
  private QueryCondition queryCond;

  private OjaiFunctionsProcessor() {
  }

  private static String getStackTrace() {
    final Throwable throwable = new Throwable();
    final StackTraceElement[] ste = throwable.getStackTrace();
    final StringBuilder sb = new StringBuilder();
    for (int i = 1; i < ste.length; ++i) {
      sb.append(ste[i].toString());
      sb.append('\n');
    }

    return sb.toString();
  }

  @Override
  public Void visitUnknown(LogicalExpression e, Void valueArg) throws RuntimeException {
    logger.debug("visitUnknown() e class " + e.getClass());
    logger.debug(getStackTrace());
    return null;
  }

  private static class Ref<T> {
    T value;
  }

  private static SchemaPath getSchemaPathArg(LogicalExpression expr) {
    final Ref<SchemaPath> ref = new Ref<>();
    expr.accept(new OjaiFunctionsProcessor() {
        @Override
        public Void visitSchemaPath(SchemaPath e, Void v) {
          ref.value = e;
          return null;
        }
     }, null);

      return ref.value;
  }

  private static String getStringArg(LogicalExpression expr) {
    final Ref<QuotedString> ref = new Ref<>();
    expr.accept(new OjaiFunctionsProcessor() {
        @Override
        public Void visitQuotedStringConstant(QuotedString e, Void v) {
          ref.value = e;
          return null;
        }
     }, null);

      return ref.value != null ? ref.value.getString() : null;
  }

  private static int getIntArg(LogicalExpression expr) {
    final Ref<Integer> ref = new Ref<>();
    expr.accept(new OjaiFunctionsProcessor() {
        @Override
        public Void visitIntConstant(IntExpression e, Void v) {
          ref.value = new Integer(e.getInt());
          return null;
        }
     }, null);

      return ref.value != null ? ref.value.intValue() : 0;
  }

  private static long getLongArg(LogicalExpression expr) {
    final Ref<Long> ref = new Ref<>();
    expr.accept(new OjaiFunctionsProcessor() {
        @Override
        public Void visitIntConstant(IntExpression e, Void v) {
          ref.value = new Long(e.getInt());
          return null;
        }

        @Override
        public Void visitLongConstant(LongExpression e, Void v) {
          ref.value = e.getLong();
          return null;
        }
     }, null);

      return ref.value != null ? ref.value.longValue() : 0;
  }

  private final static ImmutableMap<String, QueryCondition.Op> STRING_TO_RELOP;
  static {
    ImmutableMap.Builder<String, QueryCondition.Op> builder = ImmutableMap.builder();
    STRING_TO_RELOP = builder
        .put("=", QueryCondition.Op.EQUAL)
        .put("<>", QueryCondition.Op.NOT_EQUAL)
        .put("<", QueryCondition.Op.LESS)
        .put("<=", QueryCondition.Op.LESS_OR_EQUAL)
        .put(">", QueryCondition.Op.GREATER)
        .put(">=", QueryCondition.Op.GREATER_OR_EQUAL)
        .build();
  }

  @Override
  public Void visitFunctionCall(FunctionCall call, Void v) throws RuntimeException {
    final String functionName = call.getName();
    final String fieldName = FieldPathHelper.schemaPath2FieldPath(getSchemaPathArg(call.arg(0))).asPathString();
    switch(functionName) {
    case "ojai_sizeof": {
      // ojai_sizeof(field, "<rel-op>", <int-value>)
      final String relOp = getStringArg(call.arg(1));
      final long size = getLongArg(call.arg(2));
      queryCond = MapRDBImpl.newCondition()
          .sizeOf(fieldName, STRING_TO_RELOP.get(relOp), size)
          .build();
      break;
    }

    case "ojai_typeof":
    case "ojai_nottypeof": {
      // ojai_[not]typeof(field, <type-code>);
      final int typeCode = getIntArg(call.arg(1));
      final Value.Type typeValue = Value.Type.valueOf(typeCode);
      queryCond = MapRDBImpl.newCondition();
      if (functionName.equals("ojai_typeof")) {
        queryCond.typeOf(fieldName, typeValue);
      } else {
        queryCond.notTypeOf(fieldName, typeValue);
      }
      queryCond.build();
      break;
    }

    case "ojai_matches":
    case "ojai_notmatches": {
      // ojai_[not]matches(field, <regex>);
      final String regex = getStringArg(call.arg(1));
      if (functionName.equals("ojai_matches")) {
        queryCond = MapRDBImpl.newCondition()
            .matches(fieldName, regex);
      } else {
        queryCond = MapRDBImpl.newCondition()
            .notMatches(fieldName, regex);
      }
      queryCond.build();
      break;
    }

    case "ojai_condition": {
      // ojai_condition(field, <serialized-condition>);
       final String condString = getStringArg(call.arg(1));
      final byte[] condBytes = Base64.decodeBase64(condString);
      final ByteBuffer condBuffer = ByteBuffer.wrap(condBytes);
      queryCond = ConditionImpl.parseFrom(condBuffer);
      break;
    }

    default:
      throw new IllegalArgumentException("unrecognized functionName " + functionName);
    } // switch(functionName)

    return null;
  }

  public static OjaiFunctionsProcessor process(FunctionCall call) {
    final OjaiFunctionsProcessor processor = new OjaiFunctionsProcessor();

    call.accept(processor, null);
    return processor;
  }

  public QueryCondition getCondition() {
    return queryCond;
  }

}
