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

import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_static/shelf_static.dart';

import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/utils.dart';

/// The port number used by the local benchmark server.
const int benchmarkServerPort = 9999;

Future<TaskResult> runWebBenchmark({ @required bool useCanvasKit }) async {
  final String macrobenchmarksDirectory = path.join('${flutterDirectory.path}', 'dev', 'benchmarks', 'macrobenchmarks');
  return await inDirectory(macrobenchmarksDirectory, () async {
    await evalFlutter('build', options: <String>[
      'web',
      if (useCanvasKit)
        '--dart-define=FLUTTER_WEB_USE_SKIA=true',
      '--profile',
      '-t',
      'lib/web_benchmarks.dart',
    ], environment: <String, String>{
      'FLUTTER_WEB': 'true',
    });
    final Completer<List<Map<String, dynamic>>> profileData = Completer<List<Map<String, dynamic>>>();
    final List<Map<String, dynamic>> collectedProfiles = <Map<String, dynamic>>[];
    List<String> benchmarks;
    Iterator<String> benchmarkIterator;

    io.HttpServer server;
    Cascade cascade = Cascade();
    cascade = cascade.add((Request request) async {
      if (request.requestedUri.path.endsWith('/profile-data')) {
        final Map<String, dynamic> profile = json.decode(await request.readAsString()) as Map<String, dynamic>;
        final String benchmarkName = profile['name'] as String;
        if (benchmarkName != benchmarkIterator.current) {
          profileData.completeError(Exception(
            'Browser returned benchmark results from a wrong benchmark.\n'
            'Requested to run bechmark ${benchmarkIterator.current}, but '
            'got results for $benchmarkName.',
          ));
          server.close();
        }
        collectedProfiles.add(profile);
        return Response.ok('Profile received');
      } else if (request.requestedUri.path.endsWith('/next-benchmark')) {
        if (benchmarks == null) {
          benchmarks = (json.decode(await request.readAsString()) as List<dynamic>).cast<String>();
          benchmarkIterator = benchmarks.iterator;
        }
        if (benchmarkIterator.moveNext()) {
          final String nextBenchmark = benchmarkIterator.current;
          print('Launching benchmark "$nextBenchmark"');
          return Response.ok(nextBenchmark);
        } else {
          profileData.complete(collectedProfiles);
          return Response.notFound('Finished running benchmarks.');
        }
      } else {
        return Response.notFound(
            'This request is not handled by the profile-data handler.');
      }
    }).add(createStaticHandler(
      path.join('$macrobenchmarksDirectory', 'build', 'web'),
    ));

    server = await io.HttpServer.bind('localhost', benchmarkServerPort);
    io.Process chromeProcess;
    try {
      shelf_io.serveRequests(server, cascade.handler);

      final bool isChromeNoSandbox =
          io.Platform.environment['CHROME_NO_SANDBOX'] == 'true';

      final String dartToolDirectory = path.join('$macrobenchmarksDirectory/.dart_tool');
      final String userDataDir = io.Directory(dartToolDirectory).createTempSync('chrome_user_data_').path;
      final List<String> args = <String>[
        '--user-data-dir=$userDataDir',
        'http://localhost:$benchmarkServerPort/index.html',
        if (isChromeNoSandbox)
          '--no-sandbox',
        '--window-size=1024,1024',
        '--disable-extensions',
        '--disable-popup-blocking',
        // Indicates that the browser is in "browse without sign-in" (Guest session) mode.
        '--bwsi',
        '--no-first-run',
        '--no-default-browser-check',
        '--disable-default-apps',
        '--disable-translate',
      ];

      final bool isUncalibratedSmokeTest =
          io.Platform.environment['UNCALIBRATED_SMOKE_TEST'] == 'true';
      if (isUncalibratedSmokeTest) {
        print('Running in headless mode because running on uncalibrated hardware.');
        args.add('--headless');
        // When running in headless mode Chrome exits immediately unless
        // a debug port is specified.
        args.add('--remote-debugging-port=${benchmarkServerPort + 1}');
      }

      chromeProcess = await startProcess(
        _findSystemChromeExecutable(),
        args,
        workingDirectory: cwd,
      );

      bool receivedProfileData = false;
      chromeProcess.exitCode.then((int exitCode) {
        if (!receivedProfileData) {
          profileData.completeError(Exception(
            'Chrome process existed prematurely with exit code $exitCode',
          ));
        }
      });
      forwardStandardStreams(chromeProcess);

      print('Waiting for the benchmark to report benchmark profile.');

      final String backend = useCanvasKit ? 'canvaskit' : 'html';
      final Map<String, dynamic> taskResult = <String, dynamic>{};
      final List<String> benchmarkScoreKeys = <String>[];
      final List<Map<String, dynamic>> profiles = await profileData.future;
      print('Received profile data');
      receivedProfileData = true;
      for (final Map<String, dynamic> profile in profiles) {
        final String benchmarkName = profile['name'] as String;
        final String benchmarkScoreKey = '$benchmarkName.$backend.averageDrawFrameDuration';
        taskResult[benchmarkScoreKey] = profile['averageDrawFrameDuration'].toDouble(); // micros
        taskResult['$benchmarkName.$backend.drawFrameDurationNoise'] = profile['drawFrameDurationNoise'].toDouble(); // micros
        benchmarkScoreKeys.add(benchmarkScoreKey);
      }
      return TaskResult.success(taskResult, benchmarkScoreKeys: benchmarkScoreKeys);
    } finally {
      server.close();
      chromeProcess?.kill();
    }
  });
}

String _findSystemChromeExecutable() {
  if (io.Platform.isLinux) {
    final io.ProcessResult which =
        io.Process.runSync('which', <String>['google-chrome']);

    if (which.exitCode != 0) {
      throw Exception('Failed to locate system Chrome installation.');
    }

    return (which.stdout as String).trim();
  } else if (io.Platform.isMacOS) {
    return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
  } else {
    throw Exception('Web benchmarks cannot run on ${io.Platform.operatingSystem} yet.');
  }
}
