/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.waveprotocol.pst;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FileDescriptor;

import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateGroup;
import org.waveprotocol.pst.model.Message;
import org.waveprotocol.pst.model.MessageProperties;
import org.waveprotocol.pst.style.Styler;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;

/**
 * PST stands for protobuf-stringtemplate.
 *
 * The tool allows arbitrary code generation given a series of string
 * templates, each passed the description of a protocol buffer file. This allows
 * protobuf- based file generation beyond what existing protocol compilers
 * (protoc, protostuff, etc) are capable of (without modification), using the
 * convenience and safety of <a href="http://stringtemplate.org">string
 * template</a>.
 *
 * A number of sample templates are bundles (in templates), so see these as
 * examples. These templates give a complete client/server JSON message stack:
 * <ul>
 * <li><em>message</em> is a common interface.</li>
 * <li><em>messageTestImpl</em> is a simple/pure Java in-memory implementation
 * of the interface, for testing.</li>
 * <li><em>messagePojoImpl</em> is a like messageTestImpl with JSON
 * serialization and deserialization using the Gson library.</li>
 * <li><em>messageServerImpl</em> is a protobuf-backed implementation, useful
 * for a multi-server environment where the efficient serialization of protocol
 * buffers is an advantage. JSON is also supported.</li>
 * <li><em>messageClientImpl</em> is an efficent javascript implementation for
 * use with GWT.</li>
 * </ul>
 *
 * There is no particular reason why PST can only be used to generate Java, for
 * example, a pure JS rather than GWT implementation of the client JSON message
 * component could be generated[1].
 *
 * PST is implemented using the protocol buffer reflection generated by protoc
 * alongside the actual Message classes it generates; these are converted into
 * simple Java model objects with simple accessors suitable for accessing from
 * stringtemplate.
 *
 * The code generated by stringtemplate is then post-processed using a simple
 * custom code formatter since the output from stringtemplate can be hard to
 * humanly read (e.g. the indentation is unpredictable).
 *
 * [1] although, currently it is hardcoded in PST to generate .java files, and
 * the model has Java-centric methods.  The code formatter also assumes that
 * it is run over a Java file.  These all could be easily modified, however.
 *
 * @author kalman@google.com (Benjamin Kalman)
 */
public final class Pst {

  private final File outputDir;
  private final FileDescriptor fd;
  private final Styler styler;
  private final Iterable<File> templates;
  private final boolean saveBackups;
  private final boolean useInt52;

  /**
   * @param outputDir the base directory to write the generated files
   * @param fd the {@link FileDescriptor} of the protobuf to use (i.e. pass to
   *        each string template)
   * @param styler the code styler to post-process generated code with
   * @param templates the collection of string templates to use
   * @param saveBackups whether to save intermediate generated files
   * @param useInt52 whether we use doubles to serialize 64-bit integers
   */
  public Pst(File outputDir, FileDescriptor fd, Styler styler, Iterable<File> templates,
      boolean saveBackups, boolean useInt52) {
    this.outputDir = checkNotNull(outputDir, "outputDir cannot be null");
    this.fd = checkNotNull(fd, "fd cannot be null");
    this.styler = checkNotNull(styler, "styler cannot be null");
    this.templates = checkNotNull(templates, "templates cannot be null");
    this.saveBackups = saveBackups;
    this.useInt52 = useInt52;
  }

  /**
   * Runs the code generation for all templates.
   */
  public void run() throws PstException {
    List<PstException.TemplateException> exceptions = Lists.newArrayList();
    for (File template : templates) {
      try {
        MessageProperties properties = createProperties(template);
        String groupName = stripSuffix(".st", template.getName());
        String templateName = properties.hasTemplateName() ?
            properties.getTemplateName() : "";
        StringTemplateGroup group = new StringTemplateGroup(groupName + "Group", dir(template));
        StringTemplate st = group.getInstanceOf(groupName);
        for (Descriptor messageDescriptor : fd.getMessageTypes()) {
          Message message = new Message(messageDescriptor, templateName, properties);
          st.reset();
          st.setAttribute("m", message);
          write(st, new File(
              outputDir.getPath() + File.separator +
              message.getFullJavaType().replace('.', File.separatorChar) + "." +
              (properties.hasFileExtension() ? properties.getFileExtension() : "java")));
        }
      } catch (Exception e) {
        exceptions.add(new PstException.TemplateException(template.getPath(), e));
      }
    }
    if (!exceptions.isEmpty()) {
      throw new PstException(exceptions);
    }
  }

  /**
   * @return the path to the directory which contains a file, or just the path
   *         to the file itself if it's already a directory
   */
  private String dir(File f) {
    return f.isDirectory()
        ? f.getPath()
        : (Strings.isNullOrEmpty(f.getParent()) ? "." : f.getParent());
  }

  private String stripSuffix(String suffix, String s) {
    return s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s;
  }

  private void write(StringTemplate st, File output) throws IOException {
    output.getParentFile().mkdirs();
    BufferedWriter writer = new BufferedWriter(new FileWriter(output));
    try {
      writer.write(st.toString());
    } finally {
      try {
        writer.close();
      } catch (IOException e) {
        // If another exception is already propagating, we don't
        // want to throw a secondary one.
        // This means that exceptions on close() are ignored,
        // but what could usefully be done for a close()
        // exception anyway?
      }
    }
    styler.style(output, saveBackups);
  }

  private MessageProperties createProperties(File template)
      throws FileNotFoundException, IOException {
    File propertiesFile =
        new File(template.getParentFile().getPath() + File.separator + "properties");
    MessageProperties properties = propertiesFile.exists()
        ? MessageProperties.createFromFile(propertiesFile) : MessageProperties.createEmpty();
    properties.setUseInt52(useInt52);
    return properties;
  }

}
