// Copyright (c) 2016, 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.

import 'dart:convert';

import '../log/log.dart';
import '../server.dart';

typedef Writer = void Function(StringSink sink);

/// A class used to write an HTML page.
abstract class PageWriter {
  /// The object used to escape special HTML characters.
  static final HtmlEscape htmlEscape = HtmlEscape();

  /// Initialize a newly create page writer.
  new();

  /// Return the length of the common prefix for time stamps associated with the
  /// given log [entries].
  int computePrefixLength(List<LogEntry> entries) {
    var length = entries.length;
    if (length < 2) {
      return 0;
    }
    var firstTime = entries[0].timeStamp.toString();
    var lastTime = entries[length - 1].timeStamp.toString();
    var prefixLength = 0;
    var timeLength = firstTime.length;
    while (prefixLength < timeLength &&
        firstTime.codeUnitAt(prefixLength) ==
            lastTime.codeUnitAt(prefixLength)) {
      prefixLength++;
    }
    return prefixLength;
  }

  /// Return an escaped version of the given [unsafe] text.
  String escape(String unsafe) {
    // We double escape single quotes because the escaped characters are
    // processed as part of reading the HTML, which means that single quotes
    // end up terminating string literals too early when they appear in event
    // handlers (which in turn leads to JavaScript syntax errors).
    return htmlEscape.convert(unsafe).replaceAll('&#39;', '&amp;#39;');
  }

  /// Write the body of the page (without the 'body' tag) to the given [sink].
  void writeBody(StringSink sink);

  /// Write the given [date] to the given [sink].
  void writeDate(StringSink sink, DateTime date) {
    var isoString = date.toIso8601String();
    var index = isoString.indexOf('T');
    var dateString = isoString.substring(0, index);
    var timeString = isoString.substring(index + 1);
    sink.write(dateString);
    sink.write(' at ');
    sink.write(timeString);
  }

  /// Write the body of the page (without the 'body' tag) to the given [sink].
  void writeMenu(StringSink sink) {
    sink.writeln('<div class="menu">');
    sink.write('<a href="${WebServer.logPath}" class="menuItem">Log</a>');
    sink.write('&nbsp;&bullet;&nbsp;');
    sink.write('<a href="${WebServer.statsPath}" class="menuItem">Stats</a>');
    sink.writeln('</div>');
  }

  /// Write the contents of the instrumentation log to the given [sink].
  void writePage(StringSink sink) {
    sink.writeln('<!DOCTYPE html>');
    sink.writeln('<html lang="en-US">');
    sink.writeln('<head>');
    sink.writeln('<meta charset="utf-8">');
    sink.writeln(
      '<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0">',
    );
    sink.writeln('<title>Instrumentation Log</title>');
    sink.writeln('<style>');
    writeStyleSheet(sink);
    sink.writeln('</style>');
    sink.writeln('<script>');
    writeScripts(sink);
    sink.writeln('</script>');
    sink.writeln('</head>');
    sink.writeln('<body>');
    writeBody(sink);
    sink.writeln('</body>');
    sink.writeln('</html>');
  }

  /// Write the scripts for the page (without the 'script' tag) to the given
  /// [sink].
  void writeScripts(StringSink sink) {
    // No common scripts.
  }

  /// Write the content of the style sheet (without the 'script' tag) for the
  /// page to the given [sink].
  void writeStyleSheet(StringSink sink) {
    sink.writeln(r'''
a {
  color: #000000;
  text-decoration: none;
}
a.menuItem {
  font-weight: bold;
}
body {
  font-family: sans-serif;
  height: 100%;
  margin: 0px;
  overflow: hidden;
  padding: 0px;
  width: 100%;
}
div.columnHeader {
}
div.button {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid;
  height: 16px;
  text-align: center;
  vertical-align: middle;
  width: 16px;
}
div.inset {
  padding: 10px;
}
div.menu {
  background-color: #cce6ff;
  padding: 5px;
}
html {
  height: 100%;
  width: 100%;
}
span.button {
  border-radius: 5px;
  border: 1px solid;
  height: 16px;
  width: 16px;
}
span.error {
  color: #ff0000;
}
span.gray {
  color: #777777;
}
span.label {
  font-weight: bold;
}
table.fullWidth {
  border: 0px;
  width: 100%;
}
td.halfWidth {
  width: 50%;
  vertical-align: top;
}
td.int {
  text-align: right;
}
th {
  text-align: left;
}
th.narrow {
  width: 16px;
}

#container {
  height: 100%;
  min-height: 100%;
  position: relative;
  width: 100%;
}
#content {
  height: 90%;
  width: 100%;
}
''');
  }

  /// Write to the given [sink] the HTML required to display content in two
  /// columns. The content of the columns will be written by the functions
  /// [writeLeftColumn], [writeCenterColumn] and [writeRightColumn] and will be
  /// contained in 'div' elements with the id's [leftColumnId], [centerColumnId]
  /// and [rightColumnId].
  void writeThreeColumns(
    StringSink sink,
    String leftColumnId,
    Writer writeLeftColumn,
    String centerColumnId,
    Writer writeCenterColumn,
    String rightColumnId,
    Writer writeRightColumn,
  ) {
    sink.writeln('<div>');
    sink.writeln('  <div>');
    sink.writeln('    <div id="$leftColumnId">');
    sink.writeln('      <div class="inset">');
    writeLeftColumn(sink);
    sink.writeln('      </div>');
    sink.writeln('    </div>');
    sink.writeln('    <div id="$rightColumnId">');
    sink.writeln('      <div class="inset">');
    writeRightColumn(sink);
    sink.writeln('      </div>');
    sink.writeln('    </div>');
    sink.writeln('    <div id="$centerColumnId">');
    sink.writeln('      <div class="inset">');
    writeCenterColumn(sink);
    sink.writeln('      </div>');
    sink.writeln('    </div>');
    sink.writeln('  </div>');
    sink.writeln('</div>');
  }

  /// Writeto the given [sink] the styles needed by a three column section where
  /// the columns have the ids [leftColumnId], [centerColumnId] and
  /// [rightColumnId].
  void writeThreeColumnStyles(
    StringSink sink,
    String leftColumnId,
    String centerColumnId,
    String rightColumnId,
  ) {
    sink.writeln('''
#$leftColumnId {
  float: left;
  height: 100%;
  overflow: auto;
  width: 33%;
}
#$centerColumnId {
  height: 100%;
  overflow: auto;
  width: 33%;
}
#$rightColumnId {
  float: right;
  height: 100%;
  overflow: auto;
  width: 33%;
}
''');
  }

  /// Write to the given [sink] the HTML required to display content in two
  /// columns. The content of the columns will be written by the functions
  /// [writeLeftColumn] and [writeRightColumn] and will be contained in 'div'
  /// elements with the id's [leftColumnId] and [rightColumnId].
  void writeTwoColumns(
    StringSink sink,
    String leftColumnId,
    Writer writeLeftColumn,
    String rightColumnId,
    Writer writeRightColumn,
  ) {
    sink.writeln('<div id="container">');
    sink.writeln('  <div id="content">');
    sink.writeln('    <div id="$leftColumnId">');
    sink.writeln('      <div class="inset">');
    writeLeftColumn(sink);
    sink.writeln('      </div>');
    sink.writeln('    </div>');
    sink.writeln('    <div id="$rightColumnId">');
    sink.writeln('      <div class="inset">');
    writeRightColumn(sink);
    sink.writeln('      </div>');
    sink.writeln('    </div>');
    sink.writeln('  </div>');
    sink.writeln('</div>');
  }

  /// Writeto the given [sink] the styles needed by a two column section where
  /// the columns have the ids [leftColumnId] and [rightColumnId].
  void writeTwoColumnStyles(
    StringSink sink,
    String leftColumnId,
    String rightColumnId,
  ) {
    sink.writeln('''
#$leftColumnId {
  float: left;
  height: 100%;
  overflow: auto;
  width: 50%;
}
#$rightColumnId {
  float: right;
  height: 100%;
  overflow: auto;
  width: 50%;
}
''');
  }
}
