// 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:math' as math;

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

/// A page writer that will produce the page containing access to the full
/// content of the log.
class LogPage extends PageWriter {
  /// The instrumentation log to be written.
  InstrumentationLog log;

  /// The id of the entry groups to be displayed.
  late EntryGroup selectedGroup;

  /// The entries in the selected group.
  late List<LogEntry> entries;

  /// The index of the first entry to be written.
  int pageStart = 0;

  /// The number of entries to be written, or `null` if all of the entries
  /// should be written.
  int? pageLength;

  /// The number of digits in the event stamps that are the same for every
  /// entry.
  late int prefixLength;

  /// A table mapping the ids of plugins to an index for the plugin.
  Map<String, int> pluginIdMap = <String, int>{};

  /// Initialize a newly created writer to write the content of the given
  /// [instrumentationLog].
  new(this.log);

  /// Return the encoding for the given [pluginId] that is used to build
  /// anchors.
  int getPluginId(String pluginId) {
    return pluginIdMap.putIfAbsent(pluginId, () => pluginIdMap.length);
  }

  @override
  void writeBody(StringSink sink) {
    entries = log.entriesInGroup(selectedGroup)!;
    prefixLength = computePrefixLength(entries);

    writeMenu(sink);
    writeTwoColumns(
      sink,
      'leftColumn',
      _writeLeftColumn,
      'rightColumn',
      _writeRightColumn,
    );
  }

  @override
  void writeScripts(StringSink sink) {
    super.writeScripts(sink);
    sink.writeln(r'''
var highlightedRows = [];
function clearHighlight() {
  for (i = 0; i < highlightedRows.length; i++) {
    setFontWeight(highlightedRows[i], "normal");
  }
}
function highlight(requestId, responseId) {
  clearHighlight();
  setFontWeight(requestId, "bold");
  setFontWeight(responseId, "bold");
  highlightedRows = [requestId, responseId];
}
function setFontWeight(id, weight) {
  var element = document.getElementById(id);
  if (element != null) {
    element.style.fontWeight = weight;
  }
}
function setDetails(detailsContent) {
  var element = document.getElementById("details");
  if (element != null) {
    element.innerHTML = detailsContent;
  }
}
function selectEntryGroup(pageStart) {
  var element = document.getElementById("entryGroup");
  var url = "/log?group=" + element.value;
  window.location.assign(url);
}
''');
  }

  /// Write the content of the style sheet (without the 'script' tag) for the
  /// page to the given [sink].
  @override
  void writeStyleSheet(StringSink sink) {
    super.writeStyleSheet(sink);
    writeTwoColumnStyles(sink, 'leftColumn', 'rightColumn');
  }

  /// Return the number of milliseconds elapsed between the [startEntry] and the
  /// [endEntry], or a question .
  String _getDuration(LogEntry? startEntry, LogEntry? endEntry) {
    if (startEntry != null && endEntry != null) {
      return (endEntry.timeStamp - startEntry.timeStamp).toString();
    }
    return '?';
  }

  /// Write the given log [entry] to the given [sink].
  void _writeEntry(StringSink sink, LogEntry entry) {
    String? id;
    var clickHandler = 'clearHighlight()';
    var icon = '';
    var description = entry.kindName;
    if (entry is RequestEntry) {
      var entryId = entry.id;
      id = 'req$entryId';
      clickHandler = 'highlight(\'req$entryId\', \'res$entryId\')';
      icon = '&rarr;';
      description = entry.method ?? '<no method>';
    } else if (entry is ResponseEntry) {
      var entryId = entry.id;
      var request = log.requestFor(entry);
      id = 'res$entryId';
      clickHandler = 'highlight(\'req$entryId\', \'res$entryId\')';
      icon = '&larr;';
      if (request != null) {
        var latency = entry.timeStamp - request.timeStamp;
        description =
            '${request.method} <span class="gray">($latency ms)</span>';
      }
    } else if (entry is NotificationEntry) {
      id = 'e${entry.index}';
      var pairedEntry = log.pairedEntry(entry);
      if (pairedEntry != null) {
        var pairedId = 'e${pairedEntry.index}';
        clickHandler = 'highlight(\'$id\', \'$pairedId\')';
      }
      icon = '&larr;';
      description = entry.event;
      if (entry.isServerStatus) {
        var analysisStatus = entry.param('analysis');
        if (analysisStatus is Map) {
          if (analysisStatus['isAnalyzing'] as bool) {
            description = '$description <span class="gray">(analyzing)</span>';
          } else {
            var duration = _getDuration(pairedEntry, entry);
            description =
                '$description <span class="gray">(analysis - $duration ms)</span>';
          }
        }
        var pubStatus = entry.param('pub');
        if (pubStatus is Map) {
          if (pubStatus['isListingPackageDirs'] as bool) {
            description = '$description <span class="gray">(pub)</span>';
          } else {
            var duration = _getDuration(pairedEntry, entry);
            description =
                '$description <span class="gray">(pub - $duration ms)</span>';
          }
        }
      }
    } else if (entry is PluginRequestEntry) {
      var entryId = entry.id;
      var pluginId = getPluginId(entry.pluginId);
      id = 'req$pluginId.$entryId';
      clickHandler =
          'highlight(\'req$pluginId.$entryId\', \'res$pluginId.$entryId\')';
      icon = '&rarr;';
      description = '${entry.method} (${entry.shortPluginId})';
    } else if (entry is PluginResponseEntry) {
      var entryId = entry.id;
      var pluginId = getPluginId(entry.pluginId);
      var request = log.pluginRequestFor(entry);
      id = 'res$pluginId.$entryId';
      clickHandler =
          'highlight(\'req$pluginId.$entryId\', \'res$pluginId.$entryId\')';
      icon = '&larr;';
      if (request != null) {
        var latency = entry.timeStamp - request.timeStamp;
        description =
            '${request.method} <span class="gray">($latency ms)</span> (${entry.shortPluginId})';
      }
    } else if (entry is PluginNotificationEntry) {
      id = 'e${entry.index}';
      var pairedEntry = log.pairedEntry(entry);
      if (pairedEntry != null) {
        var pairedId = 'e${pairedEntry.index}';
        clickHandler = 'highlight(\'$id\', \'$pairedId\')';
      }
      icon = '&larr;';
      description = '${entry.event} (${entry.shortPluginId})';
    } else if (entry is TaskEntry) {
      description = entry.description;
    } else if (entry is ErrorEntry) {
      description = '<span class="error">$description</span>';
    } else if (entry is PluginErrorEntry) {
      description =
          '<span class="error">$description</span> (${entry.shortPluginId})';
    } else if (entry is ExceptionEntry) {
      description = '<span class="error">$description</span>';
    } else if (entry is MalformedLogEntry) {
      description = '<span class="error">$description</span>';
    }
    id = id == null ? '' : 'id="$id" ';
    clickHandler = '$clickHandler; setDetails(\'${escape(entry.details())}\')';
    var timeStamp = entry.timeStamp.toString();
    if (prefixLength > 0) {
      timeStamp = timeStamp.substring(prefixLength);
    }

    sink.writeln('<tr ${id}onclick="$clickHandler">');
    sink.writeln('<td>$icon</td>');
    sink.writeln('<td>');
    sink.writeln(timeStamp);
    sink.writeln('</td>');
    sink.writeln('<td style="white-space:nowrap;">');
    sink.writeln(description);
    sink.writeln('</td>');
    sink.writeln('</tr>');
  }

  /// Write the entries in the instrumentation log to the given [sink].
  void _writeLeftColumn(StringSink sink) {
    var length = entries.length;
    var pageLength = this.pageLength;
    var pageEnd = pageLength == null
        ? length
        : math.min(pageStart + pageLength, length);
    //
    // Write the header of the column.
    //
    sink.writeln('<div class="columnHeader">');
    sink.writeln('<div style="float: left">');
    sink.writeln('<select id="entryGroup" onchange="selectEntryGroup()">');
    for (var group in EntryGroup.groups) {
      sink.write('<option value="');
      sink.write(group.id);
      sink.write('"');
      if (group == selectedGroup) {
        sink.write(' selected');
      }
      sink.write('>');
      sink.write(group.name);
      sink.writeln('</option>');
    }
    sink.writeln('</select>');
    if (length == 0) {
      sink.writeln('No matching events');
    } else {
      sink.writeln('Events $pageStart - ${pageEnd - 1} of $length');
    }
    sink.writeln('</div>');

    sink.writeln('<div style="float: right">');
    if (pageStart == 0) {
      sink.writeln('<button type="button" disabled><b>&lt;</b></button>');
    } else {
      sink.write('<button type="button">');
      sink.write(
        '<a href="${WebServer.logPath}?group=${selectedGroup.id}&start=${pageStart - pageLength!}">',
      );
      sink.write('<b>&lt;</b>');
      sink.writeln('</a></button>');
    }
    // TODO(brianwilkerson): Add a text field for selecting the start index.
    if (pageEnd == length) {
      sink.writeln('<button type="button" disabled><b>&gt;</b></button>');
    } else {
      sink.write('<button type="button">');
      sink.write(
        '<a href="${WebServer.logPath}?group=${selectedGroup.id}&start=${pageStart + pageLength!}">',
      );
      sink.write('<b>&gt;</b>');
      sink.writeln('</a></button>');
    }
    sink.writeln('</div>');
    sink.writeln('</div>');
    //
    // Write the main body of the column.
    //
    sink.writeln('<table class="fullWidth">');
    sink.writeln('<tr>');
    sink.writeln('<th class="narrow"></th>');
    sink.writeln('<th>Time</th>');
    sink.writeln('<th>Kind</th>');
    sink.writeln('</tr>');
    for (var i = pageStart; i < pageEnd; i++) {
      var entry = entries[i];
      _writeEntry(sink, entry);
    }
    sink.writeln('</table>');
  }

  /// Write a placeholder to the given [sink] where the details of a selected
  /// entry can be displayed.
  void _writeRightColumn(StringSink sink) {
    //
    // Write the header of the column.
    //
    sink.writeln('<div class="columnHeader">');
    sink.writeln('<p><b>Entry Details</b></p>');
    sink.writeln('</div>');
    sink.writeln('<div id="details"></div>');
  }
}
