const Writer = @This();
const builtin = @import("builtin");
const is_windows = builtin.os.tag == .windows;

const std = @import("../../std.zig");
const Io = std.Io;
const File = std.Io.File;
const assert = std.debug.assert;

io: Io,
file: File,
err: ?Error = null,
mode: Mode = .positional,
/// Tracks the true seek position in the file. To obtain the logical position,
/// use `logicalPos`.
pos: u64 = 0,
write_file_err: ?WriteFileError = null,
seek_err: ?SeekError = null,
interface: Io.Writer,

pub const Mode = File.Reader.Mode;

pub const Error = error{
    DiskQuota,
    FileTooBig,
    InputOutput,
    NoSpaceLeft,
    DeviceBusy,
    /// File descriptor does not hold the required rights to write to it.
    AccessDenied,
    PermissionDenied,
    /// File is an unconnected socket, or closed its read end.
    BrokenPipe,
    /// Insufficient kernel memory to read from in_fd.
    SystemResources,
    NotOpenForWriting,
    /// The process cannot access the file because another process has locked
    /// a portion of the file. Windows-only.
    LockViolation,
    /// Non-blocking has been enabled and this operation would block.
    WouldBlock,
    /// This error occurs when a device gets disconnected before or mid-flush
    /// while it's being written to - errno(6): No such device or address.
    NoDevice,
    FileBusy,
} || Io.Cancelable || Io.UnexpectedError;

pub const WriteFileError = Error || error{
    /// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd.
    Unimplemented,
    /// Can happen on FreeBSD when using copy_file_range.
    CorruptedData,
    EndOfStream,
    ReadFailed,
};

pub const SeekError = Io.File.SeekError;

pub fn init(file: File, io: Io, buffer: []u8) Writer {
    return .{
        .io = io,
        .file = file,
        .interface = initInterface(buffer),
        .mode = .positional,
    };
}

/// Positional is more threadsafe, since the global seek position is not
/// affected, but when such syscalls are not available, preemptively
/// initializing in streaming mode will skip a failed syscall.
pub fn initStreaming(file: File, io: Io, buffer: []u8) Writer {
    return .{
        .io = io,
        .file = file,
        .interface = initInterface(buffer),
        .mode = .streaming,
    };
}

/// Detects if `file` is terminal and sets the mode accordingly.
pub fn initDetect(file: File, io: Io, buffer: []u8) Io.Cancelable!Writer {
    return .{
        .io = io,
        .file = file,
        .interface = initInterface(buffer),
        .mode = try .detect(io, file, true, .positional),
    };
}

pub fn initInterface(buffer: []u8) Io.Writer {
    return .{
        .vtable = &.{
            .drain = drain,
            .sendFile = sendFile,
        },
        .buffer = buffer,
    };
}

pub fn moveToReader(w: *Writer) File.Reader {
    defer w.* = undefined;
    return .{
        .io = w.io,
        .file = .{ .handle = w.file.handle },
        .mode = w.mode,
        .pos = w.pos,
        .interface = File.Reader.initInterface(w.interface.buffer),
        .seek_err = w.seek_err,
    };
}

pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
    const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
    switch (w.mode) {
        .positional, .positional_simple => return drainPositional(w, data, splat),
        .streaming, .streaming_simple => return drainStreaming(w, data, splat),
        .failure => return error.WriteFailed,
    }
}

fn drainPositional(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
    const io = w.io;
    const header = w.interface.buffered();
    const n = io.vtable.fileWritePositional(io.userdata, w.file, header, data, splat, w.pos) catch |err| switch (err) {
        error.Unseekable => {
            w.mode = w.mode.toStreaming();
            const pos = w.pos;
            if (pos != 0) {
                w.pos = 0;
                w.seekTo(@intCast(pos)) catch {
                    w.mode = .failure;
                    return error.WriteFailed;
                };
            }
            return 0;
        },
        else => |e| {
            w.err = e;
            return error.WriteFailed;
        },
    };
    w.pos += n;
    return w.interface.consume(n);
}

fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
    const io = w.io;
    const header = w.interface.buffered();
    const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| {
        w.err = err;
        return error.WriteFailed;
    };
    w.pos += n;
    return w.interface.consume(n);
}

pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
    const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
    switch (w.mode) {
        .positional => return sendFilePositional(w, file_reader, limit),
        .positional_simple => return error.Unimplemented,
        .streaming => return sendFileStreaming(w, file_reader, limit),
        .streaming_simple => return error.Unimplemented,
        .failure => return error.WriteFailed,
    }
}

fn sendFilePositional(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
    const io = w.io;
    const header = w.interface.buffered();
    const n = io.vtable.fileWriteFilePositional(io.userdata, w.file, header, file_reader, limit, w.pos) catch |err| switch (err) {
        error.Unseekable => {
            w.mode = w.mode.toStreaming();
            const pos = w.pos;
            if (pos != 0) {
                w.pos = 0;
                w.seekTo(@intCast(pos)) catch {
                    w.mode = .failure;
                    return error.WriteFailed;
                };
            }
            return 0;
        },
        error.Canceled => {
            w.err = error.Canceled;
            return error.WriteFailed;
        },
        error.EndOfStream => return error.EndOfStream,
        error.Unimplemented => return error.Unimplemented,
        error.ReadFailed => return error.ReadFailed,
        else => |e| {
            w.write_file_err = e;
            return error.WriteFailed;
        },
    };
    w.pos += n;
    return w.interface.consume(n);
}

fn sendFileStreaming(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
    const io = w.io;
    const header = w.interface.buffered();
    const n = io.vtable.fileWriteFileStreaming(io.userdata, w.file, header, file_reader, limit) catch |err| switch (err) {
        error.Canceled => {
            w.err = error.Canceled;
            return error.WriteFailed;
        },
        error.EndOfStream => return error.EndOfStream,
        error.Unimplemented => return error.Unimplemented,
        error.ReadFailed => return error.ReadFailed,
        else => |e| {
            w.write_file_err = e;
            return error.WriteFailed;
        },
    };
    w.pos += n;
    return w.interface.consume(n);
}

pub fn seekTo(w: *Writer, offset: u64) (SeekError || Io.Writer.Error)!void {
    try w.interface.flush();
    try seekToUnbuffered(w, offset);
}

pub fn logicalPos(w: *const Writer) u64 {
    return w.pos + w.interface.end;
}

/// Asserts that no data is currently buffered.
pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void {
    assert(w.interface.buffered().len == 0);
    const io = w.io;
    switch (w.mode) {
        .positional, .positional_simple => {
            w.pos = offset;
        },
        .streaming, .streaming_simple => {
            if (w.seek_err) |err| return err;
            io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| {
                w.seek_err = err;
                return err;
            };
            w.pos = offset;
        },
        .failure => return w.seek_err.?,
    }
}

pub const EndError = File.SetLengthError || Io.Writer.Error;

/// Flushes any buffered data and sets the end position of the file.
///
/// If not overwriting existing contents, then calling `interface.flush`
/// directly is sufficient.
///
/// Flush failure is handled by setting `err` so that it can be handled
/// along with other write failures.
pub fn end(w: *Writer) EndError!void {
    const io = w.io;
    try w.interface.flush();
    switch (w.mode) {
        .positional,
        .positional_simple,
        => w.file.setLength(io, w.pos) catch |err| switch (err) {
            error.NonResizable => return,
            else => |e| return e,
        },

        .streaming,
        .streaming_simple,
        .failure,
        => {},
    }
}
