// ignore_for_file: unnecessary_lambdas

import 'dart:io';

import 'package:xml/xml.dart';
import 'package:xml/xml_events.dart';

import '../test/utils/examples.dart';

/// Measures the time it takes to run [function] in microseconds.
///
/// It does so in two steps:
///
///  - the code is warmed up for the duration of [warmup]; and
///  - the code is benchmarked for the duration of [measure].
///
/// The resulting duration is the average time measured to run [function] once.
double benchmark(void Function() function,
    {Duration warmup = const Duration(milliseconds: 200),
    Duration measure = const Duration(seconds: 2)}) {
  _benchmark(function, warmup);
  return _benchmark(function, measure);
}

double _benchmark(void Function() function, Duration duration) {
  final watch = Stopwatch();
  final micros = duration.inMicroseconds;
  var count = 0;
  var elapsed = 0;
  watch.start();
  while (elapsed < micros) {
    function();
    elapsed = watch.elapsedMicroseconds;
    count++;
  }
  return elapsed / count;
}

/// Compare the speedup between [reference] and [comparison] in percentage.
///
/// A result of 0 means that both reference and comparison run at the same
/// speed. A positive number signifies a speedup, a negative one a slowdown.
double percentChange(double reference, double comparison) =>
    100 * (reference - comparison) / reference;

String characterData() {
  const string = '''a&bc<def"gehi'jklm>nopqr''';
  final builder = XmlBuilder();
  builder.processing('xml', 'version="1.0"');
  builder.element('character', nest: () {
    for (var i = 0; i < 20; i++) {
      builder.text('$string$string$string$string$string$string');
      builder.element('foo', nest: () {
        builder.attribute('key', '$string$string$string$string');
      });
    }
  });
  return builder.buildDocument().toString();
}

final Map<String, String> benchmarks = {
  'atom': atomXml,
  'books': booksXml,
  'bookstore': bookstoreXml,
  'complicated': complicatedXml,
  'shiporder': shiporderXsd,
  'decoding': characterData(),
};

void main(List<String> args) {
  final builder = XmlBuilder();
  addBenchmarks(builder);
  final document = builder.buildDocument();
  if (args.contains('xml')) {
    stdout.writeln(document.toXmlString(pretty: true));
  } else {
    stdout.writeln([
      '',
      ...document
          .findAllElements('benchmark')
          .first
          .findAllElements('measure')
          .map((measure) => measure.getAttribute('name'))
    ].join(';'));
    stdout.write(document
        .findAllElements('benchmark')
        .map((benchmark) => [
              benchmark.getAttribute('name'),
              ...benchmark.findAllElements('time').map((time) => time.innerText)
            ].join(';'))
        .join('\n'));
  }
}

void addBenchmarks(XmlBuilder builder) {
  builder.processing('xml', 'version="1.0"');
  builder.element('benchmarks', nest: () {
    for (final entry in benchmarks.entries) {
      addBenchmark(builder, entry);
    }
  });
}

void addBenchmark(XmlBuilder builder, MapEntry<String, String> entry) {
  builder.element('benchmark', attributes: {'name': entry.key}, nest: () {
    final source = entry.value;
    final document = XmlDocument.parse(source);
    final parser = benchmark(() => XmlDocument.parse(source));
    final streamEvents = benchmark(() => XmlEventDecoder().convert(source));
    final streamNodes = benchmark(() =>
        const XmlNodeDecoder().convert(XmlEventDecoder().convert(source)));
    final iterator = benchmark(() => parseEvents(source).toList());
    final serialize = benchmark(() => document.toXmlString());
    final serializePretty = benchmark(() => document.toXmlString(pretty: true));
    addMeasure(builder, 'parser', parser);
    addMeasure(builder, 'streamEvents', streamEvents, parser);
    addMeasure(builder, 'streamNodes', streamNodes, parser);
    addMeasure(builder, 'iterator', iterator, parser);
    addMeasure(builder, 'serialize', serialize);
    addMeasure(builder, 'serializePretty', serializePretty, serialize);
  });
}

void addMeasure(XmlBuilder builder, String name, double measure,
    [double? reference]) {
  builder.element('measure', attributes: {'name': name}, nest: () {
    builder.element('time', nest: measure.toStringAsFixed(6));
    if (reference != null) {
      final speedup = percentChange(reference, measure);
      builder.element('speedup', nest: speedup.toStringAsFixed(2));
    }
  });
}
