unit VfoOverlay;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Controls, Graphics, Types, Math;

const
  VFO_OVERLAY_ALPHA = 200;

type
  TModeFilterEvent = procedure(Mode: Integer; FilterBW: Integer) of object;
  TDSPChangeEvent  = procedure(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean) of object;
  TAGCChangeEvent  = procedure(AGCMode: Integer) of object;

  TVfoOverlay = class(TCustomControl)
  private
    FMode:       Integer;
    FFilterBW:   Integer;
    FVfoHz:      Double;
    FSMeterDB:   Double;
    FOnSelect:    TModeFilterEvent;
    FOnDSPChange: TDSPChangeEvent;
    FOnAGCChange: TAGCChangeEvent;
    FOnInvalidate: TNotifyEvent;
    FModeFilter: array[0..7] of Integer;

    FNRMode: Integer;   // 0=off, 1..4=NR/NR2/NR3/NR4
    FNBMode: Integer;   // 0=off, 1=NB, 2=NB2
    FSNBOn:  Boolean;
    FANFOn:  Boolean;
    FAGCMode:       Integer;  // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF
    FAGCPickerOpen: Boolean;

    FHitRects:   array[0..24] of TRect;
    FHitActions: array[0..24] of Integer;
    FHitCount:   Integer;
    FHotIdx:     Integer;
    FCacheBitmap: TBitmap;
    FCacheDirty: Boolean;

    procedure DrawSelf(C: TCanvas; W, H: Integer);
    procedure DrawSMeterBar(C: TCanvas; X, Y, BW, BH: Integer);
    procedure DrawModeRow(C: TCanvas; X, Y, RowW, RowH: Integer);
    procedure DrawFilterRow(C: TCanvas; X, Y, RowW, RowH: Integer);
    procedure DrawDSPRow(C: TCanvas; X, Y, RowW, RowH: Integer);
    procedure DrawAGCPickerRow(C: TCanvas; X, Y, RowW, RowH: Integer);
    procedure RegisterHit(HR: TRect; HV: Integer);
    function  CalcSLabel: string;
    function  FormatFreq(Hz: Double): string;
    function  GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
    function  GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
    function  GetFilterCountForMode(ModeIdx: Integer): Integer;
    procedure RequestInvalidate;

  protected
    procedure Paint; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
                        X, Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure MouseLeave; override;

  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure DrawOverlay(Target: TBitmap; W, H: Integer);
    procedure DrawOverlayBitmap(Target: TBitmap);
    function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
    function HandleMouseMove(X, Y: Integer): Boolean;
    function HandleMouseLeave: Boolean;
    procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
    procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
                          AAGCMode: Integer);
    procedure UpdateSMeter(DB: Double);
    procedure UpdateVfo(AVfoHz: Double);
    property HotIdx: Integer read FHotIdx;
    property OnSelect:    TModeFilterEvent read FOnSelect    write FOnSelect;
    property OnDSPChange: TDSPChangeEvent  read FOnDSPChange write FOnDSPChange;
    property OnAGCChange: TAGCChangeEvent  read FOnAGCChange write FOnAGCChange;
    property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate;
  end;

implementation

const
  KEY_COLOR = TColor($00FF00FF);

  HIT_NR  = -9;
  HIT_NB  = -10;
  HIT_SNB = -11;
  HIT_ANF = -12;
  HIT_AGC = -13;

  HIT_AGC_PICK_BASE = 100000;  // 100000+N — выбор режима AGC (>всех BW фильтров)

  AGC_NAMES: array[0..4] of string = ('FAST', 'MED', 'SLOW', 'LONG', 'OFF');

  OVL_H_NORM = 136;  // высота без пикера AGC
  OVL_H_AGC  = 159;  // высота с открытым пикером (136 + GAP(3) + ROW_H(20))

  CLR_PANEL_BG = TColor($00141414);
  CLR_BORDER   = TColor($00505050);
  CLR_TEXT     = TColor($00E8E8E8);
  CLR_DIM      = TColor($00787878);
  CLR_ACCENT   = TColor($0040FF80);
  CLR_BTN_NORM = TColor($00282828);
  CLR_BTN_HOT  = TColor($00383838);
  CLR_BTN_ACT  = TColor($00183828);
  CLR_FREQ     = TColor($0050FF80);
  CLR_SVAL     = TColor($0030C8FF);
  CLR_BAR_GRN  = TColor($0018A030);
  CLR_BAR_RED  = TColor($000030C0);
  CLR_BAR_BG   = TColor($00101010);

  // Режимы — ТОЧНО как в MainForm.pas
  // 0=LSB, 1=USB, 2=DSB, 3=CWL, 4=CWU, 5=FM, 6=AM, 7=SAM
  OVL_MODE_COUNT = 8;
  OVL_MODE_NAMES: array[0..7] of string = (
    'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');

  // Фильтры SSB (LSB=0, USB=1)
  SSB_BW:  array[0..5] of Integer = (1800,2100,2400,2700,3300,3800);
  SSB_LBL: array[0..5] of string  = ('1.8K','2.1K','2.4K','2.7K','3.3K','3.8K');

  // Фильтры DSB (DSB=2)
  DSB_BW:  array[0..5] of Integer = (16000,12000,10000,8000,5200,4000);
  DSB_LBL: array[0..5] of string  = ('16K','12K','10K','8K','5.2K','4K');

  // Фильтры CW (CWL=3, CWU=4)
  CW_BW:   array[0..5] of Integer = (500,250,150,100,50,1000);
  CW_LBL:  array[0..5] of string  = ('500','250','150','100','50','1K');

  // Фильтры FM (FM=5) — только NFM и FM, как в MainForm
  FM_BW:   array[0..1] of Integer = (11000, 16000);
  FM_LBL:  array[0..1] of string  = ('NFM', 'FM');

  // Фильтры AM/SAM (AM=6, SAM=7)
  AM_BW:   array[0..5] of Integer = (20000,16000,12000,10000,8000,6000);
  AM_LBL:  array[0..5] of string  = ('20K','16K','12K','10K','8K','6K');

  // S-шкала: S1..S9
  DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73);

procedure BlendBitmapKey(Target, Source: TBitmap; DstX, DstY: Integer; Alpha: Byte);
var
  Y, X, SX0, SY0, SX1, SY1, TY, InvA: Integer;
  SrcRow, DstRow: PByte;
begin
  if (Target = nil) or (Source = nil) or (Alpha = 0) then Exit;
  SX0 := 0;
  SY0 := 0;
  SX1 := Source.Width;
  SY1 := Source.Height;
  if DstX < 0 then begin SX0 := -DstX; DstX := 0; end;
  if DstY < 0 then begin SY0 := -DstY; DstY := 0; end;
  if DstX + (SX1 - SX0) > Target.Width then SX1 := SX0 + Target.Width - DstX;
  if DstY + (SY1 - SY0) > Target.Height then SY1 := SY0 + Target.Height - DstY;
  if (SX1 <= SX0) or (SY1 <= SY0) then Exit;

  InvA := 255 - Alpha;
  Target.BeginUpdate(False);
  Source.BeginUpdate(False);
  try
    for Y := SY0 to SY1 - 1 do
    begin
      TY := DstY + (Y - SY0);
      SrcRow := PByte(Source.ScanLine[Y]);
      DstRow := PByte(Target.ScanLine[TY]);
      if (SrcRow = nil) or (DstRow = nil) then Continue;
      Inc(SrcRow, SX0 * 4);
      Inc(DstRow, DstX * 4);
      for X := SX0 to SX1 - 1 do
      begin
{$IFDEF DARWIN}
        // ARGB layout: byte0=A, byte1=R, byte2=G, byte3=B
        if not ((SrcRow[1] = $FF) and (SrcRow[2] = $00) and (SrcRow[3] = $FF)) then
        begin
          DstRow[1] := Byte((Alpha * SrcRow[1] + InvA * DstRow[1]) div 255);
          DstRow[2] := Byte((Alpha * SrcRow[2] + InvA * DstRow[2]) div 255);
          DstRow[3] := Byte((Alpha * SrcRow[3] + InvA * DstRow[3]) div 255);
        end;
{$ELSE}
        // BGRA layout: byte0=B, byte1=G, byte2=R
        if not ((SrcRow[0] = $FF) and (SrcRow[1] = $00) and (SrcRow[2] = $FF)) then
        begin
          DstRow[0] := Byte((Alpha * SrcRow[0] + InvA * DstRow[0]) div 255);
          DstRow[1] := Byte((Alpha * SrcRow[1] + InvA * DstRow[1]) div 255);
          DstRow[2] := Byte((Alpha * SrcRow[2] + InvA * DstRow[2]) div 255);
        end;
{$ENDIF}
        Inc(SrcRow, 4);
        Inc(DstRow, 4);
      end;
    end;
  finally
    Source.EndUpdate(False);
    Target.EndUpdate(False);
  end;
end;

function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
begin
  case ModeIdx of
    0,1:   Result := SSB_BW[FilterIdx];
    2:     Result := DSB_BW[FilterIdx];
    3,4:   Result := CW_BW[FilterIdx];
    5:     Result := FM_BW[FilterIdx];
  else     Result := AM_BW[FilterIdx];  // 6=AM, 7=SAM
  end;
end;

function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
begin
  case ModeIdx of
    0,1:   Result := SSB_LBL[FilterIdx];
    2:     Result := DSB_LBL[FilterIdx];
    3,4:   Result := CW_LBL[FilterIdx];
    5:     Result := FM_LBL[FilterIdx];
  else     Result := AM_LBL[FilterIdx];
  end;
end;

function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
begin
  if ModeIdx = 5 then Result := 2   // FM: только NFM и FM
  else                Result := 6;
end;

function DBmToSLabel(DB: Double): string;
var
  I, Over: Integer;
begin
  if DB >= DB_S[9] then
  begin
    Over := Round(DB - DB_S[9]);
    if Over < 5 then
      Result := 'S9'
    else
    begin
      Over := ((Over + 5) div 10) * 10;
      if Over = 0 then Over := 10;
      Result := 'S9+' + IntToStr(Over);
    end;
  end
  else if DB <= DB_S[1] then
    Result := 'S1'
  else
  begin
    Result := 'S1';
    for I := 1 to 8 do
      if DB >= DB_S[I] then
        Result := 'S' + IntToStr(I);
  end;
end;

constructor TVfoOverlay.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := ControlStyle + [csOpaque];
  FMode      := 1;    // USB по умолчанию (как в MainForm)
  FFilterBW  := 2700;
  FVfoHz     := 14200000;
  FSMeterDB  := -121;
  FHitCount  := 0;
  FHotIdx    := -1;
  // Дефолтные фильтры для каждого режима (запоминаются при переключении)
  FModeFilter[0] := 2700;   // LSB
  FModeFilter[1] := 2700;   // USB
  FModeFilter[2] := 8000;   // DSB
  FModeFilter[3] := 500;    // CWL
  FModeFilter[4] := 500;    // CWU
  FModeFilter[5] := 11000;  // FM (NFM по умолчанию)
  FModeFilter[6] := 8000;   // AM
  FModeFilter[7] := 8000;   // SAM
  FNRMode        := 0;
  FNBMode        := 0;
  FSNBOn         := False;
  FANFOn         := False;
  FAGCMode       := 0;
  FAGCPickerOpen := False;
  FCacheBitmap := TBitmap.Create;
  FCacheBitmap.PixelFormat := pf32bit;
  FCacheDirty := True;
  Width  := 260;
  Height := OVL_H_NORM;
  Cursor     := crHandPoint;
end;

destructor TVfoOverlay.Destroy;
begin
  FCacheBitmap.Free;
  inherited Destroy;
end;

procedure TVfoOverlay.RegisterHit(HR: TRect; HV: Integer);
begin
  if FHitCount > High(FHitRects) then Exit;
  FHitRects[FHitCount]   := HR;
  FHitActions[FHitCount] := HV;
  Inc(FHitCount);
end;

procedure TVfoOverlay.RequestInvalidate;
begin
  FCacheDirty := True;
  inherited Invalidate;
  if Assigned(FOnInvalidate) then FOnInvalidate(Self);
end;

function TVfoOverlay.FormatFreq(Hz: Double): string;
var
  iM, iK, iH: Integer;
begin
  iM := Trunc(Hz / 1e6);
  iK := Trunc((Hz - iM * 1e6) / 1000);
  iH := Round(Hz - iM * 1e6 - iK * 1000);
  Result := Format('%d.%3.3d.%3.3d', [iM, iK, iH]);
end;

function TVfoOverlay.CalcSLabel: string;
begin
  Result := DBmToSLabel(FSMeterDB);
end;

procedure TVfoOverlay.DrawSMeterBar(C: TCanvas; X, Y, BW, BH: Integer);
const
  DB_MIN = -127.0;
  DB_MAX =  -13.0;
  S_POS: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
  S_LBL: array[0..7] of string = ('1','3','5','7','S9','+20','+40','+60');
var
  I, BarEnd, S9X, PX, BarH: Integer;
  T: Double;
  Lbl: string;
  TW: Integer;
begin
  BarH := BH - 11;

  C.Brush.Color := CLR_BAR_BG;
  C.Brush.Style := bsSolid;
  C.Pen.Style   := psClear;
  C.FillRect(Rect(X, Y, X+BW, Y+BarH));

  S9X := X + Round((-73 - DB_MIN) / (DB_MAX - DB_MIN) * BW);

  T      := Max(0.0, Min(1.0, (FSMeterDB - DB_MIN) / (DB_MAX - DB_MIN)));
  BarEnd := X + Round(T * BW);

  if BarEnd > X + 1 then
  begin
    if BarEnd <= S9X then
    begin
      C.Brush.Color := CLR_BAR_GRN;
      C.FillRect(Rect(X+1, Y+1, BarEnd, Y+BarH-1));
    end
    else
    begin
      C.Brush.Color := CLR_BAR_GRN;
      C.FillRect(Rect(X+1, Y+1, S9X, Y+BarH-1));
      C.Brush.Color := CLR_BAR_RED;
      C.FillRect(Rect(S9X, Y+1, Min(BarEnd, X+BW-1), Y+BarH-1));
    end;
  end;

  C.Pen.Style   := psSolid;
  C.Pen.Color   := CLR_BORDER;
  C.Brush.Style := bsClear;
  C.Rectangle(X, Y, X+BW, Y+BarH);

  C.Font.Name  := 'Courier New';
  C.Font.Size  := 5;
  C.Font.Style := [];
  for I := 0 to High(S_POS) do
  begin
    T  := (S_POS[I] - DB_MIN) / (DB_MAX - DB_MIN);
    PX := X + Round(T * BW);
    Lbl := S_LBL[I];
    TW  := C.TextWidth(Lbl);
    if I < 4 then C.Font.Color := CLR_DIM
    else          C.Font.Color := CLR_ACCENT;
    C.Pen.Color := CLR_DIM;
    C.Pen.Style := psSolid;
    C.MoveTo(PX, Y+BarH); C.LineTo(PX, Y+BarH+2);
    C.TextOut(PX - TW div 2, Y+BarH+2, Lbl);
  end;
end;

procedure TVfoOverlay.DrawModeRow(C: TCanvas; X, Y, RowW, RowH: Integer);
var
  I, BX, BRight: Integer;
  HR: TRect;
  IsAct, IsHot: Boolean;
begin
  BX := X;
  for I := 0 to OVL_MODE_COUNT - 1 do
  begin
    BRight := X + ((I + 1) * RowW) div OVL_MODE_COUNT;
    HR     := Rect(BX, Y, BRight, Y + RowH);
    IsAct  := (I = FMode);
    IsHot  := (FHotIdx >= 0) and (FHitActions[FHotIdx] = -(I + 1));

    if IsAct then      C.Brush.Color := CLR_BTN_ACT
    else if IsHot then C.Brush.Color := CLR_BTN_HOT
    else               C.Brush.Color := CLR_BTN_NORM;
    C.Brush.Style := bsSolid; C.Pen.Style := psClear;
    C.FillRect(HR);
    C.Pen.Style := psSolid; C.Pen.Color := CLR_BORDER;
    C.Brush.Style := bsClear; C.Rectangle(HR);

    if IsAct then C.Font.Color := CLR_ACCENT
    else          C.Font.Color := CLR_TEXT;
    C.Font.Size := 6; C.Font.Style := []; C.Font.Name := 'Courier New';
    C.Brush.Style := bsClear;
    C.TextOut(BX + (BRight - BX - C.TextWidth(OVL_MODE_NAMES[I])) div 2,
              Y + (RowH - C.TextHeight('A')) div 2,
              OVL_MODE_NAMES[I]);

    RegisterHit(HR, -(I + 1));  // -1=LSB, -2=USB, -3=DSB, -4=CWL, -5=CWU, -6=FM, -7=AM, -8=SAM
    BX := BRight + 1;
  end;
end;

procedure TVfoOverlay.DrawFilterRow(C: TCanvas; X, Y, RowW, RowH: Integer);
var
  FILT_COUNT: Integer;
  I, BX, BRight, BV: Integer;
  HR: TRect;
  Lbl: string;
  IsAct: Boolean;
begin
  FILT_COUNT := GetFilterCountForMode(FMode);
  BX := X;
  for I := 0 to FILT_COUNT - 1 do
  begin
    BRight := X + ((I + 1) * RowW) div FILT_COUNT;
    HR     := Rect(BX, Y, BRight, Y + RowH);
    BV     := GetFilterBWForMode(FMode, I);
    IsAct := (BV = FFilterBW);
    Lbl   := GetFilterLblForMode(FMode, I);

    if IsAct then      C.Brush.Color := CLR_BTN_ACT
    else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = BV) then
                       C.Brush.Color := CLR_BTN_HOT
    else               C.Brush.Color := CLR_BTN_NORM;
    C.Brush.Style := bsSolid; C.Pen.Style := psClear;
    C.FillRect(HR);
    C.Pen.Style := psSolid; C.Pen.Color := CLR_BORDER;
    C.Brush.Style := bsClear; C.Rectangle(HR);

    if IsAct then C.Font.Color := CLR_ACCENT
    else          C.Font.Color := CLR_TEXT;
    C.Font.Size := 7; C.Font.Name := 'Courier New';
    C.Brush.Style := bsClear;
    C.TextOut(BX + (BRight - BX - C.TextWidth(Lbl)) div 2,
              Y + (RowH - C.TextHeight('A')) div 2, Lbl);

    RegisterHit(HR, BV);  // BV всегда > 0 (BW в Гц, минимум 25)
    BX := BRight + 1;
  end;
end;

procedure TVfoOverlay.DrawDSPRow(C: TCanvas; X, Y, RowW, RowH: Integer);
const
  DSP_COUNT = 5;
  DSP_HITS: array[0..4] of Integer = (HIT_NR, HIT_NB, HIT_SNB, HIT_ANF, HIT_AGC);
var
  I, BX, BRight: Integer;
  HR: TRect;
  Lbl: string;
  IsAct, IsHot: Boolean;
begin
  BX := X;
  for I := 0 to DSP_COUNT - 1 do
  begin
    BRight := X + ((I + 1) * RowW) div DSP_COUNT;
    HR     := Rect(BX, Y, BRight, Y + RowH);
    case I of
      0: begin
           case FNRMode of
             1: Lbl := 'NR';
             2: Lbl := 'NR2';
             3: Lbl := 'NR3';
             4: Lbl := 'NR4';
           else Lbl := 'NR';
           end;
           IsAct := FNRMode <> 0;
         end;
      1: begin
           if FNBMode = 2 then Lbl := 'NB2' else Lbl := 'NB';
           IsAct := FNBMode <> 0;
         end;
      2: begin Lbl := 'SNB'; IsAct := FSNBOn; end;
      3: begin Lbl := 'ANF'; IsAct := FANFOn; end;
      4: begin Lbl := AGC_NAMES[FAGCMode]; IsAct := FAGCPickerOpen; end;
    else  Lbl := ''; IsAct := False;
    end;
    IsHot := (FHotIdx >= 0) and (FHitActions[FHotIdx] = DSP_HITS[I]);

    if IsAct then      C.Brush.Color := CLR_BTN_ACT
    else if IsHot then C.Brush.Color := CLR_BTN_HOT
    else               C.Brush.Color := CLR_BTN_NORM;
    C.Brush.Style := bsSolid; C.Pen.Style := psClear;
    C.FillRect(HR);
    C.Pen.Style := psSolid; C.Pen.Color := CLR_BORDER;
    C.Brush.Style := bsClear; C.Rectangle(HR);

    if IsAct then C.Font.Color := CLR_ACCENT
    else          C.Font.Color := CLR_TEXT;
    C.Font.Size := 7; C.Font.Name := 'Courier New';
    C.Brush.Style := bsClear;
    C.TextOut(BX + (BRight - BX - C.TextWidth(Lbl)) div 2,
              Y + (RowH - C.TextHeight('A')) div 2, Lbl);

    RegisterHit(HR, DSP_HITS[I]);
    BX := BRight + 1;
  end;
end;

procedure TVfoOverlay.DrawAGCPickerRow(C: TCanvas; X, Y, RowW, RowH: Integer);
const
  AGC_COUNT = 5;
var
  I, BX, BRight: Integer;
  HR: TRect;
  IsAct, IsHot: Boolean;
begin
  BX := X;
  for I := 0 to AGC_COUNT - 1 do
  begin
    BRight := X + ((I + 1) * RowW) div AGC_COUNT;
    HR     := Rect(BX, Y, BRight, Y + RowH);
    IsAct  := (I = FAGCMode);
    IsHot  := (FHotIdx >= 0) and
              (FHitActions[FHotIdx] = HIT_AGC_PICK_BASE + I);

    if IsAct then      C.Brush.Color := CLR_BTN_ACT
    else if IsHot then C.Brush.Color := CLR_BTN_HOT
    else               C.Brush.Color := CLR_BTN_NORM;
    C.Brush.Style := bsSolid; C.Pen.Style := psClear;
    C.FillRect(HR);
    C.Pen.Style := psSolid; C.Pen.Color := CLR_BORDER;
    C.Brush.Style := bsClear; C.Rectangle(HR);

    if IsAct then C.Font.Color := CLR_ACCENT
    else          C.Font.Color := CLR_TEXT;
    C.Font.Size := 7; C.Font.Name := 'Courier New';
    C.Brush.Style := bsClear;
    C.TextOut(BX + (BRight - BX - C.TextWidth(AGC_NAMES[I])) div 2,
              Y + (RowH - C.TextHeight('A')) div 2, AGC_NAMES[I]);

    RegisterHit(HR, HIT_AGC_PICK_BASE + I);
    BX := BRight + 1;
  end;
end;

procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
const
  PAD    = 4;
  FREQ_H = 18;
  INFO_H = 14;
  SM_H   = 27;
  ROW_H  = 20;
  GAP    = 3;
var
  FreqStr, SStr: string;
  FW, CurY: Integer;
begin
  FHitCount := 0;

  C.Brush.Color := CLR_PANEL_BG;
  C.Brush.Style := bsSolid;
  C.Pen.Color   := CLR_BORDER;
  C.Pen.Style   := psSolid;
  C.Pen.Width   := 1;
  C.RoundRect(0, 0, W, H, 6, 6);

  CurY := PAD;

  // Частота
  FreqStr := FormatFreq(FVfoHz);
  C.Font.Name := 'Courier New'; C.Font.Size := 11; C.Font.Style := [fsBold];
  C.Font.Color := CLR_FREQ;
  FW := C.TextWidth(FreqStr);
  C.Brush.Style := bsClear;
  C.TextOut((W - FW) div 2, CurY, FreqStr);
  Inc(CurY, FREQ_H);

  // Режим (слева) + S-value (справа)
  SStr := CalcSLabel;
  C.Font.Size := 8; C.Font.Style := [fsBold];
  C.Font.Color := CLR_ACCENT;
  C.TextOut(PAD + 2, CurY, OVL_MODE_NAMES[FMode]);
  C.Font.Color := CLR_SVAL;
  C.TextOut(W - PAD - C.TextWidth(SStr) - 2, CurY, SStr);
  Inc(CurY, INFO_H);

  // S-метр
  DrawSMeterBar(C, PAD + 4, CurY, W - (PAD + 4) * 2, SM_H);
  Inc(CurY, SM_H + GAP);

  // Кнопки режимов
  DrawModeRow(C, PAD, CurY, W - PAD * 2, ROW_H);
  Inc(CurY, ROW_H + GAP);

  // Кнопки фильтров
  DrawFilterRow(C, PAD, CurY, W - PAD * 2, ROW_H);
  Inc(CurY, ROW_H + GAP);

  // Кнопки DSP (NR, NB, SNB, ANF, AGC)
  DrawDSPRow(C, PAD, CurY, W - PAD * 2, ROW_H);

  // Пикер AGC (открывается кнопкой AGC)
  if FAGCPickerOpen then
  begin
    Inc(CurY, ROW_H + GAP);
    DrawAGCPickerRow(C, PAD, CurY, W - PAD * 2, ROW_H);
  end;
end;

procedure TVfoOverlay.Paint;
begin
  DrawSelf(Canvas, Width, Height);
end;

procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
begin
  if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
  if (Left >= W) or (Top >= H) then Exit;

  if (FCacheBitmap.Width <> Width) or (FCacheBitmap.Height <> Height) then
  begin
    FCacheBitmap.SetSize(Width, Height);
    FCacheDirty := True;
  end;
  if FCacheDirty then
  begin
    FCacheBitmap.Canvas.Brush.Color := KEY_COLOR;
    FCacheBitmap.Canvas.Brush.Style := bsSolid;
    FCacheBitmap.Canvas.Pen.Style := psClear;
    FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height));
    DrawSelf(FCacheBitmap.Canvas, Width, Height);
    FCacheDirty := False;
  end;
  BlendBitmapKey(Target, FCacheBitmap, Left, Top, VFO_OVERLAY_ALPHA);
end;

procedure TVfoOverlay.DrawOverlayBitmap(Target: TBitmap);
begin
  if (Target = nil) or (Width <= 0) or (Height <= 0) then Exit;
  Target.PixelFormat := pf32bit;
  Target.SetSize(Width, Height);
  Target.Canvas.Brush.Color := KEY_COLOR;
  Target.Canvas.Brush.Style := bsSolid;
  Target.Canvas.Pen.Style := psClear;
  Target.Canvas.FillRect(Rect(0, 0, Width, Height));
  DrawSelf(Target.Canvas, Width, Height);
  FCacheDirty := False;
end;

function TVfoOverlay.HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
begin
  Result := False;
  if not Visible then Exit;
  if (X < Left) or (Y < Top) or (X >= Left + Width) or (Y >= Top + Height) then Exit;
  MouseDown(Button, [], X - Left, Y - Top);
  Result := True;
end;

function TVfoOverlay.HandleMouseMove(X, Y: Integer): Boolean;
var
  I, OldHot, LX, LY: Integer;
begin
  Result := False;
  if not Visible then Exit;
  LX := X - Left;
  LY := Y - Top;
  OldHot := FHotIdx;
  FHotIdx := -1;
  if (LX >= 0) and (LY >= 0) and (LX < Width) and (LY < Height) then
    for I := 0 to FHitCount - 1 do
      if PtInRect(FHitRects[I], Point(LX, LY)) then
      begin
        FHotIdx := I;
        Break;
      end;
  if FHotIdx <> OldHot then
  begin
    RequestInvalidate;
    Result := True;
  end;
end;

function TVfoOverlay.HandleMouseLeave: Boolean;
begin
  Result := False;
  if FHotIdx >= 0 then
  begin
    FHotIdx := -1;
    RequestInvalidate;
    Result := True;
  end;
end;

procedure TVfoOverlay.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
var
  I, HV, NewMode, NewBW: Integer;
begin
  inherited;
  if Button <> mbLeft then Exit;
  for I := 0 to FHitCount - 1 do
    if PtInRect(FHitRects[I], Point(X, Y)) then
    begin
      HV := FHitActions[I];
      if HV < 0 then
      begin
        NewMode := (-HV) - 1;
        if NewMode < OVL_MODE_COUNT then
        begin
          // Кнопка режима
          if NewMode <> FMode then
          begin
            FModeFilter[FMode] := FFilterBW;
            FMode := NewMode;
            FFilterBW := FModeFilter[FMode];
            RequestInvalidate;
            if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
          end;
        end
        else
        begin
          // Кнопка DSP
          if HV = HIT_AGC then
          begin
            FAGCPickerOpen := not FAGCPickerOpen;
            if FAGCPickerOpen then Height := OVL_H_AGC
            else                   Height := OVL_H_NORM;
            RequestInvalidate;
          end
          else
          begin
            case HV of
              HIT_NR:  FNRMode := (FNRMode + 1) mod 5;
              HIT_NB:  FNBMode := (FNBMode + 1) mod 3;
              HIT_SNB: FSNBOn  := not FSNBOn;
              HIT_ANF: FANFOn  := not FANFOn;
            end;
            RequestInvalidate;
            if Assigned(FOnDSPChange) then
              FOnDSPChange(FNRMode, FNBMode, FSNBOn, FANFOn);
          end;
        end;
      end
      else if HV >= HIT_AGC_PICK_BASE then
      begin
        // Выбор режима AGC из пикера
        FAGCMode       := HV - HIT_AGC_PICK_BASE;
        FAGCPickerOpen := False;
        Height         := OVL_H_NORM;
        RequestInvalidate;
        if Assigned(FOnAGCChange) then FOnAGCChange(FAGCMode);
      end
      else if HV > 0 then
      begin
        NewBW := HV;
        if NewBW <> FFilterBW then
        begin
          FFilterBW := NewBW;
          FModeFilter[FMode] := FFilterBW;
          RequestInvalidate;
          if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
        end;
      end;
      Break;
    end;
end;

procedure TVfoOverlay.MouseMove(Shift: TShiftState; X, Y: Integer);
var
  I, OldHot: Integer;
begin
  inherited;
  OldHot := FHotIdx; FHotIdx := -1;
  for I := 0 to FHitCount - 1 do
    if PtInRect(FHitRects[I], Point(X, Y)) then
    begin FHotIdx := I; Break; end;
  if FHotIdx <> OldHot then RequestInvalidate;
end;

procedure TVfoOverlay.MouseLeave;
begin
  inherited;
  if FHotIdx >= 0 then begin FHotIdx := -1; RequestInvalidate; end;
end;

procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
begin
  FMode     := Max(0, Min(OVL_MODE_COUNT - 1, AMode));
  FFilterBW := ABW;
  // Синхронизируем память фильтра для текущего режима
  FModeFilter[FMode] := ABW;
  FVfoHz    := AVfoHz;
  FSMeterDB := ASMeterDB;
  RequestInvalidate;
end;

procedure TVfoOverlay.SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
                                  AAGCMode: Integer);
begin
  FNRMode  := ANRMode;
  FNBMode  := ANBMode;
  FSNBOn   := ASNBOn;
  FANFOn   := AANFOn;
  FAGCMode := AAGCMode;
  RequestInvalidate;
end;

procedure TVfoOverlay.UpdateSMeter(DB: Double);
begin
  // Не клипаем — передаём как есть, как в основном S-метре
  if Abs(DB - FSMeterDB) > 0.4 then
  begin
    FSMeterDB := DB;
    RequestInvalidate;
  end;
end;

procedure TVfoOverlay.UpdateVfo(AVfoHz: Double);
begin
  if FVfoHz <> AVfoHz then
  begin
    FVfoHz := AVfoHz;
    RequestInvalidate;
  end;
end;

end.
