const std = @import("../std.zig");
const builtin = @import("builtin");
const os = std.os;
const io = std.io;
const mem = std.mem;
const math = std.math;
const assert = std.debug.assert;
const windows = os.windows;
const Os = builtin.Os;
const maxInt = std.math.maxInt;

pub const File = struct {
    /// The OS-specific file descriptor or file handle.
    handle: os.fd_t,

    pub const Mode = switch (builtin.os) {
        Os.windows => void,
        else => u32,
    };

    pub const default_mode = switch (builtin.os) {
        Os.windows => {},
        else => 0o666,
    };

    pub const OpenError = windows.CreateFileError || os.OpenError;

    /// Call close to clean up.
    pub fn openRead(path: []const u8) OpenError!File {
        if (windows.is_the_target) {
            const path_w = try windows.sliceToPrefixedFileW(path);
            return openReadW(&path_w);
        }
        const path_c = try os.toPosixPath(path);
        return openReadC(&path_c);
    }

    /// `openRead` except with a null terminated path
    pub fn openReadC(path: [*]const u8) OpenError!File {
        if (windows.is_the_target) {
            const path_w = try windows.cStrToPrefixedFileW(path);
            return openReadW(&path_w);
        }
        const flags = os.O_LARGEFILE | os.O_RDONLY;
        const fd = try os.openC(path, flags, 0);
        return openHandle(fd);
    }

    /// `openRead` except with a null terminated UTF16LE encoded path
    pub fn openReadW(path_w: [*]const u16) OpenError!File {
        const handle = try windows.CreateFileW(
            path_w,
            windows.GENERIC_READ,
            windows.FILE_SHARE_READ,
            null,
            windows.OPEN_EXISTING,
            windows.FILE_ATTRIBUTE_NORMAL,
            null,
        );
        return openHandle(handle);
    }

    /// Calls `openWriteMode` with `default_mode` for the mode.
    pub fn openWrite(path: []const u8) OpenError!File {
        return openWriteMode(path, default_mode);
    }

    /// If the path does not exist it will be created.
    /// If a file already exists in the destination it will be truncated.
    /// Call close to clean up.
    pub fn openWriteMode(path: []const u8, file_mode: Mode) OpenError!File {
        if (windows.is_the_target) {
            const path_w = try windows.sliceToPrefixedFileW(path);
            return openWriteModeW(&path_w, file_mode);
        }
        const path_c = try os.toPosixPath(path);
        return openWriteModeC(&path_c, file_mode);
    }

    /// Same as `openWriteMode` except `path` is null-terminated.
    pub fn openWriteModeC(path: [*]const u8, file_mode: Mode) OpenError!File {
        if (windows.is_the_target) {
            const path_w = try windows.cStrToPrefixedFileW(path);
            return openWriteModeW(&path_w, file_mode);
        }
        const flags = os.O_LARGEFILE | os.O_WRONLY | os.O_CREAT | os.O_CLOEXEC | os.O_TRUNC;
        const fd = try os.openC(path, flags, file_mode);
        return openHandle(fd);
    }

    /// Same as `openWriteMode` except `path` is null-terminated and UTF16LE encoded
    pub fn openWriteModeW(path_w: [*]const u16, file_mode: Mode) OpenError!File {
        const handle = try windows.CreateFileW(
            path_w,
            windows.GENERIC_WRITE,
            windows.FILE_SHARE_WRITE | windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE,
            null,
            windows.CREATE_ALWAYS,
            windows.FILE_ATTRIBUTE_NORMAL,
            null,
        );
        return openHandle(handle);
    }

    /// If the path does not exist it will be created.
    /// If a file already exists in the destination this returns OpenError.PathAlreadyExists
    /// Call close to clean up.
    pub fn openWriteNoClobber(path: []const u8, file_mode: Mode) OpenError!File {
        if (windows.is_the_target) {
            const path_w = try windows.sliceToPrefixedFileW(path);
            return openWriteNoClobberW(&path_w, file_mode);
        }
        const path_c = try os.toPosixPath(path);
        return openWriteNoClobberC(&path_c, file_mode);
    }

    pub fn openWriteNoClobberC(path: [*]const u8, file_mode: Mode) OpenError!File {
        if (windows.is_the_target) {
            const path_w = try windows.cStrToPrefixedFileW(path);
            return openWriteNoClobberW(&path_w, file_mode);
        }
        const flags = os.O_LARGEFILE | os.O_WRONLY | os.O_CREAT | os.O_CLOEXEC | os.O_EXCL;
        const fd = try os.openC(path, flags, file_mode);
        return openHandle(fd);
    }

    pub fn openWriteNoClobberW(path_w: [*]const u16, file_mode: Mode) OpenError!File {
        const handle = try windows.CreateFileW(
            path_w,
            windows.GENERIC_WRITE,
            windows.FILE_SHARE_WRITE | windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE,
            null,
            windows.CREATE_NEW,
            windows.FILE_ATTRIBUTE_NORMAL,
            null,
        );
        return openHandle(handle);
    }

    pub fn openHandle(handle: os.fd_t) File {
        return File{ .handle = handle };
    }

    /// Test for the existence of `path`.
    /// `path` is UTF8-encoded.
    /// In general it is recommended to avoid this function. For example,
    /// instead of testing if a file exists and then opening it, just
    /// open it and handle the error for file not found.
    pub fn access(path: []const u8) !void {
        return os.access(path, os.F_OK);
    }

    /// Same as `access` except the parameter is null-terminated.
    pub fn accessC(path: [*]const u8) !void {
        return os.accessC(path, os.F_OK);
    }

    /// Same as `access` except the parameter is null-terminated UTF16LE-encoded.
    pub fn accessW(path: [*]const u16) !void {
        return os.accessW(path, os.F_OK);
    }

    /// Upon success, the stream is in an uninitialized state. To continue using it,
    /// you must use the open() function.
    pub fn close(self: File) void {
        return os.close(self.handle);
    }

    /// Test whether the file refers to a terminal.
    /// See also `supportsAnsiEscapeCodes`.
    pub fn isTty(self: File) bool {
        return os.isatty(self.handle);
    }

    /// Test whether ANSI escape codes will be treated as such.
    pub fn supportsAnsiEscapeCodes(self: File) bool {
        if (windows.is_the_target) {
            return os.isCygwinPty(self.handle);
        }
        return self.isTty();
    }

    pub const SeekError = os.SeekError;

    /// Repositions read/write file offset relative to the current offset.
    pub fn seekBy(self: File, offset: i64) SeekError!void {
        return os.lseek_CUR(self.handle, offset);
    }

    /// Repositions read/write file offset relative to the end.
    pub fn seekFromEnd(self: File, offset: i64) SeekError!void {
        return os.lseek_END(self.handle, offset);
    }

    /// Repositions read/write file offset relative to the beginning.
    pub fn seekTo(self: File, offset: u64) SeekError!void {
        return os.lseek_SET(self.handle, offset);
    }

    pub const GetPosError = os.SeekError || os.FStatError;

    pub fn getPos(self: File) GetPosError!u64 {
        return os.lseek_CUR_get(self.handle);
    }

    pub fn getEndPos(self: File) GetPosError!u64 {
        if (windows.is_the_target) {
            return windows.GetFileSizeEx(self.handle);
        }
        return (try self.stat()).size;
    }

    pub const ModeError = os.FStatError;

    pub fn mode(self: File) ModeError!Mode {
        if (windows.is_the_target) {
            return {};
        }
        return (try self.stat()).mode;
    }

    pub const Stat = struct {
        size: u64,
        mode: Mode,

        /// access time in nanoseconds
        atime: i64,

        /// last modification time in nanoseconds
        mtime: i64,

        /// creation time in nanoseconds
        ctime: i64,
    };

    pub const StatError = os.FStatError;

    pub fn stat(self: File) StatError!Stat {
        if (windows.is_the_target) {
            var io_status_block: windows.IO_STATUS_BLOCK = undefined;
            var info: windows.FILE_ALL_INFORMATION = undefined;
            const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
            switch (rc) {
                windows.STATUS.SUCCESS => {},
                windows.STATUS.BUFFER_OVERFLOW => {},
                else => return windows.unexpectedStatus(rc),
            }
            return Stat{
                .size = @bitCast(u64, info.StandardInformation.EndOfFile),
                .mode = {},
                .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
                .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
                .ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
            };
        }

        const st = try os.fstat(self.handle);
        const atime = st.atime();
        const mtime = st.mtime();
        const ctime = st.ctime();
        return Stat{
            .size = @bitCast(u64, st.size),
            .mode = st.mode,
            .atime = i64(atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec,
            .mtime = i64(mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec,
            .ctime = i64(ctime.tv_sec) * std.time.ns_per_s + ctime.tv_nsec,
        };
    }

    pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;

    /// `atime`: access timestamp in nanoseconds
    /// `mtime`: last modification timestamp in nanoseconds
    pub fn updateTimes(self: File, atime: i64, mtime: i64) UpdateTimesError!void {
        if (windows.is_the_target) {
            const atime_ft = windows.nanoSecondsToFileTime(atime);
            const mtime_ft = windows.nanoSecondsToFileTime(mtime);
            return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft);
        }
        const times = [2]os.timespec{
            os.timespec{
                .tv_sec = @divFloor(atime, std.time.ns_per_s),
                .tv_nsec = @mod(atime, std.time.ns_per_s),
            },
            os.timespec{
                .tv_sec = @divFloor(mtime, std.time.ns_per_s),
                .tv_nsec = @mod(mtime, std.time.ns_per_s),
            },
        };
        try os.futimens(self.handle, &times);
    }

    pub const ReadError = os.ReadError;

    pub fn read(self: File, buffer: []u8) ReadError!usize {
        return os.read(self.handle, buffer);
    }

    pub const WriteError = os.WriteError;

    pub fn write(self: File, bytes: []const u8) WriteError!void {
        return os.write(self.handle, bytes);
    }

    pub fn writev_iovec(self: File, iovecs: []const os.iovec_const) WriteError!void {
        if (std.event.Loop.instance) |loop| {
            return std.event.fs.writevPosix(loop, self.handle, iovecs);
        } else {
            return os.writev(self.handle, iovecs);
        }
    }

    pub fn inStream(file: File) InStream {
        return InStream{
            .file = file,
            .stream = InStream.Stream{ .readFn = InStream.readFn },
        };
    }

    pub fn outStream(file: File) OutStream {
        return OutStream{
            .file = file,
            .stream = OutStream.Stream{ .writeFn = OutStream.writeFn },
        };
    }

    pub fn seekableStream(file: File) SeekableStream {
        return SeekableStream{
            .file = file,
            .stream = SeekableStream.Stream{
                .seekToFn = SeekableStream.seekToFn,
                .seekByFn = SeekableStream.seekByFn,
                .getPosFn = SeekableStream.getPosFn,
                .getEndPosFn = SeekableStream.getEndPosFn,
            },
        };
    }

    /// Implementation of io.InStream trait for File
    pub const InStream = struct {
        file: File,
        stream: Stream,

        pub const Error = ReadError;
        pub const Stream = io.InStream(Error);

        fn readFn(in_stream: *Stream, buffer: []u8) Error!usize {
            const self = @fieldParentPtr(InStream, "stream", in_stream);
            return self.file.read(buffer);
        }
    };

    /// Implementation of io.OutStream trait for File
    pub const OutStream = struct {
        file: File,
        stream: Stream,

        pub const Error = WriteError;
        pub const Stream = io.OutStream(Error);

        fn writeFn(out_stream: *Stream, bytes: []const u8) Error!void {
            const self = @fieldParentPtr(OutStream, "stream", out_stream);
            return self.file.write(bytes);
        }
    };

    /// Implementation of io.SeekableStream trait for File
    pub const SeekableStream = struct {
        file: File,
        stream: Stream,

        pub const Stream = io.SeekableStream(SeekError, GetPosError);

        pub fn seekToFn(seekable_stream: *Stream, pos: u64) SeekError!void {
            const self = @fieldParentPtr(SeekableStream, "stream", seekable_stream);
            return self.file.seekTo(pos);
        }

        pub fn seekByFn(seekable_stream: *Stream, amt: i64) SeekError!void {
            const self = @fieldParentPtr(SeekableStream, "stream", seekable_stream);
            return self.file.seekBy(amt);
        }

        pub fn getEndPosFn(seekable_stream: *Stream) GetPosError!u64 {
            const self = @fieldParentPtr(SeekableStream, "stream", seekable_stream);
            return self.file.getEndPos();
        }

        pub fn getPosFn(seekable_stream: *Stream) GetPosError!u64 {
            const self = @fieldParentPtr(SeekableStream, "stream", seekable_stream);
            return self.file.getPos();
        }
    };
};
