const std = @import("std");

const List = std.ArrayList;

fn Args(comptime T: type, params: []const std.builtin.Type.Fn.Param) type {
    const method = params.len > 0 and switch (@typeInfo(params[0].type.?)) {
        .pointer => |p| p.child == T,
        else => params[0].type == T,
    };

    const argCount = if (method) params.len - 1 else params.len;

    var argTypes: [argCount]type = undefined;

    for (0..argCount) |i| {
        const p = if (method) params[i + 1] else params[i];
        argTypes[i] = p.type.?;
    }

    return @Tuple(&argTypes);
}

fn Val(comptime T: type) type {
    const ti: std.builtin.Type.Struct = switch (@typeInfo(T)) {
        .@"struct" => |s| s,
        .pointer => |p| @typeInfo(p.child).@"struct",
        else => @compileError("only struct types allowed"),
    };

    const fieldCount = ti.fields.len + ti.decls.len + 1; // + 1 for value

    var names: [fieldCount][]const u8 = undefined;
    var types: [fieldCount]type = undefined;
    var attrs: [fieldCount]std.builtin.Type.StructField.Attributes = undefined;

    // value
    {
        names[fieldCount - 1] = "value";
        const NT = struct {
            value: T = undefined,

            pub fn init(self: *@This(), t: T) void {
                self.value = t;

                inline for (ti.fields) |field| {
                    const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self));
                    @field(parent, field.name).value.init(&@field(self.value, field.name));
                }
                inline for (ti.decls) |decl| {
                    const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self));
                    @field(parent, decl.name).init();
                }
            }

            pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void {
                inline for (ti.fields) |field| {
                    const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self));
                    @field(parent, field.name).value.deinit(alloc);
                }
                inline for (ti.decls) |decl| {
                    const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self));
                    @field(parent, decl.name).deinit(alloc);
                }
            }

            pub fn get(self: *@This()) if (@typeInfo(T) == .pointer) T else *T {
                if (@typeInfo(T) == .pointer) {
                    return self.value;
                } else {
                    return &self.value;
                }
            }
        };
        types[fieldCount - 1] = NT;
        attrs[fieldCount - 1] = .{
            .@"comptime" = false,
            .@"align" = @alignOf(NT),
            .default_value_ptr = &NT{},
        };
    }

    // fields
    inline for (ti.fields, 0..) |field, idx| {
        if (field.is_comptime)
            @compileError("implement skipping comptime fields");

        const fi = @typeInfo(field.type);
        switch (fi) {
            .@"struct" => {
                names[idx] = field.name;
                const NT = Val(*field.type);
                types[idx] = NT;
                attrs[idx] = .{};
            },
            else => {
                names[idx] = field.name;
                const NT = struct {
                    const _NT = @This();

                    value: struct {
                        value: *field.type = undefined,

                        pub fn init(self: *@This(), value: *field.type) void {
                            self.value = value;
                            var parent: *_NT = @fieldParentPtr("value", self);
                            parent.on_get = .empty;
                            parent.on_set = .empty;
                        }

                        pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void {
                            var parent: *_NT = @fieldParentPtr("value", self);
                            parent.on_get.clearAndFree(alloc);
                            parent.on_set.clearAndFree(alloc);
                        }
                    },
                    on_get: List(*const fn (field.type) void) = .empty,
                    on_set: List(*const fn (field.type) void) = .empty,

                    pub fn get(self: *@This()) field.type {
                        for (self.on_get.items) |cb| cb(self.value.value.*);
                        return self.value.value.*;
                    }
                    pub fn set(self: *@This(), value: field.type) void {
                        for (self.on_set.items) |cb| cb(self.value.value.*);
                        self.value.value.* = value;
                    }
                };
                types[idx] = NT;
                attrs[idx] = .{};
            },
        }
    }

    // decls
    inline for (ti.decls, ti.fields.len..) |decl, idx| {
        names[idx] = decl.name;
        const T2 = if (@typeInfo(T) == .pointer) @typeInfo(T).pointer.child else T;
        const F = @TypeOf(@field(T2, decl.name));
        const fi = @typeInfo(F).@"fn";

        const method = fi.params.len > 0 and switch (@typeInfo(fi.params[0].type.?)) {
            .pointer => |p| p.child == T,
            else => fi.params[0].type == T,
        };
        const modifying = method and switch (@typeInfo(fi.params[0].type.?)) {
            .pointer => |p| !p.is_const,
            else => false,
        };

        const NT = struct {
            on_call: List(*const F) = .empty,
            on_modify: List(*const F) = .empty,

            pub fn init(self: *@This()) void {
                self.on_call = .empty;
                self.on_modify = .empty;
            }

            pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void {
                self.on_call.clearAndFree(alloc);
                self.on_modify.clearAndFree(alloc);
            }

            pub fn call(self: *@This(), args: Args(T, fi.params)) (fi.return_type orelse void) {
                const allArgs = blk: {
                    if (method) {
                        const parent: *Val(T) = @alignCast(@fieldParentPtr(decl.name, self));
                        const tiv = @typeInfo(@TypeOf(parent.value.value));
                        const tip = @typeInfo(fi.params[0].type.?);
                        const arg0 =
                            if (tiv == .pointer and tip != .pointer) parent.value.value.* else if (tiv != .pointer and tip == .pointer) &parent.value.value else parent.value.value;
                        break :blk .{arg0} ++ args;
                    } else {
                        break :blk args;
                    }
                };

                @call(.auto, @field(T2, decl.name), allArgs);

                for (self.on_call.items) |cb| @call(.auto, cb, allArgs);
                if (modifying)
                    for (self.on_modify.items) |cb| @call(.auto, cb, allArgs);
            }
        };
        types[idx] = NT;
        attrs[idx] = .{ .default_value_ptr = &NT{} };
    }

    return @Struct(.auto, null, &names, &types, &attrs);
}

pub fn main(init: std.process.Init) !void {
    const A = struct {
        i: i32 = 123,
        j: i32 = 456,
        s: struct {
            i: i32 = 789,
            t: struct {
                j: i32 = 341,
                pub fn yo() void {
                    std.debug.print("t.yo!\n", .{});
                }
            } = .{},
            pub fn jo() void {
                std.debug.print("s.jo!\n", .{});
            }
        } = .{},
        pub fn b() void {
            std.debug.print("B!\n", .{});
        }
        pub fn c(self: @This()) void {
            std.debug.print("C! ({})\n", .{self});
        }
        pub fn d(self: *@This(), i: i32) void {
            self.i += i;
            std.debug.print("D! ({})\n", .{self});
        }
    };

    var a: Val(A) = undefined;
    a.value.init(.{});
    defer a.value.deinit(init.gpa);

    const callbacks = struct {
        fn set1(i: i32) void {
            std.debug.print("{} was set\n", .{i});
        }
        fn get1(i: i32) void {
            std.debug.print("{} was get\n", .{i});
        }
        fn f0() void {
            std.debug.print("called!\n", .{});
        }
        fn f1(self: A) void {
            std.debug.print("called ({})!\n", .{self});
        }
        fn f2(self: *A, i: i32) void {
            std.debug.print("called ({}, {})!\n", .{ self, i });
        }
    };

    try a.j.on_get.append(init.gpa, callbacks.get1);
    try a.j.on_set.append(init.gpa, callbacks.set1);
    try a.b.on_call.append(init.gpa, callbacks.f0);
    try a.c.on_call.append(init.gpa, callbacks.f1);
    try a.d.on_call.append(init.gpa, callbacks.f2);
    try a.s.jo.on_call.append(init.gpa, callbacks.f0);
    try a.s.t.yo.on_call.append(init.gpa, callbacks.f0);

    std.debug.print("{}\n{}\n", .{ a, a.value });
    a.b.call(.{});
    std.debug.print("{}\n", .{a.value.get()});
    std.debug.print("{}\n", .{a.s.value.get()});
    a.c.call(.{});
    a.d.call(.{5});
    a.s.jo.call(.{});
    a.s.t.yo.call(.{});
    std.debug.print("{}\n", .{a.i.get()});
    std.debug.print("{}\n", .{a.j.get()});
    a.j.set(9);
    std.debug.print("{}\n", .{a.j.get()});
    std.debug.print("{}\n", .{a.j.get()});
    std.debug.print("{}\n", .{a.j.get()});
    std.debug.print("{}\n", .{a.s.i.get()});
}
