// 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 'package:logging/logging.dart';

import '../logging/human_readable_duration.dart';

var _logger = Logger('Heartbeat');

/// Base class for a heartbeat implementation.
///
/// Once [start]ed, if [waitDuration] passes between calls to [ping], then
/// [onTimeout] will be invoked with the duration.
abstract class Heartbeat {
  Stopwatch? _intervalWatch;
  Timer? _timer;

  /// The interval at which to check if [waitDuration] has passed.
  final Duration checkInterval;

  /// The amount of time between heartbeats.
  final Duration waitDuration;

  Heartbeat({
    this.checkInterval = const Duration(milliseconds: 100),
    this.waitDuration = const Duration(seconds: 5),
  });

  /// Invoked if [waitDuration] time has elapsed since the last call to [ping].
  void onTimeout(Duration elapsed);

  /// Resets the internal timers. If more than [waitDuration] elapses without
  /// this method being called, then [onTimeout] will be invoked with the
  /// duration since the last ping.
  void ping() {
    _intervalWatch!.reset();
  }

  /// Starts this heartbeat logger, must not already be started.
  ///
  /// This method can be overridden to add additional logic for handling calls
  /// to [ping], but you must call `super.start()`.
  void start() {
    if (_intervalWatch != null || _timer != null) {
      throw StateError('HeartbeatLogger already started');
    }
    _intervalWatch = Stopwatch()..start();
    ping();
    _timer = Timer.periodic(checkInterval, _checkDuration);
  }

  /// Stops this heartbeat logger, must already be started.
  ///
  /// This method can be overridden to add additional logic for cleanup
  /// purposes, but you must call `super.stop()`.
  void stop() {
    if (_intervalWatch == null || _timer == null) {
      throw StateError('HeartbeatLogger was never started');
    }
    _intervalWatch!.stop();
    _intervalWatch = null;
    _timer!.cancel();
    _timer = null;
  }

  void _checkDuration(void _) {
    if (_intervalWatch!.elapsed < waitDuration) return;
    onTimeout(_intervalWatch!.elapsed);
    ping();
  }
}

/// Watches [Logger.root] and if there are no logs for [waitDuration] then it
/// will log a heartbeat message with the current elapsed time since [start] was
/// originally invoked.
class HeartbeatLogger extends Heartbeat {
  StreamSubscription<LogRecord>? _listener;
  Stopwatch? _totalWatch;

  /// Will be invoked with each original log message and the returned value will
  /// be logged instead.
  final String Function(String original)? transformLog;

  HeartbeatLogger({
    super.checkInterval,
    super.waitDuration,
    this.transformLog,
  });

  /// Start listening to logs.
  @override
  void start() {
    _totalWatch = Stopwatch()..start();
    super.start();
    _listener = Logger.root.onRecord.listen((_) => ping());
  }

  /// Stops listenting to the logger;
  @override
  void stop() {
    super.stop();
    _listener!.cancel();
    _listener = null;
    _totalWatch!.stop();
    _totalWatch = null;
  }

  /// Logs a heartbeat message if we reach the timeout.
  @override
  void onTimeout(void _) {
    var formattedTime = humanReadable(_totalWatch!.elapsed);
    var message = '$formattedTime elapsed';
    if (transformLog != null) {
      message = transformLog!(message);
    }
    _logger.info(message);
  }
}

class HungActionsHeartbeat extends Heartbeat {
  /// Returns a description of pending actions
  final String Function() listActions;

  HungActionsHeartbeat(
    this.listActions, {
    super.checkInterval,
    super.waitDuration = const Duration(seconds: 15),
  });

  @override
  void onTimeout(Duration elapsed) {
    var formattedTime = humanReadable(elapsed);
    var message = 'No actions completed for $formattedTime, '
        'waiting on:\n${listActions()}';
    _logger.warning(message);
  }
}
