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

// -----------------------------------------------------------------------------
// TChannelController — адаптер каналов памяти поверх TRadioController.
//
// Каналы концептуально — recall сохранённого конфига (частота/режим/FM/мощность),
// т.е. клиент радио-ядра, а не часть DSP/сети. Поэтому логика применения каналов
// живёт здесь, отдельным юнитом, и оркеструет ПУБЛИЧНЫЕ команды контроллера
// (SetMode/SetFMCTCSS/SetFMRpt/SetVfoA/SetDrive/SaveCurrentBand/…). Каждая
// команда несёт DSP+сеть+состояние и эмитит Changed(field) — рендер делает UI
// через OnStateChanged. Юнит держит ТОЛЬКО канальное состояние (активный индекс
// + мощность «до канала») — это снимает последний UI-блокер для headless-режима.
//
// Наружу юнит эмитит собственное событие OnActiveChanged (рендер кнопки Channel).
// Вход/выход трансвертера идёт через FRadio.SetXvtrBand (команда несёт DSP/сеть +
// рендер/wideband/web через rfXvtr) — host-колбэков у юнита больше нет.
// -----------------------------------------------------------------------------

{$mode delphi}{$H+}

interface

uses
  Classes, SysUtils, Math,
  RadioController, ChannelStore, Settings, WDSPEngine, FMRepeater, BoardUtils;

type
  // Рендер активного канала: Active=True → подсветка + Name; False → сброс.
  TChannelActiveEvent = procedure(Idx: Integer; const Name: string; Active: Boolean) of object;

  TChannelController = class
  private
    FRadio: TRadioController;
    FActiveIdx: Integer;        // -1 = нет активного канала
    FPreChannelPower: Integer;  // drive % до применения канала (-1 = не сохранён)
    FRestoringDrive: Boolean;   // True пока юнит сам ставит drive (apply/restore)
    FApplying: Boolean;         // True пока идёт Apply (гасит промежуточный OnVfoTuned)
    FOnActiveChanged: TChannelActiveEvent;
    procedure EmitActive(Active: Boolean; const Name: string);
    procedure SetDriveGuarded(V: Integer);   // SetDrive, не сбрасывающий память канала
  public
    constructor Create(ARadio: TRadioController);

    procedure Select(Idx: Integer);        // выбор канала из дропдауна
    procedure Apply(const Ch: TChannel);   // применить канал к радио
    procedure CheckActive;                 // деактивировать при уходе с частоты
    procedure OnVfoTuned;                  // канальная оркестрация после SetVfoA
    procedure OnDriveChanged;              // реакция на rfDrive: внешнее изменение снимает память

    property ActiveIdx: Integer read FActiveIdx write FActiveIdx;
    property OnActiveChanged:  TChannelActiveEvent read FOnActiveChanged  write FOnActiveChanged;
  end;

implementation

constructor TChannelController.Create(ARadio: TRadioController);
begin
  inherited Create;
  FRadio := ARadio;
  FActiveIdx := -1;
  FPreChannelPower := -1;
end;

procedure TChannelController.EmitActive(Active: Boolean; const Name: string);
begin
  if Assigned(FOnActiveChanged) then FOnActiveChanged(FActiveIdx, Name, Active);
end;

procedure TChannelController.SetDriveGuarded(V: Integer);
// Drive, выставляемый самим юнитом (применение мощности канала / восстановление):
// rfDrive прилетит, но OnDriveChanged НЕ должен трактовать его как ручной → не
// сбрасывать FPreChannelPower.
begin
  FRestoringDrive := True;
  try
    FRadio.SetDrive(V);
  finally
    FRestoringDrive := False;
  end;
end;

procedure TChannelController.OnDriveChanged;
// Вызывается из OnControllerState(rfDrive). Любое ВНЕШНЕЕ изменение drive
// (слайдер/web/энкодер) снимает сохранённую «до канала» мощность.
begin
  if not FRestoringDrive then FPreChannelPower := -1;
end;

procedure TChannelController.Select(Idx: Integer);
var Ch: TChannel;
begin
  if (FRadio.FChannelStore = nil) or (Idx < 0) or (Idx >= FRadio.FChannelStore.Count) then Exit;
  Ch := FRadio.FChannelStore.GetChannel(Idx);
  FActiveIdx := Idx;
  EmitActive(True, Ch.Name);   // рендер кнопки Channel (подсветка + имя)
  Apply(Ch);
end;

procedure TChannelController.Apply(const Ch: TChannel);
var
  i, NewBand, XvtrIdx: Integer;
begin
  // Промежуточные retune внутри Apply (XVTR-выход → RestoreBand → SetVfoA на
  // СТАРУЮ частоту) фаерят OnAfterTune→OnVfoTuned; без этого гарда CheckActive
  // деактивировал бы канал прямо посреди его применения. Финальное состояние
  // (FActiveIdx + Ch.RXFreq) выставляем сами — оркестрация снаружи Apply.
  FApplying := True;
  try
  // Сохраняем текущее состояние до любых изменений
  FRadio.SaveCurrentBand;

  // Ищем подходящий XVTR-слот по частоте канала
  XvtrIdx := -1;
  for i := 0 to CFG_XVTR_COUNT - 1 do
    if FRadio.FXvtrSettings.Entries[i].Enabled and
       (Ch.RXFreq >= FRadio.FXvtrSettings.Entries[i].FreqBegin) and
       (Ch.RXFreq <= FRadio.FXvtrSettings.Entries[i].FreqEnd) then
    begin
      XvtrIdx := i;
      Break;
    end;

  NewBand := -1;
  if XvtrIdx >= 0 then
  begin
    // Канал на трансвертерном диапазоне — активируем XVTR если не активен
    if FRadio.FCurrentXvtr <> XvtrIdx then
      FRadio.SetXvtrBand(XvtrIdx);   // вход в слот + рендер/wideband/web (rfXvtr)
  end
  else
  begin
    // Канал на HF (или неизвестная частота). Если были на XVTR — выходим в HF,
    // иначе SetVfoA заклэмпит частоту.
    if FRadio.FCurrentXvtr >= 0 then
      FRadio.SetXvtrBand(-1);   // выход в HF (DeactivateXvtr несёт HF-возврат)
    NewBand := FreqToBandIdx(Ch.RXFreq);
    if (NewBand >= 0) and (NewBand <> FRadio.FCurrentBand) then
      FRadio.SetCurrentBandIdx(NewBand);   // band-индекс + drive-кал + рендер кнопок
  end;

  // Режим (после XVTR/Band режим мог измениться — сверяем снова). SetMode несёт
  // DSP + дефолтный фильтр под режим + рендер (FM-панели для FM).
  if FRadio.FMode <> Ch.Mode then
    FRadio.SetMode(Ch.Mode);

  // FM-параметры
  if FRadio.FMode = MODE_FM then
  begin
    FRadio.FFMCTCSSAutoActive := Ch.CTCSSOn;  // CTCSS из канала — авто, сбросится при уходе
    FRadio.SetFMCTCSSTone(Ch.CTCSSToneIdx);
    FRadio.SetFMCTCSS(Ch.CTCSSOn);
    FRadio.FFMRptOffsetHz := Ch.RptOffsetHz;  // зафиксировать до рендера в SetFMRpt
    FRadio.SetFMRpt(Ch.RptDir);               // dir + рендер кнопок/оффсета
    FRadio.FFMRptAutoActive := Ch.RptDir <> RPT_NONE;  // RPT из канала — авто
  end;

  // Мощность из канала. Запоминаем drive «до канала» для восстановления при уходе
  // (не перетираем при переходе канал→канал).
  if Ch.Power >= 0 then
  begin
    if FPreChannelPower < 0 then
      FPreChannelPower := FRadio.FDrivePercent;
    SetDriveGuarded(EnsureRange(Ch.Power, 0, 100));
  end;

  // Частота VFO A (XVTR-клэмп, CTUN, band-detect, сеть — всё в SetVfoA)
  FRadio.SetVfoA(Round(Ch.RXFreq));

  // Сохраняем итог в band/XVTR-кэш
  if (XvtrIdx >= 0) or (NewBand >= 0) then
    FRadio.SaveCurrentBand;
  finally
    FApplying := False;
  end;
end;

procedure TChannelController.CheckActive;
var Ch: TChannel;
begin
  if FActiveIdx < 0 then Exit;
  if (FRadio.FChannelStore = nil) or (FActiveIdx >= FRadio.FChannelStore.Count) then
  begin
    FActiveIdx := -1;
    EmitActive(False, '');
    Exit;
  end;
  Ch := FRadio.FChannelStore.GetChannel(FActiveIdx);
  if Round(FRadio.FVfoA) <> Round(Ch.RXFreq) then
  begin
    FActiveIdx := -1;
    EmitActive(False, '');

    // Сбрасываем CTCSS если он был включён каналом (не вручную)
    if FRadio.FFMCTCSSAutoActive then
    begin
      FRadio.FFMCTCSSAutoActive := False;
      FRadio.SetFMCTCSS(False);
    end;

    // Сбрасываем RPT если он был включён каналом (не вручную), затем даём
    // UpdateRptAutoState переоценить авто-MINUS для текущей частоты.
    if FRadio.FFMRptAutoActive then
    begin
      FRadio.SetFMRpt(RPT_NONE);   // dir NONE + рендер off (снимает авто-флаг)
      FRadio.UpdateRptAutoState;
    end;

    // Восстанавливаем мощность которая была до применения канала
    if FPreChannelPower >= 0 then
    begin
      SetDriveGuarded(FPreChannelPower);
      FPreChannelPower := -1;
    end;
  end;
end;

procedure TChannelController.OnVfoTuned;
// Вызывается из пути тюнинга (MainForm.ApplyVfoA) ПОСЛЕ FController.SetVfoA.
// Без активного канала: сброс авто-CTCSS + восстановление pre-channel drive.
// С активным каналом: CheckActive деактивирует при уходе с частоты канала.
begin
  if FApplying then Exit;   // промежуточные retune внутри Apply — не трогаем канал
  if FActiveIdx < 0 then
  begin
    if FRadio.FFMCTCSSAutoActive then
    begin
      FRadio.FFMCTCSSAutoActive := False;
      FRadio.SetFMCTCSS(False);
    end;
    if FPreChannelPower >= 0 then
    begin
      SetDriveGuarded(FPreChannelPower);   // восстановить + push + рендер слайдера
      FPreChannelPower := -1;
    end;
  end;
  CheckActive;   // no-op если FActiveIdx<0
end;

end.
