unit UDKIniFile;

interface
uses
  System.Classes,
  System.IniFiles,
  System.SysUtils;


type
  { Basically a TMemIniFile with benefits (TMemIniFile isn't virtual enough
    to allow simple additions to it). Adds helpers to handle multiple keys
    with the same name. }
  TUDKIniFile = class(TCustomIniFile)
  private
    FSections: TStringList;
    FEncoding: TEncoding;

    function GetCaseSensitive: Boolean;
    procedure SetCaseSensitive(Value: Boolean);
  protected
    function AddSection(const ASection: string): TStrings;
    procedure LoadValues;

    property Sections: TStringList read FSections;
  public
    constructor Create(const AFileName: string); overload;
    constructor Create(const AFileName: string; AEncoding: TEncoding); overload;
    destructor Destroy; override;

    procedure Clear;

    function ReadString(const ASection, AIdent, ADefault: string): string; override;
    procedure DeleteKey(const ASection, AIdent: string); override;
    procedure EraseSection(const ASection: string); override;
    procedure ReadSection(const ASection: string; AStrings: TStrings); override;
    procedure ReadSections(AStrings: TStrings); override;
    procedure ReadSectionValues(const ASection: string; AStrings: TStrings); override;
    procedure UpdateFile; override;
    procedure WriteString(const ASection, AIdent, AValue: string); override;

    procedure GetStrings(AList: TStrings);
    procedure SetStrings(AList: TStrings);

    { Helpers for duplicate keys }
    procedure ReadDuplicateStrings(const ASection, AIdent: string; AList: TStrings);
    procedure WriteDuplicateString(const ASection, AIdent, AValue: string);
    procedure DeleteDuplicateKeys(const ASection, AIdent: string);

    property CaseSensitive: Boolean read GetCaseSensitive write SetCaseSensitive;
    property Encoding: TEncoding read FEncoding write FEncoding;
  end;


implementation
type
  { Added here so we can access the protected members }
  TUDKHashedStringList = class(THashedStringList);


{ TUDKIniFile }
constructor TUDKIniFile.Create(const AFileName: string);
begin
  Create(AFilename, nil);
end;


constructor TUDKIniFile.Create(const AFileName: string; AEncoding: TEncoding);
begin
  inherited Create(AFileName);

  FEncoding := AEncoding;
  FSections := TUDKHashedStringList.Create;
  LoadValues;
end;


destructor TUDKIniFile.Destroy;
begin
  Clear;
  FreeAndNil(FSections);

  inherited Destroy;
end;


procedure TUDKIniFile.Clear;
var
  sectionIndex: Integer;

begin
  for sectionIndex := 0 to Pred(Sections.Count) do
    Sections.Objects[sectionIndex].Free;

  Sections.Clear;
end;


function TUDKIniFile.ReadString(const ASection, AIdent, ADefault: string): string;
var
  sectionIndex: Integer;
  strings: TStrings;

begin
  sectionIndex := Sections.IndexOf(ASection);
  if sectionIndex > -1 then
  begin
    strings := TStrings(Sections.Objects[sectionIndex]);
    sectionIndex := strings.IndexOfName(AIdent);

    if sectionIndex > -1 then
    begin
      Result := Copy(strings[sectionIndex], Length(AIdent) + 2, MaxInt);
      exit;
    end;
  end;

  Result := ADefault;
end;


procedure TUDKIniFile.DeleteKey(const ASection, AIdent: string);
var
  sectionIndex: Integer;
  keyIndex: Integer;
  Strings: TStrings;

begin
  sectionIndex := FSections.IndexOf(ASection);
  if sectionIndex > -1 then
  begin
    strings := TStrings(FSections.Objects[sectionIndex]);
    keyIndex := strings.IndexOfName(AIdent);

    if keyIndex > -1 then
      strings.Delete(keyIndex);
  end;
end;


procedure TUDKIniFile.EraseSection(const ASection: string);
var
  sectionIndex: Integer;

begin
  sectionIndex := Sections.IndexOf(ASection);
  if sectionIndex > -1 then
  begin
    TStrings(Sections.Objects[sectionIndex]).Free;
    Sections.Delete(sectionIndex);
  end;
end;


procedure TUDKIniFile.ReadSection(const ASection: string; AStrings: TStrings);
var
  sectionIndex: Integer;
  keyIndex: Integer;
  sectionStrings: TStrings;

begin
  AStrings.BeginUpdate;
  try
    AStrings.Clear;

    sectionIndex := Sections.IndexOf(ASection);
    if sectionIndex > -1 then
    begin
      sectionStrings := TStrings(Sections.Objects[sectionIndex]);
      for keyIndex := 0 to Pred(sectionStrings.Count) do
        AStrings.Add(sectionStrings.Names[keyIndex]);
    end;
  finally
    AStrings.EndUpdate;
  end;
end;


procedure TUDKIniFile.ReadSections(AStrings: TStrings);
begin
  AStrings.Assign(Sections);
end;


procedure TUDKIniFile.ReadSectionValues(const ASection: string; AStrings: TStrings);
var
  sectionIndex: Integer;

begin
  AStrings.BeginUpdate;
  try
    AStrings.Clear;

    sectionIndex := Sections.IndexOf(ASection);
    if sectionIndex > -1 then
      AStrings.Assign(TStrings(Sections.Objects[sectionIndex]));
  finally
    AStrings.EndUpdate;
  end;
end;


procedure TUDKIniFile.UpdateFile;
var
  list: TStringList;

begin
  list := TStringList.Create;
  try
    GetStrings(list);
    list.SaveToFile(FileName, FEncoding);
  finally
    FreeAndNil(list);
  end;
end;


procedure TUDKIniFile.WriteString(const ASection, AIdent, AValue: string);
var
  sectionIndex: Integer;
  keyIndex: Integer;
  value: string;
  strings: TStrings;

begin
  sectionIndex := Sections.IndexOf(ASection);
  if sectionIndex > -1 then
    strings := TStrings(Sections.Objects[sectionIndex])
  else
    strings := AddSection(ASection);

  value := AIdent + '=' + AValue;
  keyIndex := strings.IndexOfName(AIdent);

  if keyIndex > -1 then
    strings[keyIndex] := value
  else
    strings.Add(value);
end;


procedure TUDKIniFile.GetStrings(AList: TStrings);
var
  sectionIndex: Integer;
  strings: TStrings;
  value: string;

begin
  AList.BeginUpdate;
  try
    for sectionIndex := 0 to Pred(Sections.Count) do
    begin
      AList.Add('[' + Sections[sectionIndex] + ']');

      strings := TStrings(Sections.Objects[sectionIndex]);
      for value in strings do
        AList.Add(value);

      AList.Add('');
    end;
  finally
    AList.EndUpdate;
  end;
end;


procedure TUDKIniFile.SetStrings(AList: TStrings);
var
  lineIndex: Integer;
  line: string;
  separatorPos: Integer;
  strings: TStrings;

begin
  Clear;
  strings := nil;

  for lineIndex := 0 to Pred(AList.Count) do
  begin
    line := Trim(AList[lineIndex]);

    if (Length(line) > 0) and (line[1] <> ';') then
    begin
      if (line[1] = '[') and (line[Length(line)] = ']') then
      begin
        Delete(line, 1, 1);
        SetLength(line, Length(line) - 1);

        strings := AddSection(Trim(line));
      end
      else
        if Assigned(strings) then
        begin
          separatorPos := Pos('=', line);
          if separatorPos > 0 then
            strings.Add(Trim(Copy(line, 1, Pred(separatorPos))) + '=' + Trim(Copy(line, Succ(separatorPos), MaxInt)))
          else
            strings.Add(line);
        end;
    end;
  end;
end;


procedure TUDKIniFile.ReadDuplicateStrings(const ASection, AIdent: string; AList: TStrings);
var
  sectionIndex: Integer;
  keyIndex: Integer;
  Strings: TStrings;

begin
  sectionIndex := FSections.IndexOf(ASection);
  if sectionIndex > -1 then
  begin
    strings := TStrings(FSections.Objects[sectionIndex]);

    for keyIndex := 0 to Pred(strings.Count) do
    begin
      if (CaseSensitive and (strings.Names[keyIndex] = AIdent)) or
         ((not CaseSensitive) and SameText(strings.Names[keyIndex], AIdent)) then
      begin
        AList.Add(strings.ValueFromIndex[keyIndex]);
      end;
    end;
  end;
end;


procedure TUDKIniFile.WriteDuplicateString(const ASection, AIdent, AValue: string);
var
  sectionIndex: Integer;
  strings: TStrings;

begin
  sectionIndex := Sections.IndexOf(ASection);
  if sectionIndex > -1 then
    strings := TStrings(Sections.Objects[sectionIndex])
  else
    strings := AddSection(ASection);

  strings.Add(AIdent + '=' + AValue);
end;


procedure TUDKIniFile.DeleteDuplicateKeys(const ASection, AIdent: string);
var
  sectionIndex: Integer;
  keyIndex: Integer;
  Strings: TStrings;

begin
  sectionIndex := Sections.IndexOf(ASection);
  if sectionIndex > -1 then
  begin
    strings := TStrings(Sections.Objects[sectionIndex]);

    for keyIndex := Pred(strings.Count) downto 0 do
    begin
      if (CaseSensitive and (strings.Names[keyIndex] = AIdent)) or
         ((not CaseSensitive) and SameText(strings.Names[keyIndex], AIdent)) then
      begin
        strings.Delete(keyIndex)
      end;
    end;
  end;
end;


function TUDKIniFile.AddSection(const ASection: string): TStrings;
begin
  Result := THashedStringList.Create;
  try
    THashedStringList(Result).CaseSensitive := CaseSensitive;
    FSections.AddObject(ASection, Result);
  except
    FreeAndNil(Result);
    raise;
  end;
end;


procedure TUDKIniFile.LoadValues;
var
  size: Integer;
  buffer: TBytes;
  list: TStringList;
  stream: TFileStream;

begin
  if (Length(FileName) > 0) and FileExists(FileName) then
  begin
    stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
    try
      { Load file into buffer and detect encoding }
      size := stream.Size - stream.Position;
      SetLength(buffer, size);
      stream.Read(buffer[0], size);

      size := TEncoding.GetBufferEncoding(buffer, FEncoding);

      { Load strings from buffer }
      list := TStringList.Create;
      try
        list.Text := FEncoding.GetString(buffer, size, Length(buffer) - size);
        SetStrings(list);
      finally
        FreeAndNil(list);
      end;
    finally
      FreeAndNil(stream);
    end;
  end else
    Clear;
end;


function TUDKIniFile.GetCaseSensitive: Boolean;
begin
  Result := Sections.CaseSensitive;
end;


procedure TUDKIniFile.SetCaseSensitive(Value: Boolean);
var
  sectionIndex: Integer;
  sectionList: TUDKHashedStringList;

begin
  if Value <> Sections.CaseSensitive then
  begin
    Sections.CaseSensitive := Value;

    for sectionIndex := 0 to Pred(Sections.Count) do
    begin
      sectionList := TUDKHashedStringList(Sections.Objects[sectionIndex]);
      sectionList.CaseSensitive := Value;
      sectionList.Changed;
    end;

    TUDKHashedStringList(Sections).Changed;
  end;
end;

end.
