/*
 * Copyright 2011 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.soytree;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.basetree.CopyState;
import com.google.template.soy.basetree.SyntaxVersionUpperBound;
import com.google.template.soy.error.ErrorReporter.Checkpoint;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprparse.SoyParsingContext;
import com.google.template.soy.soytree.CommandTextAttributesParser.Attribute;
import com.google.template.soy.soytree.defn.TemplateParam;

import java.util.Collection;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

/**
 * Node representing a call to a basic template.
 *
 * <p> Important: Do not use outside of Soy code (treat as superpackage-private).
 *
 */
public final class CallBasicNode extends CallNode {

  public static final SoyErrorKind MISSING_CALLEE_NAME =
      SoyErrorKind.of("Invalid ''call'' command missing callee name: '{'call {0}'}'.");
  public static final SoyErrorKind BAD_CALLEE_NAME =
      SoyErrorKind.of("Invalid callee name \"{0}\" for ''call'' command.");

  /**
   * Helper class used by constructors. Encapsulates all the info derived from the command
   * text.
   */
  @Immutable
  private static final class CommandTextInfo extends CallNode.CommandTextInfo {

    /** The callee name string as it appears in the source code. */
    private final String srcCalleeName;

    CommandTextInfo(
        String commandText, String srcCalleeName, DataAttribute dataAttr,
        @Nullable String userSuppliedPlaceholderName,
        @Nullable SyntaxVersionUpperBound syntaxVersionBound) {
      super(commandText, dataAttr, userSuppliedPlaceholderName, syntaxVersionBound);
      this.srcCalleeName = srcCalleeName;
    }
  }

  /** Pattern for a callee name not listed as an attribute function="...". */
  private static final Pattern NONATTRIBUTE_CALLEE_NAME =
      Pattern.compile("^\\s* ([.\\w]+) (?= \\s | $)", Pattern.COMMENTS);

  /** Parser for the command text. */
  private static final CommandTextAttributesParser ATTRIBUTES_PARSER =
      new CommandTextAttributesParser("call",
          new Attribute("data", Attribute.ALLOW_ALL_VALUES, null));

  /** The callee name string as it appears in the source code. */
  private final String sourceCalleeName;

  /** The full name of the template being called. Briefly null before being set. */
  private String calleeName;

  /**
   * The list of params that need to be type checked when this node is run.  All the params that
   * could be statically verified will be checked up front (by the
   * {@code CheckCallingParamTypesVisitor}), this list contains the params that could not be
   * statically checked.
   *
   * <p>NOTE:This list will be a subset of the params of the callee, not a subset of the params
   * passed from this caller.
   */
  private ImmutableList<TemplateParam> paramsToRuntimeTypeCheck;

  /**
   * Private constructor. {@link Builder} is the public API.
   *
   * @param id The id for this node.
   * @param sourceLocation The node's source location.
   * @param commandTextInfo All the info derived from the command text.
   * @param escapingDirectiveNames Call-site escaping directives used by strict autoescaping.
   */
  private CallBasicNode(
      int id,
      SourceLocation sourceLocation,
      CommandTextInfo commandTextInfo,
      ImmutableList<String> escapingDirectiveNames,
      @Nullable String calleeName) {
    super(id, sourceLocation, "call", commandTextInfo, escapingDirectiveNames);
    this.sourceCalleeName = commandTextInfo.srcCalleeName;
    this.calleeName = calleeName;
  }

  /**
   * Copy constructor.
   * @param orig The node to copy.
   */
  private CallBasicNode(CallBasicNode orig, CopyState copyState) {
    super(orig, copyState);
    this.sourceCalleeName = orig.sourceCalleeName;
    this.calleeName = orig.calleeName;
    this.paramsToRuntimeTypeCheck = orig.paramsToRuntimeTypeCheck;
  }

  @Override public Kind getKind() {
    return Kind.CALL_BASIC_NODE;
  }

  /** Returns the callee name string as it appears in the source code. */
  public String getSrcCalleeName() {
    return sourceCalleeName;
  }


  /**
   * Sets the full name of the template being called (must not be a partial name).
   * @param calleeName The full name of the template being called.
   */
  public void setCalleeName(String calleeName) {
    Preconditions.checkState(this.calleeName == null);
    Preconditions.checkArgument(BaseUtils.isDottedIdentifier(calleeName));
    this.calleeName = calleeName;
  }

  /**
   * Sets the names of the parameters that require runtime type checking against the callees formal
   * types.
   */
  public void setParamsToRuntimeCheck(Collection<TemplateParam> paramNames) {
    this.paramsToRuntimeTypeCheck = ImmutableList.copyOf(paramNames);
  }

  @Override public Collection<TemplateParam> getParamsToRuntimeCheck(TemplateNode callee) {
    return paramsToRuntimeTypeCheck == null ? callee.getParams() : paramsToRuntimeTypeCheck;
  }

  /** Returns the full name of the template being called, or null if not yet set. */
  public String getCalleeName() {
    return calleeName;
  }

  @Override public CallBasicNode copy(CopyState copyState) {
    return new CallBasicNode(this, copyState);
  }

  public static final class Builder {

    private static CallBasicNode error() {
      return new Builder(-1, SourceLocation.UNKNOWN)
          .commandText(".error")
          .build(SoyParsingContext.exploding()); // guaranteed to be valid
    }

    private final int id;
    private final SourceLocation sourceLocation;

    private ImmutableList<String> escapingDirectiveNames = ImmutableList.of();
    private DataAttribute dataAttr = DataAttribute.none();

    @Nullable private String commandText;
    @Nullable private String userSuppliedPlaceholderName;
    @Nullable private String calleeName;
    @Nullable private String sourceCalleeName;
    @Nullable private SyntaxVersionUpperBound syntaxVersionBound;

    public Builder(int id, SourceLocation sourceLocation) {
      this.id = id;
      this.sourceLocation = sourceLocation;
    }

    public Builder calleeName(String calleeName) {
      this.calleeName = calleeName;
      return this;
    }

    public Builder commandText(String commandText) {
      this.commandText = commandText;
      return this;
    }

    public Builder escapingDirectiveNames(ImmutableList<String> escapingDirectiveNames) {
      this.escapingDirectiveNames = escapingDirectiveNames;
      return this;
    }

    public Builder dataAttribute(DataAttribute dataAttr) {
      this.dataAttr = dataAttr;
      return this;
    }

    public Builder sourceCalleeName(String sourceCalleeName) {
      this.sourceCalleeName = sourceCalleeName;
      return this;
    }

    public Builder syntaxVersionBound(SyntaxVersionUpperBound syntaxVersionBound) {
      this.syntaxVersionBound = syntaxVersionBound;
      return this;
    }

    public Builder userSuppliedPlaceholderName(String userSuppliedPlaceholderName) {
      this.userSuppliedPlaceholderName = userSuppliedPlaceholderName;
      return this;
    }

    public CallBasicNode build(SoyParsingContext context) {
      Checkpoint c = context.errorReporter().checkpoint();
      CommandTextInfo commandTextInfo = commandText != null
          ? parseCommandText(context)
          : buildCommandText();
      if (context.errorReporter().errorsSince(c)) {
        return error();
      }
      CallBasicNode callBasicNode = new CallBasicNode(
          id, sourceLocation, commandTextInfo, escapingDirectiveNames, calleeName);
      return callBasicNode;
    }

    // TODO(user): eliminate side-channel parsing. This should be a part of the grammar.
    private CommandTextInfo parseCommandText(SoyParsingContext context) {
      String cmdText =
          commandText +
              ((userSuppliedPlaceholderName != null) ?
                  " phname=\"" + userSuppliedPlaceholderName + "\"" : "");

      String cmdTextForParsing = commandText;

      SyntaxVersionUpperBound syntaxVersionBound = null;

      Matcher ncnMatcher = NONATTRIBUTE_CALLEE_NAME.matcher(cmdTextForParsing);
      if (ncnMatcher.find()) {
        sourceCalleeName = ncnMatcher.group(1);
        cmdTextForParsing = cmdTextForParsing.substring(ncnMatcher.end()).trim();
        if (! (BaseUtils.isIdentifierWithLeadingDot(sourceCalleeName) ||
            BaseUtils.isDottedIdentifier(sourceCalleeName))) {
          context.report(sourceLocation, BAD_CALLEE_NAME, sourceCalleeName);
        }
      } else {
        context.report(sourceLocation, MISSING_CALLEE_NAME, commandText);
      }

      Map<String, String> attributes
          = ATTRIBUTES_PARSER.parse(cmdTextForParsing, context, sourceLocation);

      DataAttribute dataAttrInfo =
          parseDataAttributeHelper(attributes.get("data"), sourceLocation, context);

      return new CommandTextInfo(
          cmdText, sourceCalleeName, dataAttrInfo, userSuppliedPlaceholderName, syntaxVersionBound);
    }

    // TODO(user): eliminate side-channel parsing. This should be a part of the grammar.
    private CommandTextInfo buildCommandText() {
      String commandText = sourceCalleeName;
      if (dataAttr.isPassingAllData()) {
        commandText += " data=\"all\"";
      } else if (dataAttr.isPassingData()) {
        assert dataAttr.dataExpr() != null;  // suppress warnings
        commandText += " data=\"" + dataAttr.dataExpr().toSourceString() + '"';
      }
      if (userSuppliedPlaceholderName != null) {
        commandText += " phname=\"" + userSuppliedPlaceholderName + '"';
      }

      return new CommandTextInfo(
          commandText, sourceCalleeName, dataAttr, userSuppliedPlaceholderName, syntaxVersionBound);
    }
  }
}
