// 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:developer';
import 'dart:typed_data';

import 'package:meta/meta.dart';

import '_timeline_io.dart'
  if (dart.library.js_util) '_timeline_web.dart' as impl;
import 'constants.dart';

/// Measures how long blocks of code take to run.
///
/// This class can be used as a drop-in replacement for [Timeline] as it
/// provides methods compatible with [Timeline] signature-wise, and it has
/// minimal overhead.
///
/// Provides [debugReset] and [debugCollect] methods that make it convenient to use in
/// frame-oriented environment where collected metrics can be attributed to a
/// frame, then aggregated into frame statistics, e.g. frame averages.
///
/// Forwards measurements to [Timeline] so they appear in Flutter DevTools.
abstract final class FlutterTimeline {
  static _BlockBuffer _buffer = _BlockBuffer();

  /// Whether block timings are collected and can be retrieved using the
  /// [debugCollect] method.
  ///
  /// This is always false in release mode.
  static bool get debugCollectionEnabled => _collectionEnabled;

  /// Enables metric collection.
  ///
  /// Metric collection can only be enabled in non-release modes. It is most
  /// useful in profile mode where application performance is representative
  /// of a deployed application.
  ///
  /// When disabled, resets collected data by calling [debugReset].
  ///
  /// Throws a [StateError] if invoked in release mode.
  static set debugCollectionEnabled(bool value) {
    if (kReleaseMode) {
      throw _createReleaseModeNotSupportedError();
    }
    if (value == _collectionEnabled) {
      return;
    }
    _collectionEnabled = value;
    debugReset();
  }

  static StateError _createReleaseModeNotSupportedError() {
    return StateError('FlutterTimeline metric collection not supported in release mode.');
  }

  static bool _collectionEnabled = false;

  /// Start a synchronous operation labeled `name`.
  ///
  /// Optionally takes a map of `arguments`. This slice may also optionally be
  /// associated with a [Flow] event. This operation must be finished by calling
  /// [finishSync] before returning to the event queue.
  ///
  /// This is a drop-in replacement for [Timeline.startSync].
  static void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) {
    Timeline.startSync(name, arguments: arguments, flow: flow);
    if (!kReleaseMode && _collectionEnabled) {
      _buffer.startSync(name, arguments: arguments, flow: flow);
    }
  }

  /// Finish the last synchronous operation that was started.
  ///
  /// This is a drop-in replacement for [Timeline.finishSync].
  static void finishSync() {
    Timeline.finishSync();
    if (!kReleaseMode && _collectionEnabled) {
      _buffer.finishSync();
    }
  }

  /// Emit an instant event.
  ///
  /// This is a drop-in replacement for [Timeline.instantSync].
  static void instantSync(String name, { Map<String, Object?>? arguments }) {
    Timeline.instantSync(name, arguments: arguments);
  }

  /// A utility method to time a synchronous `function`. Internally calls
  /// `function` bracketed by calls to [startSync] and [finishSync].
  ///
  /// This is a drop-in replacement for [Timeline.timeSync].
  static T timeSync<T>(String name, TimelineSyncFunction<T> function,
      { Map<String, Object?>? arguments, Flow? flow }) {
    startSync(name, arguments: arguments, flow: flow);
    try {
      return function();
    } finally {
      finishSync();
    }
  }

  /// The current time stamp from the clock used by the timeline in
  /// microseconds.
  ///
  /// When run on the Dart VM, uses the same monotonic clock as the embedding
  /// API's `Dart_TimelineGetMicros`.
  ///
  /// When run on the web, uses `window.performance.now`.
  ///
  /// This is a drop-in replacement for [Timeline.now].
  static int get now => impl.performanceTimestamp.toInt();

  /// Returns timings collected since [debugCollectionEnabled] was set to true,
  /// since the previous [debugCollect], or since the previous [debugReset],
  /// whichever was last.
  ///
  /// Resets the collected timings.
  ///
  /// This is only meant to be used in non-release modes, typically in profile
  /// mode that provides timings close to release mode timings.
  static AggregatedTimings debugCollect() {
    if (kReleaseMode) {
      throw _createReleaseModeNotSupportedError();
    }
    if (!_collectionEnabled) {
      throw StateError('Timeline metric collection not enabled.');
    }
    final AggregatedTimings result = AggregatedTimings(_buffer.computeTimings());
    debugReset();
    return result;
  }

  /// Forgets all previously collected timing data.
  ///
  /// Use this method to scope metrics to a frame, a pointer event, or any
  /// other event. To do that, call [debugReset] at the start of the event, then
  /// call [debugCollect] at the end of the event.
  ///
  /// This is only meant to be used in non-release modes.
  static void debugReset() {
    if (kReleaseMode) {
      throw _createReleaseModeNotSupportedError();
    }
    _buffer = _BlockBuffer();
  }
}

/// Provides [start], [end], and [duration] of a named block of code, timed by
/// [FlutterTimeline].
@immutable
final class TimedBlock {
  /// Creates a timed block of code from a [name], [start], and [end].
  ///
  /// The [name] should be sufficiently unique and descriptive for someone to
  /// easily tell which part of code was measured.
  const TimedBlock({
    required this.name,
    required this.start,
    required this.end,
  }) : assert(end >= start, 'The start timestamp must not be greater than the end timestamp.');

  /// A readable label for a block of code that was measured.
  ///
  /// This field should be sufficiently unique and descriptive for someone to
  /// easily tell which part of code was measured.
  final String name;

  /// The timestamp in microseconds that marks the beginning of the measured
  /// block of code.
  final double start;

  /// The timestamp in microseconds that marks the end of the measured block of
  /// code.
  final double end;

  /// How long the measured block of code took to execute in microseconds.
  double get duration => end - start;

  @override
  String toString() {
    return 'TimedBlock($name, $start, $end, $duration)';
  }
}

/// Provides aggregated results for timings collected by [FlutterTimeline].
@immutable
final class AggregatedTimings {
  /// Creates aggregated timings for the provided timed blocks.
  AggregatedTimings(this.timedBlocks);

  /// All timed blocks collected between the last reset and [FlutterTimeline.debugCollect].
  final List<TimedBlock> timedBlocks;

  /// Aggregated timed blocks collected between the last reset and [FlutterTimeline.debugCollect].
  ///
  /// Does not guarantee that all code blocks will be reported. Only those that
  /// executed since the last reset are listed here. Use [getAggregated] for
  /// graceful handling of missing code blocks.
  late final List<AggregatedTimedBlock> aggregatedBlocks = _computeAggregatedBlocks();

  List<AggregatedTimedBlock> _computeAggregatedBlocks() {
    final Map<String, (double, int)> aggregate = <String, (double, int)>{};
    for (final TimedBlock block in timedBlocks) {
      final (double, int) previousValue = aggregate.putIfAbsent(block.name, () => (0, 0));
      aggregate[block.name] = (previousValue.$1 + block.duration, previousValue.$2 + 1);
    }
    return aggregate.entries.map<AggregatedTimedBlock>(
      (MapEntry<String, (double, int)> entry) {
        return AggregatedTimedBlock(name: entry.key, duration: entry.value.$1, count: entry.value.$2);
      }
    ).toList();
  }

  /// Returns aggregated numbers for a named block of code.
  ///
  /// If the block in question never executed since the last reset, returns an
  /// aggregation with zero duration and count.
  AggregatedTimedBlock getAggregated(String name) {
    return aggregatedBlocks.singleWhere(
      (AggregatedTimedBlock block) => block.name == name,
      // Handle the case where there are no recorded blocks of the specified
      // type. In this case, the aggregated duration is simply zero, and so is
      // the number of occurrences (i.e. count).
      orElse: () => AggregatedTimedBlock(name: name, duration: 0, count: 0),
    );
  }
}

/// Aggregates multiple [TimedBlock] objects that share a [name].
///
/// It is common for the same block of code to be executed multiple times within
/// a frame. It is useful to combine multiple executions and report the total
/// amount of time attributed to that block of code.
@immutable
final class AggregatedTimedBlock {
  /// Creates a timed block of code from a [name] and [duration].
  ///
  /// The [name] should be sufficiently unique and descriptive for someone to
  /// easily tell which part of code was measured.
  const AggregatedTimedBlock({
    required this.name,
    required this.duration,
    required this.count,
  }) : assert(duration >= 0);

  /// A readable label for a block of code that was measured.
  ///
  /// This field should be sufficiently unique and descriptive for someone to
  /// easily tell which part of code was measured.
  final String name;

  /// The sum of [TimedBlock.duration] values of aggretaged blocks.
  final double duration;

  /// The number of [TimedBlock] objects aggregated.
  final int count;

  @override
  String toString() {
    return 'AggregatedTimedBlock($name, $duration, $count)';
  }
}

const int _kSliceSize = 500;

/// A growable list of float64 values with predictable [add] performance.
///
/// The list is organized into a "chain" of [Float64List]s. The object starts
/// with a `Float64List` "slice". When [add] is called, the value is added to
/// the slice. Once the slice is full, it is moved into the chain, and a new
/// slice is allocated. Slice size is static and therefore its allocation has
/// predictable cost. This is unlike the default [List] implementation, which,
/// when full, doubles its buffer size and copies all old elements into the new
/// buffer, leading to unpredictable performance. This makes it a poor choice
/// for recording performance because buffer reallocation would affect the
/// runtime.
///
/// The trade-off is that reading values back from the chain is more expensive
/// compared to [List] because it requires iterating over multiple slices. This
/// is a reasonable trade-off for performance metrics, because it is more
/// important to minimize the overhead while recording metrics, than it is when
/// reading them.
final class _Float64ListChain {
  _Float64ListChain();

  final List<Float64List> _chain = <Float64List>[];
  Float64List _slice = Float64List(_kSliceSize);
  int _pointer = 0;

  int get length => _length;
  int _length = 0;

  /// Adds and [element] to this chain.
  void add(double element) {
    _slice[_pointer] = element;
    _pointer += 1;
    _length += 1;
    if (_pointer >= _kSliceSize) {
      _chain.add(_slice);
      _slice = Float64List(_kSliceSize);
      _pointer = 0;
    }
  }

  /// Returns all elements added to this chain.
  ///
  /// This getter is not optimized to be fast. It is assumed that when metrics
  /// are read back, they do not affect the timings of the work being
  /// benchmarked.
  List<double> extractElements() {
    final List<double> result = <double>[];
    _chain.forEach(result.addAll);
    for (int i = 0; i < _pointer; i++) {
      result.add(_slice[i]);
    }
    return result;
  }
}

/// Same as [_Float64ListChain] but for recording string values.
final class _StringListChain {
  _StringListChain();

  final List<List<String?>> _chain = <List<String?>>[];
  List<String?> _slice = List<String?>.filled(_kSliceSize, null);
  int _pointer = 0;

  int get length => _length;
  int _length = 0;

  /// Adds and [element] to this chain.
  void add(String element) {
    _slice[_pointer] = element;
    _pointer += 1;
    _length += 1;
    if (_pointer >= _kSliceSize) {
      _chain.add(_slice);
      _slice = List<String?>.filled(_kSliceSize, null);
      _pointer = 0;
    }
  }

  /// Returns all elements added to this chain.
  ///
  /// This getter is not optimized to be fast. It is assumed that when metrics
  /// are read back, they do not affect the timings of the work being
  /// benchmarked.
  List<String> extractElements() {
    final List<String> result = <String>[];
    for (final List<String?> slice in _chain) {
      for (final String? element in slice) {
        result.add(element!);
      }
    }
    for (int i = 0; i < _pointer; i++) {
      result.add(_slice[i]!);
    }
    return result;
  }
}

/// A buffer that records starts and ends of code blocks, and their names.
final class _BlockBuffer {
  // Start-finish blocks can be nested. Track this nestedness by stacking the
  // start timestamps. Finish timestamps will pop timings from the stack and
  // add the (start, finish) tuple to the _block.
  static const int _stackDepth = 1000;
  static final Float64List _startStack = Float64List(_stackDepth);
  static final List<String?> _nameStack = List<String?>.filled(_stackDepth, null);
  static int _stackPointer = 0;

  final _Float64ListChain _starts = _Float64ListChain();
  final _Float64ListChain _finishes = _Float64ListChain();
  final _StringListChain _names = _StringListChain();

  List<TimedBlock> computeTimings() {
    assert(
      _stackPointer == 0,
      'Invalid sequence of `startSync` and `finishSync`.\n'
      'The operation stack was not empty. The following operations are still '
      'waiting to be finished via the `finishSync` method:\n'
      '${List<String>.generate(_stackPointer, (int i) => _nameStack[i]!).join(', ')}'
    );

    final List<TimedBlock> result = <TimedBlock>[];
    final int length = _finishes.length;
    final List<double> starts = _starts.extractElements();
    final List<double> finishes = _finishes.extractElements();
    final List<String> names = _names.extractElements();

    assert(starts.length == length);
    assert(finishes.length == length);
    assert(names.length == length);

    for (int i = 0; i < length; i++) {
      result.add(TimedBlock(
        start: starts[i],
        end: finishes[i],
        name: names[i],
      ));
    }

    return result;
  }

  void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) {
    _startStack[_stackPointer] = impl.performanceTimestamp;
    _nameStack[_stackPointer] = name;
    _stackPointer += 1;
  }

  void finishSync() {
    assert(
      _stackPointer > 0,
      'Invalid sequence of `startSync` and `finishSync`.\n'
      'Attempted to finish timing a block of code, but there are no pending '
      '`startSync` calls.'
    );

    final double finishTime = impl.performanceTimestamp;
    final double startTime = _startStack[_stackPointer - 1];
    final String name = _nameStack[_stackPointer - 1]!;
    _stackPointer -= 1;

    _starts.add(startTime);
    _finishes.add(finishTime);
    _names.add(name);
  }
}
