// Copyright (c) 2019, 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:dwds/src/debugging/remote_debugger.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

typedef _EventStreamState = ({
  StreamController controller,
  WipEventTransformer transformer,
});

/// A remote debugger with a Webkit Inspection Protocol connection.
class WebkitDebugger implements RemoteDebugger {
  WipDebugger _wipDebugger;

  /// Null until [close] is called.
  ///
  /// All subsequent calls to [close] will return this future.
  Future<void>? _closed;

  // We need to forward the events from the [WipDebugger] instance rather than
  // directly expose its streams as it may need to be recreated due to an
  // unexpected loss in connection to the debugger.
  final _onClosedController = StreamController<WipConnection>.broadcast();
  final _onConsoleAPICalledController =
      StreamController<ConsoleAPIEvent>.broadcast();
  final _onExceptionThrownController =
      StreamController<ExceptionThrownEvent>.broadcast();
  final _onGlobalObjectClearedController =
      StreamController<GlobalObjectClearedEvent>.broadcast();
  final _onPausedController = StreamController<DebuggerPausedEvent>.broadcast();
  final _onResumedController =
      StreamController<DebuggerResumedEvent>.broadcast();
  final _onScriptParsedController =
      StreamController<ScriptParsedEvent>.broadcast();
  final _onTargetCrashedController =
      StreamController<TargetCrashedEvent>.broadcast();

  /// Tracks and manages all subscriptions to streams created with
  /// `eventStream`.
  final _eventStreams = <String, _EventStreamState>{};

  late final _controllers = <StreamController>[
    _onClosedController,
    _onConsoleAPICalledController,
    _onExceptionThrownController,
    _onGlobalObjectClearedController,
    _onPausedController,
    _onResumedController,
    _onScriptParsedController,
    _onTargetCrashedController,
  ];

  final _streamSubscriptions = <StreamSubscription>[];

  @override
  Future<void> Function()? onReconnect;

  WebkitDebugger(this._wipDebugger) {
    _initialize();
  }

  void _initialize() {
    late StreamSubscription sub;
    sub = _wipDebugger.connection.onClose.listen((connection) async {
      await sub.cancel();
      if (_closed != null) {
        // The connection closing is expected.
        _onClosedController.add(connection);
        await Future.wait([
          for (final controller in _controllers) controller.close(),
          for (final MapEntry(value: (:controller, transformer: _))
              in _eventStreams.entries)
            controller.close(),
        ]);
        return;
      }
      var retry = false;
      var retryCount = 0;
      const maxAttempts = 5;
      do {
        retry = false;
        try {
          _wipDebugger = WipDebugger(
            await WipConnection.connect(connection.url),
          );
          await onReconnect?.call();
        } on Exception {
          await Future.delayed(Duration(milliseconds: 25 << retryCount));
          retry = true;
          retryCount++;
        }
      } while (retry && retryCount <= maxAttempts);
      _initialize();
    });

    final runtime = _wipDebugger.connection.runtime;
    _streamSubscriptions
      ..forEach((e) => e.cancel())
      ..clear()
      ..addAll([
        runtime.onConsoleAPICalled.listen(_onConsoleAPICalledController.add),
        runtime.onExceptionThrown.listen(_onExceptionThrownController.add),
        _wipDebugger.onGlobalObjectCleared.listen(
          _onGlobalObjectClearedController.add,
        ),
        _wipDebugger.onPaused.listen(_onPausedController.add),
        _wipDebugger.onResumed.listen(_onResumedController.add),
        _wipDebugger.onScriptParsed.listen(_onScriptParsedController.add),
        _wipDebugger
            .eventStream(
              'Inspector.targetCrashed',
              (WipEvent event) => TargetCrashedEvent(event.json),
            )
            .listen(_onTargetCrashedController.add),

        for (final MapEntry(:key, value: (:controller, :transformer))
            in _eventStreams.entries)
          _wipDebugger
              .eventStream(key, transformer)
              .listen(controller.add, onError: controller.addError),
      ]);
  }

  @override
  Stream<ConsoleAPIEvent> get onConsoleAPICalled =>
      _onConsoleAPICalledController.stream;

  @override
  Stream<ExceptionThrownEvent> get onExceptionThrown =>
      _onExceptionThrownController.stream;

  @override
  Future<WipResponse> sendCommand(
    String command, {
    Map<String, dynamic>? params,
  }) => _wipDebugger.sendCommand(command, params: params);

  @override
  Future<void> close() => _closed ??= () async {
    await _wipDebugger.connection.close();
    await Future.wait([for (final sub in _streamSubscriptions) sub.cancel()]);
  }();

  @override
  Future<void> disable() => _wipDebugger.disable();

  @override
  Future<void> enable() => _wipDebugger.enable();

  @override
  Future<String> getScriptSource(String scriptId) =>
      _wipDebugger.getScriptSource(scriptId);

  @override
  Future<WipResponse> pause() => _wipDebugger.pause();

  @override
  Future<WipResponse> resume() => _wipDebugger.resume();

  @override
  Future<WipResponse> setPauseOnExceptions(PauseState state) =>
      _wipDebugger.setPauseOnExceptions(state);

  @override
  Future<WipResponse> removeBreakpoint(String breakpointId) =>
      _wipDebugger.removeBreakpoint(breakpointId);

  @override
  Future<WipResponse> stepInto({Map<String, dynamic>? params}) =>
      _wipDebugger.stepInto(params: params);

  @override
  Future<WipResponse> stepOut() => _wipDebugger.stepOut();

  @override
  Future<WipResponse> stepOver({Map<String, dynamic>? params}) =>
      _wipDebugger.stepOver(params: params);

  @override
  Future<WipResponse> enablePage() => _wipDebugger.connection.page.enable();

  @override
  Future<WipResponse> pageReload() => _wipDebugger.connection.page.reload();

  @override
  Future<RemoteObject> evaluate(
    String expression, {
    bool? returnByValue,
    int? contextId,
  }) {
    return _wipDebugger.connection.runtime.evaluate(
      expression,
      returnByValue: returnByValue,
    );
  }

  @override
  Future<RemoteObject> evaluateOnCallFrame(
    String callFrameId,
    String expression,
  ) {
    return _wipDebugger.connection.debugger.evaluateOnCallFrame(
      callFrameId,
      expression,
    );
  }

  @override
  Future<List<WipBreakLocation>> getPossibleBreakpoints(WipLocation start) {
    return _wipDebugger.connection.debugger.getPossibleBreakpoints(start);
  }

  @override
  Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) {
    return _eventStreams
        .putIfAbsent(method, () {
          final controller = StreamController<T>();
          final stream = _wipDebugger.eventStream(method, transformer);
          stream.listen(controller.add, onError: controller.addError);
          return (controller: controller, transformer: transformer);
        })
        .controller
        .stream
        .cast<T>();
  }

  @override
  Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared =>
      _onGlobalObjectClearedController.stream;

  @override
  Stream<DebuggerPausedEvent> get onPaused => _onPausedController.stream;

  @override
  Stream<DebuggerResumedEvent> get onResumed => _onResumedController.stream;

  @override
  Stream<ScriptParsedEvent> get onScriptParsed =>
      _onScriptParsedController.stream;

  @override
  Stream<TargetCrashedEvent> get onTargetCrashed =>
      _onTargetCrashedController.stream;

  @override
  Map<String, WipScript> get scripts => _wipDebugger.scripts;

  @override
  Stream<WipConnection> get onClose => _onClosedController.stream;
}
