/*
 * Copyright 2010 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.gssfunctions;

import static com.google.common.css.compiler.gssfunctions.ColorUtil.formatColor;
import static com.google.common.css.compiler.gssfunctions.ColorUtil.hsbToColor;
import static com.google.common.css.compiler.gssfunctions.ColorUtil.testContrast;
import static com.google.common.css.compiler.gssfunctions.ColorUtil.toHsb;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssFunctionArgumentsNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssHexColorNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
import com.google.common.css.compiler.ast.CssNumericNode;
import com.google.common.css.compiler.ast.CssValueNode;
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 java.awt.Color;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Container for common GSS functions.
 *
 * @author oana@google.com (Oana Florescu)
 * @author dgajda@google.com (Damian Gajda)
 */
public class GssFunctions {

  /**
   * @return a map from each GSS function name to the function
   */
  public static Map<String, GssFunction> getFunctionMap() {
    // TODO(dgajda): Add getName() to the function interface.
    return ImmutableMap.<String, GssFunction>builder()

        // Arithmetic functions.
        .put("add", new GssFunctions.AddToNumericValue())
        .put("sub", new GssFunctions.SubtractFromNumericValue())
        .put("mult", new GssFunctions.Mult())
        // Not named "div" so it will not be confused with the HTML element.
        .put("divide", new GssFunctions.Div())
        .put("min", new GssFunctions.MinValue())
        .put("max", new GssFunctions.MaxValue())

        // Color functions.
        .put("blendColorsHsb", new BlendColorsHsb())
        .put("blendColorsRgb", new BlendColorsRgb())
        .put("makeMutedColor", new MakeMutedColor())
        .put("addHsbToCssColor", new AddHsbToCssColor())
        .put("makeContrastingColor", new MakeContrastingColor())
        .put("adjustBrightness", new AdjustBrightness())
        .put("makeTranslucent", new MakeTranslucent())

        // Logic functions.
        .put("selectFrom", new SelectFrom())

        .build();
  }

  /**
   * Round decimals to the eight places, which appears to be the smallest
   * precision that works well across all browsers. (Yes, this is crazy.)
   */
  private static final String DECIMAL_FORMAT = "#.########";

  /**
   * This class encapsulates results of background definition calculation and
   * is used to build either a list of {@link CssValueNode} instances or a string that
   * represents the background CSS property.
   */
  static class ImageBackground {

    private static final String NO_REPEAT = "no-repeat";

    private final String url;
    private final String positionH;
    private final String positionHUnit;
    private final String positionV;
    private final String positionVUnit;

    /**
     * @param url The URL to be used as the background image URL
     * @param cornerId The corner id which tells how the image is positioned
     * @param imgSize The size of the image
     * @param units The units of the image size
     */
    public ImageBackground(
        String url, String cornerId, String imgSize, String units) {
      this.url = url;

      boolean isZero = Float.parseFloat(imgSize) == 0;

      boolean isLeft = isZero || cornerId.endsWith("l");
      positionH = isLeft ? "0" : "-" + imgSize;
      positionHUnit = isLeft ? CssNumericNode.NO_UNITS : units;

      boolean isTop = isZero || cornerId.startsWith("t");
      positionV = isTop ? "0" : "-" + imgSize;
      positionVUnit = isTop ? CssNumericNode.NO_UNITS : units;
    }

    @Override
    public String toString() {
      return createUrl(url) + " " + NO_REPEAT + " " +
          positionH + positionHUnit + " " + positionV + positionVUnit;
    }

    public List<CssValueNode> toNodes(SourceCodeLocation location) {
      return ImmutableList.of(
          createUrlNode(url, location),
          new CssLiteralNode(NO_REPEAT, location),
          new CssNumericNode(positionH, positionHUnit, location),
          new CssNumericNode(positionV, positionVUnit, location));
    }
  }

  /**
   * Base implementation of the color blending GSS function. Returns a color
   * half way between the two colors supplied as arguments.
   */
  public abstract static class BaseBlendColors implements GssFunction {

    /**
     * Returns the number of expected arguments of this GSS function.
     *
     * @return Number of expected arguments
     */
    @Override
    public Integer getNumExpectedArguments() {
      return 2;
    }

    /**
     * Returns the string representation in hex format for a color half way in
     * between the two supplied colors.
     *
     * @param args The list of arguments
     * @return The computed color
     */
    @Override
    public List<CssValueNode> getCallResultNodes(List<CssValueNode> args,
        ErrorManager errorManager) {

      CssValueNode arg1 = args.get(0);
      CssValueNode arg2 = args.get(1);

      String startColorStr = arg1.getValue();
      String endColorStr = arg2.getValue();

      String resultString = blend(startColorStr, endColorStr);

      CssHexColorNode result = new CssHexColorNode(resultString,
          arg1.getSourceCodeLocation());
      return ImmutableList.of((CssValueNode) result);
    }

    @Override
    public String getCallResultString(List<String> args)
        throws GssFunctionException {
      try {
        return blend(args.get(0), args.get(1));
      } catch (IllegalArgumentException e) {
        throw new GssFunctionException("Colors could not be parsed", e);
      }
    }

    // TODO(dgajda): Hide it, this function is only visible because
    public abstract String blend(String startColor, String endColor);
  }

  /**
   * Implementation of the blendColorsHsb GSS function. Returns a color half way
   * between the two colors supplied as arguments.
   */
  public static class BlendColorsHsb extends BaseBlendColors {

    @Override
    // TODO(dgajda): Hide it, this function is only visible because
    public String blend(String startColorStr, String endColorStr) {
      Color midColor = blendHsb(
          ColorParser.parseAny(startColorStr),
          ColorParser.parseAny(endColorStr));
      return formatColor(midColor);
    }
  }

  private static Color blendHsb(Color startColor, Color endColor) {

    float[] startColorHsb = toHsb(startColor);
    float[] endColorHsb = toHsb(endColor);

    float diffHue = Math.abs(startColorHsb[0] - endColorHsb[0]);
    float sumHue = startColorHsb[0] + endColorHsb[0];
    float midHue = (diffHue <= 0.5)
        ? sumHue / 2
        : (sumHue + 1) / 2;  // Hue values range 0 to 1 and wrap (i.e. 0 == 1)
    if (midHue > 1) {
      midHue -= 1;
    }

    return Color.getHSBColor(
        midHue,
        (startColorHsb[1] + endColorHsb[1]) / 2,
        (startColorHsb[2] + endColorHsb[2]) / 2);
  }

  /**
   * Implementation of the blendColorsRgb GSS function. Returns a color half way
   * between the two colors by averaging each of red, green & blue.
   */
  public static class BlendColorsRgb extends BaseBlendColors {

    /**
     * Returns the string representation in hex format for a color half way in
     * between the two supplied colors by averaging each of red, green & blue.
     *
     * @param startColorStr The start color in string form
     * @param endColorStr The endcolor in string form
     * @return The computed color
     */
    @Override
    // TODO(dgajda): Hide it, this function is only visible because
    public String blend(String startColorStr, String endColorStr) {
      Color startColor = ColorParser.parseAny(startColorStr);
      Color endColor = ColorParser.parseAny(endColorStr);

      Color midColor = new Color(
          (startColor.getRed() + endColor.getRed()) / 2,
          (startColor.getGreen() + endColor.getGreen()) / 2,
          (startColor.getBlue() + endColor.getBlue()) / 2);

      return formatColor(midColor);
    }
  }

  /**
   * Helper method to convert a numeric value of "0" or "1" into a boolean.
   *
   * @param numericPart The string containing the value
   * @return The corresponding boolean value
   */
  public static boolean parseBoolean(String numericPart) {
    return Integer.parseInt(numericPart) == 1;
  }

  /**
   * Helper method for implementors of GssFunction to allow the creation of
   * a url entry node in a GSS file.
   *
   * @param imageUrl The url of the image to add.
   * @param location The location in the GSS file to place the node.
   * @return The node containing the url entry.
   */
  public static CssFunctionNode createUrlNode(
      String imageUrl, SourceCodeLocation location) {
    CssFunctionNode url =
        new CssFunctionNode(CssFunctionNode.Function.byName("url"), location);

    if (!imageUrl.equals("")) {
      CssLiteralNode argument = new CssLiteralNode(imageUrl, location);
      List<CssValueNode> argList = ImmutableList.of((CssValueNode) argument);
      CssFunctionArgumentsNode arguments =
          new CssFunctionArgumentsNode(argList);
      url.setArguments(arguments);
    }
    return url;
  }

  /**
   * Implementation of the addHsbToCssColor GSS function.
   */
  public static class AddHsbToCssColor implements GssFunction {

    @Override
    public Integer getNumExpectedArguments() {
      return 4;
    }

    @Override
    public List<CssValueNode> getCallResultNodes(List<CssValueNode> args,
        ErrorManager errorManager) throws GssFunctionException {
      CssValueNode arg1 = args.get(0);
      CssValueNode arg2 = args.get(1);
      CssValueNode arg3 = args.get(2);
      CssValueNode arg4 = args.get(3);

      if (!(arg1 instanceof CssHexColorNode
          || arg1 instanceof CssLiteralNode)) {
        String message =
            "The first argument must be a CssHexColorNode or a CssLiteralNode.";
        errorManager.report(
            new GssError(message, arg1.getSourceCodeLocation()));
        throw new GssFunctionException(message);
      }
      CssNumericNode numeric2, numeric3, numeric4;
      if (arg2 instanceof CssNumericNode && arg3 instanceof CssNumericNode
          && arg4 instanceof CssNumericNode) {
         numeric2 = (CssNumericNode)arg2;
         numeric3 = (CssNumericNode)arg3;
         numeric4 = (CssNumericNode)arg4;
      } else {
        String message = "Arguments number 2, 3 and 4 must be CssNumericNodes";
        errorManager.report(
            new GssError(message, arg2.getSourceCodeLocation()));
        throw new GssFunctionException(message);
      }

      try {
        String resultString =
            addHsbToCssColor(args.get(0).getValue(),
                             numeric2.getNumericPart(),
                             numeric3.getNumericPart(),
                             numeric4.getNumericPart());

        CssHexColorNode result = new CssHexColorNode(resultString,
            arg1.getSourceCodeLocation());
        return ImmutableList.of((CssValueNode)result);
      } catch (GssFunctionException e) {
        errorManager.report(
            new GssError(e.getMessage(), arg2.getSourceCodeLocation()));
        throw e;
      }
    }

    @Override
    public String getCallResultString(List<String> args)
        throws GssFunctionException {
      String baseColorString = args.get(0);
      return addHsbToCssColor(
          baseColorString, args.get(1), args.get(2), args.get(3));
    }

    protected String addHsbToCssColor(
        String baseColorString, String hueToAdd, String saturationToAdd,
        String brightnessToAdd) throws GssFunctionException {
      try {
        return addHsbToCssColor(
            baseColorString,
            Integer.parseInt(hueToAdd),
            Integer.parseInt(saturationToAdd),
            Integer.parseInt(brightnessToAdd));
      } catch (NumberFormatException e) {
        String message = String.format("Could not parse the integer arguments"
            + " for the function 'addHsbToCssColor'. The list of arguments was:"
            + " %s, %s, %s, %s. ",
            baseColorString, hueToAdd, saturationToAdd, brightnessToAdd);
        throw new GssFunctionException(message);
      } catch (IllegalArgumentException e) {
        String message = String.format("Could not parse the color argument"
            + " for the function 'addHsbToCssColor'. The list of arguments was:"
            + " %s, %s, %s, %s. ",
            baseColorString, hueToAdd, saturationToAdd, brightnessToAdd);
        throw new GssFunctionException(message);
      }
    }

    /**
     * Takes a CSS color string, and adds the specified amount of hue,
     * saturation and brightness to it.
     *
     * @param baseColorString The string representing the color to change
     * @param hueToAdd The amount of hue to add (can be negative)
     * @param saturationToAdd The amount of saturation to add (can be negative)
     * @param brightnessToAdd The amount of brightness to add (can be negative)
     * @return A CSS String representing the new color
     */
    public String addHsbToCssColor(String baseColorString,
                                   int hueToAdd,
                                   int saturationToAdd,
                                   int brightnessToAdd) {

      // Skip transformation for the transparent color.
      if ("transparent".equals(baseColorString)) {
        return baseColorString;
      }

      Color baseColor = ColorParser.parseAny(baseColorString);
      Color newColor = addValuesToHsbComponents(baseColor,
                                                hueToAdd,
                                                saturationToAdd,
                                                brightnessToAdd);

      return formatColor(newColor);
    }

    /**
     * Adds the specified amount to the specified HSB (Hue, Saturation,
     * Brightness) parameter of the given color. The amount can be negative.
     *
     * @param baseColor The color to modify
     * @param hueToAdd The amount of hue to add
     * @param saturationToAdd The amount of saturation to add
     * @param brightnessToAdd The amount of brightness to add
     * @return The modified color
     */
    public Color addValuesToHsbComponents(Color baseColor,
                                          int hueToAdd,
                                          int saturationToAdd,
                                          int brightnessToAdd) {

      float[] hsbValues = toHsb(baseColor);

      // In HSB color space, Hue goes from 0 to 360, Saturation and Brightness
      // from 0 to 100. However, in Java all three parameters vary from 0.0 to
      // 1.0, so we need some basic conversion.
      hsbValues[0] = (float) (hsbValues[0] + hueToAdd / 360.0);
      // The hue needs to wrap around, so just keep hue - floor(hue).
      hsbValues[0] -= (float) Math.floor(hsbValues[0]);

      // For saturation and brightness, no wrapping around, we just make sure
      // we don't go over 1.0 or under 0.0
      hsbValues[1] = (float) Math.min(1.0, Math.max(0,
          hsbValues[1] + saturationToAdd / 100.0));
      hsbValues[2] = (float) Math.min(1.0, Math.max(0,
          hsbValues[2] + brightnessToAdd / 100.0));

      return Color.getHSBColor(hsbValues[0], hsbValues[1], hsbValues[2]);
    }
  }

  /**
   * Implementation of the makeMutedColor GSS function. This is intended to
   * generate a muted flavor of a text or link color. Takes three arguments: the
   * background color over which this text or link will appear, and the text or
   * link color this should be a muted version of and optionally the loss of
   * saturation for muted tone (0 <= loss <= 1).
   */
  public static class MakeMutedColor implements GssFunction {

    private float LOSS_OF_SATURATION_FOR_MUTED_TONE = 0.2f;
    private String ARGUMENT_COUNT_ERROR_MESSAGE = "makeMutedColor " +
        "expected arguments: backgroundColorStr, foregroundColorStr and an " +
        "optional loss of saturation value (0 <= loss <= 1).";

    /**
     * Returns the number of expected arguments of this GSS function.
     *
     * @return Number of expected arguments
     */
    @Override
    public Integer getNumExpectedArguments() {
      return null;
    }

    /**
     * Returns the muted color corresponding to the arguments
     * documented in {@link MakeMutedColor}.
     *
     * @param args The list of arguments.
     * @return The generated muted color.
     */
    @Override
    public List<CssValueNode> getCallResultNodes(List<CssValueNode> args,
        ErrorManager errorManager) throws GssFunctionException {

      if (args.size() != 2 && args.size() != 3) {
        throw new GssFunctionException(ARGUMENT_COUNT_ERROR_MESSAGE);
      }

      CssValueNode backgroundColorNode = args.get(0);
      CssValueNode foregroundColorNode = args.get(1);
      String lossOfSaturationForMutedTone =
          String.valueOf(LOSS_OF_SATURATION_FOR_MUTED_TONE);

      if (args.size() == 3) {
        lossOfSaturationForMutedTone = args.get(2).getValue();
      }

      String backgroundColorStr = backgroundColorNode.getValue();
      String foregroundColorStr = foregroundColorNode.getValue();

      String resultStr = makeMutedColor(backgroundColorStr, foregroundColorStr,
          lossOfSaturationForMutedTone);

      CssHexColorNode result = new CssHexColorNode(resultStr,
          backgroundColorNode.getSourceCodeLocation());
      return ImmutableList.of((CssValueNode) result);
    }

    protected String makeMutedColor(
        String backgroundColorStr, String foregroundColorStr, String lossStr) {

      // If the background is transparent, or if the foreground is transparent,
      // there's really no way we can know how to pick a muted color. We thus
      // return the foreground color unchanged.
      if ("transparent".equalsIgnoreCase(backgroundColorStr)
          || "transparent".equalsIgnoreCase(foregroundColorStr)) {
        return foregroundColorStr;
      }
      Color backgroundColor = ColorParser.parseAny(backgroundColorStr);
      Color foregroundColor = ColorParser.parseAny(foregroundColorStr);

      float[] backgroundColorHsb = toHsb(backgroundColor);
      float[] foregroundColorHsb = toHsb(foregroundColor);
      float lossOfSaturationForMutedTone = Float.valueOf(lossStr);

      // Make sure that 0 <= lossOfSaturationForMutedTone <= 1
      if (lossOfSaturationForMutedTone < 0) {
        lossOfSaturationForMutedTone = 0;
      } else if (lossOfSaturationForMutedTone > 1) {
        lossOfSaturationForMutedTone = 1;
      }

      // We take the hue from the foreground color, we desaturate it a little
      // bit, and choose a brightness halfway between foreground and background.
      // For example, if the background has a brightness of 50, and 100 for the
      // foreground, the muted color will have 75. If we have a dark background,
      // it should be the reverse.
      float mutedHue = foregroundColorHsb[0];
      float mutedSaturation = Math.max(
          foregroundColorHsb[1] - lossOfSaturationForMutedTone, 0);
      float mutedBrightness = (foregroundColorHsb[2] + backgroundColorHsb[2]) /
          2;

      Color mutedColor
          = Color.getHSBColor(mutedHue, mutedSaturation, mutedBrightness);

      return formatColor(mutedColor);
    }

    protected String makeMutedColor(
        String backgroundColorStr, String foregroundColorStr) {
      return  makeMutedColor(backgroundColorStr, foregroundColorStr,
          String.valueOf(LOSS_OF_SATURATION_FOR_MUTED_TONE));
    }

    @Override
    public String getCallResultString(List<String> args) throws
        GssFunctionException {
      if (args.size() == 2) {
        return makeMutedColor(args.get(0), args.get(1));
      } else if (args.size() == 3) {
        return makeMutedColor(args.get(0), args.get(1), args.get(2));
      } else {
        throw new GssFunctionException(ARGUMENT_COUNT_ERROR_MESSAGE);
      }
    }
  }


  /**
   * Abstract class implementing the shared logic for the arithmetic functions.
   */
  private static abstract class LeftAssociativeOperator implements GssFunction {

    /**
     * Returns the number of expected arguments of this GSS function.
     *
     * @return Number of expected arguments
     */
    @Override
    public Integer getNumExpectedArguments() {
      // Takes a variable number of arguments.
      return null;
    }

    /**
     * Returns a new value of the same unit as the original.
     *
     * @param args The list of arguments
     * @return The resulting fingerprint as a numeric node in a list
     */
    @Override
    public List<CssValueNode> getCallResultNodes(List<CssValueNode> args,
        ErrorManager errorManager) throws GssFunctionException {
      List<CssNumericNode> numericList = Lists.newArrayList();
      for (CssValueNode arg : args) {
        numericList.add(getSizeNode(arg, errorManager,
            true /* isUnitOptional */));
      }
      return ImmutableList.<CssValueNode>of(
          calculate(numericList, errorManager));
    }

    @Override
    public String getCallResultString(List<String> args)
        throws GssFunctionException {
      List<CssNumericNode> numericList = Lists.newArrayList();
      for (String arg : args) {
        // Note, the unit may be 'NO_UNITS'
        Size sizeWithUnits = parseSize(arg, true /* isUnitOptional */);
        numericList.add(
            new CssNumericNode(sizeWithUnits.size, sizeWithUnits.units));
      }
      CssNumericNode result = calculate(numericList, null);
      return result.getNumericPart() + result.getUnit();
    }

    // Note: Keep an eye on the performance of these functions, as creating
    // intermediate CssNumericNodes may be wasteful. Instead, the values in the
    // nodes could be used directly for computation, though that may make
    // accurate error reporting more difficult.

    protected CssNumericNode calculate(List<CssNumericNode> args,
        ErrorManager errorManager) throws GssFunctionException {
      if (args.size() < 2) {
        throw error("Not enough arguments",
                    errorManager, args.get(0).getSourceCodeLocation());
      }

      double total = Double.valueOf(args.get(0).getNumericPart());
      String overallUnit =
          isIdentityValue(total) ? null : args.get(0).getUnit();

      for (CssNumericNode node : args.subList(1, args.size())) {
        double value = Double.valueOf(node.getNumericPart());
        if (isIdentityValue(value)) {
          continue;
        }
        if (overallUnit == null) {
          overallUnit = node.getUnit();
        } else if (!overallUnit.equals(node.getUnit())) {
          throw error(
              "Parameters' units don't match (\""
              + overallUnit + "\" vs \"" + node.getUnit() + "\")",
              errorManager, node.getSourceCodeLocation());
        }
        total = performOperation(total, value);
      }
      String resultString = new DecimalFormat(DECIMAL_FORMAT).format(total);

      return new CssNumericNode(resultString,
          overallUnit != null ? overallUnit : CssNumericNode.NO_UNITS,
          args.get(0).getSourceCodeLocation());
    }

    // Perform the mathematical operation.
    protected abstract double performOperation(double left, double right);

    /**
     * By default, this method returns {@code false}.
     * @return whether the identity value has no effect on the output. In this
     *     case, any units (px, pt, etc.) will be ignored.
     */
    protected boolean isIdentityValue(double value) {
      return false;
    }
  }


  /**
   * The "add()" function adds a list of numeric values.
   */
  public static class AddToNumericValue extends LeftAssociativeOperator {
    @Override
    protected double performOperation(double left, double right) {
      return left + right;
    }

    @Override
    protected boolean isIdentityValue(double value) {
      return value == 0.0;
    }
  }

  /**
   * The "sub()" function subtracts a list of numeric values.
   * SubtractFromNumericValue(a, b, c) evaluates to ((a - b) - c).
   */
  public static class SubtractFromNumericValue extends LeftAssociativeOperator {
    @Override
    protected double performOperation(double left, double right) {
      return left - right;
    }

    @Override
    protected boolean isIdentityValue(double value) {
      return value == 0.0;
    }
  }

  /**
   * A {@link GssFunction} that returns the max value from its list of
   * arguments.
   */
  public static class MaxValue extends LeftAssociativeOperator {
    @Override
    protected double performOperation(double left, double right) {
      return Math.max(left, right);
    }
  }

  /**
   * A {@link GssFunction} that returns the min value from its list of
   * arguments.
   */
  public static class MinValue extends LeftAssociativeOperator {
    @Override
    protected double performOperation(double left, double right) {
      return Math.min(left, right);
    }
  }

  /**
   * A {@link ScalarLeftAssociativeOperator} is a left associative operator
   * whose arguments are all scalars, with the possible exception of the first
   * argument, which may be a {@link Size} rather than a scalar.
   */
  private static abstract class ScalarLeftAssociativeOperator extends
      LeftAssociativeOperator {

    @Override
    protected CssNumericNode calculate(List<CssNumericNode> args,
        ErrorManager errorManager) throws GssFunctionException {
      if (args.size() == 0) {
        throw error("Not enough arguments",
                    errorManager, args.get(0).getSourceCodeLocation());
      }

      double total = Double.valueOf(args.get(0).getNumericPart());
      String overallUnit = args.get(0).getUnit();

      for (CssNumericNode node : args.subList(1, args.size())) {
        if (node.getUnit() != null
            && !node.getUnit().equals(CssNumericNode.NO_UNITS)) {
          throw error(
              "Only the first argument may have a unit associated with it, "
              + " but has unit: " + node.getUnit(),
              errorManager, node.getSourceCodeLocation());
        }

        double value = Double.valueOf(node.getNumericPart());
        total = performOperation(total, value);
      }
      String resultString = new DecimalFormat(DECIMAL_FORMAT).format(total);

      return new CssNumericNode(resultString,
          overallUnit != null ? overallUnit : CssNumericNode.NO_UNITS,
          args.get(0).getSourceCodeLocation());
    }
  }

  /**
   * A {@link GssFunction} that returns the product of its arguments. Only the
   * first argument may have a unit.
   */
  public static class Mult extends ScalarLeftAssociativeOperator {
    @Override
    protected double performOperation(double left, double right) {
      return left * right;
    }

    @Override
    protected boolean isIdentityValue(double value) {
      return value == 1.0;
    }
  }

  /**
   * A {@link GssFunction} that returns the quotient of its arguments. Only the
   * first argument may have a unit.
   */
  public static class Div extends ScalarLeftAssociativeOperator {
    @Override
    protected double performOperation(double left, double right) {
      return left / right;
    }

    @Override
    protected boolean isIdentityValue(double value) {
      return value == 1.0;
    }
  }

  /**
   * Implementation of the adjustBrightness GSS function. This generates
   * a slightly differentiated color suitable for hover styling. Takes the
   * color to modify as the first argument, and the requested brightness
   * difference as a second argument (between 0 and 100). This function will
   * always ensure that the returned color is different from the input, e.g.
   * attempting to "brighten" white will return a light grey instead of white,
   * but if it isn't possible to find a color that matches the request
   * difference (e.g. asking to brighten by 100 from a medium bright color),
   * the returned value will be a color with a difference from the input color
   * as close as possible to what is being requested. See the unit test for
   * some examples.
   */
  public static class AdjustBrightness implements GssFunction {

    /**
     * Returns the number of expected arguments of this GSS function.
     *
     * @return Number of expected arguments
     */
    @Override
    public Integer getNumExpectedArguments() {
      return 2;
    }

    /**
     * Returns the hover color corresponding to the argument
     * documented in {@link AdjustBrightness}.
     *
     * @param args The list of arguments.
     * @return The generated hover color.
     */
    @Override
    public List<CssValueNode> getCallResultNodes(List<CssValueNode> args,
        ErrorManager errorManager) {

      CssValueNode originalColorNode = args.get(0);
      CssValueNode brightnessAmount = args.get(1);

      String originalColorStr = originalColorNode.getValue();
      String brightnessAmountStr =
          ((CssNumericNode) brightnessAmount).getNumericPart();

      String resultStr = adjustBrightness(originalColorStr,
          brightnessAmountStr);

      CssHexColorNode result = new CssHexColorNode(resultStr,
          originalColorNode.getSourceCodeLocation());
      return ImmutableList.of((CssValueNode) result);
    }

    private float normalize(float value) {
      if (value > 1.0) {
        return 1;
      }
      if (value < 0.0) {
        return 0;
      }
      return value;
    }

    private String formatColorWithAdjustedBrightness (float[] originalHsb,
        float adjustedBrightness) {
      return formatColor(Color.getHSBColor(originalHsb[0],
          originalHsb[1], adjustedBrightness));
    }

    protected String adjustBrightness(String originalColorStr,
        String brightnessStr) {

      // If the input color is transparent, there's really no way we can know
      // how to pick a good output color. We thus return the color unchanged.
      if ("transparent".equalsIgnoreCase(originalColorStr)) {
        return originalColorStr;
      }
      Color originalColor = ColorParser.parseAny(originalColorStr);
      float brightnessFloat = Float.parseFloat(brightnessStr) / (float) 100.0;

      float[] originalColorHsb = toHsb(originalColor);
      float requestedBrightness = originalColorHsb[2] + brightnessFloat;

      // If we're not "saturating" to white or black, then we can meet
      // exactly what the caller requests.
      if (requestedBrightness >= 0.0 && requestedBrightness <= 1.0) {
        return formatColorWithAdjustedBrightness(originalColorHsb,
            requestedBrightness);
      }

      // If we can't get exactly what's requested, we try both directions to
      // be as close as possible to the requested brightness difference.
      requestedBrightness = normalize(requestedBrightness);
      float oppositeDirectionBrightness =
          normalize(originalColorHsb[2] - brightnessFloat);

      // Calculate the distance between what the caller requests and the two
      // possibilites we have, then return the closest.
      float chosenBrightness = Math.abs(brightnessFloat -
          (originalColorHsb[2] - requestedBrightness)) >
              Math.abs(brightnessFloat -
                  (originalColorHsb[2] - oppositeDirectionBrightness)) ?
                      oppositeDirectionBrightness : requestedBrightness;
      return formatColorWithAdjustedBrightness(originalColorHsb,
          chosenBrightness);
    }

    @Override
    public String getCallResultString(List<String> args) {
      return adjustBrightness(args.get(0), args.get(1));
    }
  }


  /**
   * The "makeContrastingColor" function generates a contrasting color with the
   * same hue as the input color.  The generated color passes (or almost
   * passes) the contrast test described in the W3C accessibility evaluation
   * working draft {@link "http://www.w3.org/TR/AERT#color-contrast"}.
   *
   * <p>The function takes two parameters:
   * <ul>
   * <li>The input color to find the contrasting color for,
   * <li>the similarity value (a float between 0.0 - 1.0) which tells how
   *   similar the computed color should be to the input color.
   * </ul>
   *
   * <p>The algorithm for the contrasting color generation is as follows:
   * <ol>
   * <li>First a base contrasting color is chosen.  It is either black or
   *   white, as one of these colors is guaranteed to be in contrast with any
   *   given color.  Additionally both of these colors are "hue-neutral", so
   *   choosing the contrasting color in between the input color and black or
   *   white should give pleasant results.
   *
   * <li>A closest required contrasting color is found.  This is a color in
   *   between the input color and the base contrasting color.  It has the same
   *   hue as the input color.  This color is found in HSB color space, by a
   *   bisection of a line between the input color and the base contrasting
   *   color.  To improve function performance, a limited number of the
   *   bisection steps ({@link MakeContrastingColor#NUM_ITERATIONS}) is
   *   performed.  This gives a constant expected time of function execution.
   *   At the same time the color that is found may not pass the contrast test,
   *   but is guaranteed to be close to the "contrast boundary".
   *
   * <li>The output color of the function is an interpolation of the closest
   *   contrasting color and the base contrasting color.  The interpolation is
   *   controlled using the "similarity" parameter.  If similarity is set to 1,
   *   it means that the result is the closest contrasting color.  If
   *   similarity is set to 0, it means that the result is the base contrasting
   *   color.
   * </ol>
   */
  public static class MakeContrastingColor implements GssFunction {

    /**
     * Number of iterations to approximate the closest contrasting color.
     * It is set to a number which should be enough to converge in the 8-bit
     * color component space we deal with. Since number of iterations is fixed
     * function computation time is also fixed.
     */
    private static final int NUM_ITERATIONS = 8;

    @Override
    public Integer getNumExpectedArguments() {
      return 2;
    }

    @Override
    public List<CssValueNode> getCallResultNodes(
        List<CssValueNode> args, ErrorManager errorManager) {
      CssValueNode arg1 = args.get(0);
      CssValueNode arg2 = args.get(1);

      // TODO(dgajda): We should check the type of the color node.
      String color = arg1.getValue();
      String similarity = arg2.toString();

      String resultStr = makeContrastingColor(color, similarity);

      CssHexColorNode result = new CssHexColorNode(resultStr,
          arg1.getSourceCodeLocation());
      return ImmutableList.of((CssValueNode)result);
    }

    @Override
    public String getCallResultString(List<String> args) {
      return makeContrastingColor(args.get(0), args.get(1));
    }

    protected String makeContrastingColor(
        String inputColorStr, String similarityStr) {
      if ("transparent".equalsIgnoreCase(inputColorStr)) {
        return inputColorStr;
      }
      Color inputColor = ColorParser.parseAny(inputColorStr);
      float similarity = Float.parseFloat(similarityStr);

      float[] distantColor = toHsb(
          getDistantColor(inputColor, Color.BLACK, Color.WHITE));

      float[] startColor = toHsb(inputColor);
      float[] endColor = distantColor;
      float[] closestContrastColor = null;

      for (int i = 0; i < NUM_ITERATIONS; i++) {
        closestContrastColor = blendSb(startColor, endColor, 0.5f);
        if (testContrast(inputColor, hsbToColor(closestContrastColor))) {
          endColor = closestContrastColor;
        } else {
          startColor = closestContrastColor;
        }
      }

      float[] resultColor = blendSb(
          closestContrastColor, distantColor, similarity);
      return formatColor(hsbToColor(resultColor));
    }

    private Color getDistantColor(
        Color color, Color first, Color second) {
      int firstLuminanceDiff = ColorUtil.luminanceDiff(color, first);
      int secondLuminanceDiff = ColorUtil.luminanceDiff(color, second);
      return firstLuminanceDiff >= secondLuminanceDiff ? first : second;
    }

    private float[] blendSb(float[] keepHue, float[] other, float similarity) {
      float[] result = Arrays.copyOf(keepHue, keepHue.length);
      mix(ColorUtil.S, keepHue, other, similarity, result);
      mix(ColorUtil.B, keepHue, other, similarity, result);
      return result;
    }

    private void mix(
        int componentIdx, float[] sourceHsb, float[] otherColorHsb,
        float sourceSimilarity, float[] resultHsb) {
      resultHsb[componentIdx] =
          sourceHsb[componentIdx] * sourceSimilarity
          + otherColorHsb[componentIdx] * (1f - sourceSimilarity);
    }
  }

  /**
   * Takes an input color and sets its alpha component without affecting
   * the RGB components.
   * Usage: makeTranslucent(existingColor, alphaValue);
   */
  public static class MakeTranslucent implements GssFunction {
    @Override
    public Integer getNumExpectedArguments() {
      return 2;
    }

    @Override
    public List<CssValueNode> getCallResultNodes(
        List<CssValueNode> args, ErrorManager errorManager) {
      CssValueNode arg1 = args.get(0);
      CssValueNode arg2 = args.get(1);

      String color = arg1.getValue();
      String alpha = arg2.toString();

      return ImmutableList.of(makeTranslucent(
          color, alpha, arg1.getSourceCodeLocation()));
    }

    @Override
    public String getCallResultString(List<String> args) {
      return makeTranslucent(args.get(0), args.get(1), null).getValue();
    }

    protected CssValueNode makeTranslucent(
        String inputColorStr, String alphaStr,
        @Nullable SourceCodeLocation sourceCodeLocation) {
      Color inputColor = ColorParser.parseAny(inputColorStr);
      double alpha = Math.min(1.0, Math.max(0, Float.parseFloat(alphaStr)));

      float[] rgb = inputColor.getRGBColorComponents(null);
      Color outputColor = new Color(rgb[0], rgb[1], rgb[2], (float) alpha);

      List<CssValueNode> argList = ImmutableList.<CssValueNode>of(
          new CssLiteralNode(
              Integer.toString(outputColor.getRed()), sourceCodeLocation),
          new CssLiteralNode(
              Integer.toString(outputColor.getGreen()), sourceCodeLocation),
          new CssLiteralNode(
              Integer.toString(outputColor.getBlue()), sourceCodeLocation),
          new CssLiteralNode(
              new DecimalFormat("#.###").format(outputColor.getAlpha() / 255f),
              sourceCodeLocation));
      CssValueNode argsValue = new CssCompositeValueNode(
          argList, CssCompositeValueNode.Operator.COMMA,
          sourceCodeLocation);
      CssFunctionNode result = new CssFunctionNode(
          CssFunctionNode.Function.byName("rgba"),
          sourceCodeLocation);
      result.setArguments(new CssFunctionArgumentsNode(
          ImmutableList.of(argsValue)));
      return result;
    }
  }


  /**
   * Allows the equivalent of the ternary operator in GSS, using three
   * {@code @def} statements as inputs. This GSS:
   * <p>
   * {@code @def MYDEF selectFrom(FOO, BAR, BAZ);}
   * <p>
   * implies:
   * <p>
   * {@code MYDEF = FOO ? BAR : BAZ;}
   * <p>
   * So this gss:
   * <p>
   * {@code @def FOO true;}
   * <p>
   * then implies:
   * <p>
   * {@code MYDEF = BAR;}
   */
  public static class SelectFrom implements GssFunction {
    @Override
    public Integer getNumExpectedArguments() {
      return 3;
    }

    @Override
    public List<CssValueNode> getCallResultNodes(
        List<CssValueNode> args, ErrorManager errorManager) {
      return ImmutableList.of("true".equals(args.get(0).getValue()) ?
          args.get(1) : args.get(2));
    }

    @Override
    public String getCallResultString(List<String> args) {
      return "true".equals(args.get(0)) ? args.get(1) : args.get(2);
    }
  }

  public static GssFunctionException error(
      CssValueNode node, String errorMessage, ErrorManager errorManager) {
    return error(errorMessage, errorManager, node.getSourceCodeLocation());
  }

  private static GssFunctionException error(
      String errorMessage, ErrorManager errorManager,
      SourceCodeLocation location) {
    if (errorManager != null) {
      errorManager.report(new GssError(errorMessage, location));
    }
    return new GssFunctionException(errorMessage);
  }

  private static CssNumericNode getSizeNode(CssValueNode valueNode,
      ErrorManager errorManager, boolean isUnitOptional)
      throws GssFunctionException {
    SourceCodeLocation location = valueNode.getSourceCodeLocation();
    if (valueNode instanceof CssNumericNode) {
      CssNumericNode node = (CssNumericNode)valueNode;
      checkSize(node.getNumericPart(), node.getUnit(), errorManager, location,
          isUnitOptional);
      return node;
    }
    String message = "Size must be a CssNumericNode with a unit or 0; "
        + "was: " + valueNode.toString();
    throw error(message, errorManager, location);
  }

  private static void checkSize(String valueString, String unit,
      ErrorManager errorManager, SourceCodeLocation location,
      boolean isUnitOptional) throws GssFunctionException {
    if (unit.equals(CssNumericNode.NO_UNITS)) {
      Double value = Double.parseDouble(valueString);
      if (value != 0.0 && !isUnitOptional) {
        String message = "Size must be 0 or have a unit; was: "
            + valueString + unit;
        throw error(message, errorManager, location);
      }
    }
  }

  /**
   * Helper method for implementors of GssFunction to allow the creation of
   * a url string in the GSS.
   *
   * @param url The url of the image to add.
   * @return The proper GSS url string.
   */
  public static String createUrl(String url) {
    return CssFunctionNode.Function.byName("url") + "(" + url  + ")";
  }

  private static final class Size {
    final String size;
    final String units;

    public Size(String size, String units) {
      this.size = size;
      this.units = units;
    }
  }

  /**
   * Helper class for checking if a size string contains units. This class is equivalent to
   * {@link CharMatcher#JAVA_LETTER} except that it also accepts {@code %}.
   */
  private static final CharMatcher UNIT_MATCHER = new CharMatcher() {
    @Override public boolean matches(char c) {
      return Character.isLetter(c) || c == '%';
    }

    @Override public String toString() {
      return "GssFunctions.UNIT_MATCHER";
    }
  };

  private static Size parseSize(String sizeWithUnits, boolean isUnitOptional)
      throws GssFunctionException {
    int unitIndex = UNIT_MATCHER.indexIn(sizeWithUnits);
    String size = unitIndex > 0 ?
        sizeWithUnits.substring(0, unitIndex) : sizeWithUnits;
    String units = unitIndex > 0 ?
        sizeWithUnits.substring(unitIndex) : CssNumericNode.NO_UNITS;
    checkSize(size, units, null /* errorManager */, null /* location */,
        isUnitOptional);
    return new Size(size, units);
  }
}
