const builtin = @import("builtin");

const coral = @import("./coral.zig");

const slices = @import("./slices.zig");

const std = @import("std");

pub const Writable = struct {
	data: []Byte,

	pub fn writer(self: *Writable) Writer {
		return Writer.bind(Writable, self, write);
	}

	fn write(self: *Writable, buffer: []const u8) !usize {
		const range = @min(buffer.len, self.data.len);

		@memcpy(self.data[0 .. range], buffer[0 .. range]);

		self.data = self.data[range ..];

		return buffer.len;
	}
};

pub const Byte = u8;

pub const Error = error {
	UnavailableResource,
};

pub fn Functor(comptime Output: type, comptime input_types: []const type) type {
	const InputTuple = std.meta.Tuple(input_types);

	return struct {
		context: *const anyopaque,
		apply_with_context: *const fn (*const anyopaque, InputTuple) Output,

		const Self = @This();

		pub fn apply(self: *const Self, inputs: InputTuple) Output {
			return self.apply_with_context(self.context, inputs);
		}

		pub fn bind(comptime State: type, state: *const State, comptime invoke: anytype) Self {
			const is_zero_aligned = @alignOf(State) == 0;

			return .{
				.context = if (is_zero_aligned) state else @ptrCast(state),

				.apply_with_context = struct {
					fn invoke_concrete(context: *const anyopaque, inputs: InputTuple) Output {
						if (is_zero_aligned) {
							return @call(.auto, invoke, .{@as(*const State, @ptrCast(context))} ++ inputs);
						}

						return switch (@typeInfo(@typeInfo(@TypeOf(invoke)).Fn.return_type.?)) {
							.ErrorUnion => try @call(.auto, invoke, .{@as(*const State, @ptrCast(@alignCast(context)))} ++ inputs),
							else => @call(.auto, invoke, .{@as(*const State, @ptrCast(@alignCast(context)))} ++ inputs),
						};
					}
				}.invoke_concrete,
			};
		}

		pub fn bind_fn(comptime invoke: anytype) Self {
			return .{
				.context = undefined,

				.apply_with_context = struct {
					fn invoke_concrete(_: *const anyopaque, inputs: InputTuple) Output {
						return @call(.auto, invoke, inputs);
					}
				}.invoke_concrete,
			};
		}
	};
}

pub fn Generator(comptime Output: type, comptime input_types: []const type) type {
	const InputTuple = std.meta.Tuple(input_types);

	return struct {
		context: *anyopaque,
		yield_with_context: *const fn (*anyopaque, InputTuple) Output,

		const Self = @This();

		pub fn bind(comptime State: type, state: *State, comptime invoke: anytype) Self {
			const is_zero_aligned = @alignOf(State) == 0;

			return .{
				.context = if (is_zero_aligned) state else @ptrCast(state),

				.yield_with_context = struct {
					fn invoke_concrete(context: *anyopaque, inputs: InputTuple) Output {
						if (is_zero_aligned) {
							return @call(.auto, invoke, .{@as(*State, @ptrCast(context))} ++ inputs);
						}

						return switch (@typeInfo(@typeInfo(@TypeOf(invoke)).Fn.return_type.?)) {
							.ErrorUnion => try @call(.auto, invoke, .{@as(*State, @ptrCast(@alignCast(context)))} ++ inputs),
							else => @call(.auto, invoke, .{@as(*State, @ptrCast(@alignCast(context)))} ++ inputs),
						};
					}
				}.invoke_concrete,
			};
		}

		pub fn bind_fn(comptime invoke: anytype) Self {
			return .{
				.context = undefined,

				.yield_with_context = struct {
					fn invoke_concrete(_: *const anyopaque, inputs: InputTuple) Output {
						return @call(.auto, invoke, inputs);
					}
				}.invoke_concrete,
			};
		}

		pub fn yield(self: *const Self, inputs: InputTuple) Output {
			return self.yield_with_context(self.context, inputs);
		}
	};
}

pub const PrintError = Error || error {
	IncompleteWrite,
};

pub const Reader = Generator(Error!usize, &.{[]coral.Byte});

pub const Writer = Generator(Error!usize, &.{[]const coral.Byte});

pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.Byte {
	const buffer = coral.Stack(coral.Byte){.allocator = allocator};

	errdefer buffer.deinit();

	const streamed = try stream_all(input.reader(), buffer.writer());

	return buffer.to_allocation(streamed);
}

pub const bits_per_byte = 8;

pub fn bytes_of(value: anytype) []const Byte {
	const pointer_info = @typeInfo(@TypeOf(value)).Pointer;

	return switch (pointer_info.size) {
		.One => @as([*]const Byte, @ptrCast(value))[0 .. @sizeOf(pointer_info.child)],
		.Slice => @as([*]const Byte, @ptrCast(value.ptr))[0 .. @sizeOf(pointer_info.child) * value.len],
		else => @compileError("`value` must be single-element pointer or slice type"),
	};
}

pub fn print(writer: Writer, utf8: []const u8) PrintError!void {
	if (try writer.yield(.{utf8}) != utf8.len) {
		return error.IncompleteWrite;
	}
}

pub fn skip_n(input: Reader, distance: u64) Error!void {
	var buffer = @as([512]coral.Byte, undefined);
	var remaining = distance;

	while (remaining != 0) {
		const read = try input.yield(.{buffer[0 .. @min(remaining, buffer.len)]});

		if (read == 0) {
			return error.UnavailableResource;
		}

		remaining -= read;
	}
}

pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) {
	var len = @as(usize, 0);

	while (ptr[len] != sen) {
		len += 1;
	}

	return ptr[0 .. len:sen];
}

pub fn stream_all(input: Reader, output: Writer) Error!usize {
	var buffer = @as([512]coral.Byte, undefined);
	var copied = @as(usize, 0);

	while (true) {
		const read = try input.apply(.{&buffer});

		if (read == 0) {
			return copied;
		}

		if (try output.apply(.{buffer[0 .. read]}) != read) {
			return error.UnavailableResource;
		}

		copied += read;
	}
}

pub fn stream_n(input: Reader, output: Writer, limit: usize) Error!usize {
	var buffer = @as([512]coral.Byte, undefined);
	var remaining = limit;

	while (true) {
		const read = try input.yield(.{buffer[0 .. @min(remaining, buffer.len)]});

		if (read == 0) {
			return limit - remaining;
		}

		if (try output.yield(.{buffer[0 .. read]}) != read) {
			return error.UnavailableResource;
		}

		remaining -= read;
	}
}