/*
 * Copyright 2008 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.soyc;

import com.google.gwt.core.ext.linker.CompilationMetricsArtifact;
import com.google.gwt.core.ext.linker.ModuleMetricsArtifact;
import com.google.gwt.core.ext.linker.PrecompilationMetricsArtifact;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.soyc.MakeTopLevelHtmlForPerm.DependencyLinker;
import com.google.gwt.soyc.MakeTopLevelHtmlForPerm.NullDependencyLinker;
import com.google.gwt.soyc.io.OutputDirectory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

/**
 * The command-line entry point for creating a compile report.
 */
public class SoycDashboard {
  private static class FormatException extends RuntimeException {
    public FormatException() {
      super();
    }

    public FormatException(String message) {
      super(message);
    }

    public FormatException(Throwable cause) {
      super(cause);
    }
  }

  /*
   * cleans up the RPC code categories
   */
  private static void foldInRPCHeuristic(
      final HashMap<String, CodeCollection> nameToCodeColl) {
    /**
     * Heuristic: this moves all classes that override serializable from RPC to
     * "Other Code" *if* there is no RPC generated code, i.e., if the
     * application really is not using RPC
     */

    if (nameToCodeColl.get("rpcGen").classes.size() == 0) {

      for (String className : nameToCodeColl.get("rpcUser").classes) {

        if ((!nameToCodeColl.get("widget").classes.contains(className))
            && (!nameToCodeColl.get("jre").classes.contains(className))
            && (!nameToCodeColl.get("gwtLang").classes.contains(className))) {
          nameToCodeColl.get("allOther").classes.add(className);
        }
      }
      nameToCodeColl.get("rpcUser").classes.clear();

      for (String className : nameToCodeColl.get("rpcGwt").classes) {
        if ((!nameToCodeColl.get("widget").classes.contains(className))
            && (!nameToCodeColl.get("jre").classes.contains(className))
            && (!nameToCodeColl.get("gwtLang").classes.contains(className))) {
          nameToCodeColl.get("allOther").classes.add(className);
        }
      }
      nameToCodeColl.get("rpcGwt").classes.clear();
    }
  }

  private static DefaultHandler parseXMLDocumentDependencies(
      final Map<String, Map<String, String>> allDependencies) {
    DefaultHandler handler = new DefaultHandler() {

      // may want to create a class for this later
      String curMethod;
      Map<String, String> dependencies = new TreeMap<String, String>();
      String graphExtends = null;
      StringBuilder valueBuilder = new StringBuilder();

      @Override
      public void endElement(String uri, String localName, String qName) {
        if (localName.compareTo("table") == 0) {
          if (graphExtends != null) {
            // Add in elements from the extended graph
            for (Entry<String, String> entry : allDependencies.get(graphExtends).entrySet()) {
              dependencies.put(entry.getKey(), entry.getValue());
            }
          }
        }
      }

      @Override
      public void startElement(String nsUri, String strippedName,
          String tagName, final Attributes attributes) {

        valueBuilder.delete(0, valueBuilder.length());

        if (strippedName.compareTo("table") == 0
            && (attributes.getValue("name") != null)) {
          String name = attributes.getValue("name");
          dependencies = new TreeMap<String, String>();
          allDependencies.put(StringInterner.get().intern(name), dependencies);
          if (attributes.getValue("extends") != null) {
            graphExtends = StringInterner.get().intern(attributes.getValue("extends"));
            if (!allDependencies.containsKey(graphExtends)) {
              throw new FormatException("Graph " + name
                  + " extends an unknown graph " + graphExtends);
            }
          } else {
            graphExtends = null;
          }
        } else if ((strippedName.compareTo("method") == 0)
            && (attributes.getValue("name") != null)) {
          curMethod = StringInterner.get().intern(attributes.getValue("name"));
        } else if ((strippedName.compareTo("called") == 0)
            && (attributes.getValue("by") != null)) {
          String curDepMethod = attributes.getValue("by");
          if (!dependencies.containsKey(curMethod)) {
            dependencies.put(StringInterner.get().intern(curMethod),
                StringInterner.get().intern(curDepMethod));
          }
        }
      }
    };
    return handler;
  }

  /**
   * Global information for the current permutation being emitted.
   */
  private GlobalInformation globalInformation;

  /**
   * HTML emitter for the current permutation being emitted.
   */
  private MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm;

  private final OutputDirectory outDir;

  public SoycDashboard(OutputDirectory outDir) {
    this.outDir = outDir;
  }

  public void generateCompilerMetricsForOnePermutation(
      ModuleMetricsArtifact moduleMetrics,
      PrecompilationMetricsArtifact precompilationMetrics,
      CompilationMetricsArtifact compilationMetrics)  throws IOException {
    makeTopLevelHtmlForPerm.makeCompilerMetricsPermFiles(moduleMetrics,
        precompilationMetrics, compilationMetrics);
  }

  public void generateCrossPermutationFiles(Map<String, List<String>> permInfo)
      throws IOException {
    StaticResources.emit(outDir);
    MakeTopLevelHtmlForPerm.makeTopLevelHtmlForAllPerms(permInfo, outDir);
  }

  public void generateForOnePermutation() throws IOException {
    if (globalInformation.dependencies != null) {
      makeTopLevelHtmlForPerm.makeDependenciesHtml();
    }

    if (globalInformation.getNumFragments() > 0) {
      makeTopLevelHtmlForPerm.makeSplitStatusPages();
      makeTopLevelHtmlForPerm.makeLeftoverStatusPages();
    }
    for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
      DependencyLinker linker = chooseDependencyLinker(breakdown);
      makeHTMLFiles(makeTopLevelHtmlForPerm, breakdown, linker);
    }
  }

  public void readDependencies(InputStream stream)
      throws ParserConfigurationException, SAXException, IOException {
    globalInformation.dependencies = new TreeMap<String, Map<String, String>>();
    DefaultHandler depHandler = parseXMLDocumentDependencies(globalInformation.dependencies);
    SAXParserFactory depFactoryMain = SAXParserFactory.newInstance();
    depFactoryMain.setNamespaceAware(true);
    SAXParser saxParser = depFactoryMain.newSAXParser();
    saxParser.parse(stream, depHandler);
  }

  public void readSizeMaps(InputStream stream)
      throws ParserConfigurationException, SAXException, IOException {
    DefaultHandler handler = parseXMLDocumentSizeMap(globalInformation);
    SAXParserFactory factoryMain = SAXParserFactory.newInstance();
    factoryMain.setNamespaceAware(true);
    SAXParser saxParser = factoryMain.newSAXParser();
    saxParser.parse(stream, handler);

    // Now clean up the information that has been read in various ways
    globalInformation.computePackageSizes();

    // add to "All Other Code" if none of the special categories apply
    for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
      updateAllOtherCodeType(breakdown.nameToCodeColl);
    }

    // clean up the RPC categories
    for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
      foldInRPCHeuristic(breakdown.nameToCodeColl);
    }
  }

  public void readSplitPoints(InputStream stream)
      throws ParserConfigurationException, SAXException, IOException {
    DefaultHandler splitPointHandler = parseXMLDocumentSplitPoints();
    SAXParserFactory splitPointsFactoryMain = SAXParserFactory.newInstance();
    splitPointsFactoryMain.setNamespaceAware(true);

    SAXParser saxParser = splitPointsFactoryMain.newSAXParser();
    saxParser.parse(stream, splitPointHandler);
  }

  public void startNewPermutation(String permutationId) {
    globalInformation = new GlobalInformation(permutationId);
    makeTopLevelHtmlForPerm = new MakeTopLevelHtmlForPerm(globalInformation,
        outDir);
  }

  private Collection<SizeBreakdown> breakdownsForFragment(Integer fragment) {
    List<SizeBreakdown> breakdowns = new ArrayList<SizeBreakdown>();
    breakdowns.add(globalInformation.getTotalCodeBreakdown());
    if (fragment == 0) {
      breakdowns.add(globalInformation.getInitialCodeBreakdown());
    }
    if (fragment == (globalInformation.getNumFragments() + 1)) {
      breakdowns.add(globalInformation.getLeftoversBreakdown());
    }
    if (fragment >= 1 && fragment <= globalInformation.getNumFragments()) {
      breakdowns.add(globalInformation.fragmentCodeBreakdown(fragment));
    }
    return breakdowns;
  }

  private DependencyLinker chooseDependencyLinker(SizeBreakdown breakdown) {
    if (globalInformation.dependencies == null) {
      // no dependencies are available
      return new NullDependencyLinker();
    }

    if (breakdown == globalInformation.getTotalCodeBreakdown()) {
      if (globalInformation.getNumFragments() > 0) {
        return makeTopLevelHtmlForPerm.new DependencyLinkerForTotalBreakdown();
      } else {
        return makeTopLevelHtmlForPerm.new DependencyLinkerForInitialCode();
      }
    } else if (breakdown == globalInformation.getInitialCodeBreakdown()) {
      return makeTopLevelHtmlForPerm.new DependencyLinkerForInitialCode();
    } else if (breakdown == globalInformation.getLeftoversBreakdown()) {
      assert globalInformation.getNumFragments() > 0;
      return makeTopLevelHtmlForPerm.new DependencyLinkerForLeftoversFragment();
    } else {
      return new NullDependencyLinker();
    }
  }

  /**
   * Generates all the HTML files for one size breakdown.
   */
  private void makeHTMLFiles(MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm,
      SizeBreakdown breakdown, DependencyLinker depLinker) throws IOException {
    makeTopLevelHtmlForPerm.makePackageClassesHtmls(breakdown, depLinker);
    makeTopLevelHtmlForPerm.makeCodeTypeClassesHtmls(breakdown);
    makeTopLevelHtmlForPerm.makeLiteralsClassesTableHtmls(breakdown);
    makeTopLevelHtmlForPerm.makeBreakdownShell(breakdown);
    makeTopLevelHtmlForPerm.makeTopLevelShell();
  }

  private DefaultHandler parseXMLDocumentSizeMap(
      final GlobalInformation globalInformation) {
    return new DefaultHandler() {
      int fragment = -1;

      @Override
      public void endElement(String uri, String localName, String qName) {
        if (localName.compareTo("sizemap") == 0) {
          fragment = -1;
        }
      }

      @Override
      public void startElement(String uri, String localName, String qName,
          final Attributes attributes) {
        if (localName.compareTo("sizemap") == 0) {
          // starting a new size map
          String fragString = attributes.getValue("fragment");
          if (fragString == null) {
            throw new FormatException();
          }
          try {
            fragment = Integer.valueOf(fragString);
          } catch (NumberFormatException e) {
            throw new FormatException(e);
          }
          String sizeString = attributes.getValue("size");
          if (sizeString == null) {
            throw new FormatException();
          }
          int size;
          try {
            size = Integer.valueOf(sizeString);
          } catch (NumberFormatException e) {
            throw new FormatException(e);
          }
          for (SizeBreakdown breakdown : breakdownsForFragment(fragment)) {
            breakdown.sizeAllCode += size;
          }
        } else if (localName.compareTo("size") == 0) {
          String type = attributes.getValue("type");
          if (type == null) {
            throw new FormatException();
          }
          String ref = attributes.getValue("ref");
          if (ref == null) {
            throw new FormatException();
          }
          String sizeString = attributes.getValue("size");
          if (sizeString == null) {
            throw new FormatException();
          }
          int size;
          try {
            size = Integer.valueOf(sizeString);
          } catch (NumberFormatException e) {
            throw new FormatException(e);
          }
          recordSize(type, ref, size, globalInformation);
        }
      }

      private void accountForSize(SizeBreakdown breakdown, String refType,
          String ref, int size, GlobalInformation globalInformation) {
        if (refType.equals("string")) {
          LiteralsCollection stringLiterals = breakdown.nameToLitColl.get("string");
          stringLiterals.size += size;
          stringLiterals.literals.add(ref);
        } else if (refType.equals("var")) {
          // Nothing to record, because no breakdown is provided for random
          // variables
        } else {
          if (!refType.equals("type") && !refType.equals("method") && !refType.equals("field")) {
            throw new FormatException();
          }

          if (refType.equals("method")) {
            breakdown.methodToSize.put(ref, Integer.valueOf(size));
          }

          String className = ref;
          if (className.contains("::")) {
            /*
             * It's a method or field reference. Discard the method/field part.
             */
            int idx = className.indexOf(':');
            className = className.substring(0, idx);
          }
          className = StringInterner.get().intern(className);
          // derive the package name from the class
          String packageName;
          if (!globalInformation.getClassToPackage().containsKey(className)) {
            packageName = className;
            packageName = packageName.replaceAll("\\.[A-Z].*", "");
            globalInformation.getClassToPackage().put(className, packageName);
          } else {
            packageName = globalInformation.getClassToPackage().get(className);
          }

          if (!globalInformation.getPackageToClasses().containsKey(packageName)) {
            TreeSet<String> insertSet = new TreeSet<String>();
            insertSet.add(className);
            globalInformation.getPackageToClasses().put(packageName, insertSet);
          } else {
            globalInformation.getPackageToClasses().get(packageName).add(
                className);
          }

          recordClassCategories(breakdown.nameToCodeColl, className,
              packageName);

          if (breakdown.classToSize.containsKey(className)) {
            int newSize = breakdown.classToSize.get(className) + size;
            breakdown.classToSize.put(className, newSize);
          } else {
            breakdown.classToSize.put(className, size);
          }
        }
      }

      private void recordClassCategories(
          final HashMap<String, CodeCollection> nameToCodeColl,
          String className, String packageName) {
        if (packageName.startsWith("java")) {
          nameToCodeColl.get("jre").classes.add(className);
        } else if (packageName.startsWith("com.google.gwt.lang")) {
          nameToCodeColl.get("gwtLang").classes.add(className);
        }
        if (className.contains("_CustomFieldSerializer")) {
          nameToCodeColl.get("rpcUser").classes.add(className);
        } else if (className.endsWith("_FieldSerializer")
            || className.endsWith("_Proxy")
            || className.endsWith("_TypeSerializer")) {
          nameToCodeColl.get("rpcGen").classes.add(className);
        }
      }

      private void recordSize(String refType, String ref, int size,
          GlobalInformation globalInformation) {
        refType = StringInterner.get().intern(refType);
        ref = StringInterner.get().intern(ref);
        for (SizeBreakdown breakdown : breakdownsForFragment(fragment)) {
          accountForSize(breakdown, refType, ref, size, globalInformation);
        }
      }
    };
  }

  private DefaultHandler parseXMLDocumentSplitPoints() {
    DefaultHandler handler = new DefaultHandler() {

      private boolean inInitialLoadSequence = false;

      @Override
      public void endElement(String uri, String localName, String qName) {
        if (localName.compareTo("initialesq") == 0) {
          inInitialLoadSequence = false;
        }
      }

      @Override
      public void startElement(String nsUri, String strippedName,
          String tagName, final Attributes attributes) {
        if (strippedName.compareTo("splitpoint") == 0) {
          parseSplitPoint(attributes);
        } else if (strippedName.compareTo("initialseq") == 0) {
          inInitialLoadSequence = true;
        } else if (inInitialLoadSequence
            && strippedName.compareTo("splitpointref") == 0) {
          globalInformation.getInitialFragmentLoadSequence().add(
              parseSplitPointReference(attributes));
        }
      }

      /**
       * Parses a split point entry from a splitpoints XML soyc file.
       * A split point node as in \<splitpoint id=N location=DESC/\>
       *
       * @param attributes the attributes of the splitpoint node (provided by the SAX parsing
       *                   infrastructure)
       */
      private void parseSplitPoint(final Attributes attributes) {
        if (attributes.getValue("id") != null) {
          String curSplitPoint = attributes.getValue("id");
          if (attributes.getValue("location") != null) {
            String curSplitPointLocation = attributes.getValue("location");

            curSplitPointLocation = curSplitPointLocation.replaceAll("\\(L.*",
                "");

            globalInformation.addFragmentDescriptor(
                Integer.parseInt(curSplitPoint), curSplitPointLocation);
          }
        }
      }

      private Integer parseSplitPointReference(final Attributes attributes) {
        String spString = attributes.getValue("id");
        if (spString == null) {
          throw new FormatException("Could not parse split point reference");
        }
        return Integer.valueOf(spString);
      }

    };
    return handler;
  }

  /*
   * assigns code to "all other code" if none of the special categories apply
   */
  private void updateAllOtherCodeType(
      final HashMap<String, CodeCollection> nameToCodeColl) {
    // all classes not in any of the other categories
    for (String className : globalInformation.getClassToPackage().keySet()) {
      if ((!nameToCodeColl.get("widget").classes.contains(className))
          && (!nameToCodeColl.get("rpcUser").classes.contains(className))
          && (!nameToCodeColl.get("rpcGwt").classes.contains(className))
          && (!nameToCodeColl.get("rpcGen").classes.contains(className))
          && (!nameToCodeColl.get("jre").classes.contains(className))
          && (!nameToCodeColl.get("gwtLang").classes.contains(className))) {
        nameToCodeColl.get("allOther").classes.add(className);
      }
    }
  }
}
