/*
 * Copyright 2009 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.passes;

import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.IdGenerator;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.BlockNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;

import java.util.ArrayList;
import java.util.List;

/**
 * Visitor for combining any consecutive sequences of {@code RawTextNode}s into one equivalent
 * {@code RawTextNode}.
 *
 * <p> Important: Do not use outside of Soy code (treat as superpackage-private).
 *
 */
public final class CombineConsecutiveRawTextNodesVisitor extends AbstractSoyNodeVisitor<Void> {

  /** The node id generator for the parse tree. Retrieved from the root SoyFileSetNode. */
  private IdGenerator nodeIdGen;

  @Override public Void exec(SoyNode node) {

    // Retrieve the node id generator from the root of the parse tree.
    nodeIdGen = node.getNearestAncestor(SoyFileSetNode.class).getNodeIdGenerator();

    // Execute the pass.
    return super.exec(node);
  }


  @Override protected void visitSoyNode(SoyNode node) {

    if (node instanceof ParentSoyNode<?>) {
      visitChildren((ParentSoyNode<?>) node);
    }

    if (! (node instanceof BlockNode)) {
      return;
    }
    BlockNode nodeAsBlock = (BlockNode) node;

    // Check whether there are any consecutive RawTextNode children.
    boolean hasConsecRawTextNodes = false;
    for (int i = 0; i <= nodeAsBlock.numChildren() - 2; i++) {
      if (nodeAsBlock.getChild(i) instanceof RawTextNode &&
          nodeAsBlock.getChild(i+1) instanceof RawTextNode) {
        hasConsecRawTextNodes = true;
        break;
      }
    }
    // If there aren't any consecutive RawTextNode children, we're done.
    if (!hasConsecRawTextNodes) {
      return;
    }

    // Rebuild the list of children, combining consecutive RawTextNodes into one.
    List<StandaloneNode> copyOfOrigChildren = new ArrayList<>(nodeAsBlock.getChildren());
    nodeAsBlock.clearChildren();

    List<RawTextNode> consecutiveRawTextNodes = new ArrayList<>();
    for (StandaloneNode origChild : copyOfOrigChildren) {

      if (origChild instanceof RawTextNode) {
        consecutiveRawTextNodes.add((RawTextNode) origChild);

      } else {
        // First add the preceding consecutive RawTextNodes, if any.
        addConsecutiveRawTextNodesAsOneNodeHelper(nodeAsBlock, consecutiveRawTextNodes);
        consecutiveRawTextNodes.clear();
        // Then add the current new child.
        nodeAsBlock.addChild(origChild);
      }
    }

    // Add the final group of consecutive RawTextNodes, if any.
    addConsecutiveRawTextNodesAsOneNodeHelper(nodeAsBlock, consecutiveRawTextNodes);
    consecutiveRawTextNodes.clear();
  }


  /**
   * Helper to add consecutive RawTextNodes as one child node (the raw text will be joined).
   * If the consecutive RawTextNodes list actually only has one item, then adds that node instead
   * of creating a new RawTextNode.
   *
   * Note: This function works closely with the above code. In particular, it assumes we're
   * rebuilding the whole list (thus adding to the end of the parent) instead of fixing the old
   * list in-place.
   *
   * @param parent The parent to add the new child to.
   * @param consecutiveRawTextNodes The list of consecutive RawTextNodes.
   */
  private void addConsecutiveRawTextNodesAsOneNodeHelper(
      BlockNode parent, List<RawTextNode> consecutiveRawTextNodes) {
    // Nothing to do.
    if (consecutiveRawTextNodes.isEmpty()) {
      return;
    }

    // Simply add the one RawTextNode.
    if (consecutiveRawTextNodes.size() == 1) {
      parent.addChild(consecutiveRawTextNodes.get(0));
      return;
    }

    // Create a new combined RawTextNode.
    StringBuilder rawText = new StringBuilder();
    SourceLocation sourceLocation = null;
    for (RawTextNode rtn : consecutiveRawTextNodes) {
      rawText.append(rtn.getRawText());
      sourceLocation = (sourceLocation == null)
          ? rtn.getSourceLocation()
          : sourceLocation.extend(rtn.getSourceLocation());
    }
    parent.addChild(
        new RawTextNode(nodeIdGen.genId(), rawText.toString(), sourceLocation));
  }

}
