// 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 '../log/log.dart';
import 'page_writer.dart';

/// A page writer that will produce the page containing statistics about an
/// instrumentation log.
class StatsPage extends PageWriter {
  /// The instrumentation log to be written.
  final InstrumentationLog log;

  /// A table mapping the kinds of entries in the log to the number of each
  /// kind.
  final Map<String, int> entryCounts = <String, int>{};

  /// The number of responses that returned an error.
  int errorCount = 0;

  /// The number of responses from each plugin that returned an error.
  Map<String, int> pluginErrorCount = <String, int>{};

  /// A table mapping request method names to a list of the latencies associated
  /// with those requests, where the latency is defined to be the time between
  /// when the request was sent by the client and when the server started
  /// processing the request.
  final Map<String, List<int>> latencyData = <String, List<int>>{};

  /// A table mapping request method names to a list of the latencies associated
  /// with those requests, where the latency is defined to be the time between
  /// when the request was sent by the server and when the plugin sent a
  /// response.
  final Map<String, Map<String, List<int>>> pluginResponseData =
      <String, Map<String, List<int>>>{};

  /// A list of the number of milliseconds between a completion request and the
  /// first event for that request.
  final List<int> completionResponseTimes = <int>[];

  /// Initialize a newly created page writer to write information about the
  /// given instrumentation [log].
  new(this.log) {
    _processEntries(log.logEntries);
  }

  @override
  void writeBody(StringSink sink) {
    writeMenu(sink);
    writeTwoColumns(
      sink,
      'leftColumn',
      _writeLeftColumn,
      'rightColumn',
      _writeRightColumn,
    );
  }

  /// 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 mean of the values in the given list of [values].
  int _mean(List<int> values) {
    var sum = values.fold(0, (int sum, int latency) => sum + latency);
    return sum ~/ values.length;
  }

  /// Return a table mapping the kinds of the given [entries] to the number of
  /// each kind.
  void _processEntries(List<LogEntry> entries) {
    void increment<K>(Map<K, int> map, K key) {
      map[key] = (map[key] ?? 0) + 1;
    }

    for (var entry in entries) {
      var kind = entry.kind;
      increment(entryCounts, kind);
      if (entry is ResponseEntry) {
        if (entry.result('error') != null) {
          errorCount++;
        }
      } else if (entry is RequestEntry) {
        var method = entry.method;
        var latency = entry.timeStamp - entry.clientRequestTime;
        if (method != null) {
          latencyData.putIfAbsent(method, () => <int>[]).add(latency);
        }
        if (method == 'completion.getSuggestions') {
          var response = log.responseFor(entry);
          if (response != null) {
            var id = response.result('id');
            if (id is String) {
              var events = log.completionEventsWithId(id);
              if (events != null && events.isNotEmpty) {
                completionResponseTimes.add(
                  events[0].timeStamp - entry.timeStamp,
                );
              }
            }
          }
        }
      } else if (entry is PluginResponseEntry) {
        if (entry.result('error') != null) {
          var count = pluginErrorCount[entry.pluginId] ?? 0;
          pluginErrorCount[entry.pluginId] = count + 1;
        }
      } else if (entry is PluginRequestEntry) {
        var response = log.pluginResponseFor(entry)!;
        var responseTime = response.timeStamp - entry.timeStamp;
        var pluginData = pluginResponseData.putIfAbsent(
          entry.pluginId,
          () => <String, List<int>>{},
        );
        pluginData.putIfAbsent(entry.method, () => <int>[]).add(responseTime);
      }
    }
  }

  void _writeLeftColumn(StringSink sink) {
    var filePaths = log.logFilePaths;
    var entries = log.logEntries;
    var startDate = entries[0].toTime;
    var endDate = entries[entries.length - 1].toTime;
    var duration = endDate.difference(startDate);
    var entryKinds = entryCounts.keys.toList()..sort();

    sink.writeln('<h3>General</h3>');
    sink.writeln('<p>');
    if (filePaths.length == 1) {
      sink.write('<span class="label">Log file:</span> ');
      sink.write(filePaths[0]);
    } else {
      sink.write('<span class="label">Log files:</span> ');
      var needsSeparator = false;
      for (var path in filePaths) {
        if (needsSeparator) {
          sink.write(', ');
        } else {
          needsSeparator = true;
        }
        sink.write(path);
      }
    }
    sink.writeln('<br>');
    sink.write('<span class="label">Start time:</span> ');
    writeDate(sink, startDate);
    sink.writeln('<br>');
    sink.write('<span class="label">End time:</span> ');
    writeDate(sink, endDate);
    sink.writeln('<br>');
    sink.write('<span class="label">Duration:</span> ');
    sink.write(duration.toString());
    sink.writeln('</p>');

    sink.writeln('<h3>Entries</h3>');
    sink.write('<p>');
    sink.write('<span class="label">Number of entries:</span> ');
    sink.write(entries.length);
    sink.writeln('</p>');
    sink.write('<p>');
    sink.write('<span class="label">Error count:</span> ');
    sink.write(errorCount);
    sink.writeln('</p>');
    pluginErrorCount.forEach((String pluginId, int count) {
      sink.write('<p>');
      sink.write('<span class="label">Errors from $pluginId:</span> ');
      sink.write(count);
      sink.writeln('</p>');
    });
    sink.writeln('<table>');
    sink.writeln('<tr><th>count</th><th>kind</th></tr>');
    for (var kind in entryKinds) {
      sink.write('<tr><td class="int">');
      sink.write(entryCounts[kind]);
      sink.write('</td><td>');
      sink.write(kind);
      sink.writeln('</td></tr>');
    }
    sink.write('<tr><td class="int">');
    sink.write(entries.length);
    sink.writeln('</td><td>Total</td></tr>');
    sink.writeln('</table>');
  }

  void _writeRightColumn(StringSink sink) {
    completionResponseTimes.sort();

    sink.writeln('<h3>Latency</h3>');
    sink.write('<p>');
    sink.write('<span class="label">Latency by method</span>');
    sink.writeln('</p>');
    sink.writeln('<table>');
    sink.writeln(
      '<tr><th>min</th><th>mean</th><th>max</th><th>method</th></tr>',
    );
    var methodNames = latencyData.keys.toList()..sort();
    for (var method in methodNames) {
      var latencies = latencyData[method]!..sort();
      // TODO(brianwilkerson): Add a spark-line distribution graph.
      sink.write('<tr><td class="int">');
      sink.write(latencies[0]);
      sink.write('</td><td class="int">');
      sink.write(_mean(latencies));
      sink.write('</td><td class="int">');
      sink.write(latencies[latencies.length - 1]);
      sink.write('</td><td>');
      sink.write(method);
      sink.writeln('</td></tr>');
    }
    sink.writeln('</table>');

    sink.writeln('<h3>Completion</h3>');
    sink.write('<p>');
    sink.write('<span class="label">Time to first notification:</span> ');
    sink.write(completionResponseTimes[0]);
    sink.write(', ');
    sink.write(_mean(completionResponseTimes));
    sink.write(', ');
    sink.write(completionResponseTimes[completionResponseTimes.length - 1]);
    sink.writeln('</p>');

    if (pluginResponseData.isNotEmpty) {
      sink.writeln('<h3>Plugin response times</h3>');
      pluginResponseData.forEach((
        String pluginId,
        Map<String, List<int>> responseData,
      ) {
        sink.write('<p>');
        sink.write(pluginId);
        sink.writeln('</p>');
        sink.writeln('<table>');
        var methodNames = responseData.keys.toList()..sort();
        for (var method in methodNames) {
          var responseTimes = responseData[method]!..sort();
          // TODO(brianwilkerson): Add a spark-line distribution graph.
          sink.write('<tr><td class="int">');
          sink.write(responseTimes[0]);
          sink.write('</td><td class="int">');
          sink.write(_mean(responseTimes));
          sink.write('</td><td class="int">');
          sink.write(responseTimes[responseTimes.length - 1]);
          sink.write('</td><td>');
          sink.write(method);
          sink.writeln('</td></tr>');
        }
        sink.writeln('</table>');
      });
    }
  }
}
