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

{
  TFreqDisplay — цифровой дисплей частоты с управлением по разрядам
  ===================================================================
  Отображает частоту в Гц вида  14.201.123
  Наводишь мышь на цифру → подсветка разряда.
  Колёсико мыши → меняет выделенный разряд (+/- 10^N).
  Стрелки Left/Right → переключают активный разряд.
  Стрелки Up/Down    → меняют активный разряд.
}

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Controls, Graphics, LCLType;

type
  TFreqChangeEvent = procedure(Sender: TObject; NewFreq: Int64) of object;

  TFreqDisplay = class(TCustomControl)
  private
    FFrequency:   Int64;
    FMinFreq:     Int64;
    FMaxFreq:     Int64;
    FFontSize:    Integer;
    FFontName:    string;
    FColorNormal:   TColor;
    FColorHover:    TColor;
    FColorHoverBG:  TColor;
    FColorDim:      TColor;
    FMinMhzDigits: Integer;
    FDimLeadingZeros: Boolean;
    FCenterText: Boolean;
    FOnChange:    TFreqChangeEvent;

    FHoverDigit:  Integer;          // 0=единицы .. 10=10ГГц, -1=нет
    FDigitX:      array[0..10] of Integer; // X левого края каждой цифры
    FDigitW:      Integer;
    FCharH:       Integer;

    procedure SetFrequency(V: Int64);
    procedure SetFontSize(V: Integer);
    function  ClampFreq(V: Int64): Int64;
    function  DigitAtX(X: Integer): Integer;
    function  DigitStep(D: Integer): Int64;
    procedure ChangeByDigit(D, Delta: Integer);
    procedure BuildDigitMap(const S: string; StartX: Integer);

  protected
    procedure Paint; override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure MouseLeave; override;
    function  DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
      MousePos: TPoint): Boolean; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure Click; override;

  public
    constructor Create(AOwner: TComponent); override;

    property Frequency:   Int64    read FFrequency   write SetFrequency;
    property MinFreq:     Int64    read FMinFreq     write FMinFreq;
    property MaxFreq:     Int64    read FMaxFreq     write FMaxFreq;
    property FontSize:    Integer  read FFontSize    write SetFontSize;
    property FontName:    string   read FFontName    write FFontName;
    property ColorNormal:  TColor  read FColorNormal  write FColorNormal;
    property ColorHover:   TColor  read FColorHover   write FColorHover;
    property ColorHoverBG: TColor  read FColorHoverBG write FColorHoverBG;
    property ColorDim:     TColor  read FColorDim     write FColorDim;
    property MinMhzDigits: Integer read FMinMhzDigits write FMinMhzDigits;
    property DimLeadingZeros: Boolean read FDimLeadingZeros write FDimLeadingZeros;
    property CenterText: Boolean read FCenterText write FCenterText;
    property OnChange:    TFreqChangeEvent read FOnChange write FOnChange;
  end;

implementation

constructor TFreqDisplay.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FFrequency    := 14200000;
  FMinFreq      := 0;
  FMaxFreq      := 2000000000;
  FFontSize     := 20;
  FFontName     := 'Courier New';
  FColorNormal  := TColor($00E8F0FF);
  FColorHover   := TColor($0040DDFF);
  FColorHoverBG := TColor($00182838);
  FColorDim     := TColor($00607080);
  FMinMhzDigits := 1;
  FDimLeadingZeros := False;
  FCenterText    := True;
  FHoverDigit   := -1;
  FDigitW       := 14;
  FCharH        := 24;
  TabStop       := True;
  Width         := 220;
  Height        := 40;
end;

function TFreqDisplay.ClampFreq(V: Int64): Int64;
begin
  if V < FMinFreq then Result := FMinFreq
  else if V > FMaxFreq then Result := FMaxFreq
  else Result := V;
end;

function TFreqDisplay.DigitStep(D: Integer): Int64;
var
  i:      Integer;
  Result_: Int64;
begin
  Result_ := 1;
  for i := 0 to D - 1 do
    Result_ := Result_ * 10;
  Result := Result_;
end;

function TFreqDisplay.DigitAtX(X: Integer): Integer;
var
  i: Integer;
begin
  Result := -1;
  if FDigitW = 0 then Exit;
  for i := 0 to 10 do
    if (X >= FDigitX[i]) and (X < FDigitX[i] + FDigitW) then
    begin
      Result := i;
      Exit;
    end;
end;

// Строим карту FDigitX из строки вида "14.201.123"
// Цифры нумеруются справа налево: digit0=единицы, digit8=сотни млн
procedure TFreqDisplay.BuildDigitMap(const S: string; StartX: Integer);
var
  Xs:    array[0..13] of Integer;  // X каждого символа строки (макс 14 симв)
  ci:    Integer;
  xi:    Integer;
  dIdx:  Integer;
  len:   Integer;
begin
  len := Length(S);
  if len > 13 then len := 13;

  xi := StartX;
  for ci := 0 to len - 1 do
  begin
    Xs[ci] := xi;
    Inc(xi, Canvas.TextWidth(S[ci + 1]));
  end;

  // Назначаем digit индексы справа налево, пропуская точки
  dIdx := 0;
  for ci := len - 1 downto 0 do
  begin
    if S[ci + 1] <> '.' then
    begin
      if dIdx <= 10 then
        FDigitX[dIdx] := Xs[ci];
      Inc(dIdx);
    end;
  end;
end;

procedure TFreqDisplay.Paint;
var
  C:       TCanvas;
  S:       string;
  Hz:      Int64;
  Mhz:     Integer;
  KHz:     Integer;
  Ones:    Integer;
  MhzFmt:  string;
  MhzText: string;
  LeadingMhzZeros: Integer;
  TotalW:  Integer;
  StartX:  Integer;
  ChW, ChH: Integer;
  xi:      Integer;
  ci:      Integer;
  ch:      Char;
  dIdx:    Integer;
  col:     TColor;
  DiChar:  array[0..13] of Integer;
  tmpIdx:  Integer;
  len:     Integer;
begin
  C := Canvas;

  // Фон — прозрачный (наследует от Panel)
  C.Brush.Style := bsClear;
  C.FillRect(ClientRect);

  C.Font.Name  := FFontName;
  C.Font.Size  := FFontSize;
  C.Font.Bold  := True;
  C.Font.Style := [fsBold];

  ChW := C.TextWidth('0');
  ChH := C.TextHeight('0');
  FDigitW := ChW;
  FCharH  := ChH;

  Hz   := Abs(FFrequency);
  Mhz  := Hz div 1000000;
  KHz  := (Hz div 1000) mod 1000;
  Ones := Hz mod 1000;
  if FMinMhzDigits > 1 then
    MhzFmt := '%.' + IntToStr(FMinMhzDigits) + 'd'
  else
    MhzFmt := '%d';
  MhzText := Format(MhzFmt, [Mhz]);
  S := MhzText + Format('.%3.3d.%3.3d', [KHz, Ones]);

  LeadingMhzZeros := 0;
  while FDimLeadingZeros and
        (LeadingMhzZeros < Length(MhzText) - 1) and
        (MhzText[LeadingMhzZeros + 1] = '0') do
    Inc(LeadingMhzZeros);

  TotalW := C.TextWidth(S);
  if FCenterText then
    StartX := (Width - TotalW) div 2
  else
    StartX := 2;
  if StartX < 2 then StartX := 2;

  // Строим карту digit → X
  BuildDigitMap(S, StartX);

  // Строим обратную карту символ → digit index
  len := Length(S);
  if len > 13 then len := 13;
  tmpIdx := 0;
  for ci := len - 1 downto 0 do
  begin
    if S[ci + 1] <> '.' then
    begin
      DiChar[ci] := tmpIdx;
      Inc(tmpIdx);
    end
    else
      DiChar[ci] := -1;
  end;

  // Рисуем
  xi := StartX;
  for ci := 0 to len - 1 do
  begin
    ch   := S[ci + 1];
    dIdx := DiChar[ci];

    if ch = '.' then
      col := FColorDim
    else if FDimLeadingZeros and (ci < LeadingMhzZeros) then
      col := FColorDim
    else if dIdx = FHoverDigit then
      col := FColorHover
    else
      col := FColorNormal;

    // Фоновая подсветка активного разряда
    if (dIdx >= 0) and (dIdx = FHoverDigit) then
    begin
      C.Brush.Color := FColorHoverBG;
      C.Brush.Style := bsSolid;
      C.FillRect(Rect(xi - 1, 2, xi + ChW + 1, Height - 2));
      C.Brush.Style := bsClear;
    end;

    C.Font.Color  := col;
    C.Brush.Style := bsClear;
    C.TextOut(xi, (Height - ChH) div 2, ch);
    Inc(xi, C.TextWidth(ch));
  end;

  // Рамка фокуса
  if Focused then
  begin
    C.Pen.Color  := TColor($00004060);
    C.Pen.Style  := psDot;
    C.Pen.Width  := 1;
    C.Brush.Style := bsClear;
    C.Rectangle(1, 1, Width - 2, Height - 2);
    C.Pen.Style := psSolid;
  end;
end;

procedure TFreqDisplay.ChangeByDigit(D, Delta: Integer);
var
  Step:    Int64;
  Base:    Int64;
  NewFreq: Int64;
begin
  if D < 0 then Exit;
  Step := DigitStep(D);
  // Нижняя граница текущего разряда (кратная Step)
  Base := (FFrequency div Step) * Step;
  if Delta > 0 then
  begin
    // Вверх: от нижней границы + шаг
    // 7.129.054 + 100 → base=7.129.000 → 7.129.000 + 100 = 7.129.100
    NewFreq := Base + Step * Delta;
  end
  else
  begin
    // Вниз:
    // Если уже кратна — просто шагаем: 7.129.100 - 100 = 7.129.000
    // Если не кратна — возвращаем нижнюю границу: 7.129.054 → 7.129.000
    if FFrequency = Base then
      NewFreq := Base + Step * Delta  // кратна → шагаем
    else
      NewFreq := Base;                // не кратна → снэп к нижней границе
  end;
  NewFreq := ClampFreq(NewFreq);
  if NewFreq <> FFrequency then
  begin
    FFrequency := NewFreq;
    Invalidate;
    if Assigned(FOnChange) then
      FOnChange(Self, FFrequency);
  end;
end;

procedure TFreqDisplay.MouseMove(Shift: TShiftState; X, Y: Integer);
var
  d: Integer;
begin
  inherited;
  d := DigitAtX(X);
  if d <> FHoverDigit then
  begin
    FHoverDigit := d;
    Invalidate;
  end;
  if d >= 0 then Cursor := crHandPoint
  else            Cursor := crDefault;
end;

procedure TFreqDisplay.MouseLeave;
begin
  inherited;
  if FHoverDigit <> -1 then
  begin
    FHoverDigit := -1;
    Invalidate;
  end;
  Cursor := crDefault;
end;

function TFreqDisplay.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer;
  MousePos: TPoint): Boolean;
var
  d:     Integer;
  Delta: Integer;
  Local: TPoint;
begin
  Result := False;
  Local  := ScreenToClient(MousePos);
  d := FHoverDigit;
  if d < 0 then
    d := DigitAtX(Local.X);
  if d >= 0 then
  begin
    if WheelDelta > 0 then Delta := 1 else Delta := -1;
    ChangeByDigit(d, Delta);
    Result := True;  // обработали — не передаём дальше
  end;
  if not Result then
    Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
end;

procedure TFreqDisplay.KeyDown(var Key: Word; Shift: TShiftState);
var
  d: Integer;
begin
  d := FHoverDigit;
  if d < 0 then d := 3;  // по умолчанию — кГц
  case Key of
    VK_UP:   begin ChangeByDigit(d, +1); Key := 0; end;
    VK_DOWN: begin ChangeByDigit(d, -1); Key := 0; end;
    VK_LEFT: begin
               if FHoverDigit < 0 then FHoverDigit := 3;
               if FHoverDigit < FMinMhzDigits + 5 then Inc(FHoverDigit);
               Invalidate; Key := 0;
             end;
    VK_RIGHT: begin
               if FHoverDigit < 0 then FHoverDigit := 3;
               if FHoverDigit > 0 then Dec(FHoverDigit);
               Invalidate; Key := 0;
              end;
  end;
  inherited KeyDown(Key, Shift);
end;

procedure TFreqDisplay.Click;
begin
  inherited;
  SetFocus;
end;

procedure TFreqDisplay.SetFrequency(V: Int64);
begin
  V := ClampFreq(V);
  if V <> FFrequency then
  begin
    FFrequency := V;
    Invalidate;
  end;
end;

procedure TFreqDisplay.SetFontSize(V: Integer);
begin
  if V <> FFontSize then
  begin
    FFontSize := V;
    Invalidate;
  end;
end;

end.
