// 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 static java.nio.charset.StandardCharsets.US_ASCII;

import java.io.IOException;
import java.io.OutputStream;

/**
 * A class which encapsulates the fancy curses-type stuff that you can do using
 * standard ANSI terminal control sequences.
 */
public class AnsiTerminal {
  /**
   * An enumeration of all terminal colors, containing the escape sequences for both background and
   * foreground settings.
   */
  public enum Color {
    RED("^[31m", "^[41m"),
    GREEN("^[32m", "^[42m"),
    YELLOW("^[33m", "^[43m"),
    BLUE("^[34m", "^[44m"),
    MAGENTA("^[35m", "^[45m"),
    CYAN("^[36m", "^[46m"),
    GRAY("^[37m", "^[47m"),

    DEFAULT("^[0m", "^[0m");

    private final byte[] escapeSeq;
    private final byte[] backgroundEscapeSeq;

    private Color(String escapeSeq, String backgroundEscapeSeq) {
      this.escapeSeq = escapeSeq.replace('^', (char) 27).getBytes(US_ASCII);
      this.backgroundEscapeSeq = backgroundEscapeSeq.replace('^', (char) 27).getBytes(US_ASCII);
    }

    public byte[] getEscapeSeq() {
      return escapeSeq.clone();
    }

    public byte[] getBackgroundEscapeSeq() {
      return backgroundEscapeSeq.clone();
    }
  }

  private static final byte[] csi = {27, (byte) '['}; // Control Sequence Introducer
  private static final byte UP = (byte) 'A';
  private static final byte ERASE_LINE = (byte) 'K';
  private static final byte SET_GRAPHICS = (byte) 'm';
  private static final byte TEXT_BOLD = (byte) '1';
  private static final byte[] osc = {27, (byte) ']'}; // Operating System Command
  private static final byte[] setTermTitle = {(byte) '0', (byte) ';'};
  private static final byte[] st = {27, (byte) '\\'}; // String Terminator

  private static final byte[] cr = {13};

  private final OutputStream out;

  /**
   * Creates an AnsiTerminal object wrapping an output stream which is going to
   * be displayed in an ANSI compatible terminal or shell window.
   *
   * @param out the output stream
   */
  public AnsiTerminal(OutputStream out) {
    this.out = out;
  }

  /**
   * Moves the cursor upwards by a specified number of lines. This will not
   * cause any scrolling if it tries to move above the top of the terminal
   * window.
   */
  public void cursorUp(int numLines) throws IOException {
    writeBytes(csi, ("" + numLines).getBytes(US_ASCII), new byte[] {UP});
  }

  /**
   * Clear the current terminal line from the cursor position to the end.
   */
  public void clearLine() throws IOException {
    writeControlSequence(ERASE_LINE);
  }

  /**
   * Makes any text output to the terminal appear in bold.
   */
  public void textBold() throws IOException {
    writeControlSequence(TEXT_BOLD, SET_GRAPHICS);
  }

  /**
   * Set the color of the foreground or background of the terminal.
   *
   * @param color one of the foreground or background color constants
   */
  public void setTextColor(Color color) throws IOException {
    writeBytes(color.escapeSeq);
  }

  /**
   * Resets the terminal colors and fonts to defaults.
   */
  public void resetTerminal() throws IOException {
    writeControlSequence((byte) '0', (byte) 'm');
  }

  /**
   * Makes text print on the terminal in red.
   */
  public void textRed() throws IOException {
    setTextColor(Color.RED);
  }

  /**
   * Makes text print on the terminal in green.
   */
  public void textGreen() throws IOException {
    setTextColor(Color.GREEN);
  }

  /**
   * Makes text print on the terminal in magenta.
   */
  public void textMagenta() throws IOException {
    setTextColor(Color.MAGENTA);
  }

  /** Set the terminal title. */
  @SuppressWarnings("DefaultCharset")
  public void setTitle(String title) throws IOException {
    writeBytes(osc, setTermTitle, title.getBytes(), st);
  }

  /**
   * Writes a string to the terminal using the current font, color and cursor position settings.
   *
   * @param text the text to write
   */
  @SuppressWarnings("DefaultCharset")
  public void writeString(String text) throws IOException {
    out.write(text.getBytes());
  }

  /**
   * Writes a byte sequence to the terminal using the current font, color and cursor position
   * settings.
   *
   * @param bytes the bytes to write
   */
  public void writeBytes(byte[] bytes) throws IOException {
    out.write(bytes);
  }

  /**
   * Utility method for generating control sequences. Takes a collection of byte arrays, which
   * contain the components of a control sequence, concatenates them, and prints them to the
   * terminal.
   *
   * @param stuff the byte arrays that make up the sequence to be sent to the terminal
   */
  private void writeBytes(byte[]... stuff) throws IOException {
    for (byte[] bytes : stuff) {
      out.write(bytes);
    }
  }

  /**
   * Utility method which makes it easier to generate the control sequences for the terminal.
   *
   * @param bytes bytes which should be prefixed with the terminal escape sequence to produce a
   *     valid control sequence
   */
  private void writeControlSequence(byte... bytes) throws IOException {
    writeBytes(csi, bytes);
  }

  /** Sends a carriage return to the terminal. */
  public void cr() throws IOException {
    writeBytes(cr);
  }

  /**
   * Flushes the underlying stream. This class does not do any buffering of its own, but the
   * underlying OutputStream may.
   */
  public void flush() throws IOException {
    out.flush();
  }
}
