{
    FileIO содержит классы для файлового ввода-вывода.

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

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

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

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

unit FileIO;

{$MODE DELPHI}

interface

uses
    Windows,
    Lang,
    IOStreams;

{%region public }
const
    HANDLE_STREAM_GUID = '{74C86B60-AC5B-4E8D-8ACD-29A50B5C1507}';

type
    HandleStream = interface;
    FileNotOpenException = class;
    FileInputStream = class;
    FileOutputStream = class;
    FileStream = class;

    HandleStream = interface(_Interface) [HANDLE_STREAM_GUID]
        function isInvalidHandle(): boolean;
        function getHandle(): int;
    end;

    FileNotOpenException = class(IOException)
    public
        constructor create(const fileName: AnsiString); overload;
        constructor create(const fileName: UnicodeString); overload;
    end;

    FileInputStream = class(InputStream, HandleStream)
    strict private
        owned: boolean;
        handle: int;
    public
        constructor create(handle: int); overload;
        constructor create(const fileName: AnsiString); overload;
        constructor create(const fileName: UnicodeString); overload;
        destructor destroy; override;
        function seekSupported(): boolean; override;
        function size(): long; override;
        function position(): long; override;
        function seek(delta: long): long; override;
        function read(const dst: byte_Array1d; offset, length: int): int; overload; override;
        function read(): int; overload; override;
        function isInvalidHandle(): boolean;
        function getHandle(): int;
    end;

    FileOutputStream = class(OutputStream, HandleStream)
    strict private
        owned: boolean;
        handle: int;
    public
        constructor create(handle: int); overload;
        constructor create(const fileName: AnsiString; appending: boolean = false); overload;
        constructor create(const fileName: UnicodeString; appending: boolean = false); overload;
        destructor destroy; override;
        function write(const src: byte_Array1d; offset, length: int): int; overload; override;
        function write(value: int): boolean; overload; override;
        function isInvalidHandle(): boolean;
        function getHandle(): int;
    end;

    FileStream = class(InputOutputStream, HandleStream)
    strict private
        handle: int;
        input: FileInputStream;
        output: FileOutputStream;
        function getInputStream(): FileInputStream;
        function getOutputStream(): FileOutputStream;
    public
        constructor create(const fileName: AnsiString); overload;
        constructor create(const fileName: UnicodeString); overload;
        destructor destroy; override;
        function seekSupported(): boolean; override;
        function size(): long; override;
        function position(): long; override;
        function seek(delta: long): long; override;
        function read(const dst: byte_Array1d; offset, length: int): int; overload; override;
        function read(): int; overload; override;
        function write(const src: byte_Array1d; offset, length: int): int; overload; override;
        function write(value: int): boolean; overload; override;
        function truncate(): long; override;
        function isInvalidHandle(): boolean;
        function getHandle(): int;
    end;
{%endregion}

{%region routine }
    function fileExists(const fileName: AnsiString): boolean; overload;
    function fileExists(const fileName: UnicodeString): boolean; overload;
    function enumerateFiles(const dirName: AnsiString): UnicodeString_Array1d; overload;
    function enumerateFiles(const dirName: UnicodeString): UnicodeString_Array1d; overload;
    function enumerateFiles(const dirName, extension: AnsiString): UnicodeString_Array1d; overload;
    function enumerateFiles(const dirName, extension: UnicodeString): UnicodeString_Array1d; overload;
    function enumerateDirectories(const dirName: AnsiString): UnicodeString_Array1d; overload;
    function enumerateDirectories(const dirName: UnicodeString): UnicodeString_Array1d; overload;
    function createDirectory(const dirName: AnsiString): boolean; overload;
    function createDirectory(const dirName: UnicodeString): boolean; overload;
    function destroyDirectory(const dirName: AnsiString; selfDestroying: boolean = true): boolean; overload;
    function destroyDirectory(const dirName: UnicodeString; selfDestroying: boolean = true): boolean; overload;
    function deleteFile(const fileName: AnsiString): boolean; overload;
    function deleteFile(const fileName: UnicodeString): boolean; overload;
    function deleteDirectory(const dirName: AnsiString): boolean; overload;
    function deleteDirectory(const dirName: UnicodeString): boolean; overload;
    function move(const sourceName, destinationName: AnsiString): boolean; overload;
    function move(const sourceName, destinationName: UnicodeString): boolean; overload;
    function copyFiles(const sourceDirectory, destinationDirectory: AnsiString): boolean; overload;
    function copyFiles(const sourceDirectory, destinationDirectory: UnicodeString): boolean; overload;
{%endregion}

implementation

{%region routine }
    function fileExists(const fileName: AnsiString): boolean;
    begin
        result := fileExists(toUTF16String(fileName));
    end;

    function fileExists(const fileName: UnicodeString): boolean;
    var
        srec: Windows.WIN32_FIND_DATAW;
        shan: int;
    begin
        initialize(srec);
        shan := int(Windows.findFirstFileW(PWideChar(fileName), srec));
        if shan <> -1 then begin
            result := true;
            Windows.findClose(shan);
        end else begin
            result := false;
        end;
    end;

    function enumerateFiles(const dirName: AnsiString): UnicodeString_Array1d;
    begin
        result := enumerateFiles(toUTF16String(dirName), '*');
    end;

    function enumerateFiles(const dirName: UnicodeString): UnicodeString_Array1d;
    begin
        result := enumerateFiles(dirName, '*');
    end;

    function enumerateFiles(const dirName, extension: AnsiString): UnicodeString_Array1d;
    begin
        result := enumerateFiles(toUTF16String(dirName), toUTF16String(extension));
    end;

    function enumerateFiles(const dirName, extension: UnicodeString): UnicodeString_Array1d;
    const
        faDirectory = $10;
    var
        mask: UnicodeString;
        dir: UnicodeString;
        len: int;

        function getDir(): UnicodeString;
        var
            i: int;
        begin
            result := '';
            for i := length(mask) downto 1 do begin
                if mask[i] = directorySeparator then begin
                    result := copy(mask, 1, i);
                    break;
                end;
            end;
        end;

        procedure addFile(const name: UnicodeString);
        var
            buf: UnicodeString_Array1d;
        begin
            if length(result) = len then begin
                buf := UnicodeString_Array1d_create(len + 16);
                arraycopy(result, 0, buf, 0, len);
                result := buf;
            end;
            result[len] := dir + name;
            inc(len);
        end;

    var
        buf: UnicodeString_Array1d;
        srec: Windows.WIN32_FIND_DATAW;
        shan: int;
    begin
        result := nil;
        mask := dirName + '*.' + extension;
        len := 0;
        initialize(srec);
        shan := int(Windows.findFirstFileW(PWideChar(mask), srec));
        dir := getDir();
        if shan <> -1 then begin
            repeat
                if (srec.dwFileAttributes and faDirectory) = 0 then begin
                    addFile(UnicodeString(srec.cFileName));
                end;
            until not Windows.findNextFileW(shan, srec);
            Windows.findClose(shan);
        end;
        if len <> length(result) then begin
            buf := UnicodeString_Array1d_create(len);
            arraycopy(result, 0, buf, 0, len);
            result := buf;
        end;
    end;

    function enumerateDirectories(const dirName: AnsiString): UnicodeString_Array1d;
    begin
        result := enumerateDirectories(toUTF16String(dirName));
    end;

    function enumerateDirectories(const dirName: UnicodeString): UnicodeString_Array1d;
    const
        faDirectory = $10;
    var
        len: int;

        procedure addFile(const name: UnicodeString);
        var
            buf: UnicodeString_Array1d;
        begin
            if (name <> '.') and (name <> '..') then begin
                if length(result) = len then begin
                    buf := UnicodeString_Array1d_create(len + 16);
                    arraycopy(result, 0, buf, 0, len);
                    result := buf;
                end;
                result[len] := dirName + name;
                inc(len);
            end;
        end;

    var
        buf: UnicodeString_Array1d;
        srec: Windows.WIN32_FIND_DATAW;
        shan: int;
    begin
        result := nil;
        len := 0;
        initialize(srec);
        shan := int(Windows.findFirstFileW(PWideChar(dirName + '*.*'), srec));
        if shan <> -1 then begin
            repeat
                if (srec.dwFileAttributes and faDirectory) <> 0 then begin
                    addFile(UnicodeString(srec.cFileName));
                end;
            until not Windows.findNextFileW(shan, srec);
            Windows.findClose(shan);
        end;
        if len <> length(result) then begin
            buf := UnicodeString_Array1d_create(len);
            arraycopy(result, 0, buf, 0, len);
            result := buf;
        end;
    end;

    function createDirectory(const dirName: AnsiString): boolean;
    begin
        result := createDirectory(toUTF16String(dirName));
    end;

    function createDirectory(const dirName: UnicodeString): boolean;
    var
        i: int;
        l: int;

        procedure nextSeparator();
        begin
            if i <= l then begin
                repeat
                    inc(i);
                until (i > l) or (dirName[i] = directorySeparator);
            end;
        end;

    begin
        l := length(dirName);
        i := 0;
        nextSeparator();
        nextSeparator();
        while fileExists(System.copy(dirName, 1, i - 1)) do begin
            if i > l then begin
                result := true;
                exit;
            end;
            nextSeparator();
        end;
        repeat
            result := Windows.createDirectoryW(PWideChar(System.copy(dirName, 1, i - 1)), nil);
            if not result then begin
                exit;
            end;
            if i > l then begin
                break;
            end;
            nextSeparator();
        until false;
    end;

    function destroyDirectory(const dirName: AnsiString; selfDestroying: boolean): boolean;
    begin
        result := destroyDirectory(toUTF16String(dirName), selfDestroying);
    end;

    function destroyDirectory(const dirName: UnicodeString; selfDestroying: boolean): boolean;
    const
        faDirectory = $10;
    var
        srec: Windows.WIN32_FIND_DATAW;
        shan: int;
        currentFile: UnicodeString;
    begin
        initialize(srec);
        if length(dirName) <= 2 then begin
            selfDestroying := false;
        end;
        shan := int(Windows.findFirstFileW(PWideChar(dirName + directorySeparator + '*.*'), srec));
        if shan <> -1 then begin
            result := true;
            repeat
                currentFile := UnicodeString(srec.cFileName);
                if (currentFile = '.') or (currentFile = '..') then begin
                    continue;
                end;
                currentFile := dirName + directorySeparator + currentFile;
                if (srec.dwFileAttributes and faDirectory) <> 0 then begin
                    result := (int(Windows.setFileAttributesW(PWideChar(currentFile), faDirectory)) <> 0) and destroyDirectory(currentFile, true);
                end else begin
                    result := (int(Windows.setFileAttributesW(PWideChar(currentFile), 0)) <> 0) and (int(Windows.deleteFileW(PWideChar(currentFile))) <> 0);
                end;
                if not result then begin
                    break;
                end;
            until not Windows.findNextFileW(shan, srec);
            Windows.findClose(shan);
            if result and selfDestroying then begin
                result := (int(Windows.setFileAttributesW(PWideChar(dirName), faDirectory)) <> 0) and (int(Windows.removeDirectoryW(PWideChar(dirName))) <> 0);
            end;
        end else begin
            result := false;
        end;
    end;

    function deleteFile(const fileName: AnsiString): boolean;
    begin
        result := deleteFile(toUTF16String(fileName));
    end;

    function deleteFile(const fileName: UnicodeString): boolean;
    begin
        result := int(Windows.deleteFileW(PWideChar(fileName))) <> 0;
    end;

    function deleteDirectory(const dirName: AnsiString): boolean;
    begin
        result := deleteDirectory(toUTF16String(dirName));
    end;

    function deleteDirectory(const dirName: UnicodeString): boolean;
    const
        faDirectory = $10;
    begin
        result := (int(Windows.setFileAttributesW(PWideChar(dirName), faDirectory)) <> 0) and (int(Windows.removeDirectoryW(PWideChar(dirName))) <> 0);
    end;

    function move(const sourceName, destinationName: AnsiString): boolean;
    begin
        result := move(toUTF16String(sourceName), toUTF16String(destinationName));
    end;

    function move(const sourceName, destinationName: UnicodeString): boolean;
    begin
        result := int(Windows.moveFileW(PWideChar(sourceName), PWideChar(destinationName))) <> 0;
    end;

    function copyFiles(const sourceDirectory, destinationDirectory: AnsiString): boolean;
    begin
        result := copyFiles(toUTF16String(sourceDirectory), toUTF16String(destinationDirectory));
    end;

    function copyFiles(const sourceDirectory, destinationDirectory: UnicodeString): boolean;
    var
        i: int;
        j: int;
        fragment: long;
        fragments: long;
        remainder: long;
        size: long;
        stringArray: UnicodeString_Array1d;
        buffer: byte_Array1d;
        fileName: UnicodeString;
        s: UnicodeString;
        inFile: Input;
        outFile: Output;
    begin
        stringArray := enumerateFiles(sourceDirectory);
        if not createDirectory(copy(destinationDirectory, 1, length(destinationDirectory) - 1)) then begin
            result := false;
            exit;
        end;
        buffer := byte_Array1d_create($0400);
        for i := length(stringArray) - 1 downto 0 do begin
            fileName := '';
            s := stringArray[i];
            for j := length(s) downto 1 do begin
                if s[j] = DIRECTORY_SEPARATOR then begin
                    fileName := destinationDirectory + copy(s, j + 1, length(s) - j);
                    break;
                end;
            end;
            inFile := FileInputStream.create(s);
            outFile := FileOutputStream.create(fileName);
            try
                size := inFile.size();
                fragments := divLong(size, length(buffer), remainder);
                fragment := 0;
                while fragment < fragments do begin
                    inFile.read(buffer);
                    outFile.write(buffer);
                    inc(fragment);
                end;
                if int(remainder) > 0 then begin
                    inFile.read(buffer, 0, int(remainder));
                    outFile.write(buffer, 0, int(remainder));
                end;
            finally
                inFile := nil;
                outFile := nil;
            end;
        end;
        buffer := nil;
        stringArray := enumerateDirectories(sourceDirectory);
        result := length(stringArray) > 0;
        for i := length(stringArray) - 1 downto 0 do begin
            fileName := '';
            s := stringArray[i];
            for j := length(s) downto 1 do begin
                if s[j] = DIRECTORY_SEPARATOR then begin
                    fileName := destinationDirectory + copy(s, j + 1, length(s) - j);
                    break;
                end;
            end;
            copyFiles(s + DIRECTORY_SEPARATOR, fileName + DIRECTORY_SEPARATOR);
        end;
    end;
{%endregion}

{%region FileNotOpenException }
    constructor FileNotOpenException.create(const fileName: AnsiString);
    begin
        inherited create('Не удалось открыть файл: ' + fileName);
    end;

    constructor FileNotOpenException.create(const fileName: UnicodeString);
    begin
        inherited create('Не удалось открыть файл: ' + toUTF8String(fileName));
    end;
{%endregion}

{%region FileInputStream }
    constructor FileInputStream.create(handle: int);
    begin
        inherited create();
        self.owned := false;
        self.handle := handle;
    end;

    constructor FileInputStream.create(const fileName: AnsiString);
    begin
        create(toUTF16String(fileName));
    end;

    constructor FileInputStream.create(const fileName: UnicodeString);
    var
        attr: int;
    begin
        inherited create();
        if fileExists(fileName) then begin
            attr := int(Windows.getFileAttributesW(PWideChar(fileName)));
        end else begin
            attr := Windows.FILE_ATTRIBUTE_NORMAL;
        end;
        self.owned := true;
        self.handle := int(Windows.createFileW(PWideChar(fileName), Windows.GENERIC_READ, Windows.FILE_SHARE_READ, nil, Windows.OPEN_EXISTING, attr, 0));
    end;

    destructor FileInputStream.destroy;
    begin
        if owned and (handle <> int(Windows.INVALID_HANDLE_VALUE)) then begin
            Windows.closeHandle(handle);
        end;
        inherited destroy;
    end;

    function FileInputStream.seekSupported(): boolean;
    begin
        result := handle <> int(Windows.INVALID_HANDLE_VALUE);
    end;

    function FileInputStream.size(): long;
    var
        r: LongRecord absolute result;
    begin
        result := 0;
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            r.lo := int(Windows.getFileSize(handle, @(r.hi)));
        end;
    end;

    function FileInputStream.position(): long;
    var
        r: LongRecord absolute result;
    begin
        result := 0;
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            r.lo := int(Windows.setFilePointer(handle, 0, @(r.hi), 1));
        end;
    end;

    function FileInputStream.seek(delta: long): long;
    var
        bufsize: long;
        position: long;
        r: LongRecord absolute result;
    begin
        result := 0;
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            bufsize := self.size();
            position := self.position();
            if delta > bufsize - position then begin
                delta := bufsize - position;
            end;
            if delta < -position then begin
                delta := -position;
            end;
            result := delta;
            r.lo := int(Windows.setFilePointer(handle, r.lo, @(r.hi), 1));
        end;
    end;

    function FileInputStream.read(const dst: byte_Array1d; offset, length: int): int;
    var
        lim: int;
        len: int;
    begin
        lim := offset + length;
        len := System.length(dst);
        if (lim > len) or (lim < offset) or (offset < 0) or (offset > len) then begin
            raise ArrayIndexOutOfBoundsException.create('InputStream.read: индекс элемента массива выходит из диапазона.');
        end;
        result := 0;
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            Windows.readFile(handle, dst[offset], DWORD(length), DWORD(result), nil);
        end;
    end;

    function FileInputStream.read(): int;
    var
        readed: int;
    begin
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            result := 0;
            readed := 0;
            Windows.readFile(handle, result, 1, DWORD(readed), nil);
            if readed <> 1 then begin
                result := -1;
            end;
        end else begin
            result := -1;
        end;
    end;

    function FileInputStream.isInvalidHandle(): boolean;
    begin
        result := handle = int(Windows.INVALID_HANDLE_VALUE);
    end;

    function FileInputStream.getHandle(): int;
    begin
        result := handle;
    end;
{%endregion}

{%region FileOutputStream }
    constructor FileOutputStream.create(handle: int);
    begin
        inherited create();
        self.owned := false;
        self.handle := handle;
    end;

    constructor FileOutputStream.create(const fileName: AnsiString; appending: boolean);
    begin
        create(toUTF16String(fileName), appending);
    end;

    constructor FileOutputStream.create(const fileName: UnicodeString; appending: boolean);
    var
        pos: LongRecord;
        attr: int;
        h: int;
    begin
        inherited create();
        if fileExists(fileName) then begin
            attr := int(Windows.getFileAttributesW(PWideChar(fileName)));
        end else begin
            attr := Windows.FILE_ATTRIBUTE_NORMAL;
        end;
        if appending then begin
            h := int(Windows.createFileW(PWideChar(fileName), Windows.GENERIC_READ or Windows.GENERIC_WRITE, 0, nil, Windows.OPEN_EXISTING, attr, 3));
            if h <> int(Windows.INVALID_HANDLE_VALUE) then begin
                pos.value := 0;
                pos.lo := int(Windows.setFilePointer(h, pos.lo, @(pos.hi), 2));
            end;
        end else begin
            h := int(Windows.createFileW(PWideChar(fileName), Windows.GENERIC_WRITE, 0, nil, Windows.CREATE_ALWAYS, attr, 0));
        end;
        self.owned := true;
        self.handle := h;
    end;

    destructor FileOutputStream.destroy;
    begin
        if owned and (handle <> int(Windows.INVALID_HANDLE_VALUE)) then begin
            Windows.closeHandle(handle);
        end;
        inherited destroy;
    end;

    function FileOutputStream.write(const src: byte_Array1d; offset, length: int): int;
    var
        lim: int;
        len: int;
    begin
        lim := offset + length;
        len := System.length(src);
        if (lim > len) or (lim < offset) or (offset < 0) or (offset > len) then begin
            raise ArrayIndexOutOfBoundsException.create('OutputStream.write: индекс элемента массива выходит из диапазона.');
        end;
        result := 0;
        if handle <> -1 then begin
            Windows.writeFile(handle, src[offset], DWORD(length), DWORD(result), nil);
        end;
    end;

    function FileOutputStream.write(value: int): boolean;
    var
        writed: int;
    begin
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            result := true;
            writed := 0;
            Windows.writeFile(handle, value, 1, DWORD(writed), nil);
            if writed <> 1 then begin
                result := false;
            end;
        end else begin
            result := false;
        end;
    end;

    function FileOutputStream.isInvalidHandle(): boolean;
    begin
        result := handle = int(Windows.INVALID_HANDLE_VALUE);
    end;

    function FileOutputStream.getHandle(): int;
    begin
        result := handle;
    end;
{%endregion}

{%region FileStream }
    constructor FileStream.create(const fileName: AnsiString);
    begin
        create(toUTF16String(fileName));
    end;

    constructor FileStream.create(const fileName: UnicodeString);
    var
        attr: int;
    begin
        inherited create();
        if fileExists(fileName) then begin
            attr := int(Windows.getFileAttributesW(PWideChar(fileName)));
            self.handle := int(Windows.createFileW(PWideChar(fileName), Windows.GENERIC_READ or Windows.GENERIC_WRITE, 0, nil, Windows.OPEN_EXISTING, attr, 0));
        end else begin
            attr := Windows.FILE_ATTRIBUTE_NORMAL;
            self.handle := int(Windows.createFileW(PWideChar(fileName), Windows.GENERIC_READ or Windows.GENERIC_WRITE, 0, nil, Windows.CREATE_ALWAYS, attr, 0));
        end;
    end;

    destructor FileStream.destroy;
    begin
        input.free();
        output.free();
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            Windows.closeHandle(handle);
        end;
        inherited destroy;
    end;

    function FileStream.getInputStream(): FileInputStream;
    begin
        result := input;
        if result = nil then begin
            result := FileInputStream.create(handle);
            input := result;
        end;
    end;

    function FileStream.getOutputStream(): FileOutputStream;
    begin
        result := output;
        if result = nil then begin
            result := FileOutputStream.create(handle);
            output := result;
        end;
    end;

    function FileStream.seekSupported(): boolean;
    begin
        result := getInputStream().seekSupported();
    end;

    function FileStream.size(): long;
    begin
        result := getInputStream().size();
    end;

    function FileStream.position(): long;
    begin
        result := getInputStream().position();
    end;

    function FileStream.seek(delta: long): long;
    begin
        result := getInputStream().seek(delta);
    end;

    function FileStream.read(const dst: byte_Array1d; offset, length: int): int;
    begin
        result := getInputStream().read(dst, offset, length);
    end;

    function FileStream.read(): int;
    begin
        result := getInputStream().read();
    end;

    function FileStream.write(const src: byte_Array1d; offset, length: int): int;
    begin
        result := getOutputStream().write(src, offset, length);
    end;

    function FileStream.write(value: int): boolean;
    begin
        result := getOutputStream().write(value);
    end;

    function FileStream.truncate(): long;
    begin
        if handle <> int(Windows.INVALID_HANDLE_VALUE) then begin
            Windows.setEndOfFile(handle);
            result := size();
        end else begin
            result := 0;
        end;
    end;

    function FileStream.isInvalidHandle(): boolean;
    begin
        result := handle = int(Windows.INVALID_HANDLE_VALUE);
    end;

    function FileStream.getHandle(): int;
    begin
        result := handle;
    end;
{%endregion}

end.

