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

{
  PlatformUtils.pas - small helpers for desktop/runtime platform checks.
}

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

interface

{$IFNDEF HEADLESS}
uses
  Controls;
{$ENDIF}

function HasOpenGLSpectrumSwitch: Boolean;
function CurrentScreenDPI: Integer;
// Целочисленный масштаб канвы экрана: 2 на Retina, 1 везде остальном.
// Windows/Linux всегда получают 1 ⇒ умножение на него ничего не меняет.
function GetScreenScale: Integer;
// То же, но для канвы конкретного контрола: на macOS масштаб берётся у окна,
// которому контрол принадлежит, а не у главного экрана. Это то самое число, в
// котором Cocoa рисует эту канву, и оно меняется, когда окно перетаскивают на
// монитор с другим масштабом. Так и надо считать размер offscreen-битмапа.
{$IFNDEF HEADLESS}
function GetControlScale(AControl: TControl): Integer;
{$ENDIF}
// Монотонные микросекунды одного источника для всех потоков. Нужны там, где
// интервалы считает не человек, а код: пейсинг запросов TX-аудио по TCI ведёт
// абсолютные дедлайны, и часы, способные прыгнуть от NTP, там не годятся.
function MonotonicUs: Int64;

// Тики счётчика → микросекунды, без переполнения. ★Наивное (Ticks * 1000000)
// div Freq переполняет Int64, когда Ticks переваливает за 2^63/10^6 = 9.223e12:
// при типовой для Windows 8+ частоте QPC 10 МГц это 10.7 суток аптайма, дальше
// разрыв повторяется каждые 21.35 суток (2^64/10^6 тиков). В момент разрыва
// время прыгает в большой минус, и код на абсолютных дедлайнах встаёт намертво:
// планировщик TX_CHRONO перестаёт слать маркеры, пейсинг отправителя DUC
// выпускает пакеты без выдержки. Считаем через частное и остаток: остаток
// меньше частоты, поэтому второе слагаемое не переполняется, а первое упирается
// в потолок Int64 через сотни тысяч лет. Функция вынесена в интерфейс, чтобы
// стенд мог проверить её на граничных значениях там, где Windows нет.
function TicksToUs(Ticks, Freq: Int64): Int64;

// Возвращает каталог для хранения конфигурации (с завершающим разделителем).
// macOS:   ~/Library/Application Support/ewsdr/
// Linux:   ~/.config/ewsdr/
// Windows: %APPDATA%\ewsdr\
function GetAppCfgDir: string;

implementation

uses
  SysUtils {$IFNDEF HEADLESS}, Forms{$ENDIF}
  {$IFDEF WINDOWS}, Windows{$ENDIF}
  {$IFDEF LINUX}, Linux, UnixType{$ENDIF}
  {$IF DEFINED(UNIX) AND NOT DEFINED(LINUX)}, BaseUnix{$ENDIF}
  {$IF DEFINED(DARWIN) AND NOT DEFINED(HEADLESS)}, MacScale{$ENDIF};

{$IFDEF WINDOWS}
var
  // ★ОДНО слово на всё состояние часов, и вот почему. Публикуется оно обычной
  // записью, без барьера, а читают его все потоки. Пара «значение + флаг
  // готовности» здесь недопустима: на слабой модели памяти (Windows ARM)
  // читатель вправе увидеть выставленный флаг раньше самого значения, получить
  // частоту 0 и вернуть время 0 — единичный НОЛЬ в монотонных часах, то есть
  // прыжок на десятки лет назад в бухгалтерии TX. С одним словом такого
  // расхождения не бывает: выровненная запись Int64 атомарна, а читатель
  // снимает её РОВНО ОДИН РАЗ в локальную переменную.
  //   0 — источник ещё не выбран;
  //  >0 — частота QPC;
  //  -1 — QPC непригоден, считаем по GetTickCount64 (выбор защёлкнут навсегда:
  //       смена источника на ходу — это тот же скачок времени).
  QPCFreq: Int64 = 0;

procedure InitWinClock;
// Идемпотентна. Зовётся из initialization — то есть ДО старта потоков.
var F: Int64;
begin
  if QPCFreq <> 0 then Exit;
  if QueryPerformanceFrequency(F) and (F > 0) then
    QPCFreq := F
  else
    // Счётчика нет — часы обязаны хотя бы ИДТИ: замершее время для кода на
    // абсолютных дедлайнах означает мёртвую передачу, а не потерю точности.
    QPCFreq := -1;
end;
{$ENDIF}

function TicksToUs(Ticks, Freq: Int64): Int64;
begin
  if Freq <= 0 then Exit(0);
  Result := (Ticks div Freq) * 1000000 + ((Ticks mod Freq) * 1000000) div Freq;
end;

{$IFDEF DARWIN}
// ★macOS: gettimeofday — СТЕННЫЕ часы, шаг NTP или правка руками уводят их
// вперёд и назад. Для абсолютных дедлайнов это негодный источник: назад —
// планировщик замирает до недостижимого срока, вперёд — выдаёт пачку и
// начисляет фиктивный долг. mach_absolute_time монотонен и есть на любой
// версии системы (в отличие от clock_gettime, появившегося в 10.12).
type
  TMachTimebase = record
    Numer: LongWord;
    Denom: LongWord;
  end;

function mach_absolute_time: QWord; cdecl; external 'c' name 'mach_absolute_time';
function mach_timebase_info(var Info: TMachTimebase): Integer; cdecl;
  external 'c' name 'mach_timebase_info';

var
  // ★Тоже ОДНО слово: numer в старшей половине, denom в младшей. Раздельные
  // numer/denom дали бы ту же гонку публикации, что и на Windows (Apple Silicon
  // — слабая модель памяти), а делить на прочитанный ноль нельзя вовсе.
  MachBase: Int64 = 0;

procedure InitMachClock;
// Идемпотентна. Зовётся из initialization — то есть ДО старта потоков.
var
  Info: TMachTimebase;
begin
  if MachBase <> 0 then Exit;
  Info.Numer := 0;
  Info.Denom := 0;
  if (mach_timebase_info(Info) <> 0) or (Info.Denom = 0) or (Info.Numer = 0) then
  begin
    // Тайминг-базы нет (быть такого не должно) — считаем тики наносекундами,
    // как на Intel, где numer = denom = 1.
    Info.Numer := 1;
    Info.Denom := 1;
  end;
  MachBase := (Int64(Info.Numer) shl 32) or Int64(Info.Denom);
end;
{$ENDIF}

function MonotonicUs: Int64;
{$IFDEF WINDOWS}
var
  V, F: Int64;
begin
  F := QPCFreq;                 // снимок ОДНОГО слова: см. объявление
  if F = 0 then
  begin
    InitWinClock;               // страховка: сюда доходит только вызов раньше
    F := QPCFreq;               // initialization, чего в норме не бывает
  end;
  if F < 0 then Exit(Int64(GetTickCount64) * 1000);
  QueryPerformanceCounter(V);
  Result := TicksToUs(V, F);
end;
{$ELSE}
  {$IFDEF LINUX}
var
  TS: TTimeSpec;
begin
  if clock_gettime(CLOCK_MONOTONIC, @TS) <> 0 then Exit(0);
  Result := Int64(TS.tv_sec) * 1000000 + TS.tv_nsec div 1000;
end;
  {$ELSE}
    {$IFDEF DARWIN}
var
  T, Ns, MachNumer, MachDenom: QWord;
  B: Int64;
begin
  B := MachBase;                // снимок ОДНОГО слова: см. объявление
  if B = 0 then
  begin
    InitMachClock;              // страховка: сюда доходит только вызов раньше
    B := MachBase;              // initialization, чего в норме не бывает
  end;
  MachNumer := QWord(B) shr 32;
  MachDenom := QWord(B) and $FFFFFFFF;
  if (MachNumer = 0) or (MachDenom = 0) then Exit(0);
  T := mach_absolute_time;
  // Через частное и остаток по той же причине, что и на Windows: на Apple
  // Silicon numer/denom = 125/3, и произведение тиков на 125 растёт быстро.
  Ns := (T div MachDenom) * MachNumer + ((T mod MachDenom) * MachNumer) div MachDenom;
  Result := Int64(Ns div 1000);
end;
    {$ELSE}
var
  TV: TTimeVal;
begin
  // Прочие Unix: монотонного источника под рукой нет, остаются стенные часы.
  // ★Знать об этом обязан тот, кто ведёт абсолютные дедлайны (см. TCIServer).
  fpgettimeofday(@TV, nil);
  Result := Int64(TV.tv_sec) * 1000000 + TV.tv_usec;
end;
    {$ENDIF}
  {$ENDIF}
{$ENDIF}

function HasOpenGLSpectrumSwitch: Boolean;
var
  I: Integer;
  S: string;
begin
  Result := False;
  for I := 1 to ParamCount do
  begin
    S := LowerCase(ParamStr(I));
    if (S = '--opengl') or (S = '-opengl') or (S = '/opengl') then
      Exit(True);
  end;
  // Бандл под LaunchServices не может передать argv, поэтому тот же
  // переключатель читается из окружения (Info.plist -> LSEnvironment).
  // ★Явная квалификация: на Windows в uses стоит unit Windows, и его
  // GetEnvironmentVariable(PChar;PChar;DWORD) перекрывает однопараметрический
  // из SysUtils — сборка под win64 падала на «wrong number of parameters».
  S := LowerCase(Trim(SysUtils.GetEnvironmentVariable('EWSDR_OPENGL')));
  if (S = '1') or (S = 'true') or (S = 'yes') or (S = 'on') then
    Exit(True);
end;

function GetScreenScale: Integer;
begin
{$IF DEFINED(DARWIN) AND NOT DEFINED(HEADLESS)}
  // Cocoa рисует канву в физических пикселях, а TBitmap всегда 1x. Кто рисует
  // через offscreen-битмап, должен растить его в GetScreenScale раз.
  Result := MacBackingScale;
{$ELSE}
  Result := 1;
{$ENDIF}
end;

{$IFNDEF HEADLESS}
function GetControlScale(AControl: TControl): Integer;
{$IF DEFINED(DARWIN)}
var
  Host: TWinControl;
begin
  // У TPaintBox и прочих TGraphicControl своего NSView нет — рисуют они на
  // канве ближайшего родителя с хендлом, его окно и спрашиваем.
  if AControl is TWinControl then
    Host := TWinControl(AControl)
  else if AControl <> nil then
    Host := AControl.Parent
  else
    Host := nil;
  while (Host <> nil) and not Host.HandleAllocated do
    Host := Host.Parent;

  if Host <> nil then
    Result := MacBackingScaleForView(Pointer(Host.Handle))
  else
    Result := 0;

  // Контрола ещё нет на экране: до первой отрисовки сгодится главный экран.
  if Result < 1 then
    Result := GetScreenScale;
{$ELSE}
begin
  Result := 1;
{$ENDIF}
end;
{$ENDIF}

function CurrentScreenDPI: Integer;
begin
{$IFNDEF HEADLESS}
  Result := Screen.PixelsPerInch;
  if Result <= 0 then Result := 96;
{$ELSE}
  Result := 96;   // headless-демон без LCL: экрана нет, дефолт DPI
{$ENDIF}
end;

function GetAppCfgDir: string;
begin
{$IFDEF WINDOWS}
  // Квалификация обязательна — см. HasOpenGLSpectrumSwitch.
  Result := SysUtils.GetEnvironmentVariable('APPDATA');
  if Result = '' then
    Result := SysUtils.GetEnvironmentVariable('LOCALAPPDATA');
  if Result = '' then
    Result := ExtractFilePath(ParamStr(0));
  Result := IncludeTrailingPathDelimiter(Result) + 'ewsdr' + PathDelim;
{$ELSE}
  Result := IncludeTrailingPathDelimiter(GetAppConfigDir(False));
{$ENDIF}
  if not DirectoryExists(Result) then
    ForceDirectories(Result);
end;

// ★Часы инициализируем ЗДЕСЬ, до старта любых потоков: ленивая инициализация
// из рабочего потока публиковала бы состояние обычной записью, а на слабой
// модели памяти сосед вправе увидеть её частично. Одного нуля в монотонных
// часах хватает, чтобы бухгалтерия TX получила скачок времени.
initialization
{$IFDEF WINDOWS}
  InitWinClock;
{$ENDIF}
{$IFDEF DARWIN}
  InitMachClock;
{$ENDIF}

end.
