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

import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.basetree.CopyState;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprparse.ExpressionParser;
import com.google.template.soy.exprparse.SoyParsingContext;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.soytree.SoyNode.ConditionalBlockNode;
import com.google.template.soy.soytree.SoyNode.ExprHolderNode;
import com.google.template.soy.soytree.SoyNode.LocalVarBlockNode;
import com.google.template.soy.soytree.SoyNode.LoopNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import com.google.template.soy.soytree.SoyNode.StatementNode;
import com.google.template.soy.soytree.defn.LocalVar;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Node representing a 'for' statement.
 *
 * <p> Important: Do not use outside of Soy code (treat as superpackage-private).
 *
 */
public final class ForNode extends AbstractBlockCommandNode
    implements StandaloneNode, StatementNode, ConditionalBlockNode, LoopNode, ExprHolderNode,
    LocalVarBlockNode {

  /**
   * The arguments to a {@code range(...)} expression in a {@code {for ...}} loop statement.
   */
  @AutoValue public abstract static class RangeArgs {
    static final RangeArgs ERROR = create(
        Optional.<ExprRootNode>absent(),
        new ExprRootNode(VarRefNode.ERROR),
        Optional.<ExprRootNode>absent());

    static RangeArgs create(Optional<ExprRootNode> start,
        ExprRootNode limit, Optional<ExprRootNode> increment) {
      return new AutoValue_ForNode_RangeArgs(start, limit, increment);
    }

    RangeArgs() {}

    /**
     * The expression for the iteration start point.
     *
     * <p>This is optional, the default beginning of iteration is {@code 0} if this is not set.
     */
    public abstract Optional<ExprRootNode> start();

    /**
     * The expression for the iteration end point.  This is interpreted as an exclusive limit.
     */
    public abstract ExprRootNode limit();

    /**
     * The expression for the iteration increment.
     *
     * <p>This is optional, the default increment {@code 1} if this is not set.
     */
    public abstract Optional<ExprRootNode> increment();

    /**
     * Returns true if it is statically known that the range is not empty.
     *
     * <p>Currently this is only possible if we have a range that contains constant values.
     */
    public final boolean definitelyNotEmpty() {
      int start = 0;
      if (start().isPresent()) {
        ExprRootNode startExpr = start().get();
        if (startExpr.getRoot() instanceof IntegerNode) {
          start = ((IntegerNode) startExpr.getRoot()).getValue();
        } else {
          return false; // if the start is not a constant then it might be empty
        }
      }
      // increment is irrelevant
      int limit;
      if (limit().getRoot() instanceof IntegerNode) {
        limit = ((IntegerNode) limit().getRoot()).getValue();
      } else {
        return false;
      }
      return start < limit;
    }

    private RangeArgs copy(CopyState copyState) {
      return create(
          start().isPresent()
              ? Optional.of(start().get().copy(copyState))
              : Optional.<ExprRootNode>absent(),
          limit().copy(copyState),
          increment().isPresent()
              ? Optional.of(increment().get().copy(copyState))
              : Optional.<ExprRootNode>absent());
    }
  }

  static final SoyErrorKind INVALID_COMMAND_TEXT =
      SoyErrorKind.of("Invalid ''for'' command text");
  private static final SoyErrorKind INVALID_RANGE_SPECIFICATION =
      SoyErrorKind.of("Invalid range specification");

  /** Regex pattern for the command text. */
  // 2 capturing groups: local var name, arguments to range()
  private static final Pattern COMMAND_TEXT_PATTERN =
      Pattern.compile("( [$] \\w+ ) \\s+ in \\s+ range[(] \\s* (.*) \\s* [)]",
                      Pattern.COMMENTS | Pattern.DOTALL);


  /** The Local variable for this loop. */
  private final LocalVar var;

  /** The parsed range args. */
  private final RangeArgs rangeArgs;

  /**
   * @param id The id for this node.
   * @param commandText The command text.
   * @param sourceLocation The source location for the {@code for }node.
   */
  public ForNode(
      int id,
      String commandText,
      SourceLocation sourceLocation,
      SoyParsingContext context) {
    super(id, sourceLocation, "for", commandText);

    Matcher matcher = COMMAND_TEXT_PATTERN.matcher(commandText);
    if (!matcher.matches()) {
      context.report(sourceLocation, INVALID_COMMAND_TEXT);
      this.rangeArgs = RangeArgs.ERROR;
      this.var = new LocalVar("error", this, null);
      // Return early to avoid IllegalStateException below
      return;
    }

    String varName = parseVarName(
        matcher.group(1), sourceLocation, context);
    List<ExprNode> rangeArgs = parseRangeArgs(
        matcher.group(2), sourceLocation, context);

    if (rangeArgs.size() > 3) {
      context.report(sourceLocation, INVALID_RANGE_SPECIFICATION);
      this.rangeArgs = RangeArgs.ERROR;
    } else if (rangeArgs.isEmpty()) {
      this.rangeArgs = RangeArgs.ERROR;
    } else {
      // OK, now interpret the args
      // If there are 2 or more args, then the first is the 'start' value
      ExprNode start = rangeArgs.size() >= 2 ? rangeArgs.get(0) : null;

      // If there are 3 args, then the last one is the increment.
      ExprNode increment = rangeArgs.size() == 3 ? rangeArgs.get(2) : null;

      // the limit is the first item if there is only one arg, otherwise it is the second arg
      ExprNode limit = rangeArgs.get(rangeArgs.size() == 1 ? 0 : 1);
      this.rangeArgs = RangeArgs.create(
          start == null
              ? Optional.<ExprRootNode>absent()
              : Optional.of(new ExprRootNode(start)),
          new ExprRootNode(limit),
          increment == null
              ? Optional.<ExprRootNode>absent()
              : Optional.of(new ExprRootNode(increment)));
    }

    var = new LocalVar(varName, this, null);
  }

  private static String parseVarName(
      String input, SourceLocation sourceLocation, SoyParsingContext context) {
    return new ExpressionParser(input, sourceLocation, context)
        .parseVariable()
        .getName();
  }

  private static List<ExprNode> parseRangeArgs(
      String input, SourceLocation sourceLocation, SoyParsingContext context) {
    return new ExpressionParser(input, sourceLocation, context)
        .parseExpressionList();
  }


  /**
   * Copy constructor.
   * @param orig The node to copy.
   */
  private ForNode(ForNode orig, CopyState copyState) {
    super(orig, copyState);
    this.var = new LocalVar(orig.var, this);
    this.rangeArgs = orig.rangeArgs.copy(copyState);
  }


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


  @Override public final LocalVar getVar() {
    return var;
  }


  @Override public final String getVarName() {
    return var.name();
  }


  /** Returns the parsed range args. */
  public RangeArgs getRangeArgs() {
    return rangeArgs;
  }


  @Override public List<ExprUnion> getAllExprUnions() {
    return ExprUnion.createList(
        ImmutableList.copyOf(
            Iterables.concat(
                rangeArgs.start().asSet(),
                ImmutableList.of(rangeArgs.limit()),
                rangeArgs.increment().asSet())));
  }


  @Override public BlockNode getParent() {
    return (BlockNode) super.getParent();
  }


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

}
