// Copyright (c) 2023, 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 'package:meta/meta.dart';

import 'client.dart';

/// Manages state related to stream subscriptions made by [Client]s.
abstract class StreamManager {
  final _streamListeners = <String, List<Client>>{};
  List<Client>? getListenersFor({required String stream}) =>
      _streamListeners[stream];

  /// Returns true if [client] is subscribed to [stream]
  bool isSubscribed(Client client, String stream) {
    return _streamListeners[stream]?.contains(client) ?? false;
  }

  /// Returns true if [stream] has any [ServiceExtensionClients] subscribed.
  bool hasSubscriptions(String stream) {
    return _streamListeners[stream]?.isNotEmpty ?? false;
  }

  /// Triggers [Client.streamNotify] for all clients subscribed
  /// to [stream].
  @mustCallSuper
  void postEvent(
    String stream,
    Object data, {
    Client? excludedClient,
  }) {
    final listeners = _streamListeners[stream] ?? const <Client>[];
    for (final listener in listeners) {
      if (listener == excludedClient) continue;
      listener.streamNotify(stream, data);
    }
  }

  /// Subscribes a [client] to [stream].
  @mustCallSuper
  Future<void> streamListen(
    Client client,
    String stream,
  ) async {
    _streamListeners.putIfAbsent(stream, () => <Client>[]);
    if (_streamListeners[stream]!.contains(client)) {
      throw StreamAlreadyListeningException(stream, client);
    }
    _streamListeners[stream]!.add(client);
  }

  /// Unsubscribes [client] from [stream].
  @mustCallSuper
  Future<void> streamCancel(
    Client client,
    String stream,
  ) async {
    if (!_streamListeners.containsKey(stream)) return;
    _streamListeners[stream]!.remove(client);
  }

  /// Cancels [client] from all streams.
  ///
  /// If an error is thrown while cancelling it will be passed to
  /// [onCatchErrorTest], if `true` is returned then the error will be ignored
  /// otherwise the error is thrown.
  @mustCallSuper
  Future<void> onClientDisconnect(Client client,
      {bool Function(Object)? onCatchErrorTest}) async {
    await Future.wait([
      for (final stream in _streamListeners.keys)
        streamCancel(client, stream).catchError(
          (_) => null,
          test: (e) => onCatchErrorTest == null ? false : onCatchErrorTest(e),
        ),
    ]);
  }
}

class StreamAlreadyListeningException implements Exception {
  const StreamAlreadyListeningException(this.stream, this.client);
  final String stream;
  final Client client;

  @override
  String toString() =>
      "Client, with hashCode ${client.hashCode}, is already subscribed to stream $stream";
}
