// 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.

@experimental
library;

import 'dart:async';

import 'package:build/build.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:timing/timing.dart';

import 'phase.dart';

part 'performance_tracker.g.dart';

/// The [TimeSlice] of an entire build, including all its [actions].
@JsonSerializable()
class BuildPerformance extends TimeSlice {
  /// The [TimeSlice] of each phase ran in this build.
  final Iterable<BuildPhasePerformance> phases;

  /// The [TimeSlice] of running an individual [Builder] on an individual input.
  final Iterable<BuilderActionPerformance> actions;

  BuildPerformance(
      this.phases, this.actions, DateTime startTime, DateTime stopTime)
      : super(startTime, stopTime);

  factory BuildPerformance.fromJson(Map<String, dynamic> json) =>
      _$BuildPerformanceFromJson(json);

  @override
  Map<String, dynamic> toJson() => _$BuildPerformanceToJson(this);
}

/// The [TimeSlice] of a full [BuildPhase] within a larger build.
@JsonSerializable()
class BuildPhasePerformance extends TimeSlice {
  final List<String> builderKeys;

  BuildPhasePerformance(this.builderKeys, DateTime startTime, DateTime stopTime)
      : super(startTime, stopTime);

  factory BuildPhasePerformance.fromJson(Map<String, dynamic> json) =>
      _$BuildPhasePerformanceFromJson(json);

  @override
  Map<String, dynamic> toJson() => _$BuildPhasePerformanceToJson(this);
}

/// The [TimeSlice] of a [builderKey] running on [primaryInput] within a build.
@JsonSerializable()
class BuilderActionPerformance extends TimeSlice {
  final String builderKey;

  @JsonKey(fromJson: _assetIdFromJson, toJson: _assetIdToJson)
  final AssetId primaryInput;

  final Iterable<BuilderActionStagePerformance> stages;

  Duration get innerDuration => stages.fold(
      Duration.zero, (duration, stage) => duration + stage.innerDuration);

  BuilderActionPerformance(this.builderKey, this.primaryInput, this.stages,
      DateTime startTime, DateTime stopTime)
      : super(startTime, stopTime);

  factory BuilderActionPerformance.fromJson(Map<String, dynamic> json) =>
      _$BuilderActionPerformanceFromJson(json);

  @override
  Map<String, dynamic> toJson() => _$BuilderActionPerformanceToJson(this);
}

/// The [TimeSlice] of a particular task within a builder action.
///
/// This is some slice of overall [BuilderActionPerformance].
@JsonSerializable()
class BuilderActionStagePerformance extends TimeSliceGroup {
  final String label;

  BuilderActionStagePerformance(this.label, List<TimeSlice> slices)
      : super(slices);

  factory BuilderActionStagePerformance.fromJson(Map<String, dynamic> json) =>
      _$BuilderActionStagePerformanceFromJson(json);

  @override
  Map<String, dynamic> toJson() => _$BuilderActionStagePerformanceToJson(this);
}

/// Interface for tracking the overall performance of a build.
abstract class BuildPerformanceTracker
    implements TimeTracker, BuildPerformance {
  /// Tracks [runPhase] which performs [action] on all inputs in a phase, and
  /// return the outputs generated.
  Future<Iterable<AssetId>> trackBuildPhase(
      BuildPhase action, Future<Iterable<AssetId>> Function() runPhase);

  /// Returns a [BuilderActionTracker] for tracking [builderKey] on
  /// [primaryInput] and adds it to [actions].
  BuilderActionTracker addBuilderAction(
      AssetId primaryInput, String builderKey);

  factory BuildPerformanceTracker() => _BuildPerformanceTrackerImpl();

  /// A [BuildPerformanceTracker] with as little overhead as possible. Does no
  /// actual tracking and does not implement many fields/methods.
  factory BuildPerformanceTracker.noOp() =>
      _NoOpBuildPerformanceTracker.sharedInstance;
}

/// Real implementation of [BuildPerformanceTracker].
///
/// Use [BuildPerformanceTracker] factory to get an instance.
class _BuildPerformanceTrackerImpl extends SimpleAsyncTimeTracker
    implements BuildPerformanceTracker {
  @override
  Iterable<BuildPhaseTracker> get phases => _phases;
  final _phases = <BuildPhaseTracker>[];

  @override
  Iterable<BuilderActionTracker> get actions => _actions;
  final _actions = <BuilderActionTracker>[];

  /// Tracks [action] which is ran with [runPhase].
  ///
  /// This represents running a [Builder] on a group of sources.
  ///
  /// Returns all the outputs generated by the phase.
  @override
  Future<Iterable<AssetId>> trackBuildPhase(
      BuildPhase action, Future<Iterable<AssetId>> Function() runPhase) {
    assert(isTracking);
    var tracker = BuildPhaseTracker(action);
    _phases.add(tracker);
    return tracker.track(runPhase);
  }

  /// Returns a new [BuilderActionTracker] and adds it to [actions].
  ///
  /// The [BuilderActionTracker] will already be started, but you must stop it.
  @override
  BuilderActionTracker addBuilderAction(
      AssetId primaryInput, String builderKey) {
    assert(isTracking);
    var tracker = BuilderActionTracker(primaryInput, builderKey);
    _actions.add(tracker);
    return tracker;
  }

  @override
  Map<String, dynamic> toJson() => _$BuildPerformanceToJson(this);
}

/// No-op implementation of [BuildPerformanceTracker].
///
/// Read-only fields will throw, and tracking methods just directly invoke their
/// closures without tracking anything.
///
/// Use [BuildPerformanceTracker.noOp] to get an instance.
class _NoOpBuildPerformanceTracker extends NoOpTimeTracker
    implements BuildPerformanceTracker {
  static final _NoOpBuildPerformanceTracker sharedInstance =
      _NoOpBuildPerformanceTracker();

  @override
  Iterable<BuilderActionTracker> get actions => throw UnimplementedError();

  @override
  Iterable<BuildPhaseTracker> get phases => throw UnimplementedError();

  @override
  BuilderActionTracker addBuilderAction(
          AssetId primaryInput, String builderKey) =>
      BuilderActionTracker.noOp();

  @override
  Future<Iterable<AssetId>> trackBuildPhase(
          BuildPhase action, Future<Iterable<AssetId>> Function() runPhase) =>
      runPhase();

  @override
  Map<String, dynamic> toJson() => _$BuildPerformanceToJson(this);
}

/// Internal class that tracks the [TimeSlice] of an entire [BuildPhase].
///
/// Tracks total time it took to run on all inputs available to that action.
///
/// Use [track] to start actually tracking an operation.
///
/// This is only meaningful for non-lazy phases.
class BuildPhaseTracker extends SimpleAsyncTimeTracker
    implements BuildPhasePerformance {
  @override
  final List<String> builderKeys;

  BuildPhaseTracker(BuildPhase action)
      : builderKeys = action is InBuildPhase
            ? [action.builderLabel]
            : (action as PostBuildPhase)
                .builderActions
                .map((action) => action.builderLabel)
                .toList();

  @override
  Map<String, dynamic> toJson() => _$BuildPhasePerformanceToJson(this);
}

/// Interface for tracking the [TimeSlice] of an individual [Builder] on a given
/// primary input.
abstract class BuilderActionTracker
    implements TimeTracker, BuilderActionPerformance, StageTracker {
  /// Tracks the time of [runStage] and associates it with [label].
  ///
  /// You can specify [runStage] as [isExternal] (waiting for some external
  /// resource like network, process or file IO). In that case [runStage] will
  /// be tracked as single time slice from the beginning of the stage till
  /// completion of Future returned by [runStage].
  ///
  /// Otherwise all separate time slices of asynchronous execution will be
  /// tracked, but waiting for external resources will be a gap.
  @override
  T trackStage<T>(String label, T Function() runStage,
      {bool isExternal = false});

  factory BuilderActionTracker(AssetId primaryInput, String builderKey) =>
      _BuilderActionTrackerImpl(primaryInput, builderKey);

  /// A [BuilderActionTracker] with as little overhead as possible. Does no
  /// actual tracking and does not implement many fields/methods.
  factory BuilderActionTracker.noOp() =>
      _NoOpBuilderActionTracker._sharedInstance;
}

/// Real implementation of [BuilderActionTracker] which records timings.
///
/// Use the [BuilderActionTracker] factory to get an instance.
class _BuilderActionTrackerImpl extends SimpleAsyncTimeTracker
    implements BuilderActionTracker {
  @override
  final String builderKey;
  @override
  final AssetId primaryInput;

  @override
  final List<BuilderActionStageTracker> stages = [];

  @override
  Duration get innerDuration => stages.fold(
      Duration.zero, (duration, stage) => duration + stage.innerDuration);

  _BuilderActionTrackerImpl(this.primaryInput, this.builderKey);

  @override
  T trackStage<T>(String label, T Function() action,
      {bool isExternal = false}) {
    var tracker = isExternal
        ? BuilderActionStageSimpleTracker(label)
        : BuilderActionStageAsyncTracker(label);
    stages.add(tracker);
    return tracker.track(action);
  }

  @override
  Map<String, dynamic> toJson() => _$BuilderActionPerformanceToJson(this);
}

/// No-op instance of [BuilderActionTracker] which does nothing and throws an
/// unimplemented error for everything but [trackStage], which delegates
/// directly to the wrapped function.
///
/// Use the [BuilderActionTracker.noOp] factory to get an instance.
class _NoOpBuilderActionTracker extends NoOpTimeTracker
    implements BuilderActionTracker {
  static final _NoOpBuilderActionTracker _sharedInstance =
      _NoOpBuilderActionTracker();

  @override
  String get builderKey => throw UnimplementedError();

  @override
  Duration get duration => throw UnimplementedError();

  @override
  Iterable<BuilderActionStagePerformance> get stages =>
      throw UnimplementedError();

  @override
  Duration get innerDuration => throw UnimplementedError();

  @override
  AssetId get primaryInput => throw UnimplementedError();

  @override
  T trackStage<T>(String label, T Function() runStage,
          {bool isExternal = false}) =>
      runStage();

  @override
  Map<String, dynamic> toJson() => _$BuilderActionPerformanceToJson(this);
}

/// Tracks the [TimeSliceGroup] of an individual task.
///
/// These represent a slice of the [BuilderActionPerformance].
abstract class BuilderActionStageTracker
    implements BuilderActionStagePerformance {
  T track<T>(T Function() action);
}

class BuilderActionStageAsyncTracker extends AsyncTimeTracker
    implements BuilderActionStageTracker {
  @override
  final String label;

  BuilderActionStageAsyncTracker(this.label) : super(trackNested: false);

  @override
  Map<String, dynamic> toJson() => _$BuilderActionStagePerformanceToJson(this);
}

class BuilderActionStageSimpleTracker extends BuilderActionStagePerformance
    implements BuilderActionStageTracker {
  final _tracker = SimpleAsyncTimeTracker();

  BuilderActionStageSimpleTracker(String label) : super(label, []) {
    slices.add(_tracker);
  }

  @override
  T track<T>(T Function() action) => _tracker.track(action);
}

AssetId _assetIdFromJson(String json) => AssetId.parse(json);

String _assetIdToJson(AssetId id) => id.toString();
