/**
 * 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.apache.hadoop.hive.common.log;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import jline.TerminalFactory;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hive.conf.HiveConf;
import org.fusesource.jansi.Ansi;

import javax.annotation.Nullable;
import java.io.PrintStream;
import java.io.StringWriter;
import java.text.DecimalFormat;
import java.util.List;

import static org.fusesource.jansi.Ansi.ansi;
import static org.fusesource.jansi.internal.CLibrary.*;

/**
 * Renders information from ProgressMonitor to the stream provided.
 */
public class InPlaceUpdate {

  public static final int MIN_TERMINAL_WIDTH = 94;

  // keep this within 80 chars width. If more columns needs to be added then update min terminal
  // width requirement and SEPARATOR width accordingly
  private static final String HEADER_FORMAT = "%16s%10s %13s  %5s  %9s  %7s  %7s  %6s  %6s  ";
  private static final String VERTEX_FORMAT = "%-16s%10s %13s  %5s  %9s  %7s  %7s  %6s  %6s  ";
  private static final String FOOTER_FORMAT = "%-15s  %-30s %-4s  %-25s";

  private static final int PROGRESS_BAR_CHARS = 30;
  private static final String SEPARATOR = new String(new char[MIN_TERMINAL_WIDTH]).replace("\0", "-");

  /* Pretty print the values */
  private final DecimalFormat secondsFormatter = new DecimalFormat("#0.00");
  private int lines = 0;
  private PrintStream out;

  public InPlaceUpdate(PrintStream out) {
    this.out = out;
  }

  public InPlaceUpdate() {
    this(System.out);
  }

  public static void reprintLine(PrintStream out, String line) {
    out.print(ansi().eraseLine(Ansi.Erase.ALL).a(line).a('\n').toString());
    out.flush();
  }

  public static void rePositionCursor(PrintStream ps) {
    ps.print(ansi().cursorUp(0).toString());
    ps.flush();
  }

  /**
   * NOTE: Use this method only if isUnixTerminal is true.
   * Erases the current line and prints the given line.
   *
   * @param line - line to print
   */
  private void reprintLine(String line) {
    reprintLine(out, line);
    lines++;
  }

  /**
   * NOTE: Use this method only if isUnixTerminal is true.
   * Erases the current line and prints the given line with the specified color.
   *
   * @param line  - line to print
   * @param color - color for the line
   */
  private void reprintLineWithColorAsBold(String line, Ansi.Color color) {
    out.print(ansi().eraseLine(Ansi.Erase.ALL).fg(color).bold().a(line).a('\n').boldOff().reset()
      .toString());
    out.flush();
    lines++;
  }

  /**
   * NOTE: Use this method only if isUnixTerminal is true.
   * Erases the current line and prints the given multiline. Make sure the specified line is not
   * terminated by linebreak.
   *
   * @param line - line to print
   */
  private void reprintMultiLine(String line) {
    int numLines = line.split("\r\n|\r|\n").length;
    out.print(ansi().eraseLine(Ansi.Erase.ALL).a(line).a('\n').toString());
    out.flush();
    lines += numLines;
  }

  /**
   * NOTE: Use this method only if isUnixTerminal is true.
   * Repositions the cursor back to line 0.
   */
  private void repositionCursor() {
    if (lines > 0) {
      out.print(ansi().cursorUp(lines).toString());
      out.flush();
      lines = 0;
    }
  }


  // [==================>>-----]
  private String getInPlaceProgressBar(double percent) {
    StringWriter bar = new StringWriter();
    bar.append("[");
    int remainingChars = PROGRESS_BAR_CHARS - 4;
    int completed = (int) (remainingChars * percent);
    int pending = remainingChars - completed;
    for (int i = 0; i < completed; i++) {
      bar.append("=");
    }
    bar.append(">>");
    for (int i = 0; i < pending; i++) {
      bar.append("-");
    }
    bar.append("]");
    return bar.toString();
  }

  public void render(ProgressMonitor monitor) {
    if (monitor == null) return;
    // position the cursor to line 0
    repositionCursor();

    // print header
    // -------------------------------------------------------------------------------
    //         VERTICES     STATUS  TOTAL  COMPLETED  RUNNING  PENDING  FAILED  KILLED
    // -------------------------------------------------------------------------------
    reprintLine(SEPARATOR);
    reprintLineWithColorAsBold(String.format(HEADER_FORMAT, monitor.headers().toArray()),
      Ansi.Color.CYAN);
    reprintLine(SEPARATOR);


    // Map 1 .......... container  SUCCEEDED      7          7        0        0       0       0
    List<String> printReady = Lists.transform(monitor.rows(), new Function<List<String>, String>() {
      @Nullable
      @Override
      public String apply(@Nullable List<String> row) {
        return String.format(VERTEX_FORMAT, row.toArray());
      }
    });
    reprintMultiLine(StringUtils.join(printReady, "\n"));

    // -------------------------------------------------------------------------------
    // VERTICES: 03/04            [=================>>-----] 86%  ELAPSED TIME: 1.71 s
    // -------------------------------------------------------------------------------
    String progressStr = "" + (int) (monitor.progressedPercentage() * 100) + "%";
    float et = (float) (System.currentTimeMillis() - monitor.startTime()) / (float) 1000;
    String elapsedTime = "ELAPSED TIME: " + secondsFormatter.format(et) + " s";
    String footer = String.format(
      FOOTER_FORMAT,
      monitor.footerSummary(),
      getInPlaceProgressBar(monitor.progressedPercentage()),
      progressStr,
      elapsedTime);

    reprintLine(SEPARATOR);
    reprintLineWithColorAsBold(footer, Ansi.Color.RED);
    reprintLine(SEPARATOR);
  }


  public static boolean canRenderInPlace(HiveConf conf) {
    boolean inPlaceUpdates = HiveConf.getBoolVar(conf, HiveConf.ConfVars.TEZ_EXEC_INPLACE_PROGRESS);

    // we need at least 80 chars wide terminal to display in-place updates properly
    return inPlaceUpdates && isUnixTerminal() && TerminalFactory.get().getWidth() >= MIN_TERMINAL_WIDTH;
  }

  private static boolean isUnixTerminal() {

    String os = System.getProperty("os.name");
    if (os.startsWith("Windows")) {
      // we do not support Windows, we will revisit this if we really need it for windows.
      return false;
    }

    // We must be on some unix variant..
    // check if standard out is a terminal
    try {
      // isatty system call will return 1 if the file descriptor is terminal else 0
      if (isatty(STDOUT_FILENO) == 0) {
        return false;
      }
      if (isatty(STDERR_FILENO) == 0) {
        return false;
      }
    } catch (NoClassDefFoundError | UnsatisfiedLinkError ignore) {
      // These errors happen if the JNI lib is not available for your platform.
      return false;
    }
    return true;
  }
}
