/*
 * Copyright 2015 Google Inc.
 *
 * 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 com.google.template.soy.jbcsrc;

import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;

import com.google.template.soy.basicfunctions.AugmentMapFunction;
import com.google.template.soy.basicfunctions.CeilingFunction;
import com.google.template.soy.basicfunctions.FloorFunction;
import com.google.template.soy.basicfunctions.KeysFunction;
import com.google.template.soy.basicfunctions.MaxFunction;
import com.google.template.soy.basicfunctions.MinFunction;
import com.google.template.soy.basicfunctions.RandomIntFunction;
import com.google.template.soy.basicfunctions.RoundFunction;
import com.google.template.soy.data.SoyDict;
import com.google.template.soy.data.SoyList;
import com.google.template.soy.data.SoyMap;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.aggregate.MapType;
import com.google.template.soy.types.aggregate.UnionType;
import com.google.template.soy.types.primitive.FloatType;
import com.google.template.soy.types.primitive.IntType;
import com.google.template.soy.types.primitive.StringType;
import com.google.template.soy.types.primitive.UnknownType;

import org.objectweb.asm.Label;
import org.objectweb.asm.Type;

import java.util.List;

/**
 * Implements intrinsics for built in functions.
 *
 * <p>Many soy functions are very simple but the Tofu calling convention that is used requires us to
 * <ul>
 *     <li>Box all arguments
 *     <li>Pass the arguments as a {@code List}
 *     <li>Map {@code null} -> {@code NullData} and back
 * </ul>
 *
 * <p>A lot of this cost could be avoided by swapping out an intrinsic implementation.  For example,
 * {@code isNonnull($foo)} requires code that looks like: <pre>{@code
 * 
 *   List<SoyValue> args = new ArrayList<1>();
 *   args.put(<$foo>.box());
 *   SoyValue ret = Runtime.callSoyFunction(
 *       renderContext.getFunction("isNonnull"),
 *       args);
 *
 * }</pre>
 *
 * <p>But that could be easily replaced with a simpler implementation of just
 * {@code <$foo> == null}.  This class provides intrinsic implementations of all functions that are
 * built into {@code soy}.
 */
final class PluginFunctionCompiler {
  private static final SoyType NUMBER_TYPE = 
      UnionType.of(IntType.getInstance(), FloatType.getInstance());

  private static final MethodRef AUGMENT_MAP_FN =
      MethodRef.create(AugmentMapFunction.class, "augmentMap", SoyDict.class, SoyDict.class)
          .asNonNullable();
  private static final MethodRef CEIL_FN =
      MethodRef.create(CeilingFunction.class, "ceil", SoyValue.class).asNonNullable();
  private static final MethodRef FLOOR_FN =
      MethodRef.create(FloorFunction.class, "floor", SoyValue.class).asNonNullable();
  private static final MethodRef KEYS_FN =
      MethodRef.create(KeysFunction.class, "keys", SoyMap.class);
  private static final MethodRef LIST_SIZE = MethodRef.create(List.class, "size");
  private static final MethodRef MATH_CEIL =
      MethodRef.create(Math.class, "ceil", double.class).asCheap();
  private static final MethodRef MATH_FLOOR =
      MethodRef.create(Math.class, "floor", double.class).asCheap();
  private static final MethodRef MATH_MAX_DOUBLE =
      MethodRef.create(Math.class, "max", double.class, double.class).asCheap();
  private static final MethodRef MATH_MAX_LONG =
      MethodRef.create(Math.class, "max", long.class, long.class).asCheap();
  private static final MethodRef MATH_MIN_DOUBLE =
      MethodRef.create(Math.class, "min", double.class, double.class).asCheap();
  private static final MethodRef MATH_MIN_LONG =
      MethodRef.create(Math.class, "min", long.class, long.class).asCheap();
  private static final MethodRef MATH_ROUND =
      MethodRef.create(Math.class, "round", double.class).asCheap();
  private static final MethodRef MAX_FN =
      MethodRef.create(MaxFunction.class, "max", SoyValue.class, SoyValue.class).asNonNullable();
  private static final MethodRef MIN_FN =
      MethodRef.create(MinFunction.class, "min", SoyValue.class, SoyValue.class).asNonNullable();
  private static final MethodRef RANDOM_INT_FN =
      MethodRef.create(RandomIntFunction.class, "randomInt", long.class).asCheap();
  private static final MethodRef ROUND_FN =
      MethodRef.create(RoundFunction.class, "round", SoyValue.class).asNonNullable();
  private static final MethodRef ROUND_WITH_NUM_DIGITS_AFTER_POINT_FN =
      MethodRef.create(RoundFunction.class, "round", SoyValue.class, int.class).asNonNullable();
  private static final MethodRef SOYLIST_LENGTH = MethodRef.create(SoyList.class, "length");
  private static final MethodRef STRING_CONTAINS =
      MethodRef.create(String.class, "contains", CharSequence.class);
  private static final MethodRef STRING_INDEX_OF =
      MethodRef.create(String.class, "indexOf", String.class);
  private static final MethodRef STRING_LENGTH = MethodRef.create(String.class, "length");
  private static final MethodRef STRING_SUBSTR_START =
      MethodRef.create(String.class, "substring", int.class).asNonNullable();
  private static final MethodRef STRING_SUBSTR_START_END =
      MethodRef.create(String.class, "substring", int.class, int.class);

  private final TemplateParameterLookup parameterLookup;

  PluginFunctionCompiler(TemplateParameterLookup parameterLookup) {
    this.parameterLookup = parameterLookup;
  }

  /** Returns an expression that evaluates the given soy function. */
  SoyExpression callPluginFunction(FunctionNode node, List<SoyExpression> args) {
    switch (node.getFunctionName()) {
      case "augmentMap":
        return invokeAugmentMapFunction(args.get(0), args.get(1));
      case "ceiling":
        return invokeCeilingFunction(args.get(0));
      case "floor":
        return invokeFloorFunction(args.get(0));
      case "isNonnull":
        return invokeIsNonnullFunction(args.get(0));
      case "keys":
        return invokeKeysFunction(args.get(0));
      case "length":
        return invokeLengthFunction(args.get(0));
      case "max":
        return invokeMaxFunction(args.get(0), args.get(1));
      case "min":
        return invokeMinFunction(args.get(0), args.get(1));
      case "randomInt":
        return invokeRandomIntFunction(args.get(0));
      case "round":
        if (args.size() == 1) {
          return invokeRoundFunction(args.get(0));
        }
        return invokeRoundFunction(args.get(0), args.get(1));
      case "strContains":
        return invokeStrContainsFunction(args.get(0), args.get(1));
      case "strIndexOf":
        return invokeStrIndexOfFunction(args.get(0), args.get(1));
      case "strLen":
        return invokeStrLenFunction(args.get(0));
      case "strSub":
        if (args.size() == 2) {
          return invokeStrSubFunction(args.get(0), args.get(1));
        }
        return invokeStrSubFunction(args.get(0), args.get(1), args.get(2)); 
      default:
        // TODO(lukes): add support for the BidiFunctions
        return invokeSoyFunction(node, args);
    }
  }

  /**
   * @see com.google.template.soy.basicfunctions.AugmentMapFunction
   */
  private SoyExpression invokeAugmentMapFunction(SoyExpression arg0, SoyExpression arg1) {
    Expression first = arg0.cast(SoyDict.class);
    Expression second = arg1.cast(SoyDict.class);
    // TODO(lukes): this logic should move into the ResolveExpressionTypesVisitor
    MapType mapType = MapType.of(StringType.getInstance(), 
        UnionType.of(getMapValueType(arg0.soyType()), getMapValueType(arg1.soyType())));
    return SoyExpression.forSoyValue(mapType, AUGMENT_MAP_FN.invoke(first, second));
  }

  private SoyType getMapValueType(SoyType type) {
    if (type instanceof MapType) {
      return ((MapType) type).getValueType();
    }
    return UnknownType.getInstance();
  }

  /**
   * @see com.google.template.soy.basicfunctions.CeilingFunction
   */
  private SoyExpression invokeCeilingFunction(SoyExpression argument) {
    switch (argument.resultType().getSort()) {
      case Type.LONG:
        return argument;
      case Type.DOUBLE:
        return SoyExpression.forInt(
            BytecodeUtils.numericConversion(MATH_CEIL.invoke(argument), Type.LONG_TYPE));
      default:
        return SoyExpression.forSoyValue(IntType.getInstance(), CEIL_FN.invoke(argument.box()));
    }
  }

  /**
   * @see com.google.template.soy.basicfunctions.FloorFunction
   */
  private SoyExpression invokeFloorFunction(SoyExpression argument) {
    switch (argument.resultType().getSort()) {
      case Type.LONG:
        return argument;
      case Type.DOUBLE:
        return SoyExpression.forInt(
            BytecodeUtils.numericConversion(MATH_FLOOR.invoke(argument), Type.LONG_TYPE));
      default:
        return SoyExpression.forSoyValue(IntType.getInstance(), FLOOR_FN.invoke(argument.box()));
    }
  }

  /**
   * @see com.google.template.soy.basicfunctions.IsNonnullFunction
   */
  private SoyExpression invokeIsNonnullFunction(final SoyExpression soyExpression) {
    if (BytecodeUtils.isPrimitive(soyExpression.resultType())) {
      return SoyExpression.TRUE;
    }
    return SoyExpression.forBool(new Expression(Type.BOOLEAN_TYPE, soyExpression.features()){
      @Override void doGen(CodeBuilder adapter) {
        soyExpression.gen(adapter);
        Label isNull = new Label();
        adapter.ifNull(isNull);
        // non-null
        adapter.pushBoolean(true);
        Label end = new Label();
        adapter.goTo(end);
        adapter.mark(isNull);
        adapter.pushBoolean(false);
        adapter.mark(end);
      }
    });
  }

  /**
   * @see com.google.template.soy.basicfunctions.KeysFunction
   */
  private SoyExpression invokeKeysFunction(SoyExpression soyExpression) {
    SoyType argType = soyExpression.soyType();
    // TODO(lukes): this logic should live in ResolveExpressionTypesVisitor
    SoyType listElementType;
    if (argType instanceof MapType) {
      listElementType = ((MapType) argType).getKeyType();  // pretty much just string
    } else if (argType instanceof ListType) {
      listElementType = IntType.getInstance();
    } else {
      listElementType = UnknownType.getInstance();
    }
    return SoyExpression.forList(
        ListType.of(listElementType),
        KEYS_FN.invoke(soyExpression.box().cast(SoyMap.class)));
  }

  /**
   * @see com.google.template.soy.basicfunctions.LengthFunction
   */
  private SoyExpression invokeLengthFunction(SoyExpression soyExpression) {
    Expression lengthExpressionAsInt;
    if (soyExpression.isBoxed()) {
      lengthExpressionAsInt = soyExpression.cast(SoyList.class).invoke(SOYLIST_LENGTH);
    } else {
      lengthExpressionAsInt = soyExpression.cast(List.class).invoke(LIST_SIZE);
    }
    return SoyExpression.forInt(
        BytecodeUtils.numericConversion(lengthExpressionAsInt, Type.LONG_TYPE)); 
  }

  /**
   * @see com.google.template.soy.basicfunctions.MaxFunction
   */
  private SoyExpression invokeMaxFunction(SoyExpression left, SoyExpression right) {
    if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
      return SoyExpression.forInt(
          MATH_MAX_LONG.invoke(left.unboxAs(long.class), right.unboxAs(long.class)));
    } else if (left.assignableToNullableFloat() && right.assignableToNullableFloat()) {
      return SoyExpression.forFloat(
          MATH_MAX_DOUBLE.invoke(left.unboxAs(double.class), right.unboxAs(double.class)));
    } else {
      return SoyExpression.forSoyValue(NUMBER_TYPE, MAX_FN.invoke(left.box(), right.box()));
    }
  }

  /**
   * @see com.google.template.soy.basicfunctions.MinFunction
   */
  private SoyExpression invokeMinFunction(SoyExpression left, SoyExpression right) {
    if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
      return SoyExpression.forInt(
          MATH_MIN_LONG.invoke(left.unboxAs(long.class), right.unboxAs(long.class)));
    } else if (left.assignableToNullableFloat() && right.assignableToNullableFloat()) {
      return SoyExpression.forFloat(
          MATH_MIN_DOUBLE.invoke(left.unboxAs(double.class), right.unboxAs(double.class)));
    } else {
      return SoyExpression.forSoyValue(NUMBER_TYPE, MIN_FN.invoke(left.box(), right.box()));
    }
  }

  /**
   * @see com.google.template.soy.basicfunctions.RandomIntFunction
   */
  private SoyExpression invokeRandomIntFunction(SoyExpression soyExpression) {
    return SoyExpression.forInt(RANDOM_INT_FN.invoke(soyExpression.unboxAs(long.class)));
  }

  /**
   * @see com.google.template.soy.basicfunctions.RoundFunction
   */
  private SoyExpression invokeRoundFunction(SoyExpression soyExpression) {
    if (soyExpression.assignableToNullableInt()) {
      return soyExpression;
    }
    if (soyExpression.assignableToNullableFloat()) {
      return SoyExpression.forInt(MATH_ROUND.invoke(soyExpression.unboxAs(double.class)));
    }
    return SoyExpression.forInt(ROUND_FN.invoke(soyExpression.box()));
  }

  /**
   * @see com.google.template.soy.basicfunctions.RoundFunction
   */
  private SoyExpression invokeRoundFunction(SoyExpression value, SoyExpression digitsAfterPoint) {
    return SoyExpression.forSoyValue(NUMBER_TYPE, 
        ROUND_WITH_NUM_DIGITS_AFTER_POINT_FN.invoke(value.box(), 
            BytecodeUtils.numericConversion(digitsAfterPoint.unboxAs(long.class), Type.INT_TYPE)));
  }

  private SoyExpression invokeSoyFunction(FunctionNode node, List<SoyExpression> args) {
    Expression soyJavaFunctionExpr =
        MethodRef.RENDER_CONTEXT_GET_FUNCTION.invoke(
            parameterLookup.getRenderContext(), constant(node.getFunctionName()));
    Expression list = SoyExpression.asBoxedList(args);
    return SoyExpression.forSoyValue(UnknownType.getInstance(),
        MethodRef.RUNTIME_CALL_SOY_FUNCTION.invoke(soyJavaFunctionExpr, list));
  }

  /**
   * @see com.google.template.soy.basicfunctions.StrContainsFunction
   */
  private SoyExpression invokeStrContainsFunction(SoyExpression left, SoyExpression right) {
    return SoyExpression.forBool(
        left.unboxAs(String.class).invoke(STRING_CONTAINS, right.coerceToString()));
  }
  
  /**
   * @see com.google.template.soy.basicfunctions.StrIndexOfFunction
   */
  private SoyExpression invokeStrIndexOfFunction(SoyExpression left, SoyExpression right) {
    return SoyExpression.forInt(
        BytecodeUtils.numericConversion(
            left.unboxAs(String.class).invoke(STRING_INDEX_OF, right.unboxAs(String.class)),
            Type.LONG_TYPE));
  }

  /**
   * @see com.google.template.soy.basicfunctions.StrIndexOfFunction
   */
  private SoyExpression invokeStrLenFunction(SoyExpression str) {
    return SoyExpression.forInt(BytecodeUtils.numericConversion(
        str.unboxAs(String.class).invoke(STRING_LENGTH), Type.LONG_TYPE));
  }

  /**
   * @see com.google.template.soy.basicfunctions.StrSubFunction
   */
  private SoyExpression invokeStrSubFunction(SoyExpression str, SoyExpression startIndex) {
    return SoyExpression.forString(
        str.unboxAs(String.class).invoke(STRING_SUBSTR_START,
            BytecodeUtils.numericConversion(startIndex.unboxAs(long.class), Type.INT_TYPE)));
  }

  /**
   * @see com.google.template.soy.basicfunctions.StrSubFunction
   */
  private SoyExpression invokeStrSubFunction(
      SoyExpression str, SoyExpression startIndex, SoyExpression endIndex) {
    return SoyExpression.forString(
        str.unboxAs(String.class).invoke(STRING_SUBSTR_START_END,
            BytecodeUtils.numericConversion(startIndex.unboxAs(long.class), Type.INT_TYPE),
            BytecodeUtils.numericConversion(endIndex.unboxAs(long.class), Type.INT_TYPE)));
  }
}
