/*
 * Copyright 2011 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.web.bindery.requestfactory.apt;

import com.google.gwt.dev.util.Name.BinaryName;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import javax.lang.model.SourceVersion;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

/**
 * Provides "late" validation services when server types aren't available to the
 * shared-interface compilation process. This tool is provided the name of an
 * output jar and the binary names of RequestFactory interfaces that should be
 * validated. The validation process will provide pre-computed type map builders
 * for use by the ServiceLayer.
 * 
 * @see http://code.google.com/p/google-web-toolkit/wiki/
 *      RequestFactoryInterfaceValidation
 */
public class ValidationTool {
  /**
   * A JavaFileManager that writes the class outputs into a jar file or a
   * directory.
   */
  static class JarOrDirectoryOutputFileManager extends ForwardingJavaFileManager<JavaFileManager> {
    private final List<MemoryJavaFileObject> toOutput = new ArrayList<MemoryJavaFileObject>();
    private final File output;

    JarOrDirectoryOutputFileManager(File output, JavaFileManager fileManager) {
      super(fileManager);
      this.output = output;
    }

    @Override
    public void close() throws IOException {
      if (output.isDirectory()) {
        writeToDirectory();
      } else {
        writeToJar();
      }
    }

    /**
     * Not expected to be called. Overridden to prevent accidental writes to
     * disk.
     */
    @Override
    public FileObject getFileForOutput(Location location, String packageName, String relativeName,
        FileObject sibling) throws IOException {
      throw new UnsupportedOperationException("Not expecting to write " + packageName + "/"
          + relativeName);
    }

    /**
     * This method will receive generated source and class files.
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
        FileObject sibling) throws IOException {
      String path = BinaryName.toInternalName(className);
      String suffix;
      switch (kind) {
        case CLASS:
          suffix = ".class";
          break;
        case SOURCE:
          suffix = ".java";
          break;
        default:
          throw new UnsupportedOperationException("Unexpected kind " + kind);
      }
      MemoryJavaFileObject toReturn =
          new MemoryJavaFileObject(uri("memory:///" + path + suffix), kind);
      if (StandardLocation.CLASS_OUTPUT.equals(location) && Kind.CLASS.equals(kind)) {
        toOutput.add(toReturn);
      }
      return toReturn;
    }

    @Override
    public boolean isSameFile(FileObject a, FileObject b) {
      if (a instanceof MemoryJavaFileObject && b instanceof MemoryJavaFileObject) {
        MemoryJavaFileObject memoryA = (MemoryJavaFileObject) a;
        MemoryJavaFileObject memoryB = (MemoryJavaFileObject) b;
        return memoryA.getKind().equals(memoryB.getKind())
            && memoryA.toUri().equals(memoryB.toUri());
      }
      if (a instanceof FakeJavaFileObject && b instanceof FakeJavaFileObject) {
        // Only one file ever created
        return true;
      }
      return super.isSameFile(a, b);
    }

    private void writeToDirectory() throws IOException {
      for (MemoryJavaFileObject file : toOutput) {
        String path = file.toUri().getPath();
        if (path.equals("/fake/Fake.class")) {
          // ignore dummy class
          continue;
        }
        File target = new File(output, path);
        target.getParentFile().mkdirs();
        FileOutputStream out = new FileOutputStream(target);
        out.write(file.bytes.toByteArray());
        out.close();
      }
    }

    private void writeToJar() throws IOException, FileNotFoundException {
      JarOutputStream jar = new JarOutputStream(new FileOutputStream(output));
      for (MemoryJavaFileObject file : toOutput) {
        String path = file.toUri().getPath();
        if (path.equals("/fake/Fake.class")) {
          // ignore dummy class
          continue;
        }
        // Strip leading /
        ZipEntry entry = new ZipEntry(path.substring(1));
        jar.putNextEntry(entry);
        jar.write(file.bytes.toByteArray());
      }
      jar.close();
    }
  }

  /**
   * Provides a fake type to seed the compilation process with.
   */
  private static class FakeJavaFileObject extends SimpleJavaFileObject {
    public FakeJavaFileObject() {
      super(uri("fake:///fake/Fake.java"), JavaFileObject.Kind.SOURCE);
    }

    @Override
    public CharSequence getCharContent(boolean arg0) throws IOException {
      return "package fake; interface Fake {}";
    }
  }

  /**
   * An in-memory implementation of JavaFileObject.
   */
  private static class MemoryJavaFileObject extends SimpleJavaFileObject {
    private ByteArrayOutputStream bytes;
    private StringWriter charContents;

    public MemoryJavaFileObject(URI uri, JavaFileObject.Kind kind) {
      super(uri, kind);
    }

    @Override
    public CharSequence getCharContent(boolean ignored) throws IOException {
      return charContents.toString();
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
      bytes = new ByteArrayOutputStream();
      return bytes;
    }

    @Override
    public Writer openWriter() throws IOException {
      charContents = new StringWriter();
      return charContents;
    }
  }

  public static void main(String[] args) throws IOException {
    System.exit(exec(args) ? 0 : -1);
  }

  /**
   * A testable "main" method.
   */
  public static boolean exec(String[] args) throws IOException {
    return exec(args, ToolProvider.getSystemJavaCompiler());
  }

  public static boolean exec(String[] args, JavaCompiler compiler) throws IOException {
    return exec(args, compiler, null);
  }

  public static boolean exec(String[] args, JavaCompiler compiler, Iterable<String> javacOpts)
      throws IOException {
    if (args.length < 2) {
      System.err.println("java -cp requestfactory-client.jar:your_server-code.jar "
          + ValidationTool.class.getCanonicalName()
          + " (/some/directory | output.jar) com.example.shared.MyRequestFactory");
      System.err.println("See "
          + "http://code.google.com/p/google-web-toolkit/wiki/RequestFactoryInterfaceValidation "
          + "for more information.");
      return false;
    }
    if (compiler == null) {
      System.err.println("This tool must be run with a JDK, not a JRE");
      return false;
    }
    if (!compiler.getSourceVersions().contains(SourceVersion.RELEASE_6)) {
      System.err.println("This tool must be run with a Java 1.6 compiler");
      return false;
    }

    boolean clientOnly = false;
    List<String> argList = new ArrayList<String>(Arrays.asList(args));
    if (argList.get(0).equals("-client")) {
      clientOnly = true;
      argList.remove(0);
    }

    // Control how the compile process writes data to disk
    JavaFileManager fileManager =
        new JarOrDirectoryOutputFileManager(new File(argList.remove(0)), compiler
            .getStandardFileManager(null, null, null));

    // Create a validator and require it to process the specified types
    RfValidator processor = new RfValidator();
    if (clientOnly) {
      processor.setClientOnly(true);
    } else {
      processor.setMustResolveAllMappings(true);
    }
    processor.setRootOverride(argList);

    // Create the compilation task
    CompilationTask task =
        compiler.getTask(null, fileManager, null, javacOpts, null, Arrays
            .asList(new FakeJavaFileObject()));
    task.setProcessors(Arrays.asList(processor));
    if (!task.call()) {
      return false;
    }
    // Save data only on successful compilation
    fileManager.close();
    return true;
  }

  /**
   * Convenience method for discarding {@link URISyntaxException}.
   */
  private static URI uri(String contents) {
    try {
      return new URI(contents);
    } catch (URISyntaxException e) {
      throw new RuntimeException(e);
    }
  }
}
