/*
 * 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.common.css.compiler.passes;

import com.google.common.base.CaseFormat;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.css.SourceCode;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssBlockNode;
import com.google.common.css.compiler.ast.CssClassSelectorNode;
import com.google.common.css.compiler.ast.CssClassSelectorNode.ComponentScoping;
import com.google.common.css.compiler.ast.CssCombinatorNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssComponentNode;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDefinitionNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
import com.google.common.css.compiler.ast.CssNode;
import com.google.common.css.compiler.ast.CssProvideNode;
import com.google.common.css.compiler.ast.CssPseudoClassNode;
import com.google.common.css.compiler.ast.CssRootNode;
import com.google.common.css.compiler.ast.CssRulesetNode;
import com.google.common.css.compiler.ast.CssSelectorNode;
import com.google.common.css.compiler.ast.CssTree;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.GssError;
import com.google.common.css.compiler.ast.MutatingVisitController;

import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

public class ProcessComponents<T> extends DefaultTreeVisitor
    implements CssCompilerPass {

  private static final String CLASS_SEP = "-";
  private static final String DEF_SEP = "__";

  private final Map<String, CssComponentNode> components = Maps.newHashMap();

  private final MutatingVisitController visitController;
  private final ErrorManager errorManager;
  private final Map<String, T> fileToChunk;
  private final List<CssProvideNode> provideNodes = Lists.newArrayList();
  private SourceCode lastFile = null;

  /**
   * Creates a new pass to process components for the given visit
   * controller, using the given error manager, while ignoring chunks.
   */
  public ProcessComponents(MutatingVisitController visitController, ErrorManager errorManager) {
    this(visitController, errorManager, null);
  }

  /**
   * Creates a new pass to process components for the given visit
   * controller, using the given error manager, while maintaining the
   * chunk ids on the nodes created in the process according to the
   * given map from files to chunks.
   */
  public ProcessComponents(
      MutatingVisitController visitController, ErrorManager errorManager,
      @Nullable Map<String, T> fileToChunk) {
    this.visitController = visitController;
    this.errorManager = errorManager;
    this.fileToChunk = fileToChunk;
  }

  @Override
  public boolean enterProvideNode(CssProvideNode node) {
    // Often this pass is called on a bunch of GSS files which have been concatenated
    // together, meaning that there will be multiple @provide declarations. We are only
    // interested in @provide nodes which are in the same source file as the @component.
    SourceCode sourceCode = node.getSourceCodeLocation().getSourceCode();
    if (sourceCode != lastFile) {
      provideNodes.clear();
      lastFile = sourceCode;
    }
    provideNodes.add(node);
    return false;
  }

  @Override
  public boolean enterComponent(CssComponentNode node) {
    SourceCode sourceCode = node.getSourceCodeLocation().getSourceCode();
    if (sourceCode != lastFile) {
      provideNodes.clear();
      lastFile = sourceCode;
    }
    String name = node.getName().getValue();
    if (node.isImplicitlyNamed()) {
      // together before compiling, which can result in multiple @component nodes in the same file.
      // So in the unnamed @component case, having multiple @provide is okay (use the last) but not
      // having any is still not allowed.
      if (provideNodes.size() < 1) {
        reportError("implicitly-named @components require a prior @provide declaration ", node);
        return false;
      }
      name = Iterables.getLast(provideNodes).getProvide();
    }
    if (components.containsKey(name)) {
      reportError("cannot redefine component in chunk ", node);
      return false;
    }
    CssLiteralNode parentName = node.getParentName();
    if ((parentName != null) && !components.containsKey(parentName.getValue())) {
      reportError("parent component is undefined in chunk ", node);
      return false;
    }
    visitController.replaceCurrentBlockChildWith(transformAllNodes(node), false);
    components.put(name, node);
    return false;
  }

  @Override
  public boolean enterClassSelector(CssClassSelectorNode node) {
    // Note that this works because enterComponent, above, returns false -
    // this visitor never sees class selectors inside components (the other
    // visitor does).
    if (node.getScoping() == ComponentScoping.FORCE_SCOPED) {
      reportError("'%' prefix for class selectors may only be used in the scope of an @component",
          node);
      return false;
    }
    if (node.getScoping() == ComponentScoping.FORCE_UNSCOPED) {
      reportError("'^' prefix for class selectors may only be used in the scope of an @component",
          node);
      return false;
    }
    return true;
  }

  private void reportError(String message, CssNode node) {
    if (fileToChunk != null) {
      message += String.valueOf(
          MapChunkAwareNodesToChunk.getChunk(node, fileToChunk));
    }
    errorManager.report(new GssError(message, node.getSourceCodeLocation()));
    visitController.removeCurrentNode();
  }

  private List<CssNode> transformAllNodes(CssComponentNode current) {
    Set<String> constants = Sets.newHashSet();
    List<CssNode> nodes = Lists.newLinkedList();
    transformAllParentNodes(nodes, constants, current, current.getParentName());
    nodes.addAll(transformNodes(constants, current, current));
    return nodes;
  }

  /**
   * Recursively goes up the component inheritance hierarchy and copies the
   * ancestor component contents.
   *
   * @param nodes the list of copied child nodes collected from ancestor
   *     components
   * @param constants the set of names of constants defined in the ancestor
   *     components, used to differentiate local constant names from global
   *     constant names
   * @param current the component for which the nodes are collected
   * @param parentLiteralNode the node which contains the name of the ancestor
   *     node to process, may be {@code null} if we reached the root of the
   *     inheritance tree
   */
  private void transformAllParentNodes(List<CssNode> nodes, Set<String> constants,
      CssComponentNode current, @Nullable CssLiteralNode parentLiteralNode) {
    if (parentLiteralNode == null) {
      return;
    }
    String parentName = parentLiteralNode.getValue();
    CssComponentNode parentComponent = components.get(parentName);
    transformAllParentNodes(nodes, constants, current, parentComponent.getParentName());
    nodes.addAll(transformNodes(constants, current, parentComponent));
  }

  /**
   * Copies and transforms the contents of the source component block for
   * inclusion in the expanded version of the target component.
   *
   * <p>The transformation of the source component block is basically a renaming
   * of the local constant references to their global equivalent.  Their names
   * are prefixed with the expanded component name.  Additionally ancestor
   * component contents are also emitted with appropriate renaming, although the
   * {@code @def} values are replaced with a reference to the ancestor
   * component.  For examples look at {@link ProcessComponentsTest}.
   *
   * @param constants the set of names of constants defined in the ancestor
   *     components, used to differentiate local constant names from global
   *     constant names
   * @param target the component for which the block contents are copied
   * @param source the component from which the block contents are taked
   * @return the list of transformed nodes
   */
  private List<CssNode> transformNodes(
      Set<String> constants, CssComponentNode target, CssComponentNode source) {
    CssBlockNode sourceBlock = source.getBlock();
    CssBlockNode copyBlock = new CssBlockNode(false, sourceBlock.deepCopy().getChildren());
    copyBlock.setSourceCodeLocation(source.getBlock().getSourceCodeLocation());
    CssTree tree = new CssTree(
        target.getSourceCodeLocation().getSourceCode(), new CssRootNode(copyBlock));
    new TransformNodes(constants, target, target != source,
        tree.getMutatingVisitController(), errorManager, provideNodes).runPass();
    if (fileToChunk != null) {
      T chunk = MapChunkAwareNodesToChunk.getChunk(target, fileToChunk);
      new SetChunk(tree, chunk).runPass();
    }
    return tree.getRoot().getBody().getChildren();
  }

  private static class SetChunk extends DefaultTreeVisitor
      implements CssCompilerPass {

    private final CssTree tree;
    private final Object chunk;

    public SetChunk(CssTree tree, Object chunk) {
      this.tree = tree;
      this.chunk = chunk;
    }

    @Override
    public boolean enterDefinition(CssDefinitionNode definition) {
      definition.setChunk(chunk);
      return false;
    }

    @Override
    public boolean enterSelector(CssSelectorNode selector) {
      selector.setChunk(chunk);
      return true;
    }

    @Override
    public boolean enterFunctionNode(CssFunctionNode function) {
      function.setChunk(chunk);
      return super.enterFunctionNode(function);
    }

    @Override
    public void runPass() {
      tree.getVisitController().startVisit(this);
    }
  }

  private static class TransformNodes extends DefaultTreeVisitor
    implements CssCompilerPass {

    private final boolean inAncestorBlock;
    private final MutatingVisitController visitController;
    private final ErrorManager errorManager;

    private final Set<CssDefinitionNode> renamedDefinitions = Sets.newHashSet();

    private final Set<String> componentConstants;
    private final boolean isAbstract;
    private final String classPrefix;
    private final String defPrefix;
    private final String parentName;
    private final SourceCodeLocation sourceCodeLocation;
    private boolean firstClassSelector;
    /** If non-zero, we won't process the first classname in the current selector. */
    private int nestedSelectorDepth;

    public TransformNodes(Set<String> constants, CssComponentNode current, boolean inAncestorBlock,
        MutatingVisitController visitController, ErrorManager errorManager,
        List<CssProvideNode> provideNodes) {
      this.componentConstants = constants;
      this.inAncestorBlock = inAncestorBlock;
      this.visitController = visitController;
      this.errorManager = errorManager;

      String currentName = current.getName().getValue();
      if (current.isImplicitlyNamed()) {
        currentName = Iterables.getLast(provideNodes).getProvide();
      }
      this.isAbstract = current.isAbstract();
      if (current.getPrefixStyle() == CssComponentNode.PrefixStyle.CASE_CONVERT) {
        this.classPrefix = getClassPrefixFromDottedName(currentName);
        this.defPrefix = getDefPrefixFromDottedName(currentName);
      } else {
        this.classPrefix = currentName + CLASS_SEP;
        this.defPrefix = currentName + DEF_SEP;
      }
      this.parentName = inAncestorBlock ? current.getParentName().getValue() : null;
      this.sourceCodeLocation = current.getSourceCodeLocation();
    }

    @Override
    public boolean enterComponent(CssComponentNode node) {
      if (!inAncestorBlock) {
        errorManager.report(
            new GssError("nested components are not allowed", node.getSourceCodeLocation()));
      }
      visitController.removeCurrentNode();
      return false;
    }

    @Override
    public boolean enterRuleset(CssRulesetNode node) {
      if (isAbstract) {
        visitController.removeCurrentNode();
      }
      return !isAbstract;
    }

    @Override
    public boolean enterCombinator(CssCombinatorNode combinator) {
      nestedSelectorDepth++;
      return true;
    }

    @Override
    public void leaveCombinator(CssCombinatorNode combinator) {
      nestedSelectorDepth--;
    }

    @Override
    public boolean enterSelector(CssSelectorNode selector) {
      // Only reset the 'first selector' flag if we're not in a combinator.
      // Otherwise, keep the same flag value (which may or may not have been set
      // depending on whether we saw a class selector in an earlier refiner list.)
      if (nestedSelectorDepth == 0) {
        firstClassSelector = true;
      }
      return true;
    }

    @Override
    public void leaveSelector(CssSelectorNode selector) {
      firstClassSelector = false;
    }

    // Don't reset firstClassSelector for classes in :not().
    @Override
    public boolean enterPseudoClass(CssPseudoClassNode pseudoClass) {
      nestedSelectorDepth++;
      return true;
    }

    @Override
    public void leavePseudoClass(CssPseudoClassNode pseudoClass) {
      nestedSelectorDepth--;
    }

    @Override
    public boolean enterClassSelector(CssClassSelectorNode node) {
      Preconditions.checkState(!isAbstract);
      if (!firstClassSelector && node.getScoping() == ComponentScoping.FORCE_UNSCOPED) {
        errorManager.report(new GssError(
            "'^' prefix may only be used on the first classname in a selector.",
            node.getSourceCodeLocation()));
      }
      if (firstClassSelector && node.getScoping() != ComponentScoping.FORCE_UNSCOPED
          || node.getScoping() == ComponentScoping.FORCE_SCOPED) {
        CssClassSelectorNode newNode = new CssClassSelectorNode(
            classPrefix + node.getRefinerName(),
            inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation());
        newNode.setComments(node.getComments());
        visitController.replaceCurrentBlockChildWith(ImmutableList.of(newNode), false);
      }
      firstClassSelector = false;
      return true;
    }

    @Override
    public boolean enterDefinition(CssDefinitionNode node) {
      // Do not modify the renamed node created below, but descend and modify
      // its children.
      if (renamedDefinitions.contains(node)) {
        return true;
      }
      String defName = node.getName().getValue();
      CssLiteralNode newDefLit =
          new CssLiteralNode(defPrefix + defName,
              inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation());
      CssDefinitionNode newNode;
      // When copying the ancestor block, we want to replace definition values
      // with a reference to the constant emitted when the parent component was
      // transformed.  This makes it possible to actually inherit values from
      // the parent component (parent component definitions changes will
      // propagate to descendant components).
      if (inAncestorBlock) {
        String parentRefPrefix = parentName + DEF_SEP;
        // Hack to avoid breaking hacked components with http://b/3213779
        // workarounds.  Can be removed when all workarounds are removed.
        String parentRefName = defName.startsWith(parentRefPrefix)
            ? defName : parentRefPrefix + defName;
        CssConstantReferenceNode parentRefNode =
            new CssConstantReferenceNode(parentRefName, sourceCodeLocation);
        newNode = new CssDefinitionNode(ImmutableList.<CssValueNode>of(parentRefNode),
            newDefLit, sourceCodeLocation);
      } else {
        newNode = new CssDefinitionNode(CssNode.deepCopyNodes(node.getParameters()),
            newDefLit, sourceCodeLocation);
      }
      componentConstants.add(defName);
      renamedDefinitions.add(newNode);
      visitController.replaceCurrentBlockChildWith(ImmutableList.of(newNode), true);
      return false;
    }

    @Override
    public boolean enterValueNode(CssValueNode node) {
      if (node instanceof CssConstantReferenceNode
          // Avoid renaming constant references for constants not defined in the
          // component tree.
          && componentConstants.contains(node.getValue())) {
        CssConstantReferenceNode newNode =
            new CssConstantReferenceNode(defPrefix + node.getValue(),
                inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation());
        visitController.replaceCurrentBlockChildWith(ImmutableList.of(newNode), false);
      }
      return true;
    }

    @Override
    public boolean enterArgumentNode(CssValueNode node) {
      return enterValueNode(node);
    }

    @Override
    public void runPass() {
      visitController.startVisit(this);
    }

    /**
     * Compute the name of the class prefix from the package name. This converts
     * the dot-separated package name to camel case, so foo.bar becomes fooBar.
     *
     * @param packageName the @provide package name
     * @return the converted class prefix
     */
    private String getClassPrefixFromDottedName(String packageName) {
      // CaseFormat doesn't have a format for names separated by dots, so we transform
      // the dots into dashes. Then we can use the regular CaseFormat transformation
      // to camel case instead of having to write our own.
      String packageNameWithDashes = packageName.replace('.', '-');
      return CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, packageNameWithDashes);
    }

    /**
     * Compute the name of the def prefix from the package name. This converts the dot-separated
     * package name to uppercase with underscores, so foo.bar becomes FOO_BAR_.
     *
     * @param packageName the @provide package name
     * @return the converted def prefix
     */
    private String getDefPrefixFromDottedName(String packageName) {
      return packageName.replace('.', '_').toUpperCase() + "_";
    }
  }

  @Override
  public void runPass() {
    visitController.startVisit(this);
  }
}