import 'package:http/http.dart' as http; import 'dart:html'; import 'dart:convert'; import 'dart:async'; class ChatInfo { int id; String name; ChatInfo({required this.id, required this.name}); } class Backend { static WebSocket? _socket; static String? _url; static Map _pendingActions = {}; static Map>> _chatControllers = {}; static int _nextNonce = 0; // Tracking nonce for each action static StreamController> _packetStreamController = StreamController.broadcast(); // Cached Data static List? _chats; // Authentication static ({String username, String password})? _credentials; static Completer? _loginCompleter; static Completer? _credentialsCompleter; static void setup(String url) { _url = url; connect(); } static Future signup(String username, String email, String password) async { final response = await http.post(Uri.parse("$_url/user/signup"), headers: { 'Content-Type': 'application/json', }, body: jsonEncode({ 'username': username, 'email': email, 'password': password, }) ); if (response.statusCode < 200 || response.statusCode >= 300) { throw Exception("Signup Status Code ${response.statusCode}"); } } static Future auth(String username, String password) { _socket!.send(json.encode({'username': username, 'password': password})); _credentials = (username: username, password: password); if (_credentialsCompleter != null) { _credentialsCompleter!.complete(_credentials); _credentialsCompleter = null; } _loginCompleter = Completer(); return _loginCompleter!.future; } static void connect() { _socket = WebSocket(_url! + "/ws"); _socket!.onOpen.listen((event) { print('Connected to the WebSocket server'); Backend.invalidateCache(chat: true); print('Logging in...'); if (_credentials != null) { _socket!.send(json.encode({'username': _credentials!.username, 'password': _credentials!.password})); } else { _credentialsCompleter = Completer(); _credentialsCompleter!.future.then((credentials) { _socket!.send(json.encode({'username': credentials.username, 'password': credentials.password})); }); } _loginCompleter?.complete(); }); _socket!.onError.listen((event) { print('Error: $event'); _reconnect(); }); _socket!.onClose.listen((event) { print('Disconnected. Attempting to reconnect...'); _reconnect(); }); _socket!.onMessage.listen((event) { _handleIncomingMessage(event.data); }); } static void disconnect() { _socket?.close(); _loginCompleter = null; } static void _reconnect() { Future.delayed(Duration(seconds: 5), () { connect(); }); } static Future sendAction(String action, Map body) { int nonce = _nextNonce++; Completer completer = Completer(); _pendingActions[nonce] = completer; final packet = { 'action': action, 'nonce': nonce, 'body': body, }; _socket!.send(json.encode(packet)); return completer.future.then((response) { _pendingActions.remove(nonce); // Clean up the mapping return response; }); } static void _handleIncomingMessage(String data) { var jsonData = json.decode(data); print("Incoming Packet: $jsonData"); if (jsonData['nonce'] != null) { int nonce = jsonData['nonce']; if (_pendingActions.containsKey(nonce)) { // Complete the response _pendingActions[nonce]!.complete(jsonData); } else { print("Orphan response for action nonce $nonce"); } } else if (jsonData['action'] == 'chat-message') { int chatId = jsonData['chatId']; final controller = _chatControllers[chatId]; if (controller != null && !controller.isClosed) { controller.add(jsonData); } else { print("No active listeners for chatId $chatId"); } } else { _packetStreamController.add(jsonData); } } static Stream> chatMessages(int chatId) { if (!_chatControllers.containsKey(chatId) || _chatControllers[chatId]!.isClosed) { _chatControllers[chatId] = StreamController>.broadcast(); } return _chatControllers[chatId]!.stream; } static void removeChatListener(int chatId) { final controller = _chatControllers.remove(chatId); if (controller != null && !controller.isClosed) { controller.close(); } } static Future> getChats () async { if (_chats == null) { final response = await Backend.sendAction("get-chats", {}); List chatObjects = response['chats']; _chats = chatObjects .map((msg) => ChatInfo( id: msg['id'], name: msg['name']?.isNotEmpty == true ? msg['name'] : '[Chat ${msg['id']}]', )) .toList(); } return _chats!; } static Future getChat (int chatId) async { final chats = await getChats(); try { return chats.firstWhere((chat) => chat.id == chatId); } on StateError { return null; } } static void invalidateCache ({bool chat = false}) { if (chat) { _chats = null; } } static void ping() { if (_socket!= null && _socket!.readyState == WebSocket.OPEN) { final data = json.encode({'path': 'ping'}); _socket!.send(data); print('Ping sent: $data'); } } }