/*
 * 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.gwt.resources.css;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ClassName;
import com.google.gwt.resources.gss.ClassNamesCollector;
import com.google.gwt.resources.rg.GssResourceGenerator;
import com.google.gwt.resources.rg.GssResourceGenerator.LoggerErrorManager;
import com.google.gwt.thirdparty.common.css.compiler.ast.CssTree;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssParser;
import com.google.gwt.thirdparty.common.css.compiler.ast.GssParserException;
import com.google.gwt.thirdparty.common.css.compiler.passes.CollectConstantDefinitions;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateDefinitionNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.CreateForLoopNodes;
import com.google.gwt.thirdparty.common.css.compiler.passes.UnrollLoops;
import com.google.gwt.thirdparty.guava.common.base.CaseFormat;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.StringSourceWriter;
import com.google.gwt.util.tools.ArgHandlerFile;
import com.google.gwt.util.tools.ArgHandlerFlag;
import com.google.gwt.util.tools.ArgHandlerString;
import com.google.gwt.util.tools.ToolBase;

import java.io.File;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

/**
 * A utility class for creating a Java interface declaration for a given CSS
 * file.
 */
public class InterfaceGenerator extends ToolBase {

  private static final Comparator<String> NAME_COMPARATOR = new Comparator<String>() {
    public int compare(String o1, String o2) {
      return o1.compareToIgnoreCase(o2);
    }
  };

  private static final TreeLogger.Type LOG_LEVEL = TreeLogger.WARN;

  public static void main(String[] args) {
    (new InterfaceGenerator()).execImpl(args);
  }

  private String interfaceName;
  private File inputFile;
  private TreeLogger logger;
  private boolean standaloneFile;

  private class ArgHandlerAddPackageHeader extends ArgHandlerFlag {

    public ArgHandlerAddPackageHeader() {
      addTagValue("-standalone", true);
    }

    @Override
    public String getPurposeSnippet() {
      return "Add package and import statements to generated interface so that "
          + "they are still functional when they stand alone.";
    }

    @Override
    public String getLabel() {
      return "addPackageHeader";
    }

    @Override
    public boolean setFlag(boolean value) {
      standaloneFile = value;
      logger.log(TreeLogger.DEBUG, value ? "Not creating" : "Creating" + " a standalone file");
      return true;
    }

    @Override
    public boolean getDefaultValue() {
      return standaloneFile;
    }
  }

  private InterfaceGenerator() {
    registerHandler(new ArgHandlerAddPackageHeader());
    registerHandler(new ArgHandlerString() {

      @Override
      public String getPurpose() {
        return "The name of the generated CssResource subtype";
      }

      @Override
      public String getTag() {
        return "-typeName";
      }

      @Override
      public String[] getTagArgs() {
        return new String[] {"some.package.MyCssResource"};
      }

      @Override
      public boolean isRequired() {
        return true;
      }

      @Override
      public boolean setString(String str) {
        if (str.length() == 0) {
          return false;
        }
        if (!Character.isJavaIdentifierStart(str.charAt(0))) {
          return false;
        }
        for (int i = 1, j = str.length(); i < j; i++) {
          char c = str.charAt(i);
          if (!(Character.isJavaIdentifierPart(c) || c == '.')) {
            return false;
          }
        }
        interfaceName = str;
        if (logger.isLoggable(TreeLogger.DEBUG)) {
          logger.log(TreeLogger.DEBUG, "interfaceName = " + interfaceName);
        }
        return true;
      }
    });

    // -css in.css
    registerHandler(new ArgHandlerFile() {

      @Override
      public String getPurpose() {
        return "The input CSS file to process";
      }

      @Override
      public String getTag() {
        return "-css";
      }

      @Override
      public boolean isRequired() {
        return true;
      }

      @Override
      public void setFile(File file) {
        inputFile = file;
        if (logger.isLoggable(TreeLogger.DEBUG)) {
          logger.log(TreeLogger.DEBUG, "inputFile = " + file.getAbsolutePath());
        }
      }
    });
  }

  @Override
  protected String getDescription() {
    return "Create a CssResource interface based on a CSS file";
  }

  private void execImpl(String[] args) {
    // Set up logging
    PrintWriter logWriter = new PrintWriter(System.err);
    logger = new PrintWriterTreeLogger(logWriter);
    ((PrintWriterTreeLogger) logger).setMaxDetail(LOG_LEVEL);

    // Process args or die
    if (!processArgs(args)) {
      System.exit(-1);
    }

    boolean error = false;
    try {
      System.out.println(process());
    } catch (MalformedURLException e) {
      logger.log(TreeLogger.ERROR, "Unable to load CSS", e);
      error = true;
    } catch (UnableToCompleteException e) {
      logger.log(TreeLogger.ERROR, "Unable to process CSS", e);
      error = true;
    } finally {
      // Make sure the logs are emitted
      logWriter.flush();
    }

    System.exit(error ? -1 : 0);
  }

  /**
   * Munge a CSS class name into a Java identifier.
   */
  private String methodName(String className) {
    StringBuilder sb = new StringBuilder();
    char c = className.charAt(0);
    boolean nextUpCase = false;

    if (Character.isJavaIdentifierStart(c)) {
      sb.append(Character.toLowerCase(c));
    }

    for (int i = 1, j = className.length(); i < j; i++) {
      c = className.charAt(i);
      if (!Character.isJavaIdentifierPart(c)) {
        nextUpCase = true;
        continue;
      }

      if (nextUpCase) {
        nextUpCase = false;
        c = Character.toUpperCase(c);
      }
      sb.append(c);
    }
    return sb.toString();
  }

  private String process() throws MalformedURLException,
      UnableToCompleteException {
    // Create AST
    CssTree cssTree = createAst(inputFile.toURI().toURL(), logger);

    // Sort all names
    Set<String> names = new TreeSet<String>(NAME_COMPARATOR);

    names.addAll(new ClassNamesCollector().getClassNames(cssTree));

    CollectConstantDefinitions collectConstantDefinitionsPass = new CollectConstantDefinitions(
            cssTree);
    collectConstantDefinitionsPass.runPass();
    Collection<String> renamedDefs = renameDefs(collectConstantDefinitionsPass
            .getConstantDefinitions().getConstantsNames());
    names.addAll(renamedDefs);

    // Deduplicate method names
    Set<String> methodNames = new HashSet<String>();

    // Build the interface
    SourceWriter sw = new StringSourceWriter();

    int lastDot = interfaceName.lastIndexOf('.');
    if (standaloneFile) {
      sw.println("// DO NOT EDIT");
      sw.println("// Automatically generated by "
          + InterfaceGenerator.class.getName());
      sw.println("package " + interfaceName.substring(0, lastDot) + ";");
      sw.println("import " + CssResource.class.getCanonicalName() + ";");
      sw.println("import " + ClassName.class.getCanonicalName() + ";");
    }

    sw.println("interface " + interfaceName.substring(lastDot + 1)
        + " extends CssResource {");
    sw.indent();
    for (String className : names) {
      String methodName = methodName(className);

      while (!methodNames.add(methodName)) {
        // Unusual, handles foo-bar and foo--bar
        methodName += "_";
      }

      sw.println();
      if (!methodName.equals(className)) {
        sw.println("@ClassName(\"" + Generator.escape(className) + "\")");
      }
      sw.println("String " + methodName + "();");
    }
    sw.outdent();
    sw.println("}");

    return sw.toString();
  }

  /**
   * In GSS, constant names are defined in upper case but a method name to access a constant in
   * a CssResource interface can be written in lower camel case.
   * <p>
   * This method converts all constant names in a lower camel case identifier.
   */
  private Collection<String> renameDefs(Iterable<String> constantsNames) {
    return Lists.newArrayList(Iterables.transform(constantsNames, new Function<String, String>() {
      @Override
      public String apply(String constantName) {
        String lowerCase = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, constantName);

        // If we cannot revert the method name to the original constant name, use the
        // original constant name.
        // This case happens when number are used after an underscore:
        // CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "DEF_1") returns def1
        // but CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, "def1") returns DEF1 and the
        // GssResourceGenerator is not able to match the name of the method with the name of the
        // constant .
        if (!constantName.equals(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE,
            lowerCase))) {
          return constantName;
        }

        return lowerCase;
      }
    }));
  }

  private CssTree createAst(URL sourceFileUrl, TreeLogger logger)
          throws UnableToCompleteException {
    LoggerErrorManager errorManager = new LoggerErrorManager(logger);

    CssTree cssTree;

    try {
      cssTree = new GssParser(GssResourceGenerator.readUrlContent(sourceFileUrl, logger)).parse();
    } catch (GssParserException e) {
      logger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
      throw new UnableToCompleteException();
    }

    new CreateDefinitionNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
    // Can create new style classes
    new CreateForLoopNodes(cssTree.getMutatingVisitController(), errorManager).runPass();
    new UnrollLoops(cssTree.getMutatingVisitController(), errorManager).runPass();

    return cssTree;
  }
}
