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

{ TChannelStore — хранилище каналов (channel memory).
  Формат файла channels.json (рядом с hpsdr_settings.json). }

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils,
  RadioModes;

const
  CHANNELS_FILE = 'channels.json';
  CHANNEL_MODE_MIN = RadioModes.MODE_MIN;
  CHANNEL_MODE_MAX = RadioModes.MODE_MAX;

type
  TChannel = record
    Group:         string;
    Name:          string;
    RXFreq:        Double;    // Hz
    TXFreq:        Double;    // Hz
    Mode:          Integer;   // RadioModes.MODE_* persisted index
    RptDir:        Integer;   // 0=none 1=minus 2=plus
    RptOffsetHz:   Double;    // Hz
    CTCSSOn:       Boolean;
    CTCSSToneIdx:  Integer;   // 0..37
    Power:         Integer;   // 0..100
  end;

  TChannelStore = class
  private
    FChannels: array of TChannel;
    FFilePath: string;
    function  JI(O: TJSONObject; const K: string; Def: Integer): Integer;
    function  JD(O: TJSONObject; const K: string; Def: Double): Double;
    function  JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
    function  JS(O: TJSONObject; const K: string; const Def: string): string;
  public
    constructor Create(const AFilePath: string = '');
    procedure Load;
    procedure Save;
    function  Count: Integer;
    function  GetChannel(Idx: Integer): TChannel;
    procedure AddChannel(const Ch: TChannel);
    procedure UpdateChannel(Idx: Integer; const Ch: TChannel);
    procedure DeleteChannel(Idx: Integer);
    procedure MoveUp(Idx: Integer);
    procedure MoveDown(Idx: Integer);
    class procedure DefaultChannel(out Ch: TChannel);
  end;

implementation

class procedure TChannelStore.DefaultChannel(out Ch: TChannel);
begin
  FillChar(Ch, SizeOf(Ch), 0);
  Ch.Group        := '';
  Ch.Name         := 'New Channel';
  Ch.RXFreq       := 145500000.0;
  Ch.TXFreq       := 145500000.0;
  Ch.Mode         := MODE_FM;
  Ch.RptDir       := 0;
  Ch.RptOffsetHz  := 600000.0;
  Ch.CTCSSOn      := False;
  Ch.CTCSSToneIdx := 0;
  Ch.Power        := 100;
end;

constructor TChannelStore.Create(const AFilePath: string);
begin
  inherited Create;
  if AFilePath = '' then
    FFilePath := GetAppCfgDir + CHANNELS_FILE
  else
    FFilePath := AFilePath;
  SetLength(FChannels, 0);
end;

function TChannelStore.Count: Integer;
begin
  Result := Length(FChannels);
end;

function TChannelStore.GetChannel(Idx: Integer): TChannel;
begin
  if (Idx >= 0) and (Idx < Length(FChannels)) then
    Result := FChannels[Idx]
  else
    DefaultChannel(Result);
end;

procedure TChannelStore.AddChannel(const Ch: TChannel);
var N: Integer;
begin
  N := Length(FChannels);
  SetLength(FChannels, N + 1);
  FChannels[N] := Ch;
end;

procedure TChannelStore.UpdateChannel(Idx: Integer; const Ch: TChannel);
begin
  if (Idx >= 0) and (Idx < Length(FChannels)) then
    FChannels[Idx] := Ch;
end;

procedure TChannelStore.DeleteChannel(Idx: Integer);
var i: Integer;
begin
  if (Idx < 0) or (Idx >= Length(FChannels)) then Exit;
  for i := Idx to Length(FChannels) - 2 do
    FChannels[i] := FChannels[i + 1];
  SetLength(FChannels, Length(FChannels) - 1);
end;

procedure TChannelStore.MoveUp(Idx: Integer);
var Tmp: TChannel;
begin
  if (Idx < 1) or (Idx >= Length(FChannels)) then Exit;
  Tmp            := FChannels[Idx];
  FChannels[Idx] := FChannels[Idx - 1];
  FChannels[Idx - 1] := Tmp;
end;

procedure TChannelStore.MoveDown(Idx: Integer);
var Tmp: TChannel;
begin
  if (Idx < 0) or (Idx >= Length(FChannels) - 1) then Exit;
  Tmp            := FChannels[Idx];
  FChannels[Idx] := FChannels[Idx + 1];
  FChannels[Idx + 1] := Tmp;
end;

// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------

function TChannelStore.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
var D: TJSONData;
begin
  D := O.Find(K);
  if D <> nil then try Result := D.AsInteger; except Result := Def; end
  else Result := Def;
end;

function TChannelStore.JD(O: TJSONObject; const K: string; Def: Double): Double;
var D: TJSONData;
begin
  D := O.Find(K);
  if D <> nil then try Result := D.AsFloat; except Result := Def; end
  else Result := Def;
end;

function TChannelStore.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
var D: TJSONData;
begin
  D := O.Find(K);
  if D <> nil then try Result := D.AsBoolean; except Result := Def; end
  else Result := Def;
end;

function TChannelStore.JS(O: TJSONObject; const K: string; const Def: string): string;
var D: TJSONData;
begin
  D := O.Find(K);
  if D <> nil then try Result := D.AsString; except Result := Def; end
  else Result := Def;
end;

// ---------------------------------------------------------------------------
// Load / Save
// ---------------------------------------------------------------------------

procedure TChannelStore.Load;
var
  F:    TFileStream;
  P:    TJSONParser;
  Root: TJSONData;
  Arr:  TJSONData;
  Item: TJSONData;
  O:    TJSONObject;
  Ch:   TChannel;
  i:    Integer;
begin
  SetLength(FChannels, 0);
  if not FileExists(FFilePath) then Exit;
  try
    F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
    try
      P := TJSONParser.Create(F, [joUTF8]);
      try
        Root := P.Parse;
        if (Root <> nil) and (Root is TJSONObject) then
        begin
          Arr := TJSONObject(Root).Find('channels');
          if (Arr <> nil) and (Arr is TJSONArray) then
          begin
            SetLength(FChannels, TJSONArray(Arr).Count);
            for i := 0 to TJSONArray(Arr).Count - 1 do
            begin
              Item := TJSONArray(Arr).Items[i];
              DefaultChannel(Ch);
              if Item is TJSONObject then
              begin
                O             := TJSONObject(Item);
                Ch.Group      := JS(O, 'group',      '');
                Ch.Name       := JS(O, 'name',       'Channel');
                Ch.RXFreq     := JD(O, 'rx_freq',    145500000.0);
                Ch.TXFreq     := JD(O, 'tx_freq',    145500000.0);
                Ch.Mode       := EnsureRange(JI(O, 'mode', MODE_FM),
                                             CHANNEL_MODE_MIN, CHANNEL_MODE_MAX);
                Ch.RptDir     := EnsureRange(JI(O, 'rpt_dir',    0), 0, 2);
                Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0);
                Ch.CTCSSOn    := JB(O, 'ctcss_on',   False);
                Ch.CTCSSToneIdx := EnsureRange(JI(O, 'ctcss_idx', 0), 0, 37);
                Ch.Power      := EnsureRange(JI(O, 'power',      100), 0, 100);
              end;
              FChannels[i] := Ch;
            end;
          end;
          Root.Free;
        end;
      finally
        P.Free;
      end;
    finally
      F.Free;
    end;
  except
    SetLength(FChannels, 0);
  end;
end;

procedure TChannelStore.Save;
var
  F:    TFileStream;
  Root: TJSONObject;
  Arr:  TJSONArray;
  Obj:  TJSONObject;
  i:    Integer;
  S:    string;
begin
  Root := TJSONObject.Create;
  try
    Arr := TJSONArray.Create;
    Root.Add('channels', Arr);
    for i := 0 to Length(FChannels) - 1 do
    begin
      Obj := TJSONObject.Create;
      Obj.Add('group',      FChannels[i].Group);
      Obj.Add('name',       FChannels[i].Name);
      Obj.Add('rx_freq',    FChannels[i].RXFreq);
      Obj.Add('tx_freq',    FChannels[i].TXFreq);
      Obj.Add('mode',       FChannels[i].Mode);
      Obj.Add('rpt_dir',    FChannels[i].RptDir);
      Obj.Add('rpt_offset', FChannels[i].RptOffsetHz);
      Obj.Add('ctcss_on',   FChannels[i].CTCSSOn);
      Obj.Add('ctcss_idx',  FChannels[i].CTCSSToneIdx);
      Obj.Add('power',      FChannels[i].Power);
      Arr.Add(Obj);
    end;
    try
      S := Root.FormatJSON([], 2);
      F := TFileStream.Create(FFilePath, fmCreate);
      try
        if Length(S) > 0 then F.WriteBuffer(S[1], Length(S));
      finally
        F.Free;
      end;
    except
    end;
  finally
    Root.Free;
  end;
end;

end.
