// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.util.io;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

/**
 * An implementation of {@link OutErr} that captures all out/err output into
 * a file for stdout and a file for stderr. The files are only created if any
 * output is made.
 * The OutErr assumes that the directory that will contain the output file
 * must exist.
 *
 * You should not use this object from multiple different threads.
 */
// Note that it should be safe to treat the Output and Error streams within a FileOutErr each as
// individually ThreadCompatible.
@ThreadSafety.ThreadCompatible
public class FileOutErr extends OutErr {

  /**
   * Create a new FileOutErr that will write its input,
   * if any, to the files specified by stdout/stderr.
   *
   * No other process may write to the files,
   *
   * @param stdout The file for the stdout of this outErr
   * @param stderr The file for the stderr of this outErr
   */
  public FileOutErr(Path stdout, Path stderr) {
    this(new FileRecordingOutputStream(stdout), new FileRecordingOutputStream(stderr));
  }

  /**
   * Creates a new FileOutErr that writes its input to the file specified by output. Both
   * stdout/stderr will be copied into the single file.
   *
   * @param output The file for the both stdout and stderr of this outErr.
   */
  public FileOutErr(Path output) {
    // We don't need to create a synchronized funnel here, like in the OutErr -- The
    // respective functions in the FileRecordingOutputStream take care of locking.
    this(new FileRecordingOutputStream(output));
  }

  protected FileOutErr(AbstractFileRecordingOutputStream out,
                       AbstractFileRecordingOutputStream err) {
    super(out, err);
  }

  /**
   * Creates a new FileOutErr that discards its input. Useful
   * for testing purposes.
   */
  @VisibleForTesting
  public FileOutErr() {
    this(new NullFileRecordingOutputStream());
  }

  private FileOutErr(OutputStream stream) {
    // We need this function to duplicate the single new object into both arguments
    // of the super-constructor.
    super(stream, stream);
  }

  // Set a filter for FileOutputStream
  public void setOutputFilter(OutputFilter outputFilter) {
    getFileOutputStream().setFilter(outputFilter);
  }

  // Set a filter for FileErrorStream
  public void setErrorFilter(OutputFilter outputFilter) {
    getFileOutputStream().setFilter(outputFilter);
  }

  /**
   * Returns true if any output was recorded.
   */
  public boolean hasRecordedOutput() {
    return getFileOutputStream().hasRecordedOutput() || getFileErrorStream().hasRecordedOutput();
  }

  /**
   * Returns true if output was recorded on stdout.
   */
  public boolean hasRecordedStdout() {
    return getFileOutputStream().hasRecordedOutput();
  }

  /**
   * Returns true if output was recorded on stderr.
   */
  public boolean hasRecordedStderr() {
    return getFileErrorStream().hasRecordedOutput();
  }

  /**
   * Returns the {@link Path} this OutErr uses to buffer stdout
   *
   * <p>The user must ensure that no other process is writing to the files at time of creation.
   *
   * @return the path object with the contents of stdout
   */
  public Path getOutputPath() {
    return getFileOutputStream().getFile();
  }

  /**
   * Returns the {@link Path} this OutErr uses to buffer stderr.
   *
   * @return the path object with the contents of stderr
   */
  public Path getErrorPath() {
    return getFileErrorStream().getFile();
  }

  /** Interprets the captured out content as an {@code ISO-8859-1} encoded string. */
  public String outAsLatin1() {
    return getFileOutputStream().getRecordedOutput();
  }

  /**
   * Interprets the captured err content as an {@code ISO-8859-1} encoded
   * string.
   */
  public String errAsLatin1() {
    return getFileErrorStream().getRecordedOutput();
  }

  /**
   * Closes and deletes the error stream.
   */
  public void clearErr() throws IOException {
    getFileErrorStream().clear();
  }

  /**
   * Closes and deletes the out stream.
   */
  public void clearOut() throws IOException {
    getFileOutputStream().clear();
  }


  /**
   * Writes the captured out content to the given output stream,
   * avoiding keeping the entire contents in memory.
   */
  public void dumpOutAsLatin1(OutputStream out) {
    getFileOutputStream().dumpOut(out);
  }

  /**
   * Writes the captured out content to the given output stream,
   * avoiding keeping the entire contents in memory.
   */
  public void dumpErrAsLatin1(OutputStream out) {
    getFileErrorStream().dumpOut(out);
  }

  private AbstractFileRecordingOutputStream getFileOutputStream() {
    return (AbstractFileRecordingOutputStream) getOutputStream();
  }

  private AbstractFileRecordingOutputStream getFileErrorStream() {
    return (AbstractFileRecordingOutputStream) getErrorStream();
  }

  /**
   * An abstract supertype for the two other inner classes in this type
   * to implement streams that can write to a file.
   */
  private abstract static class AbstractFileRecordingOutputStream extends OutputStream {

    /**
     * Returns true if this FileRecordingOutputStream has encountered an error.
     *
     * @return true there was an error, false otherwise.
     */
    abstract boolean hadError();

    /**
     * Returns the file this FileRecordingOutputStream is writing to.
     */
    abstract Path getFile();

    /**
     * Returns true if the FileOutErr has stored output.
     */
    abstract boolean hasRecordedOutput();

    /**
     * Returns the output this AbstractFileOutErr has recorded.
     */
    abstract String getRecordedOutput();

    /**
     * Writes the output to the given output stream,
     * avoiding keeping the entire contents in memory.
     */
    abstract void dumpOut(OutputStream out);

    /** Closes and deletes the output. */
    abstract void clear() throws IOException;

    /**
     * Set a Filter for the output
     *
     * @param outputFilter
     */
    abstract void setFilter(OutputFilter outputFilter);
  }

  /**
   * An output stream that pretends to capture all its output into a file,
   * but instead discards it.
   */
  private static class NullFileRecordingOutputStream extends AbstractFileRecordingOutputStream {

    NullFileRecordingOutputStream() {
    }

    @Override
    boolean hadError() {
      return false;
    }

    @Override
    Path getFile() {
      return null;
    }

    @Override
    boolean hasRecordedOutput() {
      return false;
    }

    @Override
    String getRecordedOutput() {
      return "";
    }

    @Override
    void dumpOut(OutputStream out) {
      return;
    }

    @Override
    public void clear() {
    }

    @Override
    void setFilter(OutputFilter outputFilter) {}


    @Override
    public void write(byte[] b, int off, int len) {
    }

    @Override
    public void write(int b) {
    }

    @Override
    public void write(byte[] b) {
    }
  }

  /** An interface to get a filtered output stream from the original one. */
  public interface OutputFilter {
    FilterOutputStream getFilteredOutputStream(OutputStream outputStream);
  }

  /**
   * An output stream that captures all output into a file.
   * The file is created only if output is received.
   *
   * The user must take care that nobody else is writing to the
   * file that is backing the output stream.
   *
   * The write() methods of type are synchronized to ensure
   * that writes from different threads are not mixed up.
   *
   * The outputStream is here only for the benefit of the pumping
   * IO we're currently using for execution - Once that is gone,
   * we can remove this output stream and fold its code into the
   * FileOutErr.
   */
  @ThreadSafety.ThreadCompatible
  protected static class FileRecordingOutputStream extends AbstractFileRecordingOutputStream {

    private final Path outputFile;
    private OutputStream outputStream;
    private String error;
    private OutputFilter outputFilter;

    protected FileRecordingOutputStream(Path outputFile) {
      this.outputFile = outputFile;
    }

    @Override
    boolean hadError() {
      return error != null;
    }

    @Override
    Path getFile() {
      return outputFile;
    }

    private OutputStream getOutputStream() throws IOException {
      // you should hold the lock before you invoke this method
      if (outputStream == null) {
        outputStream = outputFile.getOutputStream();
        if (outputFilter != null) {
          outputStream = outputFilter.getFilteredOutputStream(outputStream);
        }
      }
      return outputStream;
    }

    private boolean hasOutputStream() {
      return outputStream != null;
    }

    @Override
    public synchronized void clear() throws IOException {
      close();
      outputStream = null;
      outputFile.delete();
    }

    @Override
    void setFilter(OutputFilter outputFilter) {
      this.outputFilter = outputFilter;
    }

    /**
     * Called whenever the FileRecordingOutputStream finds an error.
     */
    protected void recordError(IOException exception) {
      String newErrorText = exception.getMessage();
      error = (error == null) ? newErrorText : error + "\n" + newErrorText;
    }

    @Override
    boolean hasRecordedOutput() {
      if (hadError()) {
        return true;
      }
      if (!outputFile.exists()) {
        return false;
      }
      try {
        return outputFile.getFileSize() > 0;
      } catch (IOException ex) {
        recordError(ex);
        return true;
      }
    }

    @Override
    String getRecordedOutput() {
      StringBuilder result = new StringBuilder();
      try {
        if (getFile().exists()) {
          result.append(FileSystemUtils.readContentAsLatin1(getFile()));
        }
      } catch (IOException ex) {
        recordError(ex);
      }

      if (hadError()) {
        result.append(error);
      }
      return result.toString();
    }

    @Override
    void dumpOut(OutputStream out) {
      try {
        if (getFile().exists()) {
          try (InputStream in = getFile().getInputStream()) {
            ByteStreams.copy(in, out);
          }
        }
      } catch (IOException ex) {
        recordError(ex);
      }

      if (hadError()) {
        PrintStream ps = new PrintStream(out);
        ps.print(error);
        ps.flush();
      }
    }

    @Override
    public synchronized void write(byte[] b, int off, int len) {
      if (len > 0) {
        try {
          getOutputStream().write(b, off, len);
        } catch (IOException ex) {
          recordError(ex);
        }
      }
    }

    @Override
    public synchronized void write(int b) {
      try {
        getOutputStream().write(b);
      } catch (IOException ex) {
        recordError(ex);
      }
    }

    @Override
    public synchronized void write(byte[] b) throws IOException {
      if (b.length > 0) {
        getOutputStream().write(b);
      }
    }

    @Override
    public synchronized void flush() throws IOException {
      if (hasOutputStream()) {
        getOutputStream().flush();
      }
    }

    @Override
    public synchronized void close() throws IOException {
      if (hasOutputStream()) {
        getOutputStream().close();
      }
    }
  }
}
