/*
 * 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.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssCustomFunctionNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
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.GssFunction;
import com.google.common.css.compiler.ast.GssFunctionException;
import com.google.common.css.compiler.ast.MutatingVisitController;
import com.google.common.css.compiler.ast.Proxiable;

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

/**
 * This compiler pass replaces {@link CssCustomFunctionNode} instances with the
 * list of nodes returned by the GssFunction.
 *
 */
public class ResolveCustomFunctionNodes extends DefaultTreeVisitor
    implements CssCompilerPass {

  private final MutatingVisitController visitController;
  protected Map<String, GssFunction> functionMap;
  private final ErrorManager errorManager;
  private final boolean allowUnknownFunctions;
  private final Set<String> allowedNonStandardFunctions;

  /**
   * Constructs the pass.
   *
   * @param visitController The visit controller
   * @param errorManager The error manager
   * @param functionMap The map from function names to resolve to GSS functions
   */
  public ResolveCustomFunctionNodes(MutatingVisitController visitController,
                                    ErrorManager errorManager,
                                    Map<String, GssFunction> functionMap) {
    this(visitController, errorManager, functionMap,
        false /* allowUnknownFunctions */);
  }

  /**
   * Constructs the pass.
   *
   * @param visitController The visit controller
   * @param errorManager The error manager
   * @param functionMap The map from function names to resolve to GSS functions
   * @param allowUnknownFunctions Whether to allow unknown function calls,
   *     leaving them as is, instead of reporting an error
   */
  public ResolveCustomFunctionNodes(MutatingVisitController visitController,
                                    ErrorManager errorManager,
                                    Map<String, GssFunction> functionMap,
                                    boolean allowUnknownFunctions) {
    this(visitController, errorManager, functionMap, allowUnknownFunctions,
        ImmutableSet.<String>of() /* allowedNonStandardFunctions */);
  }

  /**
   * Constructs the pass.
   *
   * @param visitController The visit controller
   * @param errorManager The error manager
   * @param functionMap The map from function names to resolve to GSS functions
   * @param allowUnknownFunctions Whether to allow unknown function calls,
   *     leaving them as is, instead of reporting an error
   * @param allowedNonStandardFunctions functions that should not yield a
   *     warning if they appear in a stylesheet
   */
  public ResolveCustomFunctionNodes(MutatingVisitController visitController,
                                    ErrorManager errorManager,
                                    Map<String, GssFunction> functionMap,
                                    boolean allowUnknownFunctions,
                                    Set<String> allowedNonStandardFunctions) {
    Preconditions.checkNotNull(functionMap);
    this.visitController = visitController;
    this.errorManager = errorManager;
    this.functionMap = functionMap;
    this.allowUnknownFunctions = allowUnknownFunctions;
    this.allowedNonStandardFunctions = ImmutableSet.copyOf(
        allowedNonStandardFunctions);
  }

  @Override
  public void leaveFunctionNode(CssFunctionNode functionNode) {
    if (!(functionNode instanceof Proxiable)) {
      return;
    }

    CssCustomFunctionNode node = (CssCustomFunctionNode) functionNode;

    List<CssValueNode> functionResult = node.getResult();
    if (functionResult == null) {
      // Look up the function's name in the map.
      String functionName = node.getFunctionName();
      GssFunction function = functionMap.get(functionName);
      if (function == null) {
        if (!allowUnknownFunctions && !allowedNonStandardFunctions.contains(
            functionName)) {
          errorManager.report(new GssError(
              String.format("Unknown function \"%s\"", functionName),
              node.getSourceCodeLocation()));
          visitController.removeCurrentNode();
        }
        return;
      }

      // Removes the commas from the arguments and groups arguments separated
      // by commas, e.g.:
      // verticalGradient(#fff, #f2f2f2, url(foo) left top)
      // has 7 initial arguments (2 commas, 5 words).
      // After the cleanup we end up with the 3 arguments separated by commas.
      // TODO(user): Clean this up once we get rid of the comma-as-argument hack.
      List<CssValueNode> arguments = Lists.newArrayList();
      CssValueNode lastArg = null;
      for (CssValueNode value : node.getArguments().childIterable()) {
        if (value.getValue() != null && value.getValue().equals(",")) {
          lastArg = null;
        } else {
          if (lastArg == null) {
            arguments.add(value);
            lastArg = value;
          } else {
            if (value instanceof CssLiteralNode
                && " ".equals(value.getValue())) {
              // Here we are building a parse subtree that, when
              // printed by most output passes, looks like the subtree
              // we got as input, but actually consists of just a
              // CssLiteralNode. This requires us to add some
              // whitespace to the printed representations of the
              // nodes we are merging, corresponding to the whitespace
              // eaten by the parser between lexemes and generated by
              // printing passes when flattening a tree back to a
              // string. But functions are special: the parse tree
              // includes CssLiteralNode(" ") to explicitly represent
              // function argument delimiters. We need to avoid adding
              // extra space here, just as printing passes do.
            } else if (lastArg.getValue() == null) {
              arguments.remove(lastArg);
              lastArg = new CssLiteralNode(lastArg.toString() + " "
                  + value.toString());
              arguments.add(lastArg);
            } else {
              lastArg.setValue(lastArg.getValue() + " " + value.toString());
            }
          }
        }
      }

      Integer expArgNumber = function.getNumExpectedArguments();
      int argNumber = arguments.size();
      if (expArgNumber != null && expArgNumber.intValue() != argNumber) {
        errorManager.report(new GssError("Function expects " + expArgNumber
            + " arguments but has " + argNumber,
            node.getSourceCodeLocation()));
        visitController.removeCurrentNode();
        return;
      }

      try {
        functionResult = evaluateFunction(node, function, arguments, errorManager);
      } catch (GssFunctionException e) {
        visitController.removeCurrentNode();
        return;
      } catch (RuntimeException e) {
        errorManager.report(
            new GssError(
                Throwables.getStackTraceAsString(e),
                node.getSourceCodeLocation()));
        visitController.removeCurrentNode();
        return;
      }
    }
    visitController.replaceCurrentBlockChildWith(functionResult, false);
  }

  /**
   * Evaluates the given function node.
   *
   * <p>Subclasses may sublcass this method to change the evaluation
   * process in some circumstances.
   *
   * @param node the function node to evaluate
   * @param function the GSS function matching this node
   * @param arguments the arguments of this node
   * @param errorManager the error manager passed into the GSS function call
   * @return the result of the evaluation as a list of value nodes
   * @throws GssFunctionException if the function call is invalid
   * @throws RuntimeException if the function call fails to complete
   */
  protected List<CssValueNode> evaluateFunction(
      CssCustomFunctionNode node,
      GssFunction function,
      List<CssValueNode> arguments,
      ErrorManager errorManager) throws GssFunctionException {

    List<CssValueNode> functionResult =
        function.getCallResultNodes(arguments, errorManager);
    node.setResult(functionResult);
    return functionResult;
  }

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