/*
 * 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.InternalCompilerException;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JRunAsync;
import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer;
import com.google.gwt.dev.jjs.impl.codesplitter.MultipleDependencyGraphRecorder;
import com.google.gwt.util.tools.Utility;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.zip.GZIPOutputStream;

/**
 * The control-flow dependency recorder for Compile Report.
 */
public class DependencyRecorder implements MultipleDependencyGraphRecorder {
  /**
   * DependencyRecorder is not allowed to throw checked exceptions, because if
   * it did then {@link com.google.gwt.dev.jjs.impl.codesplitter.CodeSplitter} and
   * {@link ControlFlowAnalyzer} would throw exceptions all over the place.
   * Instead, this class throws NestedIOExceptions that wrap them.
   */
  public static class NestedIOException extends RuntimeException {
    public NestedIOException(IOException e) {
      super(e);
    }
  }

  private final StringBuilder builder = new StringBuilder();
  private final OutputStream finalOutput;
  private OutputStreamWriter writer;

  public DependencyRecorder(OutputStream out) {
    this.finalOutput = out;
  }

  @Override
  public void close() {
    printPost();
    flushOutput();
    try {
      writer.close();
    } catch (IOException e) {
      throw new NestedIOException(e);
    }
  }

  @Override
  public void endDependencyGraph() {
    builder.append("</table>");
    flushOutput();
  }

  /**
   * Used to record the dependencies of a specific method.
   */
  @Override
  public void methodIsLiveBecause(JMethod liveMethod, List<JMethod> dependencyChain) {
    printMethodDependency(dependencyChain);
  }

  @Override
  public void open() {
    try {
      this.writer = new OutputStreamWriter(new GZIPOutputStream(finalOutput), "UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new InternalCompilerException("UTF-8 is an unsupported encoding", e);
    } catch (IOException e) {
      throw new NestedIOException(e);
    }

    printPre();
  }

  @Override
  public void startDependencyGraph(String identifier, String extnds) {
    builder.append("<table name=\"");
    builder.append(identifier);
    builder.append("\"");
    if (extnds != null) {
      builder.append(" extends=\"");
      builder.append(extnds);
      builder.append("\"");
    }
    builder.append(">\n");
  }

  /**
   * Used to record dependencies of a program.
   */
  protected void recordDependenciesImpl(TreeLogger logger, JProgram jprogram) {

    logger = logger.branch(TreeLogger.DEBUG, "Creating dependencies file for the compile report");

    ControlFlowAnalyzer dependencyAnalyzer = new ControlFlowAnalyzer(jprogram);
    dependencyAnalyzer.setDependencyRecorder(this);

    try {
      printPre();
      for (JMethod method : jprogram.getEntryMethods()) {
        dependencyAnalyzer.traverseFrom(method);
        maybeFlushOutput();
      }
      for (JRunAsync runAsync : jprogram.getRunAsyncs()) {
        dependencyAnalyzer.traverseFromRunAsync(runAsync);
        maybeFlushOutput();
      }
      printPost();

      flushOutput();
      Utility.close(writer);

    } catch (Throwable e) {
      logger.log(TreeLogger.WARN, "Could not write dependency file; proceeding anyway.", e);
    }
  }

  private void flushOutput() {
    try {
      writer.write(builder.toString());
    } catch (IOException e) {
      throw new NestedIOException(e);
    }
    builder.setLength(0);
  }

  private void maybeFlushOutput() {
    if (builder.length() > 8 * 1024) {
      flushOutput();
    }
  }

  /**
   * Records one dependency chain to a file. More specifically, it records the
   * last link of the dependency chain. The full dependency chain can be
   * recovered by code that reads the entire dependencies file, because it can
   * do repeated lookups into the dependencies table to follow the chain.
   */
  private void printMethodDependency(List<JMethod> dependencyChain) {
    int size = dependencyChain.size();
    if (size < 2) {
      return;
    }

    printMethodDependencyBetween(dependencyChain.get(size - 1), dependencyChain.get(size - 2));
  }

  protected void printMethodDependencyBetween(JMethod curMethod, JMethod depMethod) {
    builder.append("<method name=\"");
    if (curMethod.getEnclosingType() != null) {
      builder.append(curMethod.getEnclosingType().getName());
      builder.append("::");
    }
    builder.append(curMethod.getName());
    builder.append("\">\n");

    builder.append("<called by=\"");
    if (depMethod.getEnclosingType() != null) {
      builder.append(depMethod.getEnclosingType().getName());
      builder.append("::");
    }
    builder.append(depMethod.getName());
    builder.append("\"/>\n");
    builder.append("</method>\n");
  }

  /**
   * Prints the closing lines necessary for the dependencies file.
   */
  private void printPost() {
    builder.append("</soyc-dependencies>\n");
  }

  /**
   * Prints the preamble necessary for the dependencies file.
   */
  private void printPre() {
    builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soyc-dependencies>\n");
  }
}
