{
  Copyright (C)
  2026 - Uladzimir Karpenka, EW8BAK

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
}

unit DeviceStore;

{$mode objfpc}{$H+}

// Общее хранилище устройств — единый источник правды для десктоп-диалога
// (DeviceForm) и web-оверлея. Владеет сохранёнными устройствами (persist в
// hpsdr_devices.ini) и текущим списком найденных при discovery.
// Контроллер (TRadioController) держит один экземпляр; фронтенды работают
// через него, поэтому списки не расходятся.

interface

uses
  Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils, RadioBackend;

const
  DEVICE_CFG_NAME = 'hpsdr_devices.ini';

type
  // Запись о сохранённом устройстве
  TSavedDevice = record
    Name:      string;   // пользовательское имя
    IPAddress: string;
    BoardType: Integer;
    AutoStart: Boolean;  // запускать автоматически при старте
    Kind:      TBackendKind;   // bkHPSDR / bkPluto
    URI:       string;         // Pluto: 'ip:..' / 'usb:..'
    Serial:    string;         // Pluto serial
    // MAC радио, узнанный при прошлом подключении. Настройки персистятся по MAC,
    // а старт по сохранённому IP (AutoStart/CONNECT) идёт без дискавери — без
    // этого поля профиль уезжал бы в «нулевой» MAC. Пусто = ещё не знаем.
    MAC:       array[0..5] of Byte;
  end;

  // Найденное устройство (результат discovery)
  TDiscoveredDevice = record
    IPAddress:   string;
    DisplayName: string;
    BoardType:   Integer;
    MAC:         array[0..5] of Byte;   // нужен для preload-rate lookup (LoadDevice по MAC)
    Kind:        TBackendKind;          // bkHPSDR / bkPluto
    URI:         string;                // Pluto: context URI
    Serial:      string;                // Pluto serial
  end;

  { TDeviceStore }
  TDeviceStore = class
  private
    FSaved:      array of TSavedDevice;
    FSavedCount: Integer;
    FDiscovered: array of TDiscoveredDevice;
    FDiscoveredCount: Integer;
  public
    constructor Create;

    // ---- Сохранённые устройства ----
    procedure LoadSaved;
    procedure SaveSaved;
    function  SavedCount: Integer;
    function  Saved(Idx: Integer): TSavedDevice;
    // Добавляет запись (Name пустое → 'HPSDR'); возвращает индекс новой записи.
    function  AddSaved(const AName, IP: string; BoardType: Integer): Integer;
    // Добавляет Pluto-устройство (Kind=bkPluto) с URI/Serial.
    function  AddSavedPluto(const AName, AURI, ASerial: string): Integer;
    procedure RemoveSaved(Idx: Integer);
    procedure SetAutoStart(Idx: Integer);   // эксклюзивно: только одно autostart
    function  AutoStartIP: string;
    function  AutoStartBoardType: Integer;
    // Полная autostart-запись (Kind/URI/Serial) — для бэкенд-агностичного автозапуска
    // (Pluto открывается по URI, у USB-Pluto IPAddress пуст). False — нет autostart.
    function  AutoStartDevice(out Dev: TSavedDevice): Boolean;
    function  SavedBoardType(Idx: Integer): Integer;
    function  SavedDisplay(Idx: Integer): string;   // строка для списка (с * и board)

    // ---- Найденные (discovery) ----
    procedure ClearDiscovered;
    procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
                            const MAC: array of Byte;
                            Kind: TBackendKind = bkHPSDR;
                            const URI: string = ''; const Serial: string = '');
    function  DiscoveredCount: Integer;
    function  Discovered(Idx: Integer): TDiscoveredDevice;
    // IP найденного устройства по IP (для preload-rate lookup и т.п.)
    function  FindDiscoveredByIP(const IP: string): Integer;

    // ---- MAC сохранённого устройства ----
    // Поиск сохранённой записи по IP или URI (Pluto хранит адрес в обоих полях).
    function  FindSavedByAddr(const Addr: string): Integer;
    // Запоминает MAC за адресом (после успешного connect). Пишет ini только при
    // изменении, чтобы не дёргать диск на каждом старте.
    procedure SetSavedMac(const Addr: string; const MAC: array of Byte);
  end;

// MAC == 00:00:00:00:00:00 — «неизвестен».
function MacIsZero(const MAC: array of Byte): Boolean;

implementation

function MacIsZero(const MAC: array of Byte): Boolean;
var I: Integer;
begin
  Result := True;
  for I := 0 to High(MAC) do
    if MAC[I] <> 0 then Exit(False);
end;

function MacToIniStr(const MAC: array of Byte): string;
var I: Integer;
begin
  Result := '';
  for I := 0 to 5 do
  begin
    if I > 0 then Result := Result + ':';
    Result := Result + IntToHex(MAC[I], 2);
  end;
end;

procedure IniStrToMac(const S: string; out MAC: array of Byte);
var Parts: TStringArray; I, V: Integer;
begin
  FillChar(MAC[0], 6, 0);
  Parts := S.Split([':']);
  if Length(Parts) <> 6 then Exit;
  for I := 0 to 5 do
  begin
    V := StrToIntDef('$' + Parts[I], -1);
    if (V < 0) or (V > 255) then begin FillChar(MAC[0], 6, 0); Exit; end;
    MAC[I] := Byte(V);
  end;
end;

{ TDeviceStore }

constructor TDeviceStore.Create;
begin
  inherited Create;
  FSavedCount      := 0;
  FDiscoveredCount := 0;
  LoadSaved;
end;

procedure TDeviceStore.LoadSaved;
var
  Ini: TIniFile;
  I, N: Integer;
  Section: string;
begin
  FSavedCount := 0;
  SetLength(FSaved, 0);
  if not FileExists(GetAppCfgDir + DEVICE_CFG_NAME) then Exit;

  Ini := TIniFile.Create(GetAppCfgDir + DEVICE_CFG_NAME);
  try
    N := Ini.ReadInteger('Devices', 'Count', 0);
    SetLength(FSaved, N);
    for I := 0 to N - 1 do
    begin
      Section := 'Device' + IntToStr(I);
      FSaved[I].Name      := Ini.ReadString (Section, 'Name',      'HPSDR');
      FSaved[I].IPAddress := Ini.ReadString (Section, 'IP',        '');
      FSaved[I].BoardType := Ini.ReadInteger(Section, 'BoardType', 0);
      FSaved[I].AutoStart := Ini.ReadBool   (Section, 'AutoStart', False);
      FSaved[I].Kind      := TBackendKind(Ini.ReadInteger(Section, 'Kind', 0));
      FSaved[I].URI       := Ini.ReadString (Section, 'URI',       '');
      FSaved[I].Serial    := Ini.ReadString (Section, 'Serial',    '');
      IniStrToMac(Ini.ReadString(Section, 'MAC', ''), FSaved[I].MAC);
      Inc(FSavedCount);
    end;
  finally
    Ini.Free;
  end;
end;

procedure TDeviceStore.SaveSaved;
var
  Ini: TIniFile;
  I:   Integer;
  Section: string;
begin
  Ini := TIniFile.Create(GetAppCfgDir + DEVICE_CFG_NAME);
  try
    Ini.WriteInteger('Devices', 'Count', FSavedCount);
    for I := 0 to FSavedCount - 1 do
    begin
      Section := 'Device' + IntToStr(I);
      Ini.WriteString (Section, 'Name',      FSaved[I].Name);
      Ini.WriteString (Section, 'IP',        FSaved[I].IPAddress);
      Ini.WriteInteger(Section, 'BoardType', FSaved[I].BoardType);
      Ini.WriteBool   (Section, 'AutoStart', FSaved[I].AutoStart);
      Ini.WriteInteger(Section, 'Kind',      Ord(FSaved[I].Kind));
      Ini.WriteString (Section, 'URI',       FSaved[I].URI);
      Ini.WriteString (Section, 'Serial',    FSaved[I].Serial);
      if MacIsZero(FSaved[I].MAC) then
        Ini.DeleteKey(Section, 'MAC')
      else
        Ini.WriteString(Section, 'MAC', MacToIniStr(FSaved[I].MAC));
    end;
  finally
    Ini.Free;
  end;
end;

function TDeviceStore.SavedCount: Integer;
begin
  Result := FSavedCount;
end;

function TDeviceStore.Saved(Idx: Integer): TSavedDevice;
begin
  if (Idx >= 0) and (Idx < FSavedCount) then
    Result := FSaved[Idx]
  else
  begin
    Result.Name := ''; Result.IPAddress := '';
    Result.BoardType := 0; Result.AutoStart := False;
    FillChar(Result.MAC, SizeOf(Result.MAC), 0);
  end;
end;

function TDeviceStore.AddSaved(const AName, IP: string; BoardType: Integer): Integer;
begin
  Result := FSavedCount;
  Inc(FSavedCount);
  SetLength(FSaved, FSavedCount);
  if Trim(AName) <> '' then FSaved[Result].Name := Trim(AName)
  else                      FSaved[Result].Name := 'HPSDR';
  FSaved[Result].IPAddress := Trim(IP);
  FSaved[Result].BoardType := BoardType;
  FSaved[Result].AutoStart := False;
  FSaved[Result].Kind      := bkHPSDR;
  FSaved[Result].URI       := '';
  FSaved[Result].Serial    := '';
  FillChar(FSaved[Result].MAC, SizeOf(FSaved[Result].MAC), 0);
  SaveSaved;
end;

function TDeviceStore.AddSavedPluto(const AName, AURI, ASerial: string): Integer;
begin
  Result := FSavedCount;
  Inc(FSavedCount);
  SetLength(FSaved, FSavedCount);
  if Trim(AName) <> '' then FSaved[Result].Name := Trim(AName)
  else                      FSaved[Result].Name := 'PlutoSDR';
  FSaved[Result].IPAddress := Trim(AURI);   // в списке показываем URI
  FSaved[Result].BoardType := 0;
  FSaved[Result].AutoStart := False;
  FSaved[Result].Kind      := bkPluto;
  FSaved[Result].URI       := Trim(AURI);
  FSaved[Result].Serial    := Trim(ASerial);
  FillChar(FSaved[Result].MAC, SizeOf(FSaved[Result].MAC), 0);
  SaveSaved;
end;

procedure TDeviceStore.RemoveSaved(Idx: Integer);
var
  I: Integer;
begin
  if (Idx < 0) or (Idx >= FSavedCount) then Exit;
  for I := Idx to FSavedCount - 2 do
    FSaved[I] := FSaved[I + 1];
  Dec(FSavedCount);
  SetLength(FSaved, FSavedCount);
  SaveSaved;
end;

procedure TDeviceStore.SetAutoStart(Idx: Integer);
var
  I: Integer;
begin
  if (Idx < 0) or (Idx >= FSavedCount) then Exit;
  // Только одно устройство может быть AutoStart
  for I := 0 to FSavedCount - 1 do
    FSaved[I].AutoStart := (I = Idx);
  SaveSaved;
end;

function TDeviceStore.AutoStartIP: string;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to FSavedCount - 1 do
    if FSaved[I].AutoStart then
    begin
      Result := FSaved[I].IPAddress;
      Exit;
    end;
end;

function TDeviceStore.AutoStartBoardType: Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := 0 to FSavedCount - 1 do
    if FSaved[I].AutoStart then
    begin
      Result := FSaved[I].BoardType;
      Exit;
    end;
end;

function TDeviceStore.AutoStartDevice(out Dev: TSavedDevice): Boolean;
var
  I: Integer;
begin
  Result := False;
  for I := 0 to FSavedCount - 1 do
    if FSaved[I].AutoStart then
    begin
      Dev := FSaved[I];
      Exit(True);
    end;
end;

function TDeviceStore.SavedBoardType(Idx: Integer): Integer;
begin
  if (Idx >= 0) and (Idx < FSavedCount) then
    Result := FSaved[Idx].BoardType
  else
    Result := 0;
end;

function TDeviceStore.SavedDisplay(Idx: Integer): string;
begin
  Result := '';
  if (Idx < 0) or (Idx >= FSavedCount) then Exit;
  Result := FSaved[Idx].Name + '  [' + FSaved[Idx].IPAddress + ']';
  if FSaved[Idx].BoardType > 0 then
    Result := Result + '  ' + BoardTypeName(FSaved[Idx].BoardType);
  if FSaved[Idx].AutoStart then
    Result := '* ' + Result;
end;

procedure TDeviceStore.ClearDiscovered;
begin
  FDiscoveredCount := 0;
  SetLength(FDiscovered, 0);
end;

procedure TDeviceStore.AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
  const MAC: array of Byte; Kind: TBackendKind; const URI: string; const Serial: string);
var
  Idx, J: Integer;
begin
  Idx := FDiscoveredCount;
  Inc(FDiscoveredCount);
  SetLength(FDiscovered, FDiscoveredCount);
  FDiscovered[Idx].IPAddress   := IP;
  FDiscovered[Idx].DisplayName := DisplayName;
  FDiscovered[Idx].BoardType   := BoardType;
  FDiscovered[Idx].Kind        := Kind;
  FDiscovered[Idx].URI         := URI;
  FDiscovered[Idx].Serial      := Serial;
  FillChar(FDiscovered[Idx].MAC, SizeOf(FDiscovered[Idx].MAC), 0);
  for J := 0 to High(MAC) do
    if J <= 5 then FDiscovered[Idx].MAC[J] := MAC[J];
end;

function TDeviceStore.DiscoveredCount: Integer;
begin
  Result := FDiscoveredCount;
end;

function TDeviceStore.Discovered(Idx: Integer): TDiscoveredDevice;
begin
  if (Idx >= 0) and (Idx < FDiscoveredCount) then
    Result := FDiscovered[Idx]
  else
  begin
    Result.IPAddress := ''; Result.DisplayName := ''; Result.BoardType := 0;
  end;
end;

function TDeviceStore.FindDiscoveredByIP(const IP: string): Integer;
var
  I: Integer;
begin
  Result := -1;
  for I := 0 to FDiscoveredCount - 1 do
    if SameText(FDiscovered[I].IPAddress, IP) then
    begin
      Result := I;
      Exit;
    end;
end;

function TDeviceStore.FindSavedByAddr(const Addr: string): Integer;
var
  I: Integer;
begin
  Result := -1;
  if Trim(Addr) = '' then Exit;
  for I := 0 to FSavedCount - 1 do
    if SameText(FSaved[I].IPAddress, Addr) or SameText(FSaved[I].URI, Addr) then
    begin
      Result := I;
      Exit;
    end;
end;

procedure TDeviceStore.SetSavedMac(const Addr: string; const MAC: array of Byte);
var
  Idx, J: Integer;
  Cur: array[0..5] of Byte;
begin
  if MacIsZero(MAC) then Exit;
  Idx := FindSavedByAddr(Addr);
  if Idx < 0 then Exit;
  FillChar(Cur, SizeOf(Cur), 0);
  for J := 0 to High(MAC) do
    if J <= 5 then Cur[J] := MAC[J];
  if CompareByte(Cur, FSaved[Idx].MAC, 6) = 0 then Exit;   // уже записан
  Move(Cur[0], FSaved[Idx].MAC[0], 6);
  SaveSaved;
end;

end.
