
import io.files.raw.*;



//
// File
//

record File (
    ptr : SharedPointer[FileData],
);

private alias BUFFER_SIZE = 4096;

private record FileData (
    file : RawFile,

    readBuffer : Array[Byte, BUFFER_SIZE],
    readBegin : Pointer[Byte],
    readEnd : Pointer[Byte],

    lineBuffered? : Bool,
    writeBuffer : Array[Byte, BUFFER_SIZE],
    writeCount : SizeT,
);



//
// constructors
//

[S | CCompatibleString?(S)]
overload File(path:S, mode:OpenMode) =
    File(new(FileData(RawFile(path, mode))));

[S | CCompatibleString?(S)]
overload File(path:S) = File(new(FileData(RawFile(path))));

overload File(rvalue f:RawFile) =
    File(new(FileData(move(f))));

overload FileData(rvalue file:RawFile) --> returned:FileData {
    var lineBuffered? = terminal?(file);
    var buf = Array[Byte,BUFFER_SIZE]();
    return FileData(move(file),
                    buf, null(Byte), null(Byte),
                    lineBuffered?, buf, SizeT(0));
}



//
// destroy
//

overload destroy(fd:FileData) {
    if (fd.writeCount > 0) {
        try {
            write(fd.file, begin(fd.writeBuffer), fd.writeCount);
        }
        catch (e) {
            errorNoThrow(e);
        }
        fd.writeCount = 0;
    }
    destroy(fd.file);
}



//
// stdinFile, stdoutFile, stderrFile
//

stdinFile()  = File(new(FileData(stdinRawFile())));
stdoutFile() = File(new(FileData(stdoutRawFile())));
stderrFile() = File(new(FileData(stderrRawFile())));



//
// fileHandle
//

overload fileHandle(f:File) = fileHandle(f.ptr^.file);



//
// read, write, flush
//

overload read(f:File, buffer:Pointer[Byte], _requested:SizeT) {
    ref fd = f.ptr^;
    var requested = _requested;
    var pending = requested;
    var dest = buffer;
    ref src = fd.readBegin;
    ref end = fd.readEnd;

    while (pending > 0) {
        var n = min(pending, SizeT(end - src));
        if (n > 0) {
            copyNonoverlappingMemory(dest, src, src + n);
            src += n;
            dest += n;
            pending -= n;
        }
        else {
            src = begin(fd.readBuffer);
            end = src; // initialize 'end' too as read may throw
            var nRead = read(fd.file, src, SizeT(BUFFER_SIZE));
            end += nRead;
            if (nRead == 0)
                break;
        }
    }
    return requested - pending;
}

overload write(f:File, buffer:Pointer[Byte], n:SizeT) {
    ref fd = f.ptr^;

    var src = buffer;
    var srcEnd = src + n;
    var dest = begin(fd.writeBuffer) + fd.writeCount;
    var destEnd = begin(fd.writeBuffer) + BUFFER_SIZE;

    var pendingNewlines = SizeT(0);

    while (src != srcEnd) {
        if (dest == destEnd) {
            fd.writeCount = dest - begin(fd.writeBuffer);
            flushData(fd);
            dest = begin(fd.writeBuffer);
            pendingNewlines = 0;
        }

        pendingNewlines += SizeT(src^ == Byte('\n'));

        dest^ = src^;
        inc(dest);
        inc(src);
    }

    fd.writeCount = dest - begin(fd.writeBuffer);

    if (fd.lineBuffered? and (pendingNewlines > 0))
        flushData(fd);
}

overload flush(f:File) {
    flushData(f.ptr^);
    flush(f.ptr^.file);
}

private flushData(fd:FileData) {
    if (fd.writeCount > 0) {
        write(fd.file, begin(fd.writeBuffer), fd.writeCount);
        fd.writeCount = 0;
    }
}



//
// fileSize, seek
//

overload fileSize(f:File) {
    flushData(f.ptr^);
    return fileSize(f.ptr^.file);
}

overload seek(f:File, offset, whence) {
    ref fd = f.ptr^;
    fd.readBegin = begin(fd.readBuffer);
    fd.readEnd = fd.readBegin;
    flushData(fd);
    return seek(fd.file, offset, whence);
}



//
// terminal?
//

overload terminal?(f:File) = terminal?(f.ptr^.file);
