/*
 * 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.jssrc.internal;

import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.jssrc.restricted.JsExpr;
import com.google.template.soy.types.SoyType;

/**
 * Details of helper functions that decompose structured data and which survive Closure Compiler's
 * <a href="https://developers.google.com/closure/compiler/docs/api-tutorial3#propnames">
 * aggressive property renaming</a>.
 *
 * <h3>Field Names in Aggressively Renamed JavaScript</h3>
 * <p>
 * In uncompiled JavaScript, it may be obvious that {@code obj.field}, {@code obj['field']}, and
 * {@code obj.getField()} are similar.
 * We could easily write a generic function that takes {@code obj} and a field name and reads
 * that field.
 * <p>
 * Aaggressive property renaming does not preserve the relationship between the
 * string {@code "field"} and any methods or properties on a JavaScript prototype object.
 * The compiler is free to inline methods like {@code getField}, so there may be no
 * name, guessable or not, for a getter.
 *
 * <h3>Value Conversion</h3>
 * <p>Sometimes values need to be converted.
 * For example, the closure class {@code goog.html.SafeHtml} is related to the protocol buffer
 * {@code goog.html.SafeHtmlProto}.  Both represent safe-by-construction strings of HTML.
 * Soy has a {@code html} type that specifies the same contract.
 * <p>When reading a protocol buffer field yields a {@code SafeHtmlProto} value, we need to
 * convert it to a type that the rest of the Soy runtime understands, like
 * {@code goog.html.SafeHtml} or {@code soydata.SanitizedContent}.</p>
 * <p>Pluggable value converters allow that, also via helper functions.</p>
 */
final class HelperFunctions {

  /**
   * Common information needed for a particular helper function.
   */
  abstract static class BaseHelperInfo {
    /**
     * The path of the function in closure.
     * If this is {@code "foo.Bar"} then a generated JS file would need to
     * {@code goog.require("foo.Bar")}.
     */
    final String closurePath;  // autogenerated.
    /**
     * Indicates whether the JS code generator is responsible for producing an output with
     * a {@code goog.provide(closurePath)}.  For example, non-union value converters are
     * provided by library code.
     */
    final CodeLocation codeLocation;
    /**
     * True if calls to this helper might appear in a field lookup whose type is a union type.
     * If so, any generated helper would need to check the type assumption.
     * If not, then no runtime-type check is needed.
     */
    boolean appearsInUnion;

    /**
     * @param closurePath The path of the function in closure.
     *   If this is {@code "foo.Bar"} then a generated JS file would need to
     *   {@code goog.require("foo.Bar")}.
     * @param codeLocation Indicates whether the JS code generator is responsible for producing
     *   an output with a {@code goog.provide(closurePath)}.  For example, non-union value
     *   converters are provided by library code.
     */
    BaseHelperInfo(String closurePath, CodeLocation codeLocation) {
      this.closurePath = Preconditions.checkNotNull(closurePath);
      this.codeLocation = Preconditions.checkNotNull(codeLocation);
    }
  }


  /** The location of a helper function. */
  enum CodeLocation {
    /**
     * Generated by the JS backend, so needs to be provided, not required by the helper fn file.
     * Template output files need to require the code regardless.
     */
    GENERATED,
    /** Defined by linked library code so needs to be required by generated outputs. */
    EXTERNAL,
    ;
  }


  /**
   * <p>Value converters do the work of converting field values into types that can be used by Soy.
   *
   * <p>Value converters are linked library code so we don’t need to generate code for them, but we
   * do need to generate {@code goog.require} directives.
   */
  static final class ValueConverterHelperInfo extends BaseHelperInfo {
    /** The type of the value being converted. */
    final SoyType valueType;

    /**
     * @param closurePath The path of the function in closure.
     *   If this is {@code "foo.Bar"} then a generated JS file would need to
     *   {@code goog.require("foo.Bar")}.
     * @param codeLocation Indicates whether the JS code generator is responsible for producing
     *   an output with a {@code goog.provide(closurePath)}.  For example, non-union value
     *   converters are provided by library code.
     * @param valueType The type of the value being converted.
     */
    ValueConverterHelperInfo(String closurePath, CodeLocation codeLocation, SoyType valueType) {
      super(closurePath, codeLocation);
      this.valueType = Preconditions.checkNotNull(valueType);
    }
  }


  /**
   * Field accessors check the runtime type of their input, and then try to access the field.
   * They return undefined on failure.
   *
   * <p>A field accessor for an atomic (non-union) type looks something like:
   * <pre>
   *   function $soy_read_containertype_fieldname(container) {
   *     if (container instanceof containertype) {
   *       /** @type{containertype} *<!--->/
   *       var typed_container = (/** @type {containertype} *<!--->/ container);
   *       return typed_container.getFieldName();  // or other strategy as appropriate.
   *     }
   *     return (void 0);
   *   }</pre>
   *
   * <p>If a particular field access helper never appears in a type union,
   * then we could dispense with the runtime check.
   */
  static final class FieldAccessorHelperInfo extends BaseHelperInfo {
    /** For {@code $obj.field}, the type of {@code $obj}. */
    final SoyType containerType;
    /** For {@code $obj.field}, the text of the identifier {@code field}. */
    final String fieldName;

    /**
     * @param closurePath The path of the function in closure.
     *   If this is {@code "foo.Bar"} then a generated JS file would need to
     *   {@code goog.require("foo.Bar")}.
     * @param codeLocation Indicates whether the JS code generator is responsible for producing
     *   an output with a {@code goog.provide(closurePath)}.  For example, non-union value
     *   converters are provided by library code.
     * @param containerType For {@code $obj.field}, the type of {@code $obj}.
     * @param fieldName For {@code $obj.field}, the text of the identifier {@code field}.
     */
    FieldAccessorHelperInfo(
        String closurePath, CodeLocation codeLocation,
        SoyType containerType, String fieldName) {
      super(closurePath, codeLocation);
      this.containerType = Preconditions.checkNotNull(containerType);
      this.fieldName = Preconditions.checkNotNull(fieldName);
    }
  }


  /**
   * A strategy for reading a field from a container.
   */
  @AutoValue abstract static class FieldAccessStrategy {
    static FieldAccessStrategy create(FieldAccessOperator op, String fieldKey) {
      return new AutoValue_HelperFunctions_FieldAccessStrategy(op, fieldKey);
    }

    /** Specifies the kind of JS that is used to read the field. */
    abstract FieldAccessOperator op();
    /** A string used in conjunction with op to generate a JS expression that reads the field. */
    abstract String fieldKey();


    /** An expression that does the read. */
    JsExpr read(JsExpr container) {
      switch (op()) {
        case DOT:
          if (!BaseUtils.isIdentifier(fieldKey())) {
            throw new AssertionError(fieldKey());
          }
          return new JsExpr(container.getText() + "." + fieldKey(), container.getPrecedence());
        case BRACKET:
          return new JsExpr(
              container.getText() + "[" + BaseUtils.escapeToSoyString(fieldKey(), true) + "]",
              Integer.MAX_VALUE);
        case METHOD:
          if (!BaseUtils.isIdentifier(fieldKey())) {
            throw new AssertionError(fieldKey());
          }
          return new JsExpr(container.getText() + "." + fieldKey() + "()", 
              container.getPrecedence());
      }
      throw new AssertionError("unexpected op " + op());
    }
  }


  /** Specifies the kind of JS that is used to read the field. */
  enum FieldAccessOperator {
    /** {@code container.fieldKey} which can be renamed by the JSCompiler */
    DOT,
    /** {@code container["fieldKey"]} which uses a string literal to prevent renaming. */
    BRACKET,
    /** {@code container.fieldKey()} which treats the fieldKey as a method name. */
    METHOD,
    /** {@code fieldKey(container)} which passes the container to a function. */
    LIBRARY_FN,
  }


  /**
   * The closure path of a function that converts values of the given type,
   * or absent if none is needed.
   */
  static Optional<String> converterForType(SoyType valueType) {
    throw new AssertionError("TODO");
  }


  static FieldAccessStrategy strategyForFieldLookup(SoyType containerType, String fieldName) {
    throw new AssertionError("TODO");
  }

  static FieldAccessStrategy defaultStrategyForField(String fieldName) {
    if (BaseUtils.isIdentifier(fieldName)
        // foo.if is valid ES5, but not in strict mode.
        && !JsSrcUtils.isReservedWord(fieldName)) {
      return FieldAccessStrategy.create(FieldAccessOperator.DOT, fieldName);
    } else {
      return FieldAccessStrategy.create(FieldAccessOperator.BRACKET, fieldName);
    }
  }
}
