const std = @import("std");
const Allocator = std.mem.Allocator;

/// Type which could be borrowed or owned
/// The name is from the Rust std's `Cow` type
/// Can't think of a better name
pub fn Cow(comptime T: type, comptime VTable: type) type {
    const info = @typeInfo(T);
    if (info == .pointer and info.pointer.size == .slice) {
        @compileError("Cow should not be used with slice types. Use CowSlice or CowSliceZ instead.");
    }

    const Handler = struct {
        fn copy(this: *const T, allocator: Allocator) T {
            if (!@hasDecl(VTable, "copy")) @compileError(@typeName(VTable) ++ " needs `copy()` function");
            return VTable.copy(this, allocator);
        }

        fn deinit(this: *T, allocator: Allocator) void {
            if (!@hasDecl(VTable, "deinit")) @compileError(@typeName(VTable) ++ " needs `deinit()` function");
            return VTable.deinit(this, allocator);
        }
    };

    return union(enum) {
        borrowed: *const T,
        owned: T,

        pub fn borrow(val: *const T) @This() {
            return .{
                .borrowed = val,
            };
        }

        pub fn own(val: T) @This() {
            return .{
                .owned = val,
            };
        }

        pub fn replace(this: *@This(), allocator: Allocator, newval: T) void {
            if (this.* == .owned) {
                this.deinit(allocator);
            }
            this.* = .{ .owned = newval };
        }

        /// Get the underlying value.
        pub inline fn inner(this: *const @This()) *const T {
            return switch (this.*) {
                .borrowed => this.borrowed,
                .owned => &this.owned,
            };
        }

        pub inline fn innerMut(this: *@This()) ?*T {
            return switch (this.*) {
                .borrowed => null,
                .owned => &this.owned,
            };
        }

        pub fn toOwned(this: *@This(), allocator: Allocator) *T {
            switch (this.*) {
                .borrowed => {
                    this.* = .{
                        .owned = Handler.copy(this.borrowed, allocator),
                    };
                },
                .owned => {},
            }
            return &this.owned;
        }

        pub fn deinit(this: *@This(), allocator: Allocator) void {
            if (this.* == .owned) {
                Handler.deinit(&this.owned, allocator);
            }
        }
    };
}
