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

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.Compiler;
import com.google.gwt.dev.CompilerOptionsImpl;
import com.google.gwt.dev.util.arg.OptionOptimize;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.collect.HashMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import com.google.gwt.thirdparty.guava.common.io.MoreFiles;
import com.google.gwt.thirdparty.guava.common.io.RecursiveDeleteOption;

import junit.framework.TestCase;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;

/**
 * Basic tests for Source maps and (new) soyc reports.
 */
public class SymbolMapTest extends TestCase {

  private static File[] filterByName(File dir, final String namePattern) {
    return dir.listFiles(new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        return pathname.getName().matches(namePattern);
      }
    });
  }

  /**
   * This class represents each row in a generated SymbolMap file.  Because not all fields are
   * serialized, such as CastableTypeMap, some methods are not implemented.
   *
   */
  static final class SimpleSymbolData implements SymbolData {

    static Map<String, SimpleSymbolData> readSymbolMap(File filePath) throws IOException {
      Map<String, SimpleSymbolData> sdata = Maps.newLinkedHashMap();

      BufferedReader reader = new BufferedReader(new FileReader(filePath));
      String line;
      while ((line = reader.readLine()) != null) {
        if (line.startsWith("#")) {
          // reading a comment
          continue;
        }

        SimpleSymbolData symbolData = new SimpleSymbolData(line);
        String key = symbolData.getJsniIdent();
        assertFalse("Duplicate signature <" + key + "> in symbol maps", sdata.containsKey(key));
        sdata.put(key,symbolData);
      }

      return sdata;
    }

    private static final String NOT_IMPLEMENTED_MESSAGE =
        "Data not available in current serialized SymbolMap";

    private String jsName;
    private String jsniIdent;
    private String className;
    private String memberName;
    private String sourceUri;
    private int sourceLine;
    private int fragmentNumber;

    public SimpleSymbolData(String line) {
      this.parseFromLine(line);
    }

    @Override
    public String getClassName() {
      return this.className;
    }

    @Override
    public int getFragmentNumber() {
      return this.fragmentNumber;
    }

    @Override
    public String getJsniIdent() {
      return this.jsniIdent;
    }

    @Override
    public String getMemberName() {
      return this.memberName;
    }

    @Override
    public String getRuntimeTypeId() {
      throw new UnsupportedOperationException(NOT_IMPLEMENTED_MESSAGE);
    }

    @Override
    public int getSourceLine() {
      return this.sourceLine;
    }

    @Override
    public String getSourceUri() {
      return this.sourceUri;
    }

    @Override
    public String getSymbolName() {
      return this.jsName;
    }

    @Override
    public boolean isClass() {
      return this.memberName == null || this.memberName.isEmpty();
    }

    @Override
    public boolean isField() {
      return !this.isClass() && jsniIdent.indexOf("(") < 0;
    }

    @Override
    public boolean isMethod() {
      return !this.isClass() && jsniIdent.indexOf("(") >= 0;
    }

    @Override
    public String toString() {
      return jsniIdent + " -> " + jsName;
    }

    private void parseFromLine(String line) {
      String[] fields = line.split(",");

      this.jsName = fields[0];
      this.jsniIdent = fields[1].isEmpty() ? fields[2] : fields[1];
      this.className = fields[2];
      this.memberName = fields[3]; // may be empty
      this.sourceUri = fields[4];
      this.sourceLine = Integer.parseInt(fields[5]);
      this.fragmentNumber = Integer.parseInt(fields[6]);
    }
  }

  /**
   * Loads the symbol map from a file.
   */
  private Iterable<Map<String, SimpleSymbolData>> loadSymbolMaps(File root) throws Exception {
    // Testing SourceMaps as SymbolMap replacement
    // make sure the files have been produced
    assertTrue(root.exists());

    File[] symbolMapFiles = filterByName(root, "(.*)\\.symbolMap");

    assertTrue(symbolMapFiles.length >= 1);

    return Iterables.transform(Arrays.asList(symbolMapFiles),
        new Function<File, Map<String, SimpleSymbolData>>() {
          @Override
          public Map<String, SimpleSymbolData> apply(File file) {
            try {
              return SimpleSymbolData.readSymbolMap(file);
            } catch (IOException e) {
              fail("Error reading symbol map " + file.getAbsolutePath());
            }
            return null;
          }
        });
  }

  private static final String JSE_METHOD =
      "com.google.gwt.core.client.JavaScriptException::getThrown()Ljava/lang/Object;";
  private static final String JSE_FIELD = "com.google.gwt.core.client.JavaScriptException::message";
  private static final String JSE_CLASS = "com.google.gwt.core.client.JavaScriptException";
  private static final String UNINSTANTIABLE_CLASS = "com.google.gwt.lang.Array";

  /**
   * Tests for the presence of some elements.
   */
  private void assertSymbolMapSanity(int optimizeLevel) throws Exception {
    String benchmark = "hello";
    String module = "com.google.gwt.sample.hello.Hello";

    File work = Files.createTempDirectory(benchmark + "work").toFile();
    try {
      CompilerOptionsImpl options = new CompilerOptionsImpl();
      options.addModuleName(module);
      options.setWarDir(new File(work, "war"));
      options.setExtraDir(new File(work, "extra"));
      options.setOptimizationLevel(optimizeLevel);
      PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
      logger.setMaxDetail(TreeLogger.ERROR);
      Compiler.compile(logger, options);
      // Change parentDir for cached/pre-built reports
      String parentDir = options.getExtraDir() + "/" + benchmark;
      for (Map<String, SimpleSymbolData> symbolDataByJsniIdentifier :
          loadSymbolMaps(new File(parentDir + "/symbolMaps/"))) {
        assertTrue(!symbolDataByJsniIdentifier.isEmpty());
        assertNotNull(symbolDataByJsniIdentifier.get(JSE_METHOD));
        assertTrue(symbolDataByJsniIdentifier.get(JSE_METHOD).isMethod());
        assertFalse(symbolDataByJsniIdentifier.get(JSE_METHOD).isField());
        assertFalse(symbolDataByJsniIdentifier.get(JSE_METHOD).isClass());
        assertNotNull(symbolDataByJsniIdentifier.get(JSE_FIELD));
        assertTrue(symbolDataByJsniIdentifier.get(JSE_FIELD).isField());
        assertFalse(symbolDataByJsniIdentifier.get(JSE_FIELD).isMethod());
        assertFalse(symbolDataByJsniIdentifier.get(JSE_FIELD).isClass());
        assertNotNull(symbolDataByJsniIdentifier.get(JSE_CLASS));
        assertTrue(symbolDataByJsniIdentifier.get(JSE_CLASS).isClass());
        assertFalse(symbolDataByJsniIdentifier.get(JSE_CLASS).isField());
        assertFalse(symbolDataByJsniIdentifier.get(JSE_CLASS).isMethod());
        if (optimizeLevel == OptionOptimize.OPTIMIZE_LEVEL_DRAFT) {
          assertNotNull(symbolDataByJsniIdentifier.get(UNINSTANTIABLE_CLASS));
        } else {
          assertNull(symbolDataByJsniIdentifier.get(UNINSTANTIABLE_CLASS));
        }
        assertSymbolUniquenessForMethods(symbolDataByJsniIdentifier);
      }
    } finally {
      MoreFiles.deleteRecursively(work.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
    }
  }

  private void assertSymbolUniquenessForMethods(
      Map<String, SimpleSymbolData> symbolDataByJsniIdentifier) {
    Multimap<String, SymbolData> methodSymbolDataBySymbol = HashMultimap.create();
    for (SymbolData symbolData : symbolDataByJsniIdentifier.values()) {
      if (symbolData.isMethod()) {
        methodSymbolDataBySymbol.put(symbolData.getSymbolName(), symbolData);
      }
    }
    Iterator<String> iterator = methodSymbolDataBySymbol.keySet().iterator();
    while (iterator.hasNext()) {
      String key = iterator.next();
      if (methodSymbolDataBySymbol.get(key).size() <= 1) {
        iterator.remove();
      }
    }
    assertTrue("The following method symbols where not unique " + methodSymbolDataBySymbol,
        methodSymbolDataBySymbol.isEmpty());
  }

  public void testSymbolMapSanityDraft() throws Exception {
    assertSymbolMapSanity(OptionOptimize.OPTIMIZE_LEVEL_DRAFT);
  }

  public void testSymbolMapSanityOptimized() throws Exception {
    assertSymbolMapSanity(OptionOptimize.OPTIMIZE_LEVEL_MAX);
  }
}
