unit jsonini;

{$mode objfpc}
{$h+}

interface

uses
  Classes, SysUtils, inifiles, fpjson, jsonscanner, jsonparser, dateutils;

type

  { TJSONIniFile }

  TJSONIniFile = class(TCustomIniFile)
  Private
    FJSON: TJSONObject;
    FCacheUpdates: Boolean;
    FDirty : Boolean;
    FStream: TStream;
    procedure SetCacheUpdates(const AValue: Boolean);
  protected
    Function GetRoot : TJSONObject;
    Function GetSection(Const ASectionName : String; AllowCreate : Boolean) : TJSONObject;
    Function GetKeyData(Const ASectionName,AKeyName : String) : TJSONData;
    // Return true if an existing item was replaced
    Function SetKeyData(Const ASectionName,AKeyName : String; AData : TJSONData) : Boolean;
    procedure MaybeUpdateFile;
    property Dirty : Boolean Read FDirty;
  public
    constructor Create(const AFileName: string; AOptions : TIniFileOptions = []); override; overload;
    constructor Create(AStream: TStream; AOptions : TJSONOptions); overload;
    destructor Destroy; override;
    Class Procedure ConvertIni(Const AIniFile,AJSONFile : String; StringsOnly : Boolean = True);
    function ReadString(const Section, Ident, Default: string): string; override;
    function ReadInteger(const Section, Ident: string; Default: Longint): Longint; override;
    function ReadInt64(const Section, Ident: string; Default: Int64): Int64; override;
    function ReadBool(const Section, Ident: string; Default: Boolean): Boolean; override;
    function ReadDate(const Section, Ident: string; Default: TDateTime): TDateTime; override;
    function ReadDateTime(const Section, Ident: string; Default: TDateTime): TDateTime; override;
    function ReadFloat(const Section, Ident: string; Default: Double): Double; override;
    function ReadTime(const Section, Ident: string; Default: TDateTime): TDateTime; override;
    procedure WriteString(const Section, Ident, Value: String); override;
    procedure WriteDate(const Section, Ident: string; Value: TDateTime); override;
    procedure WriteDateTime(const Section, Ident: string; Value: TDateTime); override;
    procedure WriteFloat(const Section, Ident: string; Value: Double); override;
    procedure WriteTime(const Section, Ident: string; Value: TDateTime); override;
    procedure WriteInteger(const Section, Ident: string; Value: Longint); override;
    procedure WriteInt64(const Section, Ident: string; Value: Int64); override;
    procedure WriteBool(const Section, Ident: string; Value: Boolean); override;
    procedure ReadSection(const Section: string; Strings: TStrings); override;
    procedure ReadSections(Strings: TStrings); override;
    procedure ReadSectionValues(const Section: string; Strings: TStrings; AOptions : TSectionValuesOptions = [svoIncludeInvalid]); overload; override;
    procedure EraseSection(const Section: string); override;
    procedure DeleteKey(const Section, Ident: String); override;
    procedure UpdateFile; override; overload;
    procedure UpdateFile(Const AFileName : string); overload;
    property Stream: TStream read FStream;
    property CacheUpdates : Boolean read FCacheUpdates write SetCacheUpdates;
  end;

implementation

{ TJSONIniFile }

procedure TJSONIniFile.SetCacheUpdates(const AValue: Boolean);
begin
  if FCacheUpdates and not AValue and FDirty then
    UpdateFile;
end;

function TJSONIniFile.GetRoot: TJSONObject;
begin
  Result:=FJSON;
end;

function TJSONIniFile.GetSection(const ASectionName: String; AllowCreate: Boolean): TJSONObject;

Var
  I : Integer;
  R : TJSONObject;

begin
  Result:=Nil;
  R:=GetRoot;
  I:=R.IndexOfName(ASectionName,True);
  if (I<>-1) and (R.Items[i].JSONType=jtObject) then
    Result:=R.Items[i] as TJSONObject
  else if AllowCreate then
    begin
    if (I<>-1) then
      R.Delete(I);
    Result:=TJSONObject.Create;
    R.Add(ASectionName,Result);
    end;
end;

function TJSONIniFile.GetKeyData(const ASectionName, AKeyName: String): TJSONData;

Var
  O : TJSONObject;
  I : integer;

begin
  Result:=Nil;
  O:=GetSection(ASectionName,False);
  if Assigned(O) then
    begin
    I:=O.IndexOfName(AKeyName,True);
    if (I<>-1) and (O.Items[i].JSONType in ActualValueJSONTypes) then
      Result:=O.Items[i];
    end
end;

function TJSONIniFile.SetKeyData(const ASectionName, AKeyName: String; AData: TJSONData): Boolean;
Var
  O : TJSONObject;
  I : integer;

begin
  O:=GetSection(ASectionName,true);
  I:=O.IndexOfName(AKeyName,True);
  Result:=(I<>-1);
  if Result then
    O.Delete(I);
  O.Add(aKeyName,AData);
  FDirty:=True;
end;

procedure TJSONIniFile.MaybeUpdateFile;
begin
  If FCacheUpdates then
    FDirty:=True
  else
    UpdateFile;
end;

constructor TJSONIniFile.Create(const AFileName: string; AOptions : TIniFileOptions = []);

Var
  F : TFileStream;

begin
  Inherited Create(AFileName,AOptions);
  if Not FileExists(AFileName) then
    FJSON:=TJSONObject.Create
  else
    begin
    F:=TFileStream.Create(AFileName,fmOpenRead or fmShareDenyWrite);
    try
      Create(F,[joUTF8,joComments,joIgnoreTrailingComma]);
    finally
      F.Free;
    end;
    end;
end;

constructor TJSONIniFile.Create(AStream: TStream; AOptions: TJSONOptions);

Var
  P : TJSONParser;
  D : TJSONData;

begin
  D:=Nil;
  P:=TJSONParser.Create(AStream,AOptions);
  try
    D:=P.Parse;
    if (D is TJSONObject) then
      begin
      FJSON:=D as TJSONObject;
      D:=Nil;
      end
    else
      FJSON:=TJSONObject.Create;
  finally
    D.Free;
    P.Free;
  end;
end;

destructor TJSONIniFile.Destroy;
begin
  FreeAndNil(FJSON);
  inherited Destroy;
end;

class procedure TJSONIniFile.ConvertIni(const AIniFile, AJSONFile: String; StringsOnly: Boolean = true);

Var
  SIni : TMemIniFile;
  Dini : TJSONIniFile;
  S,K : TStrings;
  SN,KN,V : String;
  I6 : Int64;
  F : Double;
  B : Boolean;
  DT : TDateTime;

begin
  S:=Nil;
  K:=Nil;
  Dini:=Nil;
  SIni:=TMemIniFile.Create(AIniFile);
  try
    DIni:=Self.Create(AJSONFile);
    S:=TStringList.Create;
    K:=TStringList.Create;
    SIni.ReadSections(S);
    For SN in S do
      begin
      SIni.ReadSection(SN,K);
      For KN in K do
        begin
        V:=Sini.ReadString(SN,KN,'');
        if StringsOnly then
          Dini.WriteString(SN,KN,V)
        else
          begin
          If TryStrToInt64(V,I6) then
            Dini.WriteInt64(SN,KN,I6)
          else If TryStrToFloat(V,F) then
            Dini.WriteFloat(SN,KN,F)
          else If TryStrToBool(V,B) then
            Dini.WriteBool(SN,KN,B)
          else
            begin
            DT:=SIni.ReadTime(SN,KN,-1);
            B:=DT<>-1;
            if B then
              DIni.WriteTime(SN,KN,DT)
            else
              begin
              DT:=SIni.ReadDate(SN,KN,0);
              B:=DT<>0;
              if B then
                DIni.WriteDate(SN,KN,DT)
              else
                begin
                DT:=SIni.ReadDateTime(SN,KN,0);
                B:=DT<>0;
                if B then
                  DIni.WriteDateTime(SN,KN,DT)
                end;
              end;
            if Not B then
              Dini.WriteString(SN,KN,V)
            end;
          end;
        end;
      end;
    Dini.UpdateFile;
  finally
    FreeAndNil(S);
    FreeAndNil(K);
    FreeAndNil(Dini);
    FreeAndNil(Sini);
  end;
end;

function TJSONIniFile.ReadString(const Section, Ident, Default: string): string;

Var
  D : TJSONData;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else
    begin
    if D.JSONType in StructuredJSONTypes then
      Result:=D.AsJSON
    else
      Result:=D.AsString;
    end
end;

function TJSONIniFile.ReadInteger(const Section, Ident: string; Default: Longint): Longint;

Var
  D : TJSONData;
begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else
    if D.JSONType=jtNumber then
      Result:=D.AsInteger
    else
      if not TryStrToInt(D.AsString,Result) then
        Result:=Default;
end;

function TJSONIniFile.ReadInt64(const Section, Ident: string; Default: Int64): Int64;

Var
  D : TJSONData;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else
    if D.JSONType=jtNumber then
      Result:=D.AsInt64
    else
      if not TryStrToInt64(D.AsString,Result) then
        Result:=Default;
end;

function TJSONIniFile.ReadBool(const Section, Ident: string; Default: Boolean): Boolean;

Var
  D : TJSONData;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else
    // Avoid exception frame
    if D.JSONType=jtBoolean then
      Result:=D.AsBoolean
    else
      try
        Result:=D.AsBoolean;
      except
        Result:=Default;
      end;
end;

function TJSONIniFile.ReadDate(const Section, Ident: string; Default: TDateTime): TDateTime;

Var
  D : TJSONData;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else if D.JSONType=jtNumber then
    Result:=TDateTime(D.AsFloat)
  else
    Result:=ScanDateTime('yyyy"-"mm"-"dd',D.AsString);
end;

function TJSONIniFile.ReadDateTime(const Section, Ident: string; Default: TDateTime): TDateTime;
Var
  D : TJSONData;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else if D.JSONType=jtNumber then
    Result:=TDateTime(D.AsFloat)
  else
    Result:=ScanDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss"."zzz',D.AsString);
end;

function TJSONIniFile.ReadFloat(const Section, Ident: string; Default: Double): Double;
Var
  D : TJSONData;
  C : Integer;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else
    if D.JSONType=jtNumber then
      Result:=D.AsFloat
    else
      // Localized
      if not TryStrToFloat(D.AsString,Result) then
        begin
        // Not localized
        Val(D.AsString,Result,C);
        if (C<>0) then
          Result:=Default;
        end;
end;

function TJSONIniFile.ReadTime(const Section, Ident: string; Default: TDateTime): TDateTime;

Var
  D : TJSONData;

begin
  D:=GetKeyData(Section,Ident);
  if Not Assigned(D) then
    Result:=Default
  else if D.JSONType=jtNumber then
    Result:=Frac(TDateTime(D.AsFloat))
  else
    Result:=ScanDateTime('"0000-00-00T"hh":"nn":"ss"."zzz',D.AsString);
end;

procedure TJSONIniFile.WriteString(const Section, Ident, Value: String);
begin
  SetKeyData(Section,Ident,CreateJSON(Value));
end;

procedure TJSONIniFile.WriteDate(const Section, Ident: string; Value: TDateTime);
begin
  SetKeyData(Section,Ident,CreateJSON(FormatDateTime('yyyy"-"mm"-"dd"T"00":"00":"00.zzz',Value)));
end;

procedure TJSONIniFile.WriteDateTime(const Section, Ident: string; Value: TDateTime);
begin
  SetKeyData(Section,Ident,CreateJSON(FormatDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss.zzz',Value)));
end;

procedure TJSONIniFile.WriteFloat(const Section, Ident: string; Value: Double);
begin
  SetKeyData(Section,Ident,CreateJSON(Value));
end;

procedure TJSONIniFile.WriteTime(const Section, Ident: string; Value: TDateTime);
begin
  SetKeyData(Section,Ident,CreateJSON(FormatDateTime('0000"-"00"-"00"T"hh":"nn":"ss.zzz',Value)));
end;

procedure TJSONIniFile.WriteInteger(const Section, Ident: string; Value: Longint);
begin
  SetKeyData(Section,Ident,CreateJSON(Value));
end;

procedure TJSONIniFile.WriteInt64(const Section, Ident: string; Value: Int64);
begin
  SetKeyData(Section,Ident,CreateJSON(Value));
end;

procedure TJSONIniFile.WriteBool(const Section, Ident: string; Value: Boolean);
begin
  SetKeyData(Section,Ident,CreateJSON(Value));
end;

procedure TJSONIniFile.ReadSection(const Section: string; Strings: TStrings);
Var
  O : TJSONObject;
  E : TJSONEnum;

begin
  O:=GetSection(Section,False);
  if Assigned(O) then
    For E in O do
      If (E.Value.JSONType in ActualValueJSONTypes) then
        Strings.Add(E.Key);
end;

procedure TJSONIniFile.ReadSections(Strings: TStrings);

Var
  R : TJSONObject;
  E : TJSONEnum;

begin
  R:=GetRoot;
  for E in R do
    if E.Value.JSONType=jtObject then
      Strings.Add(E.Key);
end;

procedure TJSONIniFile.ReadSectionValues(const Section: string; Strings: TStrings; AOptions: TSectionValuesOptions);

Var
  O : TJSONObject;
  E : TJSONEnum;
  V : TJSONStringType;

begin
  O:=GetSection(Section,False);
  if Assigned(O) then
    For E in O do
      begin
      If (E.Value.JSONType in ActualValueJSONTypes) then
        begin
        V:=E.Value.AsString;
        Strings.Add(E.Key+'='+V);
        end
      else if (svoIncludeInvalid in AOptions) then
        begin
        V:=E.Value.AsJSON;
        Strings.Add(E.Key+'='+V);
        end
      end;
end;

procedure TJSONIniFile.EraseSection(const Section: string);

Var
  I : Integer;

begin
  I:=GetRoot.IndexOfName(Section,True);
  if (I<>-1) then
    begin
    GetRoot.Delete(I);
    MaybeUpdateFile;
    end;
end;

procedure TJSONIniFile.DeleteKey(const Section, Ident: String);

Var
  O : TJSONObject;
  I : integer;

begin
  O:=GetSection(Section,False);
  if O<>Nil then
    begin
    I:=O.IndexOfName(Ident,True);
    if I<>-1 then
      begin
      O.Delete(I);
      MaybeUpdateFile;
      end;
    end;
end;

procedure TJSONIniFile.UpdateFile;


begin
  If (FileName<>'') then
    UpdateFile(FileName)
end;

procedure TJSONIniFile.UpdateFile(const AFileName: string);

Var
  S : TJSONStringType;

begin
  With TFileStream.Create(AFileName,fmCreate) do
    try
      S:=FJSON.FormatJSON();
      WriteBuffer(S[1],Length(S));
    finally
      Free;
    end;
end;

end.

