/*
 * Copyright 2011 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.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssPseudoClassNode;
import com.google.common.css.compiler.ast.CssPseudoClassNode.FunctionType;
import com.google.common.css.compiler.ast.CssPseudoElementNode;
import com.google.common.css.compiler.ast.CssRefinerListNode;
import com.google.common.css.compiler.ast.CssRefinerNode;
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;

/**
 * Compiler pass which ensures that refiners are correctly formated because
 * not every invalid format is rejected by the parser. This pass checks for
 * a correct nth-format and can make it compact. In addition, the pass checks
 * the constraints for the :not pseudo-class.
 *
 * @author fbenz@google.com (Florian Benz)
 */
public class ProcessRefiners extends DefaultTreeVisitor
    implements CssCompilerPass {
  @VisibleForTesting
  static final String INVALID_NTH_ERROR_MESSAGE =
    "the format for NTH is not in the form an+b, 'odd', or 'even' where a " +
    "and b are (signed) integers that can be omitted";
  @VisibleForTesting
  static final String INVALID_NOT_SELECTOR_ERROR_MESSAGE =
    "a :not selector and pseudo-elements ('::') are not allowed inside of" +
    " a :not";
  @VisibleForTesting
  static final String NOT_LANG_ERROR_MESSAGE =
    "a pseudo-class which takes arguments has to be ':lang()' or has to " +
    "start with 'nth-'";
  private static final CharMatcher CSS_WHITESPACE =
      CharMatcher.anyOf(" \t\r\n\f");

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

  public ProcessRefiners(MutatingVisitController visitController,
      ErrorManager errorManager, boolean simplifyCss) {
    this.visitController = visitController;
    this.errorManager = errorManager;
    this.simplifyCss = simplifyCss;
  }

  @Override
  public boolean enterPseudoClass(CssPseudoClassNode refiner) {
    FunctionType functionType = refiner.getFunctionType();
    switch (functionType) {
      case NONE:
        return true;
      case LANG:
        return handleLang(refiner);
      case NTH:
        return handleNth(refiner);
      case NOT:
        return handleNot(refiner);
    }
    return true;
  }

  private static String trim(String input) {
    return CSS_WHITESPACE.trimFrom(input);
  }

  private boolean handleLang(CssPseudoClassNode refiner) {
    if (!refiner.getRefinerName().equals("lang")) {
      errorManager.report(new GssError(NOT_LANG_ERROR_MESSAGE,
          refiner.getSourceCodeLocation()));
      return false;
    }
    return true;
  }

  private boolean handleNot(CssPseudoClassNode refiner) {
    if (refiner.getNotSelector() == null) {
      errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
          refiner.getSourceCodeLocation()));
      return false;
    }
    CssRefinerListNode refinerList = refiner.getNotSelector().getRefiners();
    if (refinerList.numChildren() == 0) {
      return true;
    } else if (refinerList.numChildren() > 1) {
      // should not be possible due to the grammar
      errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
          refiner.getSourceCodeLocation()));
      return false;
    }
    CssRefinerNode nestedRefiner = refinerList.getChildAt(0);
    // a pseudo-element is not allowed inside a :not
    if (nestedRefiner instanceof CssPseudoElementNode) {
      errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
          refiner.getSourceCodeLocation()));
      return false;
    }
    // the negation pseudo-class is not allowed inside a :not
    if (nestedRefiner instanceof CssPseudoClassNode) {
      CssPseudoClassNode pseudoClass = (CssPseudoClassNode) nestedRefiner;
      if (pseudoClass.getFunctionType() == FunctionType.NOT) {
        errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
            refiner.getSourceCodeLocation()));
        return false;
      }
    }
    return true;
  }

  private boolean handleNth(CssPseudoClassNode refiner) {
    String argument = trim(refiner.getArgument());
    if (argument.contains(".")) {
      errorManager.report(new GssError(INVALID_NTH_ERROR_MESSAGE,
          refiner.getSourceCodeLocation()));
      return false;
    }
    // general pattern: an+b
    int a, b;
    if (argument.equals("even")) {
      // 2n
      a = 2;
      b = 0;
    } else if (argument.equals("odd")) {
      // 2n+1
      a = 2;
      b = 1;
    } else {
      try {
        int indexOfN = argument.indexOf('n');
        a = parseA(argument, indexOfN);
        b = parseB(argument, indexOfN);
      } catch (NumberFormatException e) {
        errorManager.report(new GssError(INVALID_NTH_ERROR_MESSAGE,
            refiner.getSourceCodeLocation()));
        return false;
      }
    }
    if (simplifyCss) {
      refiner.setArgument(compactRepresentation(a, b));
    }
    return true;
  }

  private String compactRepresentation(int a, int b) {
    if (a == 2 && b == 1) {
      // 2n+1 -> odd
      return "odd";
    }
    if (a == 0 && b == 0) {
      return "0";
    }
    StringBuilder compact = new StringBuilder();
    if (a != 0) {
      if (a != 1 || b == 0 /* for WebKit */) {
        compact.append(Integer.toString(a));
      }
      compact.append("n");
    }
    if (b > 0 && a != 0) {
      compact.append("+");
    }
    if (b != 0) {
      compact.append(Integer.toString(b));
    }
    return compact.toString();
  }

  @VisibleForTesting
  int parseA(String argument, int indexOfN) {
    if (indexOfN == -1) {
      // b
      return 0;
    } else {
      if (indexOfN > 0) {
        String aStr = trim(argument.substring(0, indexOfN));
        if (aStr.equals("+")) {
          // +n+b
          return 1;
        } else if (aStr.equals("-")) {
          // -n+b
          return -1;
        } else {
          // an+b
          aStr = aStr.replace("+", "");
          return Integer.parseInt(aStr);
        }
      } else {
        // n+b
        return 1;
      }
    }
  }

  @VisibleForTesting
  int parseB(String argument, int indexOfN) {
    if (indexOfN == -1) {
      // b
      argument = trim(argument.replace("+", ""));
      return Integer.parseInt(argument);
    } else {
      if (indexOfN + 1 < argument.length()) {
        // an+b
        String bStr = argument.substring(indexOfN + 1, argument.length());
        bStr = trim(bStr);
        bStr = bStr.replace("+", "");
        return Integer.parseInt(bStr);
      } else {
        // an
        return 0;
      }
    }
  }

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

@@ -3374,16 +3374,17 @@
 ls(%22lang
+(
 %22)) %7B%0A  

Fix issue with lang