{
    SettingFiles содержит классы для работы с файлами инициализации (.ini).

    Copyright © 2016, 2019, 2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять её и/или
    изменять её на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она может быть полезна,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЁННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <http://www.gnu.org/licenses/>.
}

unit SettingFiles;

{$MODE DELPHI}

interface

uses
    Lang,
    IOStreams,
    FileIO,
    TextFiles;

{%region public }
type
    InvalidSectionIdentException = class;
    AbstractInitializationFile = class;
    NormalInitializationFile = class;

    InvalidSectionIdentException = class(Exception)
    public
        constructor create(); overload;
        constructor create(const message: AnsiString); overload;
    end;

    AbstractInitializationFile = class(_Object)
    public
        function readSections(): AnsiString_Array1d; virtual; abstract;
        function readSection(const section: AnsiString): AnsiString_Array1d; virtual; abstract;
        function readString(const section, ident, default: AnsiString): AnsiString; virtual; abstract;
        procedure writeString(const section, ident, value: AnsiString); virtual; abstract;
        procedure eraseSection(const section: AnsiString); virtual; abstract;
        procedure deleteIdent(const section, ident: AnsiString); virtual; abstract;
        procedure loadFromStream(stream: Input); virtual; abstract;
        procedure saveToStream(stream: Output); virtual; abstract;
        procedure updateFile(); virtual; abstract;
        function isSectionExists(const section: AnsiString): boolean; virtual;
        function isIdentExists(const section, ident: AnsiString): boolean; virtual;
        function readBoolean(const section, ident: AnsiString; default: boolean): boolean; virtual;
        function readInt(const section, ident: AnsiString; default: int): int; virtual;
        function readLong(const section, ident: AnsiString; default: long): long; virtual;
        function readFloat(const section, ident: AnsiString; default: float): float; virtual;
        function readDouble(const section, ident: AnsiString; default: double): double; virtual;
        function readReal(const section, ident: AnsiString; default: real): real; virtual;
        procedure writeBoolean(const section, ident: AnsiString; value: boolean); virtual;
        procedure writeInt(const section, ident: AnsiString; value: int); virtual;
        procedure writeLong(const section, ident: AnsiString; value: long); virtual;
        procedure writeFloat(const section, ident: AnsiString; value: float); virtual;
        procedure writeDouble(const section, ident: AnsiString; value: double); virtual;
        procedure writeReal(const section, ident: AnsiString; value: real); virtual;
    end;

    NormalInitializationFile = class(AbstractInitializationFile)
    private
        fileName: AnsiString;
        sections: int_Array1d;
        strings: AnsiString_Array1d;
        sectionsCount: int;
        stringsCount: int;
        function getSectionAt(index: int): AnsiString;
        function indexOfSection(const section: AnsiString): int;
        function indexOfIdent(const section, ident: AnsiString): int;
        procedure insertString(index: int; const str: AnsiString);
        procedure deleteString(index: int);
    public
        constructor create(); overload;
        constructor create(const fileName: AnsiString); overload;
        function isSectionExists(const section: AnsiString): boolean; override;
        function isIdentExists(const section, ident: AnsiString): boolean; override;
        function readSections(): AnsiString_Array1d; override;
        function readSection(const section: AnsiString): AnsiString_Array1d; override;
        function readString(const section, ident, default: AnsiString): AnsiString; override;
        procedure writeString(const section, ident, value: AnsiString); override;
        procedure eraseSection(const section: AnsiString); override;
        procedure deleteIdent(const section, ident: AnsiString); override;
        procedure loadFromStream(stream: Input); override;
        procedure saveToStream(stream: Output); override;
        procedure updateFile(); override;
        function getFileName(): AnsiString; virtual;

    strict private const
        BRACKET_LEFT = '[';
        BRACKET_RIGHT = ']';
        IDENT_VALUE_SEPARATOR = '=';
        LINE_ESCAPE = '\';
        COMMENT_START = ';';
    end;
{%endregion}

implementation

{%region routine }
    function booleanToString(value: boolean): AnsiString;
    begin
        if value = false then begin
            result := '0';
        end else begin
            result := '1';
        end;
    end;
{%endregion}

{%region InvalidSectionIdentException }
    constructor InvalidSectionIdentException.create();
    begin
        inherited create();
    end;

    constructor InvalidSectionIdentException.create(const message: AnsiString);
    begin
        inherited create(message);
    end;
{%endregion}

{%region AbstractInitializationFile }
    function AbstractInitializationFile.isSectionExists(const section: AnsiString): boolean;
    var
        str: AnsiString;
        s: AnsiString_Array1d;
        i: int;
    begin
        str := trim(section);
        s := readSections();
        for i := length(s) - 1 downto 0 do begin
            if trim(s[i]) = str then begin
                result := true;
                exit;
            end;
        end;
        result := false;
    end;

    function AbstractInitializationFile.isIdentExists(const section, ident: AnsiString): boolean;
    var
        str: AnsiString;
        s: AnsiString_Array1d;
        i: int;
    begin
        str := trim(ident);
        s := readSection(trim(section));
        for i := length(s) - 1 downto 0 do begin
            if trim(s[i]) = str then begin
                result := true;
                exit;
            end;
        end;
        result := false;
    end;

    function AbstractInitializationFile.readBoolean(const section, ident: AnsiString; default: boolean): boolean;
    var
        s: AnsiString;
    begin
        s := trim(readString(section, ident, booleanToString(default)));
        if length(s) <> 1 then begin
            result := default;
            exit;
        end;
        case s[1] of
        '1':
            result := true;
        '0':
            result := false;
        else
            result := default;
        end;
    end;

    function AbstractInitializationFile.readInt(const section, ident: AnsiString; default: int): int;
    var
        s: AnsiString;
    begin
        s := trim(readString(section, ident, toDecString(default)));
        if startsWith('0x', s) then begin
            result := parseHexInt(copy(s, 3, length(s) - 2), default);
            exit;
        end;
        if startsWith('$', s) then begin
            result := parseHexInt(copy(s, 2, length(s) - 1), default);
            exit;
        end;
        result := parseDecInt(s, default);
    end;

    function AbstractInitializationFile.readLong(const section, ident: AnsiString; default: long): long;
    var
        s: AnsiString;
    begin
        s := trim(readString(section, ident, toDecString(default)));
        if startsWith('0x', s) then begin
            result := parseHexLong(copy(s, 3, length(s) - 2), default);
            exit;
        end;
        if startsWith('$', s) then begin
            result := parseHexLong(copy(s, 2, length(s) - 1), default);
            exit;
        end;
        result := parseDecLong(s, default);
    end;

    function AbstractInitializationFile.readFloat(const section, ident: AnsiString; default: float): float;
    var
        r: real;
    begin
        r := toReal(default);
        result := toFloat(parseReal(trim(readString(section, ident, toDecString(r))), r));
    end;

    function AbstractInitializationFile.readDouble(const section, ident: AnsiString; default: double): double;
    var
        r: real;
    begin
        r := toReal(default);
        result := toFloat(parseReal(trim(readString(section, ident, toDecString(r))), r));
    end;

    function AbstractInitializationFile.readReal(const section, ident: AnsiString; default: real): real;
    begin
        result := toFloat(parseReal(trim(readString(section, ident, toDecString(default))), default));
    end;

    procedure AbstractInitializationFile.writeBoolean(const section, ident: AnsiString; value: boolean);
    begin
        writeString(section, ident, booleanToString(value));
    end;

    procedure AbstractInitializationFile.writeInt(const section, ident: AnsiString; value: int);
    begin
        writeString(section, ident, toDecString(value));
    end;

    procedure AbstractInitializationFile.writeLong(const section, ident: AnsiString; value: long);
    begin
        writeString(section, ident, toDecString(value));
    end;

    procedure AbstractInitializationFile.writeFloat(const section, ident: AnsiString; value: float);
    begin
        writeString(section, ident, toDecString(toReal(value)));
    end;

    procedure AbstractInitializationFile.writeDouble(const section, ident: AnsiString; value: double);
    begin
        writeString(section, ident, toDecString(toReal(value)));
    end;

    procedure AbstractInitializationFile.writeReal(const section, ident: AnsiString; value: real);
    begin
        writeString(section, ident, toDecString(value));
    end;
{%endregion}

{%region NormalInitializationFile }
    constructor NormalInitializationFile.create();
    begin
        create('');
    end;

    constructor NormalInitializationFile.create(const fileName: AnsiString);
    var
        stream: FileInputStream;
    begin
        inherited create();
        self.fileName := fileName;
        if length(fileName) = 0 then begin
            exit;
        end;
        stream := FileInputStream.create(fileName);
        if stream.isInvalidHandle() then begin
            stream.free();
            exit;
        end;
        loadFromStream(stream);
    end;

    function NormalInitializationFile.getSectionAt(index: int): AnsiString;
    begin
        if (index < 0) or (index >= sectionsCount) then begin
            result := '';
            exit;
        end;
        index := sections[index];
        result := trim(strings[index]);
        result := trim(copy(result, 2, length(result) - 2));
    end;

    function NormalInitializationFile.indexOfSection(const section: AnsiString): int;
    var
        i: int;
        s: AnsiString;
    begin
        s := trim(section);
        for i := 0 to sectionsCount - 1 do begin
            if getSectionAt(i) = s then begin
                result := i;
                exit;
            end;
        end;
        result := -1;
    end;

    function NormalInitializationFile.indexOfIdent(const section, ident: AnsiString): int;
    var
        e: int;
        i: int;
        j: int;
        l: int;
        s: AnsiString_Array1d;
        t: AnsiString;
        d: AnsiString;
    begin
        j := indexOfSection(section);
        if j < 0 then begin
            result := -1;
            exit;
        end;
        s := strings;
        d := trim(ident);
        for i := sections[j] + 1 to stringsCount - 1 do begin
            t := trim(s[i]);
            l := length(t);
            if (l <= 0) or (t[1] = COMMENT_START) then begin
                continue;
            end;
            if (t[1] = BRACKET_LEFT) and (t[l] = BRACKET_RIGHT) then begin
                break;
            end;
            e := pos(IDENT_VALUE_SEPARATOR, t);
            if e <= 0 then begin
                continue;
            end;
            if trim(copy(t, 1, e - 1)) = d then begin
                result := i;
                exit;
            end;
        end;
        result := -1;
    end;

    procedure NormalInitializationFile.insertString(index: int; const str: AnsiString);
    var
        s: AnsiString_Array1d;
        t: int_Array1d;
        c: int;
        i: int;
        j: int;
        ts: AnsiString;
    begin
        s := strings;
        c := stringsCount;
        if c = length(s) then begin
            s := String_Array1d_create((c shl 1) + 1);
            arraycopy(strings, 0, s, 0, c);
            strings := s;
        end;
        arraycopy(s, index, s, index + 1, c - index);
        s[index] := str;
        stringsCount := c + 1;
        t := sections;
        c := sectionsCount;
        for i := c - 1 downto 0 do begin
            j := t[i];
            if j < index then begin
                break;
            end;
            t[i] := j + 1;
        end;
        ts := trim(str);
        if startsWith(BRACKET_LEFT, ts) and endsWith(BRACKET_RIGHT, ts) then begin
            j := c;
            for i := 0 to c - 1 do begin
                if t[i] > index then begin
                    j := i;
                    break;
                end;
            end;
            if c = length(t) then begin
                t := int_Array1d_create((c shl 1) + 1);
                arraycopy(sections, 0, t, 0, c);
                sections := t;
            end;
            arraycopy(t, j, t, j + 1, c - j);
            t[j] := index;
            sectionsCount := c + 1;
        end;
    end;

    procedure NormalInitializationFile.deleteString(index: int);
    var
        s: AnsiString_Array1d;
        t: int_Array1d;
        c: int;
        i: int;
        j: int;
    begin
        s := strings;
        c := stringsCount;
        arraycopy(s, index + 1, s, index, c - index - 1);
        s[c - 1] := '';
        stringsCount := c - 1;
        t := sections;
        c := sectionsCount;
        for i := c - 1 downto 0 do begin
            j := t[i];
            if j = index then begin
                arraycopy(t, i + 1, t, i, c - i - 1);
                sectionsCount := c - 1;
                break;
            end;
            if j < index then begin
                break;
            end;
            t[i] := j - 1;
        end;
    end;

    function NormalInitializationFile.isSectionExists(const section: AnsiString): boolean;
    begin
        result := indexOfSection(section) >= 0;
    end;

    function NormalInitializationFile.isIdentExists(const section, ident: AnsiString): boolean;
    begin
        result := indexOfIdent(section, ident) >= 0;
    end;

    function NormalInitializationFile.readSections(): AnsiString_Array1d;
    var
        i: int;
    begin
        result := String_Array1d_create(sectionsCount);
        for i := sectionsCount - 1 downto 0 do begin
            result[i] := getSectionAt(i);
        end;
    end;

    function NormalInitializationFile.readSection(const section: AnsiString): AnsiString_Array1d;
    var
        e: int;
        i: int;
        j: int;
        l: int;
        z: int;
        s: AnsiString_Array1d;
        x: AnsiString_Array1d;
        t: AnsiString;
    begin
        j := indexOfSection(section);
        if j < 0 then begin
            result := nil;
            exit;
        end;
        z := 0;
        s := strings;
        x := String_Array1d_create(8);
        for i := sections[j] + 1 to stringsCount - 1 do begin
            t := trim(s[i]);
            l := length(t);
            if (l <= 0) or (t[1] = COMMENT_START) then begin
                continue;
            end;
            if (t[1] = BRACKET_LEFT) and (t[l] = BRACKET_RIGHT) then begin
                break;
            end;
            e := pos(IDENT_VALUE_SEPARATOR, t);
            if e <= 0 then begin
                continue;
            end;
            if length(x) = z then begin
                result := x;
                x := String_Array1d_create((z shl 1) + 1);
                arraycopy(result, 0, x, 0, z);
                result := nil;
            end;
            x[z] := trim(copy(t, 1, e - 1));
            inc(z);
        end;
        result := String_Array1d_create(z);
        arraycopy(x, 0, result, 0, z);
    end;

    function NormalInitializationFile.readString(const section, ident, default: AnsiString): AnsiString;
    var
        i: int;
        j: int;
        s: AnsiString;
    begin
        i := indexOfIdent(section, ident);
        if i < 0 then begin
            result := default;
            exit;
        end;
        s := strings[i];
        j := pos(IDENT_VALUE_SEPARATOR, s);
        result := trim(copy(s, j + 1, length(s) - j));
    end;

    procedure NormalInitializationFile.writeString(const section, ident, value: AnsiString);
    var
        i: int;
    begin
        if length(section) = 0 then begin
            raise InvalidSectionIdentException.create('Название секции не может быть пустой строкой.');
        end;
        if length(ident) = 0 then begin
            raise InvalidSectionIdentException.create('Название параметра не может быть пустой строкой.');
        end;
        if pos(IDENT_VALUE_SEPARATOR, ident) > 0 then begin
            raise InvalidSectionIdentException.create('Название параметра не может содержать символ «' + IDENT_VALUE_SEPARATOR + '».');
        end;
        i := indexOfIdent(section, ident);
        if i < 0 then begin
            i := indexOfSection(section);
            if i < 0 then begin
                insertString(stringsCount, '');
                insertString(stringsCount, BRACKET_LEFT + trim(section) + BRACKET_RIGHT);
                insertString(stringsCount, trim(ident) + IDENT_VALUE_SEPARATOR + trim(value));
                exit;
            end;
            insertString(sections[i] + 1, trim(ident) + IDENT_VALUE_SEPARATOR + trim(value));
            exit;
        end;
        strings[i] := trim(ident) + IDENT_VALUE_SEPARATOR + trim(value);
    end;

    procedure NormalInitializationFile.eraseSection(const section: AnsiString);
    var
        i: int;
        j: int;
        k: int;
        t: int_Array1d;
        s: AnsiString_Array1d;
    begin
        k := indexOfSection(section);
        if k < 0 then begin
            exit;
        end;
        t := sections;
        i := t[k];
        if k < sectionsCount - 1 then begin
            j := t[k + 1];
        end else begin
            j := stringsCount;
        end;
        s := strings;
        k := stringsCount;
        arraycopy(s, j, s, i, k - j);
        stringsCount := k - (j - i);
        for i := stringsCount to k - 1 do begin
            s[i] := '';
        end;
    end;

    procedure NormalInitializationFile.deleteIdent(const section, ident: AnsiString);
    var
        i: int;
    begin
        i := indexOfIdent(section, ident);
        if i >= 0 then begin
            deleteString(i);
        end;
    end;

    procedure NormalInitializationFile.loadFromStream(stream: Input);
    var
        i: int;
        j: int;
        k: int;
        t: int_Array1d;
        s: AnsiString_Array1d;
        str: AnsiString;
    begin
        s := loadStringsFromStream(stream);
        k := length(s);
        for i := k - 1 downto 1 do begin
            str := s[i - 1];
            if endsWith(LINE_ESCAPE, str) then begin
                s[i - 1] := copy(str, 1, length(str) - 1) + trim(s[i]);
                arraycopy(s, i + 1, s, i, k - i - 1);
                dec(k);
                s[k] := '';
            end;
        end;
        j := 0;
        for i := 0 to k - 1 do begin
            str := trim(s[i]);
            if startsWith(BRACKET_LEFT, str) and endsWith(BRACKET_RIGHT, str) then begin
                inc(j);
            end;
        end;
        t := int_Array1d_create(j);
        j := 0;
        for i := 0 to k - 1 do begin
            str := trim(s[i]);
            if startsWith(BRACKET_LEFT, str) and endsWith(BRACKET_RIGHT, str) then begin
                t[j] := i;
                inc(j);
            end;
        end;
        sections := t;
        strings := s;
        sectionsCount := j;
        stringsCount := k;
    end;

    procedure NormalInitializationFile.saveToStream(stream: Output);
    begin
        saveStringsToStream(stream, strings, stringsCount);
    end;

    procedure NormalInitializationFile.updateFile();
    var
        f: FileOutputStream;
        fn: AnsiString;
    begin
        fn := fileName;
        if length(fn) = 0 then begin
            raise IOException.create('Имя файла не было задано при создании этого экземпляра класса ' + self.getClass().getName());
        end;
        f := FileOutputStream.create(fn);
        if f.isInvalidHandle() then begin
            f.free();
            raise FileNotOpenException.create('Не удалось записать данные в файл: ' + fn);
        end;
        saveToStream(f);
    end;

    function NormalInitializationFile.getFileName(): AnsiString;
    begin
        result := fileName;
    end;
{%endregion}

end.

