/*
 * 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.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssConstantReferenceNode;
import com.google.common.css.compiler.ast.CssDeclarationNode;
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.CssNode;
import com.google.common.css.compiler.ast.CssNumericNode;
import com.google.common.css.compiler.ast.CssPriorityNode;
import com.google.common.css.compiler.ast.CssPropertyNode;
import com.google.common.css.compiler.ast.CssPropertyValueNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.MutatingVisitController;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
 * Compiler pass that BiDi flips all the flippable nodes.
 * TODO(user): Need to add a function to return tree before flipping.
 *
 * @author roozbeh@google.com (Roozbeh Pournader)
 */
public class BiDiFlipper extends DefaultTreeVisitor
    implements CssCompilerPass {

  private final DecimalFormat percentFormatter = new DecimalFormat("#.########");
  private final MutatingVisitController visitController;

  boolean shouldSwapLeftRightInUrl;
  boolean shouldSwapLtrRtlInUrl;
  boolean shouldFlipConstantReferences;

  private static final Logger logger = Logger.getLogger(
      BiDiFlipper.class.getName());

  public BiDiFlipper(MutatingVisitController visitController,
      boolean swapLtrRtlInUrl,
      boolean swapLeftRightInUrl,
      boolean shouldFlipConstantReferences) {
    this.visitController = visitController;
    this.shouldSwapLtrRtlInUrl = swapLtrRtlInUrl;
    this.shouldSwapLeftRightInUrl = swapLeftRightInUrl;
    this.shouldFlipConstantReferences = shouldFlipConstantReferences;
  }

  public BiDiFlipper(MutatingVisitController visitController,
      boolean swapLtrRtlInUrl,
      boolean swapLeftRightInUrl) {
    this(visitController,
        swapLtrRtlInUrl,
        swapLeftRightInUrl,
        false /* Don't flip constant reference by default. */);
  }

  /**
   * Map with exact strings to match and their corresponding flipped value.
   * For example, in "float: left" we need an exact match to flip "left" because
   * we don't want to touch things like "background: left.png".
   */
  private static final Map<String, String> EXACT_MATCHING_FOR_FLIPPING =
    new ImmutableMap.Builder<String, String>()
    .put("ltr", "rtl")
    .put("rtl", "ltr")
    .put("left", "right")
    .put("right", "left")
    .put("e-resize", "w-resize")
    .put("w-resize", "e-resize")
    .put("ne-resize", "nw-resize")
    .put("nw-resize", "ne-resize")
    .put("nesw-resize", "nwse-resize")
    .put("nwse-resize", "nesw-resize")
    .put("se-resize", "sw-resize")
    .put("sw-resize", "se-resize")
    .build();

  /**
   * Map with the "ends-with" substrings that can be flipped and their
   * corresponding flipped value.
   * For example, for
   * <p>
   *   padding-right: 2px
   * <p>
   * we need to match that the property name ends with "-right".
   */
  private static final Map<String, String> ENDS_WITH_MATCHING_FOR_FLIPPING =
    new ImmutableMap.Builder<String, String>()
    .put("-left", "-right")
    .put("-right", "-left")
    .put("-bottomleft", "-bottomright")
    .put("-topleft", "-topright")
    .put("-bottomright", "-bottomleft")
    .put("-topright", "-topleft")
    .build();

  /**
   * Map with the "contains" substrings that can be flipped and their
   * corresponding flipped value.
   * For example, for
   * <p>
   *   border-right-width: 2px
   * <p>
   * we need to match that the property name contains "-right-".
   */
  private static final Map<String, String> CONTAINS_MATCHING_FOR_FLIPPING =
    new ImmutableMap.Builder<String, String>()
    .put("-left-", "-right-")
    .put("-right-", "-left-")
    .build();

  /**
   * Set of properties that have flippable percentage values.
   */
  private static final Set<String> PROPERTIES_WITH_FLIPPABLE_PERCENTAGE =
    ImmutableSet.of("background",
                    "background-position",
                    "background-position-x",
                    "-ms-background-position-x");

  /*
   * Set of properties that are equivalent to border-radius.
   * TODO(roozbeh): Replace the explicit listing of prefixes with a general
   * pattern of "-[a-z]+-" to avoid maintaining a prefix list.
   */
  private static final Set<String> BORDER_RADIUS_PROPERTIES =
    ImmutableSet.of("border-radius",
                    "-webkit-border-radius",
                    "-moz-border-radius");

  /**
   * Set of properties whose property values may flip if they match the
   * four-part pattern.
   */
  private static final Set<String> FOUR_PART_PROPERTIES_THAT_SHOULD_FLIP =
    ImmutableSet.of("border-color",
                    "border-style",
                    "border-width",
                    "margin",
                    "padding");

  /**
   * Map with the patterns to match URLs against if swap_ltr_rtl_in_url flag is
   * true, and their replacement string. Only the first occurrence of the
   * pattern is flipped. This would match "ltr" and "rtl" if they occur as a
   * word inside the path specified by the url.
   * For example, for
   * <p>
   *   background: url(/foo/rtl/bkg.gif)
   * <p>
   * the flipped value would be
   * <p>
   *   background: url(/foo/ltr/bkg.gif)
   * <p>
   * whereas for
   * <p>
   *   background: url(/foo/bkg-ltr.gif)
   * <p>
   * the flipped value would be
   * <p>
   *   background: url(/foo/bkg-rtl.gif)
   * <p>
   */
  private static final Map<Pattern, String> URL_LTRTL_PATTERN_FOR_FLIPPING =
    new ImmutableMap.Builder<Pattern, String>()
    .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)ltr([-_\\./]+)"),
        "$1rtl$2")
    .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)rtl([-_\\./]+)"),
        "$1ltr$2")
    .build();

  /**
   * Map with the patterns to match URLs against if swap_left_right_in_url flag
   * is true, and their replacement string. Only the first occurrence of the
   * pattern is flipped. This would match "left" and "right" if they occur as a
   * word inside the path specified by the url.
   * For example, for
   * <p>
   *   background: url(/foo/right/bkg.gif)
   * <p>
   * the flipped value would be
   * <p>
   *   background: url(/foo/left/bkg.gif)
   * <p>
   * whereas for
   * <p>
   *   background: url(/foo/bkg-left.gif)
   * <p>
   * the flipped value would be
   * <p>
   *   background: url(/foo/bkg-right.gif)
   * <p>
   */
  private static final Map<Pattern, String> URL_LEFTRIGHT_PATTERN_FOR_FLIPPING =
    new ImmutableMap.Builder<Pattern, String>()
    .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)left([-_\\./]+)"),
        "$1right$2")
    .put(Pattern.compile("(?<![a-zA-Z])([-_\\./]*)right([-_\\./]+)"),
        "$1left$2")
    .build();

  /**
   * Return if the string is "left" or "center" or "right".
   */
  private boolean isLeftOrCenterOrRight(String value) {
    return "left".equals(value)
        || "center".equals(value)
        || "right".equals(value);
  }

  /**
   * Return if the node is CssHexColorNode.
   */
  private boolean isCssHexColorNode(CssValueNode valueNode) {
    return (valueNode instanceof CssHexColorNode);
  }

  /**
   * Return if the node is CssLiteralNode.
   */
  private boolean isCssLiteralNode(CssValueNode valueNode) {
    return (valueNode instanceof CssLiteralNode);
  }

  /**
   * Return if the node is CssNumericNode.
   */
  private boolean isNumericNode(CssValueNode valueNode) {
    return (valueNode instanceof CssNumericNode);
  }

  /**
   * Return if the node is a slash operator node.
   */
  private boolean isSlashNode(CssValueNode valueNode) {
    if (valueNode instanceof CssCompositeValueNode) {
      CssCompositeValueNode compositeNode = (CssCompositeValueNode) valueNode;
      return
          compositeNode.getOperator() == CssCompositeValueNode.Operator.SLASH;
    }
    return false;
  }

  /**
   * Return if the node is ConstantReference and also flippable.
   */
  private boolean shouldFlipConstantReference(CssValueNode valueNode) {
    if (!shouldFlipConstantReferences) {
      return false;
    }
    if (!(valueNode instanceof CssConstantReferenceNode)) {
      return false;
    }
    String ref = valueNode.getValue();
    if (ref.startsWith(ResolveCustomFunctionNodesForChunks.DEF_PREFIX)) {
      // Since gss function could generate multiple values, we can't do flip if
      // there's gss function call in place, simply skip this case.
      return false;
    }
    return true;
  }

  /**
   * Return if the node is numeric and also has '%'.
   */
  private boolean isNumericAndHasPercentage(CssValueNode value) {
    if (!isNumericNode(value)) {
      return false;
    }
    CssNumericNode numericNode = (CssNumericNode) value;
    return "%".equals(numericNode.getUnit());
  }

  /**
   * Returns if the percentage value of this node is flippable.
   *
   * Assumes simpler CSS 2.1 use of background and background-position
   * (multi-layer is not supported yet, neither is the extended CSS 3 syntax
   * for positioning, like "right 10% top 20%").
   * TODO(roozbeh): add support CSS 3 multi-layer backgrounds.
   * TODO(roozbeh): add support for extended CSS 3 syntax for positioning.
   */
  private boolean isValidForPercentageFlipping(
      CssPropertyNode propertyNode, CssPropertyValueNode propertyValueNode,
      int valueIndex) {

    String propertyName = propertyNode.getPropertyName();
    if (PROPERTIES_WITH_FLIPPABLE_PERCENTAGE.contains(propertyName)) {
      if (valueIndex == 0) {
        return true; // If this is the first value, it's always flippable
      }
      if ("background".equals(propertyName)) {
        // Make sure this is not the vertical position: Only flip if the
        // previous value is not numeric or "left", "center", or "right".
        CssValueNode previousValueNode =
            propertyValueNode.getChildAt(valueIndex - 1);
        if (!isNumericNode(previousValueNode)
            && !isLeftOrCenterOrRight(previousValueNode.getValue())) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Sets the percentage to flipped value(100 - 'old value'), if the node is
   * valid numeric node with percentage.
   */
  private CssValueNode flipPercentageValueNode(CssValueNode valueNode) {
    if (!isNumericAndHasPercentage(valueNode)) {
      return valueNode;
    }

    CssNumericNode numericNode = (CssNumericNode) valueNode;
    String oldPercentageValue = numericNode.getNumericPart();
    double newPercentValue = 100 - Double.parseDouble(oldPercentageValue);
    CssValueNode newNumericNode = new CssNumericNode(
        percentFormatter.format(newPercentValue), "%");

    return newNumericNode;
  }

  /**
   * Flips corners of a border-radius property. Corners are reordered in the
   * following way:
   * <ul>
   *   <li>0 1 is replaced with 1 0,
   *   <li> 0 1 2 is replaced with 1 0 1 2, and
   *   <li> 0 1 2 3 is replaced with 1 0 3 2.
   * </ul>
   *
   * <p>Lists of other lengths are returned unchanged.
   *
   * @param valueNodes the list of values representing the corners of a
   * border-radius property.
   * @return a list of values with the corners flipped.
   */
  private List<CssValueNode> flipCorners(List<CssValueNode> valueNodes) {
    switch (valueNodes.size()) {
      case 2:
        return Lists.newArrayList(
            valueNodes.get(1),
            valueNodes.get(0));
      case 3:
        return Lists.newArrayList(
            valueNodes.get(1),
            valueNodes.get(0),
            valueNodes.get(1).deepCopy(),
            valueNodes.get(2));
      case 4:
        return Lists.newArrayList(
            valueNodes.get(1),
            valueNodes.get(0),
            valueNodes.get(3),
            valueNodes.get(2));
      default:
        return valueNodes;
    }
  }

  /**
   * Takes a list of property values that belong to a border-radius property
   * and flips them. If there is a slash in the values, the data is divided
   * around the slash. Then for each section, flipCorners is called.
   */
  private List<CssValueNode> flipBorderRadius(List<CssValueNode> valueNodes) {

    int count = 0, slashLocation = -1;
    CssCompositeValueNode slashNode = null;
    for (CssValueNode valueNode : valueNodes) {
      if (isSlashNode(valueNode)) {
        slashLocation = count;
        slashNode = (CssCompositeValueNode) valueNode;
        break;
      }
      ++count;
    }

    if (slashLocation == -1) { // No slash found, just one set of values
      return flipCorners(valueNodes);
    }

    // The parser treats slashes as combinging the two values around the slash
    // into one composite value node. This is not really the correct semantics
    // for the border-radius properties, as the parser will treat
    // "border-radius: 1px 2px / 5px 6px" as having three value nodes: the first
    // one will be "1px", the second one the composite value "2px / 5px",
    // and the third one "6px". We work in this unfortunate parser model here,
    // first deconstructing and later reconstructing that tree.

    List<CssValueNode> slashNodeValues = slashNode.getValues();

    // Create a list of horizontal values and flip them
    List<CssValueNode> horizontalValues = Lists.newArrayList();
    horizontalValues.addAll(valueNodes.subList(0, slashLocation));
    horizontalValues.add(slashNodeValues.get(0));
    List<CssValueNode> newHorizontalValues = flipCorners(horizontalValues);

    // Do the same for vertical values
    List<CssValueNode> verticalValues = Lists.newArrayList();
    verticalValues.add(slashNodeValues.get(1));
    verticalValues.addAll(valueNodes.subList(slashLocation + 1,
        valueNodes.size()));
    List<CssValueNode> newVerticalValues = flipCorners(verticalValues);

    // Create a new slash node
    List<CssValueNode> newSlashNodeValues = Lists.newArrayList();
    newSlashNodeValues.add(newHorizontalValues.get(
        newHorizontalValues.size() - 1));
    newSlashNodeValues.add(newVerticalValues.get(0));
    CssCompositeValueNode newSlashNode = new CssCompositeValueNode(
      newSlashNodeValues,
      CssCompositeValueNode.Operator.SLASH,
      null
    );

    List<CssValueNode> newValueList = Lists.newArrayList();
    newValueList.addAll(newHorizontalValues.subList(0,
        newHorizontalValues.size() - 1));
    newValueList.add(newSlashNode);
    newValueList.addAll(newVerticalValues.subList(1, newVerticalValues.size()));

    return newValueList;
  }

  /**
   * Takes the list of property values, validate them, then swap the second
   * and last values. So that 0 1 2 3 becomes 0 3 2 1.
   *
   * That is unless the length of the list is not four, it belongs to a property
   * that shouldn't be flipped, or it's border-radius, where it will be
   * specially handled.
   *
   * TODO(roozbeh): Add explicit flipping for 'border-image*' and '*-shadow'
   * properties.
   */
  private List<CssValueNode> flipNumericValues(
      List<CssValueNode> valueNodes,
      String propertyName) {

    if (BORDER_RADIUS_PROPERTIES.contains(propertyName)) {
      return flipBorderRadius(valueNodes);
    } else if (valueNodes.size() != 4
        || !FOUR_PART_PROPERTIES_THAT_SHOULD_FLIP.contains(propertyName)) {
      return valueNodes;
    }

    int count = 0;
    CssValueNode secondValueNode = null;
    CssValueNode fourthValueNode = null;
    for (CssValueNode valueNode : valueNodes) {
      if (isNumericNode(valueNode)
          || isCssLiteralNode(valueNode)
          || isCssHexColorNode(valueNode)
          || shouldFlipConstantReference(valueNode)) {
        switch (count) {
          case 3:
            fourthValueNode = valueNode.deepCopy();
            break;
          case 1:
            secondValueNode = valueNode.deepCopy();
        }
      } else {
        return valueNodes;
      }
      count++;
    }

    // Swap second and last in the new list.
    count = 0;
    List<CssValueNode> newValueList = Lists.newArrayList();
    for (CssValueNode valueNode : valueNodes) {
      if (1 == count) {
        newValueList.add(fourthValueNode);
      } else if (3 == count) {
        newValueList.add(secondValueNode);
      } else {
        newValueList.add(valueNode);
      }
      count++;
    }
    return newValueList;
  }

  /**
   * Performs appropriate replacements needed for BiDi flipping.
   */
  private String flipValue(String value) {
    for (String s : EXACT_MATCHING_FOR_FLIPPING.keySet()) {
      if (value.equals(s)) {
        value = EXACT_MATCHING_FOR_FLIPPING.get(s);
        break;
      }
    }
    for (String s : ENDS_WITH_MATCHING_FOR_FLIPPING.keySet()) {
      if (value.endsWith(s)) {
        value = value.replace(s, ENDS_WITH_MATCHING_FOR_FLIPPING.get(s));
        break;
      }
    }
    for (String s : CONTAINS_MATCHING_FOR_FLIPPING.keySet()) {
      if (value.indexOf(s) > 0) {
        value = value.replace(s, CONTAINS_MATCHING_FOR_FLIPPING.get(s));
        break;
      }
    }
    return value;
  }

  /**
   * Returns flipped node after making appropriate replacements needed for
   * BiDi flipping, if the node is either a LiteralNode or PropertyNode.
   * Eg: PropertyNode 'padding-right' would become 'padding-left'.
   */
  private <T extends CssValueNode> T flipNode(T tNode) {
    if (tNode instanceof CssLiteralNode) {
      CssLiteralNode literalNode = (CssLiteralNode) tNode;
      String oldValue = literalNode.getValue();
      if (null == oldValue) {
        return tNode;
      }
      String flippedValue = flipValue(oldValue);
      if (flippedValue.equals(oldValue)) {
        return tNode;
      }

      // This is safe because of the instanceof check above.
      @SuppressWarnings("unchecked")
      T flippedLiteralNode = (T) new CssLiteralNode(flippedValue);

      return flippedLiteralNode;
    } else if (tNode instanceof CssPropertyNode) {
      CssPropertyNode propertyNode = (CssPropertyNode) tNode;
      String oldValue = propertyNode.getPropertyName();
      if (null == oldValue) {
        return tNode;
      }
      String flippedValue = flipValue(oldValue);
      if (flippedValue.equals(oldValue)) {
        return tNode;
      }

      // This is safe because of the instanceof check above.
      @SuppressWarnings("unchecked")
      T flippedPropertyNode = (T) new CssPropertyNode(flippedValue);

      return flippedPropertyNode;
    } else {
      return tNode;
    }
   }

  /**
   * Performs appropriate replacements required for flipping url.
   */
  private String flipUrlValue(String value) {
    if (null == value) {
      return null;
    }
    if (shouldSwapLtrRtlInUrl) {
      for (Pattern p : URL_LTRTL_PATTERN_FOR_FLIPPING.keySet()) {
        if (p.matcher(value).find()) {
          String s = URL_LTRTL_PATTERN_FOR_FLIPPING.get(p);
          value =  p.matcher(value).replaceFirst(s);
          break;
        }
      }
    }
    if (shouldSwapLeftRightInUrl) {
      for (Pattern p : URL_LEFTRIGHT_PATTERN_FOR_FLIPPING.keySet()) {
        if (p.matcher(value).find()) {
          String s = URL_LEFTRIGHT_PATTERN_FOR_FLIPPING.get(p);
          value = p.matcher(value).replaceFirst(s);
          break;
        }
      }
    }
    return value;
  }

  /**
   * Return node with flipped url, if it is a 'CssFunctionNode' with
   * function 'URL'.
   */
  private CssValueNode flipUrlNode(CssValueNode valueNode) {
    if (!((valueNode instanceof CssFunctionNode)
          && ("url".equals(((CssFunctionNode) valueNode).getFunctionName())))) {
      return valueNode;
    }

    // Get the url to be flipped.
    CssFunctionNode oldFunctionNode = (CssFunctionNode) valueNode;
    CssFunctionArgumentsNode functionArguments = oldFunctionNode.getArguments();

    // Asserting if url function has more than one argument, which
    // is unusual.
    Preconditions.checkArgument((1 == functionArguments.numChildren()),
                                "url function taking more than one argument");

    CssValueNode oldArgument = functionArguments.getChildAt(0);
    String oldUrlValue = oldArgument.getValue();
    // Get the flipped url.
    String newUrlValue = flipUrlValue(oldUrlValue);

    // Make a new FunctionNode out of flipped url argument.
    CssValueNode newArgument = oldArgument.deepCopy();
    newArgument.setValue(newUrlValue);
    List<CssValueNode> newArgumentsList = new ArrayList<CssValueNode>();
    newArgumentsList.add(newArgument);

    CssFunctionNode newFunctionNode = oldFunctionNode.deepCopy();
    newFunctionNode.setArguments(new CssFunctionArgumentsNode(newArgumentsList));
    return newFunctionNode;
  }

  @Override
  public boolean enterDeclaration(CssDeclarationNode declarationNode) {
    // Return if node is set to non-flippable.
    if (!declarationNode.getShouldBeFlipped()) {
      return true;
    }

    // Update the property name in the declaration.
    CssDeclarationNode newDeclarationNode = declarationNode.deepCopy();
    CssPropertyNode propertyNode = declarationNode.getPropertyName();
    newDeclarationNode.setPropertyName(flipNode(propertyNode));

    // Update the property value.
    CssPropertyValueNode propertyValueNode = declarationNode.getPropertyValue();
    List<CssValueNode> valueNodes = Lists.newArrayList();
    int valueIndex = 0;
    for (CssValueNode valueNode : propertyValueNode.childIterable()) {
      // Flip URL argument, if it is a valid url function.
      CssValueNode temp = flipUrlNode(valueNode);
      // Flip node value, if it is a property node or literal node with value
      // that required flipping.
      temp = flipNode(temp);
      // Flip node value, if it is numeric and has percentage that
      // needs flipping.
      if (isValidForPercentageFlipping(propertyNode, propertyValueNode,
                                       valueIndex)) {
        temp = flipPercentageValueNode(temp);
      }
      valueNodes.add(temp.deepCopy());
      valueIndex++;
    }
    if (valueNodes.size() != 0) {
      CssValueNode priority = null;
      // Remove possible !important priority node.
      if (!valueNodes.isEmpty() && Iterables.getLast(valueNodes) instanceof CssPriorityNode) {
        priority = Iterables.getLast(valueNodes);
        valueNodes = valueNodes.subList(0, valueNodes.size() - 1);
      }
      List<CssValueNode> newValueList =
          flipNumericValues(valueNodes, propertyNode.getPropertyName());
      // Re-add priority node if we removed it earlier.
      if (priority != null) {
        newValueList.add(priority);
      }
      newDeclarationNode.setPropertyValue(new CssPropertyValueNode(newValueList));
    } else {
      newDeclarationNode.setPropertyValue(propertyValueNode.deepCopy());
    }

    List<CssNode> replacementList = Lists.newArrayList();
    replacementList.add(newDeclarationNode);
    visitController.replaceCurrentBlockChildWith(replacementList, false);
    return true;
  }

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