// Copyright 2014 The Flutter Authors. 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:async';
import 'dart:convert' show json;
import 'dart:html' as html;

import 'package:macrobenchmarks/src/web/bench_text_out_of_picture_bounds.dart';

import 'src/web/bench_build_material_checkbox.dart';
import 'src/web/bench_card_infinite_scroll.dart';
import 'src/web/bench_draw_rect.dart';
import 'src/web/bench_simple_lazy_text_scroll.dart';
import 'src/web/bench_text_out_of_picture_bounds.dart';
import 'src/web/recorder.dart';

typedef RecorderFactory = Recorder Function();

final Map<String, RecorderFactory> benchmarks = <String, RecorderFactory>{
  BenchCardInfiniteScroll.benchmarkName: () => BenchCardInfiniteScroll(),
  BenchDrawRect.benchmarkName: () => BenchDrawRect(),
  BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(),
  BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(),
  BenchBuildMaterialCheckbox.benchmarkName: () => BenchBuildMaterialCheckbox(),
};

/// Whether we fell back to manual mode.
///
/// This happens when you run benchmarks using plain `flutter run` rather than
/// devicelab test harness. The test harness spins up a special server that
/// provides API for automatically picking the next benchmark to run.
bool isInManualMode = false;

Future<void> main() async {
  // Check if the benchmark server wants us to run a specific benchmark.
  final html.HttpRequest request = await requestXhr(
    '/next-benchmark',
    method: 'POST',
    mimeType: 'application/json',
    sendData: json.encode(benchmarks.keys.toList()),
  );

  // 404 is expected in the following cases:
  // - The benchmark is ran using plain `flutter run`, which does not provide "next-benchmark" handler.
  // - We ran all benchmarks and the benchmark is telling us there are no more benchmarks to run.
  if (request.status == 404) {
    _fallbackToManual('The server did not tell us which benchmark to run next.');
    return;
  }

  final String benchmarkName = request.responseText;
  await _runBenchmark(benchmarkName);
  html.window.location.reload();
}

Future<void> _runBenchmark(String benchmarkName) async {
  final RecorderFactory recorderFactory = benchmarks[benchmarkName];

  if (recorderFactory == null) {
    _fallbackToManual('Benchmark $benchmarkName not found.');
    return;
  }

  final Recorder recorder = recorderFactory();
  final Profile profile = await recorder.run();

  if (!isInManualMode) {
    final html.HttpRequest request = await html.HttpRequest.request(
      '/profile-data',
      method: 'POST',
      mimeType: 'application/json',
      sendData: json.encode(profile.toJson()),
    );
    if (request.status != 200) {
      throw Exception(
        'Failed to report profile data to benchmark server. '
        'The server responded with status code ${request.status}.'
      );
    }
  } else {
    print(profile);
  }
}

void _fallbackToManual(String error) {
  isInManualMode = true;
  html.document.body.appendHtml('''
    <div id="manual-panel">
      <h3>$error</h3>

      <p>Choose one of the following benchmarks:</p>

      <!-- Absolutely position it so it receives the clicks and not the glasspane -->
      <ul style="position: absolute">
        ${
          benchmarks.keys
            .map((String name) => '<li><button id="$name">$name</button></li>')
            .join('\n')
        }
      </ul>
    </div>
  ''', validator: html.NodeValidatorBuilder()..allowHtml5()..allowInlineStyles());

  for (final String benchmarkName in benchmarks.keys) {
    final html.Element button = html.document.querySelector('#$benchmarkName');
    button.addEventListener('click', (_) {
      final html.Element manualPanel = html.document.querySelector('#manual-panel');
      manualPanel?.remove();
      _runBenchmark(benchmarkName);
    });
  }
}

Future<html.HttpRequest> requestXhr(
  String url, {
  String method,
  bool withCredentials,
  String responseType,
  String mimeType,
  Map<String, String> requestHeaders,
  dynamic sendData,
}) {
  final Completer<html.HttpRequest> completer = Completer<html.HttpRequest>();
  final html.HttpRequest xhr = html.HttpRequest();

  method ??= 'GET';
  xhr.open(method, url, async: true);

  if (withCredentials != null) {
    xhr.withCredentials = withCredentials;
  }

  if (responseType != null) {
    xhr.responseType = responseType;
  }

  if (mimeType != null) {
    xhr.overrideMimeType(mimeType);
  }

  if (requestHeaders != null) {
    requestHeaders.forEach((String header, String value) {
      xhr.setRequestHeader(header, value);
    });
  }

  xhr.onLoad.listen((html.ProgressEvent e) {
    completer.complete(xhr);
  });

  xhr.onError.listen(completer.completeError);

  if (sendData != null) {
    xhr.send(sendData);
  } else {
    xhr.send();
  }

  return completer.future;
}