{
  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 DeviceForm;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FlatButton, FlatEdit, FlatListBox, AppTheme, Forms, Controls, Graphics, Dialogs,
  StdCtrls, ExtCtrls, ComCtrls, LCLType,
  BoardUtils, DeviceStore, RadioBackend, DpiUtils;

type
  // Результат диалога
  TDeviceDialogResult = record
    Accepted:  Boolean;
    IPAddress: string;
    SavedIdx:  Integer;  // -1 если выбрали из discovery, иначе индекс в SavedDevices
  end;

  { TDeviceDialog }
  TDeviceDialog = class(TForm)
  private
    // Общее хранилище устройств (saved+discovered) — единый источник правды,
    // владелец контроллер; диалог только рендерит и редактирует через него.
    FStore:        TDeviceStore;
    FResult:       TDeviceDialogResult;

    // UI
    PanelLeft:     TPanel;
    PanelRight:    TPanel;

    LblSaved:      TLabel;
    LstSaved:      TFlatListBox;
    BtnAdd:        TFlatButton;
    BtnRemove:     TFlatButton;
    BtnSetAuto:    TFlatButton;
    EdName:        TFlatEdit;
    EdIP:          TFlatEdit;
    LblName:       TLabel;
    LblIP:         TLabel;
    LblAutoHint:   TLabel;

    LblFound:      TLabel;
    LstFound:      TFlatListBox;
    BtnDiscover:   TFlatButton;
    BtnAddFound:   TFlatButton;

    BtnConnect:    TFlatButton;
    BtnCancel:     TFlatButton;

    FOnDiscover:   TNotifyEvent;  // внешний callback для запуска discovery

    procedure BuildUI;
    procedure SetStore(AStore: TDeviceStore);
    procedure RefreshSavedList;

    procedure BtnDiscoverClick(Sender: TObject);
    procedure BtnAddClick(Sender: TObject);
    procedure BtnRemoveClick(Sender: TObject);
    procedure BtnSetAutoClick(Sender: TObject);
    procedure BtnAddFoundClick(Sender: TObject);
    procedure BtnConnectClick(Sender: TObject);
    procedure BtnCancelClick(Sender: TObject);
    procedure LstSavedDblClick(Sender: TObject);
    procedure LstFoundDblClick(Sender: TObject);
    procedure LstSavedClick(Sender: TObject);

    function  MakeBtn(AParent: TWinControl; const Cap: string;
                      X, Y, W, H: Integer; AClick: TNotifyEvent): TFlatButton;
    function  MakeLbl(AParent: TWinControl; const Cap: string;
                      X, Y: Integer): TLabel;
  public
    constructor Create(AOwner: TComponent); override;

    procedure SetTheme(const T: TAppTheme);

    // Перерисовать список найденных из store (вызывается по rfDeviceList)
    procedure RefreshFound;
    procedure ClearDiscovered;
    procedure NoDevicesFound;   // UI-сообщение «нет устройств» в список найденных

    // Автозапуск: возвращает IP если есть устройство с AutoStart=True
    function  GetAutoStartIP: string;
    function  GetAutoStartBoardType: Integer;
    function  GetSavedBoardType(Idx: Integer): Integer;

    // Текст поля ручного ввода IP/URI (для probe сетевых Pluto при discovery).
    function  ManualIP: string;

    // Получить/сбросить результат
    procedure ClearResult;
    // Общее хранилище устройств — назначается владельцем (MainForm/контроллер)
    // до показа диалога; диалог рендерит и редактирует через него.
    property Store: TDeviceStore read FStore write SetStore;
    property DialogResult: TDeviceDialogResult read FResult;
    property OnDiscover: TNotifyEvent read FOnDiscover write FOnDiscover;
  end;

implementation

const
  CLR_BG      = TColor($00121212);
  CLR_PANEL   = TColor($001A1A1A);
  CLR_TEXT    = TColor($00E0E0E0);
  CLR_TEXTDIM = TColor($00888888);
  CLR_ACCENT  = TColor($0040FF80);
  CLR_AUTO    = TColor($0000CCFF);  // цвет авто-устройства
  DLG_W       = 760;
  DLG_H       = 500;
  GAP         = 12;
  PAD         = 14;
  BTN_H       = 28;
  EDIT_H      = 26;
  PANEL_H     = 412;
  LEFT_W      = 348;
  RIGHT_W     = 372;

{ TDeviceDialog }

constructor TDeviceDialog.Create(AOwner: TComponent);
begin
  inherited CreateNew(AOwner);
  // Форма целиком строится кодом и уже масштабирует геометрию через DpiScale.
  // Автоскейл LCL поверх неё дал бы повторный множитель (125% -> 156.25%).
  Scaled       := False;
  Caption      := 'Device Selection';
  Width        := DpiScale(DLG_W);
  Height       := DpiScale(DLG_H);
  Position     := poScreenCenter;
  BorderStyle  := bsDialog;
  Color        := CLR_BG;
  Font.Size    := 8;
  Font.Color   := CLR_TEXT;

  FStore           := nil;   // назначается владельцем через property Store до показа
  ClearResult;

  BuildUI;
  SetTheme(DarkTheme);
end;

procedure TDeviceDialog.SetStore(AStore: TDeviceStore);
begin
  FStore := AStore;
  if FStore <> nil then RefreshSavedList;
end;

function TDeviceDialog.MakeBtn(AParent: TWinControl; const Cap: string;
  X, Y, W, H: Integer; AClick: TNotifyEvent): TFlatButton;
begin
  // MakeFlatBtn — общая фабрика в FlatButton.pas, используется и другими
  // окнами (PanafallPanel/PopSignalPopup/...) со своей раскладкой; масштаб
  // применяем только здесь, на границе вызова из DeviceForm, а не внутри
  // самой MakeFlatBtn (иначе задело бы все остальные вызовы).
  Result := MakeFlatBtn(AParent, Cap, DpiScale(X), DpiScale(Y), DpiScale(W), DpiScale(H), AClick);
end;

function TDeviceDialog.MakeLbl(AParent: TWinControl; const Cap: string;
  X, Y: Integer): TLabel;
begin
  Result := TLabel.Create(Self);
  Result.Parent  := AParent;
  Result.Caption := Cap;
  Result.Left    := DpiScale(X); Result.Top := DpiScale(Y);
  Result.Font.Size  := 8;
  Result.Font.Color := CLR_TEXTDIM;
end;

procedure TDeviceDialog.BuildUI;
const
  LabelH = 18;
var
  FieldTop, ButtonsTop, BottomTop: Integer;
begin
  // --- Левая панель: сохранённые устройства ---
  PanelLeft := TPanel.Create(Self);
  PanelLeft.Parent     := Self;
  PanelLeft.SetBounds(DpiScale(GAP), DpiScale(GAP), DpiScale(LEFT_W), DpiScale(PANEL_H));
  PanelLeft.BevelOuter := bvNone;
  PanelLeft.Color      := CLR_PANEL;

  LblSaved := MakeLbl(PanelLeft, 'SAVED DEVICES', PAD, PAD);

  LstSaved := TFlatListBox.Create(Self);
  LstSaved.Parent    := PanelLeft;
  LstSaved.SetBounds(DpiScale(PAD), DpiScale(PAD + LabelH), DpiScale(LEFT_W - PAD * 2), DpiScale(174));
  LstSaved.Color     := CLR_BG;
  LstSaved.Font.Color:= CLR_TEXT;
  LstSaved.Font.Size := 8;
  LstSaved.OnClick    := @LstSavedClick;
  LstSaved.OnDblClick := @LstSavedDblClick;

  FieldTop := 212;
  LblName := MakeLbl(PanelLeft, 'Name:', PAD, FieldTop + 5);
  EdName := TFlatEdit.Create(Self);
  EdName.Parent    := PanelLeft;
  EdName.SetBounds(DpiScale(76), DpiScale(FieldTop), DpiScale(LEFT_W - 76 - PAD), DpiScale(EDIT_H));
  EdName.Color     := CLR_BG;
  EdName.Font.Color:= CLR_TEXT;
  EdName.Font.Size := 8;

  Inc(FieldTop, EDIT_H + 10);
  LblIP := MakeLbl(PanelLeft, 'IP:', PAD, FieldTop + 5);
  EdIP := TFlatEdit.Create(Self);
  EdIP.Parent    := PanelLeft;
  EdIP.SetBounds(DpiScale(76), DpiScale(FieldTop), DpiScale(LEFT_W - 76 - PAD), DpiScale(EDIT_H));
  EdIP.Color     := CLR_BG;
  EdIP.Font.Color:= CLR_TEXT;
  EdIP.Font.Size := 8;
  EdIP.TextHint  := '192.168.1.x';

  ButtonsTop := FieldTop + EDIT_H + 12;
  BtnAdd    := MakeBtn(PanelLeft, 'ADD',    PAD, ButtonsTop, 94, BTN_H, @BtnAddClick);
  BtnRemove := MakeBtn(PanelLeft, 'REMOVE', PAD + 102, ButtonsTop, 94, BTN_H, @BtnRemoveClick);

  Inc(ButtonsTop, BTN_H + 8);
  BtnSetAuto := MakeBtn(PanelLeft, 'SET AUTOSTART', PAD, ButtonsTop, 154, BTN_H, @BtnSetAutoClick);
  LblAutoHint := MakeLbl(PanelLeft, '* = autostart',
    PAD + 168, ButtonsTop + 6);
  LblAutoHint.Font.Color := CLR_AUTO;

  BottomTop := PANEL_H - PAD - BTN_H - 6;
  BtnConnect := MakeBtn(PanelLeft, 'CONNECT', PAD, BottomTop, LEFT_W - PAD * 2, BTN_H + 6, @BtnConnectClick);
  BtnConnect.ClrText    := CLR_ACCENT;
  BtnConnect.ClrTextAct := CLR_ACCENT;

  // --- Правая панель: discovery ---
  PanelRight := TPanel.Create(Self);
  PanelRight.Parent     := Self;
  PanelRight.SetBounds(DpiScale(GAP + LEFT_W + GAP), DpiScale(GAP), DpiScale(RIGHT_W), DpiScale(PANEL_H));
  PanelRight.BevelOuter := bvNone;
  PanelRight.Color      := CLR_PANEL;

  LblFound := MakeLbl(PanelRight, 'DISCOVERED DEVICES', PAD, PAD);

  LstFound := TFlatListBox.Create(Self);
  LstFound.Parent    := PanelRight;
  LstFound.SetBounds(DpiScale(PAD), DpiScale(PAD + LabelH), DpiScale(RIGHT_W - PAD * 2), DpiScale(250));
  LstFound.Color     := CLR_BG;
  LstFound.Font.Color:= CLR_TEXT;
  LstFound.Font.Size := 8;
  LstFound.OnDblClick := @LstFoundDblClick;

  ButtonsTop := PAD + LabelH + 250 + 14;
  BtnDiscover := MakeBtn(PanelRight, 'DISCOVER', PAD, ButtonsTop, 116, BTN_H, @BtnDiscoverClick);
  BtnAddFound := MakeBtn(PanelRight, 'SAVE DEVICE', PAD + 128, ButtonsTop, 140, BTN_H, @BtnAddFoundClick);

  BtnCancel := MakeBtn(PanelRight, 'CANCEL', RIGHT_W - PAD - 116, BottomTop, 116, BTN_H + 6, @BtnCancelClick);
end;

procedure TDeviceDialog.SetTheme(const T: TAppTheme);

  procedure StyleButton(B: TFlatButton; Active: Boolean);
  begin
    if B = nil then Exit;
    B.Active     := Active;
    B.ClrNorm    := T.BtnNorm;
    B.ClrActive  := T.BtnActive;
    B.ClrHot     := T.BtnHot;
    if Active then B.ClrBorder := T.BtnBorderActive
    else           B.ClrBorder := T.BtnBorderNorm;
    B.ClrText    := T.BtnText;
    B.ClrTextAct := T.BtnTextActive;
    B.Font.Size  := 8;
    B.Font.Color := T.Text;
    B.Invalidate;
  end;

  procedure StyleDiscoverButton(B: TFlatButton);
  begin
    if B = nil then Exit;
    B.Active     := False;
    B.ClrNorm    := T.TbDiscoverNorm;
    B.ClrHot     := T.TbDiscoverHot;
    B.ClrActive  := T.TbDiscoverHot;
    B.ClrBorder  := T.TbDiscoverBorder;
    B.ClrText    := T.TbDiscoverText;
    B.ClrTextAct := T.TbDiscoverText;
    B.Font.Size  := 8;
    B.Font.Color := T.TbDiscoverText;
    B.Invalidate;
  end;

  procedure StyleConnectButton(B: TFlatButton);
  begin
    if B = nil then Exit;
    B.Active     := False;
    B.ClrNorm    := T.TbStartNorm;
    B.ClrActive  := T.BtnActive;
    B.ClrHot     := T.BtnHot;
    B.ClrBorder  := T.TbStartBorder;
    B.ClrText    := T.TbStartText;
    B.ClrTextAct := T.TbStartText;
    B.Font.Size  := 8;
    B.Font.Color := T.TbStartText;
    B.Invalidate;
  end;

  procedure WalkLabels(C: TWinControl);
  var i: Integer; Ctrl: TControl;
  begin
    for i := 0 to C.ControlCount - 1 do
    begin
      Ctrl := C.Controls[i];
      if Ctrl is TLabel then
        TLabel(Ctrl).Font.Color := T.TextDim
      else if Ctrl is TWinControl then
        WalkLabels(TWinControl(Ctrl));
    end;
  end;

begin
  Color      := T.BG;
  Font.Color := T.Text;
  if PanelLeft  <> nil then PanelLeft.Color  := T.Panel;
  if PanelRight <> nil then PanelRight.Color := T.Panel;
  if LstSaved <> nil then LstSaved.SetAppTheme(T);
  if LstFound <> nil then LstFound.SetAppTheme(T);
  StyleButton(BtnAdd,      False);
  StyleButton(BtnRemove,   False);
  StyleButton(BtnSetAuto,  False);
  StyleDiscoverButton(BtnDiscover);
  StyleButton(BtnAddFound, False);
  StyleButton(BtnCancel,   False);
  StyleConnectButton(BtnConnect);
  if EdName <> nil then EdName.SetAppTheme(T);
  if EdIP   <> nil then EdIP.SetAppTheme(T);
  WalkLabels(Self);
  if LblAutoHint <> nil then LblAutoHint.Font.Color := T.Amber;
  Invalidate;
end;

procedure TDeviceDialog.RefreshSavedList;
var
  I: Integer;
begin
  LstSaved.Items.Clear;
  if FStore = nil then Exit;
  for I := 0 to FStore.SavedCount - 1 do
    LstSaved.Items.Add(FStore.SavedDisplay(I));
end;

procedure TDeviceDialog.LstSavedClick(Sender: TObject);
var
  Idx: Integer;
  Dev: TSavedDevice;
begin
  Idx := LstSaved.ItemIndex;
  if (FStore = nil) or (Idx < 0) or (Idx >= FStore.SavedCount) then Exit;
  Dev := FStore.Saved(Idx);
  EdName.Text := Dev.Name;
  EdIP.Text   := Dev.IPAddress;
end;

procedure TDeviceDialog.LstSavedDblClick(Sender: TObject);
begin
  BtnConnectClick(nil);
end;

procedure TDeviceDialog.LstFoundDblClick(Sender: TObject);
begin
  BtnConnectClick(nil);
end;

procedure TDeviceDialog.BtnDiscoverClick(Sender: TObject);
begin
  LstFound.Items.Clear;
  LstFound.Items.Add('Searching...');
  if Assigned(FOnDiscover) then
    FOnDiscover(Self);
end;

procedure TDeviceDialog.ClearDiscovered;
begin
  if FStore <> nil then FStore.ClearDiscovered;
  LstFound.Items.Clear;
end;

procedure TDeviceDialog.RefreshFound;
// Рендер списка найденных из store. Пустой store во время поиска → 'Searching...'.
var
  I: Integer;
  S: string;
  D: TDiscoveredDevice;
begin
  if FStore = nil then Exit;
  LstFound.Items.Clear;
  if FStore.DiscoveredCount = 0 then
  begin
    LstFound.Items.Add('Searching...');
    Exit;
  end;
  for I := 0 to FStore.DiscoveredCount - 1 do
  begin
    D := FStore.Discovered(I);
    S := D.DisplayName;
    if D.BoardType > 0 then
      S := S + '  ' + BoardTypeName(D.BoardType);
    LstFound.Items.Add(S);
  end;
end;

procedure TDeviceDialog.NoDevicesFound;
begin
  ClearDiscovered;
  LstFound.Items.Add('-- no device found --');
end;

procedure TDeviceDialog.BtnAddClick(Sender: TObject);
var
  Idx: Integer;
begin
  if FStore = nil then Exit;
  if Trim(EdIP.Text) = '' then
  begin
    ShowMessage('Enter IP address');
    Exit;
  end;
  Idx := FStore.AddSaved(EdName.Text, EdIP.Text, 0);
  RefreshSavedList;
  LstSaved.ItemIndex := Idx;
end;

procedure TDeviceDialog.BtnRemoveClick(Sender: TObject);
var
  Idx: Integer;
begin
  Idx := LstSaved.ItemIndex;
  if (FStore = nil) or (Idx < 0) or (Idx >= FStore.SavedCount) then Exit;
  FStore.RemoveSaved(Idx);
  RefreshSavedList;
  EdName.Text := '';
  EdIP.Text   := '';
end;

procedure TDeviceDialog.BtnSetAutoClick(Sender: TObject);
var
  Idx: Integer;
begin
  Idx := LstSaved.ItemIndex;
  if (FStore = nil) or (Idx < 0) or (Idx >= FStore.SavedCount) then
  begin
    ShowMessage('Select a device first');
    Exit;
  end;
  FStore.SetAutoStart(Idx);
  RefreshSavedList;
  LstSaved.ItemIndex := Idx;
end;

procedure TDeviceDialog.BtnAddFoundClick(Sender: TObject);
var
  Idx, NewIdx: Integer;
  D: TDiscoveredDevice;
begin
  Idx := LstFound.ItemIndex;
  if (FStore = nil) or (Idx < 0) or (Idx >= FStore.DiscoveredCount) then
  begin
    ShowMessage('Select a discovered device first');
    Exit;
  end;
  D := FStore.Discovered(Idx);
  // Pluto сохраняем с Kind/URI/Serial (иначе после перезапуска резолвится как
  // HPSDR по голому IP и не подключается).
  if D.Kind = bkPluto then
    NewIdx := FStore.AddSavedPluto('PlutoSDR', D.URI, D.Serial)
  else
    NewIdx := FStore.AddSaved(D.DisplayName, D.IPAddress, D.BoardType);
  RefreshSavedList;
  LstSaved.ItemIndex := NewIdx;
end;

procedure TDeviceDialog.BtnConnectClick(Sender: TObject);
var
  IP: string;
  Idx: Integer;
begin
  IP := '';
  if FStore = nil then Exit;

  // Приоритет: выбранное сохранённое > выбранное найденное > ручной IP
  Idx := LstSaved.ItemIndex;
  if (Idx >= 0) and (Idx < FStore.SavedCount) then
  begin
    IP := FStore.Saved(Idx).IPAddress;
    FResult.SavedIdx := Idx;
  end
  else
  begin
    Idx := LstFound.ItemIndex;
    if (Idx >= 0) and (Idx < FStore.DiscoveredCount) then
    begin
      IP := FStore.Discovered(Idx).IPAddress;
      FResult.SavedIdx := -1;
    end
    else if Trim(EdIP.Text) <> '' then
    begin
      IP := Trim(EdIP.Text);
      FResult.SavedIdx := -1;
    end;
  end;

  if IP = '' then
  begin
    ShowMessage('Select or enter a device to connect');
    Exit;
  end;

  FResult.Accepted  := True;
  FResult.IPAddress := IP;
  ModalResult := mrOk;
end;

procedure TDeviceDialog.BtnCancelClick(Sender: TObject);
begin
  FResult.Accepted := False;
  ModalResult := mrCancel;
end;

function TDeviceDialog.ManualIP: string;
begin
  if EdIP <> nil then Result := Trim(EdIP.Text) else Result := '';
end;

procedure TDeviceDialog.ClearResult;
begin
  FResult.Accepted  := False;
  FResult.IPAddress := '';
  FResult.SavedIdx  := -1;
end;

function TDeviceDialog.GetAutoStartIP: string;
begin
  if FStore <> nil then Result := FStore.AutoStartIP
  else                   Result := '';
end;

function TDeviceDialog.GetAutoStartBoardType: Integer;
begin
  if FStore <> nil then Result := FStore.AutoStartBoardType
  else                   Result := 0;
end;

function TDeviceDialog.GetSavedBoardType(Idx: Integer): Integer;
begin
  if FStore <> nil then Result := FStore.SavedBoardType(Idx)
  else                   Result := 0;
end;

end.
