// =============================================================================
//  services/concentrador_service.dart — USB serial link to AR-Concentrador
// =============================================================================
//
//  The AR-Concentrador exposes two separate CH340N virtual COM ports:
//    rxPort  — USB-OUT: concentrador broadcasts $PARP,STATUS + NMEA at 2 Hz
//    txPort  — USB-IN : display sends $PARP commands to the concentrador
//
//  Both ports run at 115 200 baud, 8N1, no flow control.
//
//  Usage:
//    final svc = ConcentradorService(rxPort: 'COM3', txPort: 'COM4', stationId: 2);
//    svc.onStatus = (status) { ... };
//    svc.onConnectionChanged = (connected) { ... };
//    await svc.connect();
//    svc.sendEngage(headingDeg: 125.0);
//    svc.disconnect();
// =============================================================================

import 'dart:async';
import 'dart:typed_data';

import 'package:flutter_libserialport/flutter_libserialport.dart';

import 'parp_codec.dart';

/// Callback type for status updates from the concentrador.
typedef StatusCallback = void Function(ParpStatus status);

/// Callback type for connection state changes.
typedef ConnectionCallback = void Function(bool connected);

class ConcentradorService {
  ConcentradorService({
    required this.rxPort,
    required this.txPort,
    this.stationId = 2,
    this.baudRate = 115200,
  });

  final String rxPort;
  final String txPort;
  final int stationId;
  final int baudRate;

  /// Called whenever a valid $PARP,STATUS sentence is received.
  StatusCallback? onStatus;

  /// Called when the connection state changes.
  ConnectionCallback? onConnectionChanged;

  SerialPort? _rx;
  SerialPort? _tx;
  SerialPortReader? _reader;
  StreamSubscription<Uint8List>? _rxSub;

  bool _connected = false;
  bool get isConnected => _connected;

  // Accumulation buffer for partial sentences
  final StringBuffer _buf = StringBuffer();

  // ---------------------------------------------------------------------------
  //  Connection lifecycle
  // ---------------------------------------------------------------------------

  /// Open both serial ports and start listening for STATUS sentences.
  ///
  /// Throws [SerialPortError] if either port cannot be opened.
  Future<void> connect() async {
    await disconnect(); // clean slate

    _rx = SerialPort(rxPort);
    _tx = SerialPort(txPort);

    _configPort(_rx!);
    _configPort(_tx!);

    if (!_rx!.openRead()) {
      throw SerialPortError('Cannot open RX port $rxPort');
    }
    if (!_tx!.openWrite()) {
      throw SerialPortError('Cannot open TX port $txPort');
    }

    _reader = SerialPortReader(_rx!);
    _rxSub = _reader!.stream.listen(
      _onData,
      onError: (_) => _handleDisconnect(),
      onDone: _handleDisconnect,
    );

    _connected = true;
    onConnectionChanged?.call(true);
  }

  /// Close both ports gracefully.
  Future<void> disconnect() async {
    _rxSub?.cancel();
    _rxSub = null;
    _reader = null;

    _rx?.close();
    _tx?.close();
    _rx = null;
    _tx = null;

    if (_connected) {
      _connected = false;
      onConnectionChanged?.call(false);
    }
  }

  // ---------------------------------------------------------------------------
  //  Command senders
  // ---------------------------------------------------------------------------

  void sendEngage(double headingDeg) =>
      _send(ParpCodec.engage(stationId, headingDeg));

  void sendDisengage() =>
      _send(ParpCodec.disengage(stationId));

  void sendSetHeading(double headingDeg) =>
      _send(ParpCodec.setHeading(stationId, headingDeg));

  void sendPortOne(double setpointDeg) =>
      _send(ParpCodec.portOne(stationId, setpointDeg));

  void sendStbdOne(double setpointDeg) =>
      _send(ParpCodec.stbdOne(stationId, setpointDeg));

  void sendPortTen(double setpointDeg) =>
      _send(ParpCodec.portTen(stationId, setpointDeg));

  void sendStbdTen(double setpointDeg) =>
      _send(ParpCodec.stbdTen(stationId, setpointDeg));

  void sendReqCmd() =>
      _send(ParpCodec.reqCmd(stationId));

  void sendRelCmd() =>
      _send(ParpCodec.relCmd(stationId));

  // ---------------------------------------------------------------------------
  //  Port listing (for settings screen)
  // ---------------------------------------------------------------------------

  /// All serial ports currently visible to the OS.
  static List<String> availablePorts() => SerialPort.availablePorts;

  // ---------------------------------------------------------------------------
  //  Private
  // ---------------------------------------------------------------------------

  void _configPort(SerialPort port) {
    final cfg = SerialPortConfig()
      ..baudRate = baudRate
      ..bits = 8
      ..stopBits = 1
      ..parity = SerialPortParity.none
      ..setFlowControl(SerialPortFlowControl.none);
    port.config = cfg;
  }

  void _onData(Uint8List data) {
    _buf.write(String.fromCharCodes(data));
    final raw = _buf.toString();
    final lines = raw.split('\n');

    // Keep the last (possibly incomplete) chunk in the buffer.
    _buf.clear();
    _buf.write(lines.last);

    for (int i = 0; i < lines.length - 1; i++) {
      final line = lines[i].trim();
      if (line.isEmpty) continue;
      final status = ParpCodec.parseStatus(line);
      if (status != null) {
        onStatus?.call(status);
      }
    }
  }

  void _send(String sentence) {
    if (_tx == null || !_connected) return;
    try {
      _tx!.write(Uint8List.fromList(sentence.codeUnits));
    } catch (_) {
      _handleDisconnect();
    }
  }

  void _handleDisconnect() {
    disconnect();
  }
}
