unit uMeatMeetBLE;

interface

uses
  System.SysUtils, System.Classes, System.Bluetooth, System.Bluetooth.Components,
  System.Generics.Collections, System.Threading, System.StrUtils;

type
  TMeatMeetReading = record
    Zona1: Double;
    Zona2: Double;
    Zona3: Double;
    Zona4: Double;
    Zona5: Double;
    Ambiente: Double;
    Bateria: Byte;
    Timestamp: TDateTime;
    IsValid: Boolean;
  end;

  TBleTemperatureEvent = procedure(AReading: TMeatMeetReading) of object;
  TBleDeviceEvent = procedure(ADevice: TBluetoothLEDevice) of object;
  TBleConnectionEvent = procedure(AConnected: Boolean) of object;
  TBleLogEvent = procedure(const AMsg: string) of object;

  TMeatMeetParser = class
  public
    class function ParsePacket(const AData: TBytes): TMeatMeetReading;
  end;

  TMeatMeetBLEManager = class
  private
    FBluetoothLE: TBluetoothLE;
    FDiscoveredDevices: TList<TBluetoothLEDevice>;
    FCurrentDevice: TBluetoothLEDevice;
    FGattService: TBluetoothGattService;
    FGattCharacteristicNotify: TBluetoothGattCharacteristic;
    FGattCharacteristicWrite: TBluetoothGattCharacteristic;
    FIsConnected: Boolean;

    FOnTemperatureReceived: TBleTemperatureEvent;
    FOnDeviceDiscovered: TBleDeviceEvent;
    FOnConnectionStateChanged: TBleConnectionEvent;
    FOnLogMessage: TBleLogEvent;

    procedure BLEEndDiscoverDevices(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
    procedure BLECharacteristicRead(const Sender: TObject; const ACharacteristic: TBluetoothGattCharacteristic; AGattStatus: TBluetoothGattStatus);
    procedure DoLog(const AMsg: string);
    procedure SendInitCommand;
    procedure NotifyConnectionState(AConnected: Boolean);
    procedure DoConnectAndSubscribe;
  public
    const
      // UUID del servicio principal del MeatMeet Pro (para descubrimiento y Write)
      SERVICE_UUID       = '{A6ED0401-D344-460A-8075-B9E8EC90D71B}';
      // UUID del servicio que contiene la characteristic de notificación
      NOTIFY_SERVICE_UUID = '{0000F5A0-0000-1000-8000-00805F9B34FB}';
      // UUID de la characteristic que emite las notificaciones (54 bytes)
      NOTIFY_CHAR_UUID   = '{0000F5A1-0000-1000-8000-00805F9B34FB}';
      // UUID de la characteristic de escritura (para init/keep-alive)
      WRITE_CHAR_UUID    = '{A6ED0404-D344-460A-8075-B9E8EC90D71B}';

    constructor Create;
    destructor Destroy; override;

    procedure ScanForDevices;
    function GetDiscoveredDevices: TList<TBluetoothLEDevice>;
    procedure ConnectToDevice(ADevice: TBluetoothLEDevice);
    procedure Disconnect;
    function IsConnected: Boolean;

    property OnTemperatureReceived: TBleTemperatureEvent read FOnTemperatureReceived write FOnTemperatureReceived;
    property OnDeviceDiscovered: TBleDeviceEvent read FOnDeviceDiscovered write FOnDeviceDiscovered;
    property OnConnectionStateChanged: TBleConnectionEvent read FOnConnectionStateChanged write FOnConnectionStateChanged;
    property OnLogMessage: TBleLogEvent read FOnLogMessage write FOnLogMessage;
  end;

implementation

{ TMeatMeetParser }

class function TMeatMeetParser.ParsePacket(const AData: TBytes): TMeatMeetReading;
var
  RawTemp: Word;
begin
  Result.IsValid := False;
  Result.Timestamp := Now;

  if Length(AData) < 54 then Exit;
  if (AData[0] <> $4D) or (AData[1] <> $45) then Exit;

  Result.Bateria := AData[8];

  RawTemp := AData[11] or (AData[12] shl 8);
  Result.Zona1 := RawTemp / 100.0;

  Result.Ambiente := AData[13];

  RawTemp := AData[16] or (AData[17] shl 8);
  Result.Zona2 := RawTemp / 100.0;

  RawTemp := AData[18] or (AData[19] shl 8);
  Result.Zona3 := RawTemp / 100.0;

  RawTemp := AData[20] or (AData[21] shl 8);
  Result.Zona4 := RawTemp / 100.0;

  RawTemp := AData[22] or (AData[23] shl 8);
  Result.Zona5 := RawTemp / 100.0;

  Result.IsValid := True;
end;

{ TMeatMeetBLEManager }

constructor TMeatMeetBLEManager.Create;
begin
  FIsConnected := False;
  FDiscoveredDevices := TList<TBluetoothLEDevice>.Create;
  FBluetoothLE := TBluetoothLE.Create(nil);
  FBluetoothLE.OnEndDiscoverDevices := BLEEndDiscoverDevices;
  FBluetoothLE.OnCharacteristicRead := BLECharacteristicRead;
  // Habilitar el componente BLE (necesario en Windows)
  FBluetoothLE.Enabled := True;
end;

destructor TMeatMeetBLEManager.Destroy;
begin
  Disconnect;
  FBluetoothLE.Free;
  FDiscoveredDevices.Free;
  inherited;
end;

procedure TMeatMeetBLEManager.DoLog(const AMsg: string);
begin
  if Assigned(FOnLogMessage) then
  begin
    // Si estamos en el hilo principal, llamar directamente
    if TThread.Current.ThreadID = MainThreadID then
      FOnLogMessage(AMsg)
    else
      TThread.Queue(nil,
        TThreadProcedure(
          procedure
          begin
            if Assigned(FOnLogMessage) then
              FOnLogMessage(AMsg);
          end));
  end;
end;

procedure TMeatMeetBLEManager.NotifyConnectionState(AConnected: Boolean);
begin
  FIsConnected := AConnected;
  if Assigned(FOnConnectionStateChanged) then
  begin
    if TThread.Current.ThreadID = MainThreadID then
      FOnConnectionStateChanged(AConnected)
    else
      TThread.Queue(nil,
        TThreadProcedure(
          procedure
          begin
            if Assigned(FOnConnectionStateChanged) then
              FOnConnectionStateChanged(AConnected);
          end));
  end;
end;

procedure TMeatMeetBLEManager.ScanForDevices;
begin
  FDiscoveredDevices.Clear;
  DoLog('Iniciando escaneo BLE (10 segundos)...');
  DoLog('Buscando TODOS los dispositivos BLE cercanos...');

  // IMPORTANTE: DiscoverDevices debe llamarse desde el HILO PRINCIPAL
  // ya que los eventos OnEndDiscoverDevices son eventos VCL del componente.
  // DiscoverDevices es internamente asíncrono — NO bloquea la UI.
  // El parámetro es el timeout en milisegundos.
  try
    FBluetoothLE.DiscoverDevices(10000);
  except
    on E: Exception do
      DoLog('ERROR al iniciar escaneo: ' + E.Message +
            '. Verifique que el Bluetooth está activado.');
  end;
end;

procedure TMeatMeetBLEManager.BLEEndDiscoverDevices(const Sender: TObject;
  const ADeviceList: TBluetoothLEDeviceList);
var
  i: Integer;
  LDevice: TBluetoothLEDevice;
  LName, LAddr: string;
begin
  DoLog('Escaneo finalizado. Total dispositivos: ' + IntToStr(ADeviceList.Count));

  for i := 0 to ADeviceList.Count - 1 do
  begin
    LDevice := ADeviceList[i];
    LName := LDevice.DeviceName;
    LAddr := LDevice.Address;

    // Mostrar TODOS los dispositivos en el log (incluso sin nombre)
    if LName = '' then
      DoLog('  [' + IntToStr(i) + '] (sin nombre) MAC=' + LAddr)
    else
      DoLog('  [' + IntToStr(i) + '] ' + LName + ' MAC=' + LAddr);

    // Añadir al listado solo los que tienen nombre
    if LName <> '' then
    begin
      FDiscoveredDevices.Add(LDevice);
      if Assigned(FOnDeviceDiscovered) then
        FOnDeviceDiscovered(LDevice);
    end;
  end;

  if FDiscoveredDevices.Count = 0 then
  begin
    DoLog('');
    DoLog('⚠ No se encontraron dispositivos BLE con nombre.');
    DoLog('  Posibles causas:');
    DoLog('  1. El termómetro no está encendido');
    DoLog('  2. Está fuera de alcance');
    DoLog('  3. Otra app lo tiene conectado (ciérrela)');
    DoLog('  4. Pruebe desemparejar "ME_BOX01" de Windows');
    DoLog('     e intente escanear de nuevo');
  end
  else
    DoLog('Encontrados ' + IntToStr(FDiscoveredDevices.Count) +
          ' dispositivo(s). Seleccione uno y pulse Conectar.');
end;

function TMeatMeetBLEManager.GetDiscoveredDevices: TList<TBluetoothLEDevice>;
begin
  Result := FDiscoveredDevices;
end;

procedure TMeatMeetBLEManager.ConnectToDevice(ADevice: TBluetoothLEDevice);
begin
  DoLog('');
  DoLog('Conectando a "' + ADevice.DeviceName + '"...');
  FCurrentDevice := ADevice;

  // La conexión GATT y descubrimiento de servicios SÍ pueden bloquear,
  // por lo que se ejecutan en un hilo secundario
  TTask.Run(
    procedure
    begin
      DoConnectAndSubscribe;
    end);
end;

procedure TMeatMeetBLEManager.DoConnectAndSubscribe;
var
  LServices: TBluetoothGattServiceList;
  LService: TBluetoothGattService;
  LNotifyService: TBluetoothGattService;
  i: Integer;
begin
  try
    // Asegurar que el escaneo BLE está completamente detenido antes de conectar
    try
      FBluetoothLE.CancelDiscovery;
    except
    end;

    // Paso 1: Conectar y descubrir servicios GATT
    // En Windows WinRT BLE, TBluetoothLEDevice.Connect retorna False por diseño de Embarcadero (DoConnect no implementado).
    // La conexión GATT se realiza automáticamente al consultar los servicios con DiscoverServices.
    DoLog('  Paso 1/4: Conectando y descubriendo servicios GATT...');
    var LDiscovered := False;
    for var LAttempt := 1 to 3 do
    begin
      if LAttempt > 1 then
      begin
        DoLog('    Reintentando descubrir servicios (intento ' + IntToStr(LAttempt) + ' de 3)...');
        Sleep(800);
      end;
      try
        if FCurrentDevice.DiscoverServices then
        begin
          LDiscovered := True;
          Break;
        end;
      except
        on E: Exception do
          DoLog('    Aviso en intento ' + IntToStr(LAttempt) + ': ' + E.Message);
      end;
    end;

    if not LDiscovered then
    begin
      DoLog('  ✗ No se pudieron descubrir los servicios.');
      DoLog('    Posibles causas:');
      DoLog('    - El termómetro sigue conectado en la app Web o el móvil');
      DoLog('      (ciérrelos primero, solo permite 1 conexión a la vez)');
      DoLog('    - El dispositivo entró en reposo.');
      NotifyConnectionState(False);
      Exit;
    end;

    LServices := FCurrentDevice.Services;
    DoLog('  ✓ Servicios descubiertos: ' + IntToStr(LServices.Count));
    for i := 0 to LServices.Count - 1 do
      DoLog('    → ' + GUIDToString(LServices[i].UUID));

    // Paso 2: Buscar servicio de notificación
    DoLog('  Paso 2/4: Buscando servicio de notificación...');
    LNotifyService := nil;

    // Primero intentar con F5A0 (servicio de datos del MeatMeet)
    for i := 0 to LServices.Count - 1 do
    begin
      if ContainsText(GUIDToString(LServices[i].UUID), 'F5A0') then
      begin
        LNotifyService := LServices[i];
        DoLog('  ✓ Servicio F5A0 encontrado.');
        Break;
      end;
    end;

    // Fallback: servicio principal A6ED0401
    if not Assigned(LNotifyService) then
    begin
      DoLog('  Servicio F5A0 no encontrado, probando A6ED0401...');
      for i := 0 to LServices.Count - 1 do
      begin
        if ContainsText(GUIDToString(LServices[i].UUID), 'A6ED0401') then
        begin
          LNotifyService := LServices[i];
          DoLog('  ✓ Servicio A6ED0401 encontrado.');
          Break;
        end;
      end;
    end;

    if not Assigned(LNotifyService) then
    begin
      DoLog('  ✗ No se encontró ningún servicio compatible.');
      DoLog('  Servicios disponibles:');
      for i := 0 to LServices.Count - 1 do
        DoLog('    ' + GUIDToString(LServices[i].UUID));
      NotifyConnectionState(False);
      Exit;
    end;

    FGattService := LNotifyService;

    // Buscar characteristic de notificación (F5A1)
    FGattCharacteristicNotify := nil;
    for i := 0 to LNotifyService.Characteristics.Count - 1 do
    begin
      DoLog('    Char: ' + GUIDToString(LNotifyService.Characteristics[i].UUID));
      if ContainsText(GUIDToString(LNotifyService.Characteristics[i].UUID), 'F5A1') then
        FGattCharacteristicNotify := LNotifyService.Characteristics[i];
    end;

    if not Assigned(FGattCharacteristicNotify) then
    begin
      DoLog('  ✗ Characteristic de notificación F5A1 no encontrada.');
      NotifyConnectionState(False);
      Exit;
    end;
    DoLog('  ✓ Characteristic de notificación encontrada.');

    // Buscar characteristic de escritura (A6ED0404) en servicio principal
    FGattCharacteristicWrite := nil;
    LService := nil;
    for i := 0 to LServices.Count - 1 do
    begin
      if ContainsText(GUIDToString(LServices[i].UUID), 'A6ED0401') then
      begin
        LService := LServices[i];
        Break;
      end;
    end;

    if Assigned(LService) then
    begin
      for i := 0 to LService.Characteristics.Count - 1 do
      begin
        if ContainsText(GUIDToString(LService.Characteristics[i].UUID), 'A6ED0404') then
        begin
          FGattCharacteristicWrite := LService.Characteristics[i];
          DoLog('  ✓ Characteristic de escritura encontrada.');
          Break;
        end;
      end;
    end;

    // Paso 3: Suscribirse a notificaciones
    DoLog('  Paso 3/4: Suscribiendo a notificaciones...');
    var LSubscribed := False;
    if FBluetoothLE.SubscribeToCharacteristic(FCurrentDevice, FGattCharacteristicNotify) then
      LSubscribed := True
    else
    begin
      // Fallback
      DoLog('  Subscribe directo falló, probando método alternativo...');
      FCurrentDevice.SetCharacteristicNotification(FGattCharacteristicNotify, True);
      LSubscribed := True;
    end;

    if LSubscribed then
      DoLog('  ✓ Notificaciones activadas.')
    else
      DoLog('  ⚠ No se pudo confirmar la suscripción de notificaciones.');

    // Paso 4: Enviar comando de inicialización
    DoLog('  Paso 4/4: Enviando comando init [01 06 02]...');
    SendInitCommand;
    Sleep(500);

    DoLog('  ✓ ¡CONECTADO! Recibiendo temperaturas...');
    NotifyConnectionState(True);
  except
    on E: Exception do
    begin
      DoLog('  ✗ EXCEPCIÓN: ' + E.Message);
      NotifyConnectionState(False);
    end;
  end;
end;

procedure TMeatMeetBLEManager.SendInitCommand;
var
  LInitData: TBytes;
begin
  if Assigned(FGattCharacteristicWrite) then
  begin
    LInitData := TBytes.Create($01, $06, $02);
    FGattCharacteristicWrite.Value := LInitData;
    FCurrentDevice.WriteCharacteristic(FGattCharacteristicWrite);
    DoLog('    ✓ Comando de inicialización enviado.');
  end
  else
    DoLog('    ⚠ Write characteristic no disponible.');
end;

procedure TMeatMeetBLEManager.Disconnect;
begin
  try
    if Assigned(FCurrentDevice) then
    begin
      if Assigned(FGattCharacteristicNotify) then
      begin
        try
          FBluetoothLE.UnSubscribeToCharacteristic(FCurrentDevice, FGattCharacteristicNotify);
        except
        end;
      end;
      try
        FCurrentDevice.Disconnect;
      except
      end;
    end;
  finally
    NotifyConnectionState(False);
    FCurrentDevice := nil;
    FGattService := nil;
    FGattCharacteristicNotify := nil;
    FGattCharacteristicWrite := nil;
    DoLog('Desconectado.');
  end;
end;

function TMeatMeetBLEManager.IsConnected: Boolean;
begin
  Result := FIsConnected and Assigned(FCurrentDevice);
end;

procedure TMeatMeetBLEManager.BLECharacteristicRead(const Sender: TObject;
  const ACharacteristic: TBluetoothGattCharacteristic; AGattStatus: TBluetoothGattStatus);
var
  LData: TBytes;
  LReading: TMeatMeetReading;
begin
  if AGattStatus <> TBluetoothGattStatus.Success then Exit;

  LData := ACharacteristic.Value;
  LReading := TMeatMeetParser.ParsePacket(LData);
  if LReading.IsValid and Assigned(FOnTemperatureReceived) then
  begin
    TThread.Queue(nil,
      TThreadProcedure(
        procedure
        begin
          if Assigned(FOnTemperatureReceived) then
            FOnTemperatureReceived(LReading);
        end));
  end;
end;

end.
