// Copyright (c) 2017, 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:async';
import 'dart:io';

import 'package:path/path.dart' as path;

import '../benchmarks.dart';
import 'memory_tests.dart';

/// benchmarks:
///   - analysis-server-warm-analysis
///   - analysis-server-warm-memory
///   - analysis-server-edit
///   - analysis-server-completion
class AnalysisBenchmark extends Benchmark {
  final AbstractBenchmarkTest Function() testConstructor;

  new(ServerBenchmark benchmarkTest)
    : testConstructor = benchmarkTest.testConstructor,
      super(
        benchmarkTest.id,
        '${benchmarkTest.name} benchmarks of a large project, with an existing '
        'driver cache.',
        kind: 'group',
      );

  @override
  Future<BenchMarkResult> run({
    required String dartSdkPath,
    bool quick = false,
    bool verbose = false,
  }) async {
    var stopwatch = Stopwatch()..start();

    var test = testConstructor();
    if (verbose) {
      test.debugStdio();
    }
    await test.setUp(dartSdkPath, getProjectRoots(quick: quick));
    await test.analysisFinished;

    stopwatch.stop();
    var usedBytes = await test.getMemoryUsage();

    var result = CompoundBenchMarkResult(id);
    result.add(
      'warm-analysis',
      BenchMarkResult('micros', stopwatch.elapsedMicroseconds),
    );
    result.add('warm-memory', BenchMarkResult('bytes', usedBytes));

    if (!quick) {
      // change timing
      var editMicros = await _calcEditTiming(test);
      result.add('edit', BenchMarkResult('micros', editMicros));

      // code completion
      var completionMicros = await _calcCompletionTiming(test);
      result.add('completion', BenchMarkResult('micros', completionMicros));
    }

    await test.shutdown();

    return result;
  }

  Future<int> _calcCompletionTiming(AbstractBenchmarkTest test) async {
    const kGroupCount = 10;

    var filePath = path.join(
      analysisServerSrcPath,
      'lib',
      'src',
      'analysis_server.dart',
    );
    var contents = File(filePath).readAsStringSync();

    await test.openFile(filePath, contents);

    var completionCount = 0;
    var stopwatch = Stopwatch()..start();

    Future<void> complete(int offset) async {
      await test.complete(filePath, offset, isWarmUp: false);
      completionCount++;
    }

    for (var i = 0; i < kGroupCount; i++) {
      var startIndex = i * (contents.length ~/ (kGroupCount + 2));
      // Look for a line with a period in it that ends with a semi-colon.
      var index = contents.indexOf(
        RegExp(r'\..*;$', multiLine: true),
        startIndex,
      );

      await complete(index - 10);
      await complete(index - 1);
      await complete(index);
      await complete(index + 1);
      await complete(index + 10);

      if (i + 1 < kGroupCount) {
        // mutate
        index = contents.indexOf(';', index);
        contents =
            '${contents.substring(0, index + 1)} ${contents.substring(index + 1)}';
        await test.updateFile(filePath, contents);
      }
    }

    stopwatch.stop();

    await test.closeFile(filePath);

    return stopwatch.elapsedMicroseconds ~/ completionCount;
  }

  Future<int> _calcEditTiming(AbstractBenchmarkTest test) async {
    const kGroupCount = 5;

    var filePath = path.join(
      analysisServerSrcPath,
      'lib',
      'src',
      'analysis_server.dart',
    );
    var contents = File(filePath).readAsStringSync();

    // Get the future for `analysisFinished` *before* the call to [openFile]
    // as the analysis triggered by opening is sometimes so fast that it
    // finishes before starting to listen if only starting *after* the call to
    // [openFile].
    Future<void> analysisFinished = test.analysisFinished;
    await test.openFile(filePath, contents);
    await analysisFinished;

    var stopwatch = Stopwatch()..start();

    for (var i = 0; i < kGroupCount; i++) {
      var startIndex = i * (contents.length ~/ (kGroupCount + 2));
      var index = contents.indexOf(';', startIndex);
      contents =
          '${contents.substring(0, index + 1)} ${contents.substring(index + 1)}';
      await test.updateFile(filePath, contents);
      await test.analysisFinished;
    }

    stopwatch.stop();

    await test.closeFile(filePath);

    return stopwatch.elapsedMicroseconds ~/ kGroupCount;
  }
}

/// benchmarks:
///   - analysis-server-cold-analysis
///   - analysis-server-cold-memory
class ColdAnalysisBenchmark extends Benchmark {
  final AbstractBenchmarkTest Function() testConstructor;

  new(ServerBenchmark benchmarkTest)
    : testConstructor = benchmarkTest.testConstructor,
      super(
        '${benchmarkTest.id}-cold',
        '${benchmarkTest.name} benchmarks of a large project on start-up, no '
            'existing driver cache.',
        kind: 'group',
      );

  @override
  int get maxIterations => 3;

  @override
  Future<BenchMarkResult> run({
    required String dartSdkPath,
    bool quick = false,
    bool verbose = false,
  }) async {
    if (!quick) {
      deleteServerCache();
    }

    var stopwatch = Stopwatch()..start();

    var test = testConstructor();
    await test.setUp(dartSdkPath, getProjectRoots(quick: quick));
    await test.analysisFinished;

    stopwatch.stop();
    var usedBytes = await test.getMemoryUsage();

    var result = CompoundBenchMarkResult(id);
    result.add(
      'analysis',
      BenchMarkResult('micros', stopwatch.elapsedMicroseconds),
    );
    result.add('memory', BenchMarkResult('bytes', usedBytes));

    await test.shutdown();

    return result;
  }
}

class ServerBenchmark {
  static final das = ServerBenchmark(
    'analysis-server',
    'Analysis Server',
    AnalysisServerBenchmarkTest.new,
  );
  static final lsp = ServerBenchmark(
    'lsp-analysis-server',
    'LSP Analysis Server',
    LspAnalysisServerBenchmarkTest.new,
  );
  final String id;

  final String name;
  final AbstractBenchmarkTest Function() testConstructor;

  new(this.id, this.name, this.testConstructor);
}
