/*
 * 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.core.ext.soyc.impl;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.SizeBreakdown;
import com.google.gwt.dev.js.ast.JsLiteral;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsStringLiteral;
import com.google.gwt.dev.util.Util;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Comparator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.zip.GZIPOutputStream;

/**
 * Records an array of {@link SizeBreakdown} to a gzipped XML file. That file is
 * then read to produce a Story of Your Compile.
 */
public class SizeMapRecorder {

  /**
   * A human-accessible type and description of a program reference. These are
   * produced by
   * {@link SizeMapRecorder#typedProgramReference(JsName, JavaToJavaScriptMap)}
   * and used by
   * {@link SizeMapRecorder#recordMap(TreeLogger, OutputStream, SizeBreakdown[], JavaToJavaScriptMap)}
   * .
   */
  private static class TypedProgramReference {
    public final String description;
    public final String type;

    public TypedProgramReference(String type, String description) {
      this.type = type;
      this.description = description;
    }
  }

  /**
   * Sorts by JsName.getIdent().
   */
  private static final Comparator<JsName> JSNAME_SORT = new Comparator<JsName>() {
    @Override
    public int compare(JsName o1, JsName o2) {
      return o1.getIdent().compareTo(o2.getIdent());
    }
  };

  /**
   * Returns the hexadecimal representation of a character.
   */
  public static StringBuilder charToHex(char c) {
    char hexDigit[] =
        {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    StringBuilder toReturn = new StringBuilder();
    byte charByte = (byte) (c >>> 8);
    toReturn.append(hexDigit[(charByte >> 4) & 0x0F]);
    toReturn.append(hexDigit[charByte & 0x0F]);
    charByte = (byte) (c & 0xFF);
    toReturn.append(hexDigit[(charByte >> 4) & 0x0F]);
    toReturn.append(hexDigit[charByte & 0x0F]);
    return toReturn;
  }

/**
   * Escapes '&', '<', '>', '"', and '\'' to their XML entity equivalents.
   */
  public static String escapeXml(String unescaped) {
    StringBuilder builder = new StringBuilder();
    escapeXml(unescaped, 0, unescaped.length(), true, builder);
    return builder.toString();
  }

/**
   * Escapes '&', '<', '>', '"', and optionally ''' to their XML entity
   * equivalents. The portion of the input string between start (inclusive) and
   * end (exclusive) is scanned.  The output is appended to the given
   * StringBuilder.
   *
   * @param code the input String
   * @param start the first character position to scan.
   * @param end the character position following the last character to scan.
   * @param quoteApostrophe if true, the &apos; character is quoted as
   *     &amp;apos;
   * @param builder a StringBuilder to be appended with the output.
   */
  public static void escapeXml(String code, int start, int end, boolean quoteApostrophe,
      StringBuilder builder) {
    // See http://www.w3.org/TR/2006/REC-xml11-20060816/#charsets.
    int lastIndex = 0;
    int len = end - start;
    char[] c = new char[len];

    code.getChars(start, end, c, 0);
    for (int i = 0; i < len; i++) {
      if ((c[i] < '\u0020')) {
        builder.append(c, lastIndex, i - lastIndex);
        if (c[i] == '\u0000') {
          builder.append("\\0");
        } else if (c[i] == '\u0009') {
          builder.append("\\t");
        } else if (c[i] == '\n') {
          builder.append("\\n");
        } else if (c[i] == '\r') {
          builder.append("\\r");
        } else {
          builder.append("(invalid xml character: \\u" + charToHex(c[i]) + ")");
        }
        lastIndex = i + 1;
      } else if (((c[i] >= '\u007F') && (c[i] <= '\u0084'))
          || ((c[i] >= '\u0086') && (c[i] <= '\u009F'))
          || ((c[i] >= '\uD800') && (c[i] <= '\uDBFF'))
          || ((c[i] >= '\uDC00') && (c[i] <= '\uDFFF'))
          || ((c[i] >= '\uFDD0') && (c[i] <= '\uFDDF')) || (c[i] == '\u00A0') || (c[i] == '\uFFFF')
          || (c[i] == '\uFFFE')) {
        builder.append(c, lastIndex, i - lastIndex);
        builder.append("(invalid xml character: \\u" + charToHex(c[i]) + ")");
        lastIndex = i + 1;
      } else if (c[i] == '&') {
        builder.append(c, lastIndex, i - lastIndex);
        builder.append("&amp;");
        lastIndex = i + 1;
      } else if (c[i] == '>') {
        builder.append(c, lastIndex, i - lastIndex);
        builder.append("&gt;");
        lastIndex = i + 1;
      } else if (c[i] == '<') {
        builder.append(c, lastIndex, i - lastIndex);
        builder.append("&lt;");
        lastIndex = i + 1;
      } else if (c[i] == '\"') {
        builder.append(c, lastIndex, i - lastIndex);
        builder.append("&quot;");
        lastIndex = i + 1;
      } else if (c[i] == '\'') {
        if (quoteApostrophe) {
          builder.append(c, lastIndex, i - lastIndex);
          builder.append("&apos;");
          lastIndex = i + 1;
        }
      } else if (c[i] > '\u007F') {
        builder.append(c, lastIndex, i - lastIndex);
        builder.append("&#x" + charToHex(c[i]) + ";");
        lastIndex = i + 1;
      }
    }
    builder.append(c, lastIndex, len - lastIndex);
  }

  /**
   * @param logger a TreeLogger
   */
  public static void recordMap(TreeLogger logger, OutputStream out, SizeBreakdown[] sizeBreakdowns,
      JavaToJavaScriptMap jjsmap, Map<JsName, JsLiteral> internedLiteralByVariableName) throws IOException {
    out = new GZIPOutputStream(out);
    Writer writer = new OutputStreamWriter(out, Util.DEFAULT_ENCODING);

    writer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    writer.append("<sizemaps>\n");

    for (int i = 0; i < sizeBreakdowns.length; i++) {
      writer.append("<sizemap fragment=\"" + i + "\" " + "size=\"" + sizeBreakdowns[i].getSize()
          + "\">\n");
      Map<JsName, Integer> sizeMap = new TreeMap<JsName, Integer>(JSNAME_SORT);
      sizeMap.putAll(sizeBreakdowns[i].getSizeMap());
      for (Entry<JsName, Integer> sizeMapEntry : sizeMap.entrySet()) {
        JsName name = sizeMapEntry.getKey();
        int size = sizeMapEntry.getValue();
        TypedProgramReference typedRef = typedProgramReference(name, jjsmap,
            internedLiteralByVariableName);
        writer.append("  <size " + "type=\"" + escapeXml(typedRef.type) + "\" " + "ref=\""
            + escapeXml(typedRef.description) + "\" " + "size=\"" + size + "\"/>\n");
      }
      writer.append("</sizemap>\n");
    }

    writer.append("</sizemaps>");
    writer.close();
  }

  private static TypedProgramReference typedProgramReference(JsName name,
      JavaToJavaScriptMap jjsmap, Map<JsName, JsLiteral> internedLiteralByVariableName) {
    JMethod method = jjsmap.nameToMethod(name);
    if (method != null) {
      return new TypedProgramReference("method", method.getJsniSignature(true, true));
    }

    JField field = jjsmap.nameToField(name);
    if ((field != null) && (field.getEnclosingType() != null)) {
      StringBuilder sb = new StringBuilder();
      sb.append(field.getEnclosingType().getName());
      sb.append("::");
      sb.append(field.getName());
      return new TypedProgramReference("field", sb.toString());
    }

    JClassType type = jjsmap.nameToType(name);
    if (type != null) {
      return new TypedProgramReference("type", type.getName());
    }

    JsLiteral literal = internedLiteralByVariableName.get(name);
    if (literal instanceof JsStringLiteral) {
      return new TypedProgramReference("string", ((JsStringLiteral) literal).getValue());
    }

    return new TypedProgramReference("var", name.getShortIdent());
  }
}
