/*
 * Copyright 2008 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.restricted;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.internalutils.NodeContentKinds;
import com.google.template.soy.exprtree.Operator;

import java.util.List;

import javax.annotation.Nullable;

/**
 * Common utilities for dealing with JS expressions.
 *
 * <p> Important: This class may only be used in implementing plugins (e.g. functions, directives).
 *
 */
public class JsExprUtils {

  /** Expression constant for empty string. */
  private static final JsExpr EMPTY_STRING = new JsExpr("''", Integer.MAX_VALUE);


  private JsExprUtils() {}


  /**
   * Builds one JS expression that computes the concatenation of the given JS expressions. The '+'
   * operator is used for concatenation. Operands will be protected with an extra pair of
   * parentheses if and only if needed.
   *
   * The resulting expression is not guaranteed to be a string if the operands do not produce
   * strings when combined with the plus operator; e.g. 2+2 might be 4 instead of '22'.
   *
   * @param jsExprs The JS expressions to concatentate.
   * @return One JS expression that computes the concatenation of the given JS expressions.
   */
  public static JsExpr concatJsExprs(List<? extends JsExpr> jsExprs) {
    if (jsExprs.isEmpty()) {
      return EMPTY_STRING;
    }

    if (jsExprs.size() == 1) {
      return jsExprs.get(0);
    }

    int plusOpPrec = Operator.PLUS.getPrecedence();
    StringBuilder resultSb = new StringBuilder();

    boolean isFirst = true;
    for (JsExpr jsExpr : jsExprs) {

      // The first operand needs protection only if it's strictly lower precedence. The non-first
      // operands need protection when they're lower or equal precedence. (This is true for all
      // left-associative operators.)
      boolean needsProtection = isFirst ? jsExpr.getPrecedence() < plusOpPrec
                                        : jsExpr.getPrecedence() <= plusOpPrec;

      if (isFirst) {
        isFirst = false;
      } else {
        resultSb.append(" + ");
      }

      if (needsProtection) {
        resultSb.append('(').append(jsExpr.getText()).append(')');
      } else {
        resultSb.append(jsExpr.getText());
      }
    }

    return new JsExpr(resultSb.toString(), plusOpPrec);
  }


  /**
   * Builds a JS expression which is the string concatenation of individual expressions. This
   * doesn't assume that the inputs are necessarily strings, but guarantees that the output is
   * a string.
   *
   * @param jsExprs The JS expressions to concatentate.
   * @return One JS expression that computes the guaranteed string concatenation of the given JS
   *     expressions.
   */
  public static JsExpr concatJsExprsForceString(List<? extends JsExpr> jsExprs) {
    // If the first or second expression in the list is a string, the plus operator (if any) will
    // produce a string concatenation, so we are set.
    if ((!jsExprs.isEmpty() && isStringLiteral(jsExprs.get(0)))
        || (jsExprs.size() > 1 && isStringLiteral(jsExprs.get(1)))) {
      return concatJsExprs(jsExprs);
    }
    // Add empty string first, which ensures the plus operator always means string concatenation.
    // Consider:
    //    '' + 6 + 6 + 6 = '666'
    //    6 + 6 + 6 + '' = '18'
    return concatJsExprs(ImmutableList.<JsExpr>builder().add(EMPTY_STRING).addAll(jsExprs).build());
  }


  @VisibleForTesting
  static boolean isStringLiteral(JsExpr jsExpr) {
    String jsExprText = jsExpr.getText();
    int jsExprTextLastIndex = jsExprText.length() - 1;
    if (jsExprTextLastIndex < 1 || jsExprText.charAt(0) != '\''
        || jsExprText.charAt(jsExprTextLastIndex) != '\'') {
      return false;
    }
    for (int i = 1; i < jsExprTextLastIndex; ++i) {
      char c = jsExprText.charAt(i);
      if (c == '\'') {
        return false;
      }
      if (c == '\\') {
        // We do not bother skipping through the whole escape if it takes up more than one character
        // beyond the backslash, e.g. \u1234 or \123 or \x12, since none of such escapes' characters
        // can be an apostrophe, which is all we really care about. Nor do we check that the escape
        // doesn't include the final apostrophe, since that would mean the JS expression is invalid.
        ++i;
      }
    }
    return true;
  }


  public static JsExpr toString(JsExpr expr) {
    return concatJsExprsForceString(ImmutableList.of(expr));
  }


  /**
   * Wraps an expression in a function call.
   *
   * @param functionExprText expression for the function to invoke, such as a function name or
   *     constructor phrase (such as "new SomeClass").
   * @param jsExpr the expression to compute the argument to the function
   * @return a JS expression consisting of a call to the specified function, applied to the
   *     provided expression.
   */
  @VisibleForTesting
  static JsExpr wrapWithFunction(String functionExprText, JsExpr jsExpr) {
    Preconditions.checkNotNull(functionExprText);
    return new JsExpr(functionExprText + "(" + jsExpr.getText() + ")", Integer.MAX_VALUE);
  }


  /**
   * Wraps with the proper SanitizedContent constructor if contentKind is non-null.
   *
   * @param contentKind The kind of sanitized content.
   * @param jsExpr The expression to wrap.
   */
  public static JsExpr maybeWrapAsSanitizedContent(
      @Nullable ContentKind contentKind, JsExpr jsExpr) {
    if (contentKind == null) {
      return jsExpr;
    } else {
      return wrapWithFunction(NodeContentKinds.toJsSanitizedContentOrdainer(contentKind), jsExpr);
    }
  }


  /**
   * Wraps with the proper SanitizedContent constructor if contentKind is non-null, in a way
   * specific for let/param blocks.
   *
   * We generate slightly different code for param and let blocks so that empty strings evaluate
   * to false; specifically, by not wrapping them in SanitizedContent. However, template return
   * values are not affected, so that the external interface to Soy remains pristine.
   *
   * @param contentKind The kind of sanitized content.
   * @param jsExpr The expression to wrap.
   */
  public static JsExpr maybeWrapAsSanitizedContentForInternalBlocks(
      @Nullable ContentKind contentKind, JsExpr jsExpr) {
    if (contentKind == null) {
      return jsExpr;
    } else {
      return wrapWithFunction(
          NodeContentKinds.toJsSanitizedContentOrdainerForInternalBlocks(contentKind), jsExpr);
    }
  }
}
