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

{
  Оверлей бэндплана QO-100 (узкополосный транспондер) — тонкая цветная полоска
  вдоль оси частот ВНИЗУ спектра (над ruler) с подписями сегментов.

  Производительность — модель VfoOverlay: контент рендерится в кэш-битмап ОДИН раз
  и пересобирается только при смене вида (center/span/размер/тема/enabled), а каждый
  кадр идёт лишь композит полоски (W × STRIP_H) с постоянной альфой. Дорогих
  попиксельных проходов на кадр нет. GL-путь берёт тот же битмап текстурой
  (DrawOverlayBitmap) и заливает её только при dirty — одинаковый вид в CPU и GL,
  т.к. GL-UploadBitmap поддерживает color-key + постоянную альфу (как у VfoOverlay).

  Сегменты заданы таблицей в DOWNLINK-Гц (QO100_NB). Границы — по плану AMSAT-DL
  NB-транспондера; правятся в одном месте.
}

{$mode objfpc}{$H+}

interface

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

const
  BANDPLAN_STRIP_BASE_H = 20; // базовая высота полоски @96dpi (чуть больше ruler=18);
                              // фактическая FStripH масштабируется по DPI экрана
  BANDPLAN_ALPHA   = 188;    // глобальная альфа композита (0..255)

type
  // Тип сегмента → цвет. Правится таблицей SEG_COLOR ниже.
  TSegKind = (skBeacon, skCW, skSSB, skDigi, skNBDigi, skDV, skMixed);

  TBandSeg = record
    Lo, Hi: Double;     // downlink-Гц
    Name:   string;
    Kind:   TSegKind;
  end;

  TBandPlanOverlay = class(TComponent)
  private
    FCenterFreq: Double;
    FSpanHz:     Double;
    FEnabled:    Boolean;

    FCache:      TBitmap;     // W × STRIP_H, непрозрачный; альфа применяется при композите
    FCacheW:     Integer;
    FCacheDirty: Boolean;
    // ключ кэша — пересборка только при изменении
    FKeyCenter:  Double;
    FKeySpan:    Double;
    FKeyW:       Integer;
    FKeyEnabled: Boolean;
    FStripH:     Integer;    // фактическая высота полоски (px), масштаб по DPI

    function  FreqToX(FreqHz: Double; W: Integer): Integer;
    procedure DrawSelf(C: TCanvas; W: Integer);
    procedure RebuildCache(W: Integer);
    function  NeedsRebuild(W: Integer): Boolean;
    procedure CompositeStrip(Target: TBitmap; DstY: Integer);
  public
    constructor Create(AOwner: TComponent); override;
    destructor  Destroy; override;
    // Текущий вид спектра (downlink-домен). Дёргается при тюнинге/смене span.
    procedure SetView(ACenterHz, ASpanHz: Double);
    procedure SetEnabled(En: Boolean);
    function  Active: Boolean;
    // CPU-путь: композит полоски внизу Target (над ruler).
    procedure DrawOverlay(Target: TBitmap; W, H: Integer);
    // GL-путь: рендер полоски в Target-битмап (W × STRIP_H) для заливки текстурой.
    procedure DrawOverlayBitmap(Target: TBitmap; W: Integer);
    property Enabled: Boolean read FEnabled;
    property CenterFreq: Double read FCenterFreq;   // для GL-инвалидизации текстуры
    property SpanHz: Double read FSpanHz;
  end;

implementation

const
  // Палитра по типу сегмента (TColor = $00BBGGRR).
  SEG_COLOR: array[TSegKind] of TColor = (
    $005258C8,   // skBeacon — приглушённый красный (с прохладным оттенком палитры)
    $00C07820,   // skCW     — синий
    $002F9A3A,   // skSSB    — зелёный
    $00B0479E,   // skDigi   — фиолетовый (яркий)
    $00794A6E,   // skNBDigi — приглушённый фиолетовый (NB DIGI, тусклее DIGI)
    $009A9A20,   // skDV     — бирюзовый
    $00606060);  // skMixed  — серый

  CLR_STRIP_BG  = TColor($00181818);   // фон полоски вне транспондера
  CLR_STRIP_TOP = TColor($00585858);   // верхняя кромка (отделить от спектра)
  CLR_SEP       = TColor($000C0C0C);   // разделители сегментов
  CLR_LBL       = TColor($00F0F0F0);   // подписи

  // ── Бэндплан QO-100 NB-транспондера (DOWNLINK Гц) ────────────────────────────
  // Транспондер 10489.500–10490.000 (500 кГц). Mode-заливки между маяками; маяки —
  // отдельными ярко-янтарными ячейками на своих диапазонах. Границы правятся ЗДЕСЬ.
  QO100_NB: array[0..5] of TBandSeg = (
    (Lo: 10489505000.0; Hi: 10489540000.0; Name: 'CW ONLY';     Kind: skCW),
    (Lo: 10489540000.0; Hi: 10489580000.0; Name: 'NB DIGI';     Kind: skNBDigi),
    (Lo: 10489580000.0; Hi: 10489650000.0; Name: 'DIGI';        Kind: skDigi),
    (Lo: 10489650000.0; Hi: 10489745000.0; Name: 'SSB';         Kind: skSSB),
    (Lo: 10489755000.0; Hi: 10489850000.0; Name: 'SSB';         Kind: skSSB),
    (Lo: 10489850000.0; Hi: 10489990000.0; Name: 'MIXED MODES'; Kind: skDV));

  // Маяки QO-100 (диапазоны): нижний CW, средний PSK, верхний. Ярко-янтарные ячейки.
  QO100_BEACONS: array[0..2] of TBandSeg = (
    (Lo: 10489500000.0; Hi: 10489505000.0; Name: 'CW';  Kind: skBeacon),  // нижний CW-маяк
    (Lo: 10489745000.0; Hi: 10489755000.0; Name: 'PSK'; Kind: skBeacon),  // средний PSK-маяк
    (Lo: 10489990000.0; Hi: 10490000000.0; Name: 'EXP'; Kind: skBeacon)); // верхний эксп. маяк

function Darken(C: TColor; Num, Den: Integer): TColor;
var r, g, b: Integer;
begin
  r := (C and $FF) * Num div Den;
  g := ((C shr 8) and $FF) * Num div Den;
  b := ((C shr 16) and $FF) * Num div Den;
  Result := TColor((b shl 16) or (g shl 8) or r);
end;

constructor TBandPlanOverlay.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  // Высота полоски масштабируется по DPI экрана (как RULER_H в MainForm), иначе на
  // Windows-масштабе 125/150/200% полоска осталась бы фиксированно тонкой.
  FStripH := Round(BANDPLAN_STRIP_BASE_H * Screen.PixelsPerInch / 96);
  if FStripH < BANDPLAN_STRIP_BASE_H then FStripH := BANDPLAN_STRIP_BASE_H;
  FCenterFreq := 10489750000.0;
  FSpanHz     := 500000.0;
  FEnabled    := False;
  FCache      := TBitmap.Create;
  FCache.PixelFormat := pf32bit;
  FCacheW     := 0;
  FCacheDirty := True;
  FKeyCenter  := -1; FKeySpan := -1; FKeyW := -1; FKeyEnabled := False;
end;

destructor TBandPlanOverlay.Destroy;
begin
  FCache.Free;
  inherited Destroy;
end;

procedure TBandPlanOverlay.SetView(ACenterHz, ASpanHz: Double);
begin
  // Квантуем сравнение, чтобы микродрейф не вызывал постоянную пересборку.
  if (Abs(ACenterHz - FCenterFreq) < 1.0) and (Abs(ASpanHz - FSpanHz) < 1.0) then Exit;
  FCenterFreq := ACenterHz;
  FSpanHz     := ASpanHz;
  FCacheDirty := True;
end;

procedure TBandPlanOverlay.SetEnabled(En: Boolean);
begin
  if FEnabled = En then Exit;
  FEnabled := En;
  FCacheDirty := True;
end;

function TBandPlanOverlay.Active: Boolean;
begin
  Result := FEnabled;
end;

function TBandPlanOverlay.FreqToX(FreqHz: Double; W: Integer): Integer;
begin
  if FSpanHz <= 0 then Result := -1
  else Result := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
end;

procedure TBandPlanOverlay.DrawSelf(C: TCanvas; W: Integer);
// Рендерит полоску в канвас битмапа W × FStripH, начало (0,0), НЕПРОЗРАЧНО.
// Прозрачность накладывается позже (CompositeStrip / GL FixedAlpha).
var
  i, ty, th, SH: Integer;

  // Единый стиль для всех ячеек (mode-сегменты И маяки): приглушённая заливка +
  // яркая кромка цвета сверху + разделитель слева + подпись по центру если влезает.
  procedure DrawSeg(const seg: TBandSeg);
  var x1, x2, cx, tw: Integer; col: TColor;
  begin
    x1 := FreqToX(seg.Lo, W);
    x2 := FreqToX(seg.Hi, W);
    if (x2 <= 0) or (x1 >= W) then Exit;       // вне экрана
    if x1 < 0 then x1 := 0;
    if x2 > W then x2 := W;
    if x2 <= x1 then x2 := x1 + 1;             // минимум 1px видимости (узкие маяки)

    col := SEG_COLOR[seg.Kind];
    C.Brush.Color := Darken(col, 3, 5); C.Brush.Style := bsSolid; C.Pen.Style := psClear;
    C.FillRect(Rect(x1, 1, x2, SH));
    C.Brush.Color := col;                      // яркая кромка-«легенда» сверху
    C.FillRect(Rect(x1, 1, x2, 3));

    C.Pen.Style := psSolid; C.Pen.Color := CLR_SEP; C.Pen.Width := 1;
    C.MoveTo(x1, 1); C.LineTo(x1, SH);

    tw := C.TextWidth(seg.Name);
    if (x2 - x1) >= tw + 6 then
    begin
      cx := x1 + ((x2 - x1) - tw) div 2;
      C.Brush.Style := bsClear; C.Font.Color := CLR_LBL;
      C.TextOut(cx, ty, seg.Name);
    end;
  end;

begin
  SH := FStripH;

  // Фон всей полоски (зона вне транспондера остаётся этим тёмным фоном).
  C.Brush.Color := CLR_STRIP_BG; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
  C.FillRect(Rect(0, 0, W, SH));

  // Высота шрифта привязана к высоте полоски (а не point-size), поэтому текст
  // всегда влезает при любом DPI-масштабе Windows. Отриц. Height = glyph-px.
  C.Font.Name := 'Courier New'; C.Font.Style := [];
  C.Font.Height := -Max(8, Round(SH * 0.5));
  th := C.TextHeight('BCN');
  // Вертикальный центр под caps: +1 компенсирует пустой descent у заглавных.
  ty := (SH - th) div 2 + 1;
  if ty < 1 then ty := 1;

  // Mode-сегменты, затем маяки (поверх) — одинаковый стиль.
  for i := Low(QO100_NB) to High(QO100_NB) do DrawSeg(QO100_NB[i]);
  for i := Low(QO100_BEACONS) to High(QO100_BEACONS) do DrawSeg(QO100_BEACONS[i]);

  // Верхняя кромка полоски — отделить от спектра.
  C.Pen.Style := psSolid; C.Pen.Color := CLR_STRIP_TOP; C.Pen.Width := 1;
  C.MoveTo(0, 0); C.LineTo(W, 0);
  C.Pen.Style := psSolid;
end;

function TBandPlanOverlay.NeedsRebuild(W: Integer): Boolean;
var HalfPxHz: Double;
begin
  // Порог перестройки — ПОЛПИКСЕЛЯ, а не 1 Гц: суб-пиксельный сдвиг центра
  // не меняет картинку, а на QO-100 с decoder-lock центр подтюнивается
  // непрерывно — порог 1 Гц заставлял полный canvas-ребилд полоски (и
  // canvas<->raw флип её кэша) на каждом кадре (~1.2 мс/кадр впустую).
  HalfPxHz := 0.5 * FSpanHz / Max(W, 1);
  if HalfPxHz < 1.0 then HalfPxHz := 1.0;
  Result := FCacheDirty or (FCacheW <> W)
            or (FKeyW <> W) or (FKeyEnabled <> FEnabled)
            or (Abs(FKeyCenter - FCenterFreq) >= HalfPxHz)
            or (Abs(FKeySpan - FSpanHz) >= HalfPxHz);
end;

procedure TBandPlanOverlay.RebuildCache(W: Integer);
begin
  if W <= 0 then begin FCacheW := 0; FCacheDirty := False; Exit; end;
  if (FCache.Width <> W) or (FCache.Height <> FStripH) then
    FCache.SetSize(W, FStripH);
  DrawSelf(FCache.Canvas, W);
  FCacheW     := W;
  FKeyCenter  := FCenterFreq; FKeySpan := FSpanHz; FKeyW := W;
  FKeyEnabled := FEnabled;
  FCacheDirty := False;
end;

procedure TBandPlanOverlay.CompositeStrip(Target: TBitmap; DstY: Integer);
// Альфа-композит FCache поверх Target в (0, DstY) с постоянной BANDPLAN_ALPHA.
// Один проход по W × STRIP_H пикселей за кадр. Дворд-блендинг: два 16-битных
// лейна на умножение, деление /256 вместо /255 (расхождение <0.4%, глазом не
// видно) — побайтовый вариант с div 255 стоил ~1мс/кадр на полной ширине.
var
  X, Y, ty, N: Integer;
  A256, I256, S, D: LongWord;
  RS, RD: PLongWord;
begin
  if (Target = nil) or (FCacheW <= 0) then Exit;
  A256 := BANDPLAN_ALPHA + (BANDPLAN_ALPHA shr 7);   // 0..256
  I256 := 256 - A256;
  N := Min(FCacheW, Target.Width);
  Target.BeginUpdate(False);
  FCache.BeginUpdate(False);
  try
    for Y := 0 to FStripH - 1 do
    begin
      ty := DstY + Y;
      if (ty < 0) or (ty >= Target.Height) then Continue;
      RS := PLongWord(FCache.ScanLine[Y]);
      RD := PLongWord(Target.ScanLine[ty]);
      if (RS = nil) or (RD = nil) then Continue;
      for X := 0 to N - 1 do
      begin
        S := RS^; D := RD^;
        RD^ := ((((S and $00FF00FF) * A256 + (D and $00FF00FF) * I256) shr 8)
                 and $00FF00FF)
            or ((((S shr 8) and $00FF00FF) * A256 +
                 ((D shr 8) and $00FF00FF) * I256) and $FF00FF00)
{$IFDEF DARWIN}
            or $000000FF;   // ARGB: альфа в байте 0
{$ELSE}
            or $FF000000;   // BGRA: альфа в байте 3
{$ENDIF}
        Inc(RS); Inc(RD);
      end;
    end;
  finally
    FCache.EndUpdate(False);
    Target.EndUpdate(False);
  end;
end;

procedure TBandPlanOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
begin
  if (not FEnabled) or (Target = nil) or (W <= 0) or (H <= FStripH) then Exit;
  if NeedsRebuild(W) then RebuildCache(W);
  CompositeStrip(Target, H - FStripH);
end;

procedure TBandPlanOverlay.DrawOverlayBitmap(Target: TBitmap; W: Integer);
begin
  if (Target = nil) or (W <= 0) then Exit;
  Target.PixelFormat := pf32bit;
  Target.SetSize(W, FStripH);
  DrawSelf(Target.Canvas, W);
end;

end.
