/*
 * Copyright 2010 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.precompress.linker;

import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.ConfigurationProperty;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.core.ext.linker.SyntheticArtifact;

import junit.framework.TestCase;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * Tests {@link PrecompressLinker}.
 */
public class PrecompressLinkerTest extends TestCase {
  private static class MockConfigurationProperty implements
      ConfigurationProperty, Comparable<MockConfigurationProperty> {
    private boolean hasMultipleValues;
    private String name;
    private List<String> values = new ArrayList<String>();

    public MockConfigurationProperty(String name, boolean hasMultipleValues) {
      this.name = name;
      this.hasMultipleValues = hasMultipleValues;
    }

    @Override
    public int compareTo(MockConfigurationProperty o) {
      return getName().compareTo(o.getName());
    }

    @Override
    public String getName() {
      return name;
    }

    @Override
    public List<String> getValues() {
      return values;
    }

    @Override
    public boolean hasMultipleValues() {
      return hasMultipleValues;
    }

    public void setValue(String value) {
      values.clear();
      values.add(value);
    }
  }

  private class MockLinkerContext implements LinkerContext {
    @Override
    public SortedSet<ConfigurationProperty> getConfigurationProperties() {
      return new TreeSet<ConfigurationProperty>(Arrays.asList(
          propLeaveOriginals, propPathRegexes));
    }

    @Override
    public String getModuleFunctionName() {
      return "MockModule";
    }

    @Override
    public long getModuleLastModified() {
      return 0;
    }

    @Override
    public String getModuleName() {
      return "MockModule";
    }

    @Override
    public SortedSet<SelectionProperty> getProperties() {
      return new TreeSet<SelectionProperty>();
    }

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

    @Override
    public String optimizeJavaScript(TreeLogger logger, String jsProgram) {
      return jsProgram;
    }
  }

  private static void assertEqualBytes(byte[] expected, byte[] actual) {
    assertEquals(expected.length, actual.length);
    for (int i = 0; i < expected.length; i++) {
      assertEquals(expected[i], actual[i]);
    }
  }

  private static byte[] compress(byte[] content) {
    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      GZIPOutputStream gzip = new GZIPOutputStream(baos);
      InputStream in = new ByteArrayInputStream(content);

      byte[] buf = new byte[10000];
      int n;
      while ((n = in.read(buf)) > 0) {
        gzip.write(buf, 0, n);
      }
      gzip.close();

      return baos.toByteArray();
    } catch (IOException e) {
      throw new RuntimeException(
          "Unexpected IO exception from memory operations");
    }
  }

  private static byte[] contents(EmittedArtifact art)
      throws UnableToCompleteException, IOException {
    InputStream input = art.getContents(TreeLogger.NULL);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    byte[] buf = new byte[10000];
    int n;
    while ((n = input.read(buf)) > 0) {
      baos.write(buf, 0, n);
    }

    return baos.toByteArray();
  }

  private static byte[] decompress(byte[] compressed) throws IOException {
    GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(
        compressed));
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buf = new byte[10000];
    int n;
    while ((n = gzip.read(buf)) > 0) {
      baos.write(buf, 0, n);
    }

    return baos.toByteArray();
  }

  private static SyntheticArtifact emit(String path, byte[] content) {
    return new SyntheticArtifact(PrecompressLinker.class, path, content);
  }

  private static SyntheticArtifact emit(String path, String contents) {
    try {
      return emit(path, contents.getBytes("UTF-8"));
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e.getMessage());
    }
  }

  private static SyntheticArtifact emitPrivate(String string, String contents) {
    SyntheticArtifact art = emit(string, contents);
    art.setVisibility(Visibility.Private);
    return art;
  }

  private static EmittedArtifact findArtifact(ArtifactSet artifacts, String path) {
    for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) {
      if (art.getPartialPath().equals(path)) {
        return art;
      }
    }

    return null;
  }

  /**
   * Return a highly compressible string.
   */
  private static String fooFileContents() {
    StringBuffer buf = new StringBuffer();
    for (int i = 0; i < 1000; i++) {
      buf.append("another identical line\n");
    }
    return buf.toString();
  }

  private static byte[] uncompressibleContent() {
    try {
      byte[] content = fooFileContents().getBytes("UTF-8");
      while (true) {
        byte[] updated = compress(content);
        if (updated.length >= content.length) {
          return content;
        }
        content = updated;
      }
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e.getMessage());
    }
  }

  private ArtifactSet artifacts;
  private LinkerContext context = new MockLinkerContext();

  private MockConfigurationProperty propLeaveOriginals;

  private MockConfigurationProperty propPathRegexes;

  /**
   * Test that foo.js gets compressed to foo.js.gz, and bar.js is left alone.
   */
  public void testBasics() throws UnableToCompleteException, IOException {
    ArtifactSet updated = linkArtifacts();

    EmittedArtifact foo = findArtifact(updated, "foo.js");
    assertNotNull(foo);

    EmittedArtifact fooGz = findArtifact(updated, "foo.js.gz");
    assertNotNull(fooGz);
    assertEqualBytes(contents(foo), decompress(contents(fooGz)));

    EmittedArtifact barGz = findArtifact(updated, "bar.js.gz");
    assertNull("bar.js is private and should not have been compressed", barGz);

    EmittedArtifact uncompressibleGz = findArtifact(updated,
        "uncompressible.js.gz");
    assertNull(
        "uncompressible.js is not compressible and should have been left alone",
        uncompressibleGz);
  }

  /**
   * Test that the blacklist takes effect.
   */
  public void testBlackList() throws UnableToCompleteException {
    propPathRegexes.values.add("-foo\\.js");
    ArtifactSet updated = linkArtifacts();

    // foo.txt is not in the list of patterns, so don't compress
    EmittedArtifact stuffGz = findArtifact(updated, "stuff.txt.gz");
    assertNull("stuff.txt should not have been compressed", stuffGz);

    // foo.js matches two regexes; the last should win
    EmittedArtifact fooGz = findArtifact(updated, "foo.js.gz");
    assertNull("foo.js should not have been compressed", fooGz);
  }

  /**
   * Tests that if precompress.leave.original if false, the originals are
   * removed.
   */
  public void testRemovingOriginals() throws UnableToCompleteException {
    propLeaveOriginals.setValue("false");
    ArtifactSet updated = linkArtifacts();
    EmittedArtifact foo = findArtifact(updated, "foo.js");
    assertNull("foo.js should have been removed", foo);
  }

  @Override
  protected void setUp() {
    // add some artifacts to test with
    artifacts = new ArtifactSet();
    artifacts.add(emit("foo.js", fooFileContents()));
    artifacts.add(emitPrivate("bar.js", fooFileContents()));
    artifacts.add(emit("uncompressible.js", uncompressibleContent()));
    artifacts.add(emit("stuff.txt", fooFileContents()));
    artifacts.add(emit("data.xml", fooFileContents()));
    artifacts.freeze();

    propLeaveOriginals = new MockConfigurationProperty(
        "precompress.leave.originals", false);
    propLeaveOriginals.setValue("true");

    propPathRegexes = new MockConfigurationProperty("precompress.path.regexes",
        true);
    propPathRegexes.values.add(".*\\.html");
    propPathRegexes.values.add(".*\\.js");
    propPathRegexes.values.add(".*\\.css");
  }

  private ArtifactSet linkArtifacts() throws UnableToCompleteException {
    return new PrecompressLinker().link(TreeLogger.NULL, context, artifacts,
        true);
  }
}
