// Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

package com.google.dart.compiler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * A default implementation of {@link DartArtifactProvider} specifying
 * generated files be placed in the same directory as source files.
 */
public class DefaultDartArtifactProvider extends DartArtifactProvider {

  private final File outputDirectory;

  public DefaultDartArtifactProvider() {
    this(new File("out"));
  }

  public DefaultDartArtifactProvider(File outputDirectory) {
    this.outputDirectory = outputDirectory;
  }

  @Override
  public Reader getArtifactReader(Source source, String part, String extension)
      throws IOException {
    if (SystemLibraryManager.isDartUri(source.getUri())) {
      DartSource bundledSource = getBundledArtifact(source, source, part, extension);
      if (bundledSource != null) {
        Reader reader = null;
        try {
          reader = bundledSource.getSourceReader();
        } catch (FileNotFoundException e) {
          /* thrown if file doesn't exist, which is fine */
        }
        if (reader != null) {
          return new BufferedReader(reader);
        }
      }
    }
    File file = getArtifactFile(source, part, extension);
    if (!file.exists()) {
      return null;
    }
    return new BufferedReader(new FileReader(file));
  }

  @Override
  public URI getArtifactUri(Source source, String part, String extension) {
    try {
      return new URI("file", getArtifactFile(source, part, extension).getPath(), null);
    } catch (URISyntaxException e) {
      throw new IllegalArgumentException(e);
    }
  }

  @Override
  public Writer getArtifactWriter(Source source, String part, String extension) throws IOException {
    return new BufferedWriter(new FileWriter(makeDirectories(getArtifactFile(source, part,
        extension))));
  }

  @Override
  public boolean isOutOfDate(Source source, Source base, String extension) {
    if (SystemLibraryManager.isDartUri(base.getUri())) {
      Source bundledSource = getBundledArtifact(source, base, "", extension);
      if (bundledSource != null) {
        // Note: Artifacts bundled with sources are always up to date
        return false;
      }
    }
    File artifactFile = getArtifactFile(base, "", extension);
    return artifactFile.lastModified() < source.getLastModified();
  }

  protected DartSource getBundledArtifact(Source source, Source base, String part, String extension) {
    LibrarySource library = libraryOf(base);
    URI relativeUri = library.getUri().resolve(".").normalize().relativize(base.getUri());
    DartSource bundledSource;
    if (!relativeUri.isAbsolute()) {
      bundledSource = library.getSourceFor(fullname(relativeUri.getPath(), part, extension));
    } else {
      bundledSource = null;
    }
    return bundledSource;
  }

  private LibrarySource libraryOf(Source base) throws AssertionError {
    if (base instanceof DartSource) {
      return ((DartSource) base).getLibrary();
    } else if (base instanceof LibrarySource){
      return (LibrarySource) base;
    } else {
      throw new AssertionError(base.getClass().getName());
    }
  }

  /**
   * Answer the artifact file associated with the specified source. Only one
   * artifact may be associated with the given extension.
   *
   * @param source the source file (not <code>null</code>)
   * @param part a component of the source file to get a reader for (may be empty).
   * @param extension the file extension for this artifact (not
   *          <code>null</code>, not empty)
   * @return the artifact file (not <code>null</code>)
   */
  protected File getArtifactFile(Source source, String part, String extension) {
    String name = source.getName();
    name = URI.create(name).normalize().toString();
    name = normalizeArtifactName(name);
    File file = new File(outputDirectory, fullname(name, part, extension));
    return file;
  }
  
  /*
   * Removes extraneous punctuation and file path syntax.
   */
  private String normalizeArtifactName(String name) {
    /**
     * For efficiency, String operations are replaced with a single pass over
     * the character array data.
     * 
     * Note: This is a refactor of a previous version which used repeated calls
     * to String.replace(), which turns out to be unnecessarily expensive. This
     * particular method has been identified as being called a large number of
     * times, thus the need for the micro-optimization here.
     * 
     * This is the original logic being implemented here:
     * 
     * <code>
     * name = name.replace("//", File.separator);
     * name = name.replace(":", "");
     * name = name.replace("!", "");
     * name = name.replace("..", "_");
     * </code>
     * 
     * Please update the above if the logic being implemented ever changes.
     * 
     * TODO(jbrosenberg): Figure out a better way such that this normalization
     * is no longer needed in the first place. Source objects could be built
     * from pre-normalized prefixes, etc. Or if it can be called less often,
     * then use pre-compiled Patterns and Matchers instead.
     */
    boolean lastCharWasSlash = false;
    boolean lastCharWasPeriod = false;
    boolean madeChanges = false;
    int nameLen = name.length();
    char[] newName = new char[nameLen];
    int idx = 0;
    for (char ch : name.toCharArray()) {
      if (lastCharWasPeriod && ch != '.') {
        // didn't get a second period, so append the one we did get
        newName[idx++] = '.';
        lastCharWasPeriod = false;
      } else if (lastCharWasSlash && ch != '/') {
        // didn't get a second slash, so append the one we did get
        newName[idx++] = '/';
        lastCharWasSlash = false;
      }

      switch (ch) {
        case ':':
        case '!':
            // replace ':'s and '!'s with empty string
            madeChanges = true;
            break;
        case '/':
            if (lastCharWasSlash) {
              // got a second slash, replace with File.separatorChar
              madeChanges = true;
              newName[idx++] = File.separatorChar;
              lastCharWasSlash = false;
            } else {
              lastCharWasSlash = true;
            }
            break;
        case '.':
            if (lastCharWasPeriod) {
              // got a second period, replace with a '_'
              madeChanges = true;
              newName[idx++] = ('_');
              lastCharWasPeriod = false;
            } else {
              lastCharWasPeriod = true;
            }
            break;
        default:
            newName[idx++] = ch;
            lastCharWasSlash = false;
            lastCharWasPeriod = false;
      }
    }
    if (lastCharWasPeriod) {
      // didn't get a final second period, so append the one we did get
      newName[idx++] = '.';
    } else if (lastCharWasSlash) {
      // didn't get a final second slash, so append the one we did get
      newName[idx++] = '/';
    }

    if (madeChanges) {
      name = new String(newName, 0, idx);
    }

    return name;
  }
  
  private File makeDirectories(File file) {
    file.getParentFile().mkdirs();
    return file;
  }

  private String fullname(String name, String part, String extension) {
    if (part.isEmpty()) {
      return name + "." + extension;
    } else {
      return name + "$" + part + "." + extension;
    }
  }
}
