// 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.profiler.chart;

import java.io.PrintStream;
import java.util.List;
import java.util.Locale;

/**
 * {@link ChartVisitor} that builds HTML from the visited chart and prints it
 * out to the given {@link PrintStream}.
 */
public class HtmlChartVisitor implements ChartVisitor {

  /** The default width of a second in the chart. */
  private static final int DEFAULT_PIXEL_PER_SECOND = 50;

  /** The horizontal offset of second zero. */
  private static final int H_OFFSET = 40;

  /** The font size of the row labels. */
  private static final int ROW_LABEL_FONT_SIZE = 7;

  /** The height of a bar in pixels. */
  private static final int BAR_HEIGHT = 8;

  /** The space between twp bars in pixels. */
  private static final int BAR_SPACE = 2;

  /** The height of a row. */
  private static final int ROW_HEIGHT = BAR_HEIGHT + BAR_SPACE;

  /** The {@link PrintStream} to output the HTML to. */
  private final PrintStream out;

  /** The maxmimum stop time of any bar in the chart. */
  private long maxStop;

  /** The width of a second in the chart. */
  private final int pixelsPerSecond;

  /**
   * Creates the visitor, with a default width of a second of 50 pixels.
   *
   * @param out the {@link PrintStream} to output the HTML to
   */
  public HtmlChartVisitor(PrintStream out) {
    this(out, DEFAULT_PIXEL_PER_SECOND);
  }

  /**
   * Creates the visitor.
   *
   * @param out the {@link PrintStream} to output the HTML to
   * @param pixelsPerSecond The width of a second in the chart. (In pixels)
   */
  public HtmlChartVisitor(PrintStream out, int pixelsPerSecond) {
    this.out = out;
    this.pixelsPerSecond = pixelsPerSecond;
  }

  @Override
  public void visit(Chart chart) {
    maxStop = chart.getMaxStop();

    printContentBox();

    heading("Tasks", 2);
    out.println("<p>To get more information about a task point the mouse at one of the bars.</p>");

    out.printf("<div style='position:relative; height: %dpx; margin: %dpx'>\n",
        chart.getRowCount() * ROW_HEIGHT, H_OFFSET + 10);
  }

  @Override
  public void endVisit(Chart chart) {
    printTimeAxis(chart);
    out.println("</div>");

    heading("Legend", 2);
    printLegend(chart.getSortedTypes());
}

  @Override
  public void visit(ChartColumn column) {
    int width = scale(column.getWidth());
    if (width == 0) {
      return;
    }
    int left = scale(column.getStart());
    int height = column.getRowCount() * ROW_HEIGHT;
    String style = chartTypeNameAsCSSClass(column.getType().getName());
    box(left, 0, width, height, style, column.getLabel(), 10);
  }


  @Override
  public void visit(ChartRow slot) {
    String style = slot.getIndex() % 2 == 0 ? "shade-even" : "shade-odd";
    int top = slot.getIndex() * ROW_HEIGHT;
    int width = scale(maxStop) + 1;

    label(-H_OFFSET, top, width + H_OFFSET, ROW_HEIGHT, ROW_LABEL_FONT_SIZE, slot.getId());
    box(0, top, width, ROW_HEIGHT, style, "", 0);
  }

  @Override
  public void visit(ChartBar bar) {
    int width = scale(bar.getWidth());
    if (width == 0) {
      return;
    }
    int left = scale(bar.getStart());
    int top = bar.getRow().getIndex() * ROW_HEIGHT;
    String style = chartTypeNameAsCSSClass(bar.getType().getName());
    if (bar.getHighlight()) {
      style += "-highlight";
    }
    box(left, top + 2, width, BAR_HEIGHT, style, bar.getLabel(), 20);
  }

  @Override
  public void visit(ChartLine chartLine) {
    int start = chartLine.getStartRow().getIndex() * ROW_HEIGHT;
    int stop = chartLine.getStopRow().getIndex() * ROW_HEIGHT;
    int time = scale(chartLine.getStartTime());

    if (start < stop) {
      verticalLine(time, start + 1, 1, (stop - start) + ROW_HEIGHT, Color.RED);
    } else {
      verticalLine(time, stop + 1, 1, (start - stop) + ROW_HEIGHT, Color.RED);
    }
  }

  /**
   * Converts the given value from the bar of the chart to pixels.
   */
  private int scale(long value) {
    return (int) (value / (1000000000L / pixelsPerSecond));
  }

  /**
   * Prints a box with links to the sections of the generated HTML document.
   */
  private void printContentBox() {
    out.println("<div style='position:fixed; top:1em; right:1em; z-index:50; padding: 1ex;"
        + "border:1px solid #888; background-color:#eee; width:100px'><h3>Content</h3>");
    out.println("<p style='text-align:left;font-size:small;margin:2px'>"
        + "<a href='#Tasks'>Tasks</a></p>");
    out.println("<p style='text-align:left;font-size:small;margin:2px'>"
        + "<a href='#Legend'>Legend</a></p>");
    out.println("<p style='text-align:left;font-size:small;margin:2px'>"
        + "<a href='#Statistics'>Statistics</a></p></div>");
  }

  /**
   * Prints the time axis of the chart and vertical lines for every second.
   */
  private void printTimeAxis(Chart chart) {
    int location = 0;
    int second = 0;
    int end = scale(chart.getMaxStop());
    while (location < end) {
      label(location + 4, -17, pixelsPerSecond, ROW_HEIGHT, 0, second + "s");
      verticalLine(location, -20, 1, chart.getRowCount() * ROW_HEIGHT + 20, Color.GRAY);
      location += pixelsPerSecond;
      second += 1;
    }
  }

  public void printCss(List<ChartBarType> types) {
    out.println("<style type=\"text/css\"><!--");
    out.println("body { font-family: Sans; }");
    out.printf("div.shade-even { position:absolute; border: 0px; background-color:#dddddd }\n");
    out.printf("div.shade-odd { position:absolute; border: 0px; background-color:#eeeeee }\n");
    for (ChartBarType type : types) {
      String name = chartTypeNameAsCSSClass(type.getName());
      String color = formatColor(type.getColor());

      out.printf(
          "div.%s-border { position:absolute; border:1px solid grey; background-color:%s }\n",
          name, color);
      out.printf(
          "div.%s-highlight { position:absolute; border:1px solid red; background-color:%s }\n",
          name, color);
      out.printf("div.%s { position:absolute; border:0px; margin:1px; background-color:%s }\n",
          name, color);
    }
    out.println("--></style>");
  }

  /**
   * Prints the legend for the chart at the current position in the document. The
   * legend is printed in columns of 10 rows each.
   *
   * @param types the list of {@link ChartBarType}s to print in the legend.
   */
  private void printLegend(List<ChartBarType> types) {
    final int boxHeight = 20;
    final int lineHeight = 25;
    final int entriesPerColumn = 10;
    final int legendWidth = 350;
    int legendHeight;
    if (types.size() / entriesPerColumn >= 1) {
      legendHeight = entriesPerColumn;
    } else {
      legendHeight = types.size() % entriesPerColumn;
    }

    out.printf("<div style='position:relative; height: %dpx;'>",
        (legendHeight + 1) * lineHeight);

    int left = -legendWidth;
    int top;
    int i = 0;
    for (ChartBarType type : types) {
      if (i % entriesPerColumn == 0) {
        left += legendWidth;
        i = 0;
      }
      top = lineHeight * i;
      String style = chartTypeNameAsCSSClass(type.getName()) + "-border";
      box(left, top, boxHeight, boxHeight, style, type.getName(), 0);
      label(left + lineHeight + 10, top, legendWidth - 10, boxHeight, 0, type.getName());
      i++;
    }
    out.println("</div>");
  }

  /**
   * Prints a head-line at the current position in the document.
   *
   * @param text the text to print
   * @param level the headline level
   */
  private void heading(String text, int level) {
    anchor(text);
    out.printf("<h%d >%s</h%d>\n", level, text, level);
  }

  /**
   * Prints a box with the given location, size, background color and border.
   *
   * @param x the x location of the top left corner of the box
   * @param y the y location of the top left corner of the box
   * @param width the width location of the box
   * @param height the height location of the box
   * @param style the CSS style class to use for the box
   * @param title the text displayed when the mouse hovers over the box
   */
  private void box(int x, int y, int width, int height, String style, String title, int zIndex) {
    out.printf("<div class=\"%s\" title=\"%s\" "
        + "style=\"left:%dpx; top:%dpx; width:%dpx; height:%dpx; z-index:%d\"></div>\n",
        style, title, x, y, width, height, zIndex);
  }

  /**
   * Prints a label with the given location, size, background color and border.
   *
   * @param x the x location of the top left corner of the box
   * @param y the y location of the top left corner of the box
   * @param width the width location of the box
   * @param height the height location of the box
   * @param fontSize the font size of text in the box, 0 for default
   * @param text the text displayed in the box
   */
  private void label(int x, int y, int width, int height, int fontSize, String text) {
    if (fontSize > 0) {
      out.printf("<div style=\"position:absolute; left:%dpx; top:%dpx; width:%dpx; "
          + "height:%dpx; font-size:%dpt\">%s</div>\n",
          x, y, width, height, fontSize, text);
    } else {
      out.printf("<div style=\"position:absolute; left:%dpx; top:%dpx; width:%dpx; "
          + "height:%dpx\">%s</div>\n",
          x, y, width, height, text);
    }
  }

  /**
   * Prints a vertical line of given width, height and color at the given
   * location.
   *
   * @param x the x location of the start point of the line
   * @param y the y location of the start point of the line
   * @param width the width of the line
   * @param length the length of the line
   * @param color the color of the line
   */
  private void verticalLine(int x, int y, int width, int length, Color color) {
    out.printf("<div style='position: absolute; left: %dpx; top: %dpx; width: %dpx; "
        + "height: %dpx; border-left: %dpx solid %s'" + "></div>\n",
        x, y, width, length, width, formatColor(color));
  }

  /**
   * Prints an HTML anchor with the given name,
   */
  private void anchor(String name) {
    out.println("<a name='" + name + "'/>");
  }

  /** Formats the given {@link Color} to a css style color string. */
  public static String formatColor(Color color) {
    int r = color.getRed();
    int g = color.getGreen();
    int b = color.getBlue();
    int a = color.getAlpha();

    // US Locale is used to ensure a dot as decimal separator
    return String.format(Locale.US, "rgba(%d,%d,%d,%f)", r, g, b, (a / 255.0));
  }

  /**
   * Transform the name into a form suitable as a css class.
   */
  private String chartTypeNameAsCSSClass(String name) {
    return name.replace(' ', '_');
  }
}
