const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var input = Signal{ .digital = 1, .analog = 0.5 };

    var circuit = Circuit.init(allocator);
    defer circuit.deinit();

    var not1 = try circuit.addComponent(Not);
    not1.invert_output = false;
    not1.component.inputs.items[0] = .{ .signal = &input }; // manually set the input here

    var battery1 = try circuit.addComponent(Battery);
    var battery2 = try circuit.addComponent(Battery);
    var battery3 = try circuit.addComponent(Battery);
    battery1.value = -0.5;
    battery2.value = 0.5;
    battery3.value = -1.0;

    var or1 = try circuit.addComponent(Or);
    var or2 = try circuit.addComponent(Or);
    or1.arithmetic_mode = true;
    or2.arithmetic_mode = true;
    try or1.component.setNumInputs(allocator, 3);
    try or2.component.setNumInputs(allocator, 3);

    var or3 = try circuit.addComponent(Or);
    var or4 = try circuit.addComponent(Or);
    var or5 = try circuit.addComponent(Or);

    var or6 = try circuit.addComponent(Or);
    var or7 = try circuit.addComponent(Or);
    var or8 = try circuit.addComponent(Or);
    var and1 = try circuit.addComponent(And);
    var and2 = try circuit.addComponent(And);
    or6.arithmetic_mode = true;
    or7.arithmetic_mode = true;
    or8.arithmetic_mode = true;
    and1.arithmetic_mode = true;
    and2.arithmetic_mode = true;

    battery1.component.connect(0, &or3.component, 1);
    battery1.component.connect(0, &or4.component, 1);
    battery1.component.connect(0, &or5.component, 1);

    battery2.component.connect(0, &or6.component, 0);
    battery2.component.connect(0, &or7.component, 0);
    battery2.component.connect(0, &or8.component, 0);

    battery3.component.connect(0, &and1.component, 0);
    battery3.component.connect(0, &and2.component, 0);

    not1.component.connect(0, &or3.component, 0);
    not1.component.connect(0, &or1.component, 0);
    not1.component.connect(0, &or1.component, 1);

    or1.component.connect(0, &or4.component, 0);
    or1.component.connect(0, &or2.component, 0);
    or1.component.connect(0, &or2.component, 1);
    or2.component.connect(0, &or5.component, 0);

    or3.component.connect(0, &or6.component, 1);
    or4.component.connect(0, &or7.component, 1);
    or5.component.connect(0, &or8.component, 1);

    or6.component.connect(0, &and1.component, 1);
    and1.component.connect(0, &or1.component, 2);
    or7.component.connect(0, &and2.component, 1);
    and2.component.connect(0, &or2.component, 2);

    try circuit.tick();

    // battery1.component.process();
    // battery2.component.process();
    // battery3.component.process();
    // not1.component.process();
    // or3.component.process();
    // or6.component.process();
    // and1.component.process();
    // or1.component.process();
    // or4.component.process();
    // or7.component.process();
    // and2.component.process();
    // or2.component.process();
    // or5.component.process();
    // or8.component.process();

    // std.debug.print("Input:\n{}\n\n", .{input});
    // std.debug.print("{}\n{}\n\n", .{
    //     or1.component.outputs.items[0].signal,
    //     or2.component.outputs.items[0].signal,
    // });
    // std.debug.print("{}\n{}\n{}\n\n", .{
    //     or3.component.outputs.items[0].signal,
    //     or4.component.outputs.items[0].signal,
    //     or5.component.outputs.items[0].signal,
    // });
    // std.debug.print("{}\n{}\n{}\n{}\n{}\n\n", .{
    //     or6.component.outputs.items[0].signal,
    //     and1.component.outputs.items[0].signal,
    //     or7.component.outputs.items[0].signal,
    //     and2.component.outputs.items[0].signal,
    //     or8.component.outputs.items[0].signal,
    // });
}

pub const Circuit = struct {
    allocator: std.mem.Allocator,
    components: Components,
    source_components: SourceComponents,

    pub fn init(allocator: std.mem.Allocator) Circuit {
        return .{
            .allocator = allocator,
            .components = Components.empty,
            .source_components = SourceComponents.empty,
        };
    }

    pub fn deinit(self: *Circuit) void {
        self.source_components.deinit(self.allocator);
        for (0..self.components.items.len) |i| self.components.items[i].deinit(self.allocator);
        self.components.deinit(self.allocator);
    }

    pub fn addComponent(self: *Circuit, comptime T: type) !T {
        var c = try T.init(self.allocator);
        errdefer c.deinit(self.allocator);
        try self.components.append(self.allocator, c.component);
        if (T == Battery) try self.source_components.append(self.allocator, &c.component);
        return c;
    }

    pub fn tick(self: *Circuit) !void {
        var process_order_solver = try ProcessOrderSolver.init(self);
        defer process_order_solver.deinit();
        const process_order = process_order_solver.solve();
        _ = process_order;
    }

    const Components = std.ArrayListUnmanaged(Component);
    const SourceComponents = std.ArrayListUnmanaged(*Component);

    const ProcessOrder = []*Component;
    const ProcessOrderSolver = struct {
        circuit: *Circuit,
        solved: []bool,

        pub fn init(circuit: *Circuit) !ProcessOrderSolver {
            return .{
                .circuit = circuit,
                .solved = try circuit.allocator.alloc(bool, circuit.components.items.len),
            };
        }

        pub fn deinit(self: *ProcessOrderSolver) void {
            self.circuit.allocator.free(self.solved);
        }

        pub fn solve(self: *ProcessOrderSolver) ProcessOrder {
            for (self.circuit.source_components.items) |source_component| {
                var component = source_component;
                // while (true) blk: {
                // component.process();
                self.solved[self.componentIndex(component).?] = true;
                component = component.outputs.items[0].connection.?;
                std.debug.print("{any}\n", .{component});
                // }
            }
            return &[_]*Component{};
        }

        fn componentIndex(self: ProcessOrderSolver, component: *Component) ?usize {
            for (self.circuit.components.items, 0..) |c, i| {
                std.debug.print("{any} == {any}\n", .{ component, &c });
                if (component == &c) {
                    std.debug.print("component index {d}\n", .{i});
                    return i;
                }
            }
            return null;
            // return std.mem.indexOfScalar(Component, self.circuit.components.items, component.*);
        }
    };
};

var null_signal = Signal{};

pub const Component = struct {
    inputs: Inputs,
    outputs: Outputs,

    processFn: *const fn (*Component) void,

    pub fn init(allocator: std.mem.Allocator, inputs_len: usize, outputs_len: usize, processFn: *const fn (*Component) void) !Component {
        var inputs = Inputs.empty;
        errdefer inputs.deinit(allocator);
        try inputs.resize(allocator, inputs_len);
        for (0..inputs.items.len) |i| inputs.items[i] = .{};

        var outputs = Outputs.empty;
        errdefer outputs.deinit(allocator);
        try outputs.resize(allocator, outputs_len);
        for (0..outputs.items.len) |i| outputs.items[i] = .{};

        return .{
            .inputs = inputs,
            .outputs = outputs,
            .processFn = processFn,
        };
    }

    pub fn deinit(self: *Component, allocator: std.mem.Allocator) void {
        self.inputs.deinit(allocator);
        self.outputs.deinit(allocator);
    }

    pub fn process(self: *Component) void {
        self.processFn(self);
    }

    // TODO allow inserting the new elements at an arbitrary index
    // TODO ensure this won't break the opposide side's connections
    pub fn setNumInputs(self: *Component, allocator: std.mem.Allocator, new_len: usize) !void {
        const old_len = self.inputs.items.len;
        try self.inputs.resize(allocator, new_len);
        if (new_len > old_len) for (old_len..new_len) |i| {
            self.inputs.items[i] = .{};
        };
    }

    // TODO allow inserting the new elements at an arbitrary index
    // TODO is this allocating the new output Signals on the stack or dumbthing?
    // TODO ensure this won't break the opposide side's connections
    pub fn setNumOutputs(self: *Component, allocator: std.mem.Allocator, new_len: usize) !void {
        const old_len = self.outputs.items.len;
        try self.outputs.resize(allocator, new_len);
        if (new_len > old_len) for (old_len..new_len) |i| {
            self.outputs.items[i] = .{};
        };
    }

    pub fn connect(self: *Component, self_idx: usize, to: *Component, to_idx: usize) void {
        to.inputs.items[to_idx] = .{
            .signal = &self.outputs.items[self_idx].signal,
            .connection = self,
            .idx = self_idx,
        };
    }

    pub const Input = struct {
        signal: *Signal = &null_signal,
        connection: ?*Component = null,
        idx: usize = 0,
    };
    pub const Output = struct {
        signal: Signal = .{},
        connection: ?*Component = null,
        idx: usize = 0,
    };
    const Inputs = std.ArrayListUnmanaged(Input);
    const Outputs = std.ArrayListUnmanaged(Output);

    // const Connection = struct {
    //     from: *Component,
    //     from_idx: usize,
    //     to: *Component,
    //     to_idx: usize,
    // };
};

pub const Signal = struct {
    digital: i2 = 0,
    analog: f32 = 0.0,
    color: u24 = 0,

    pub fn format(
        self: Signal,
        comptime fmt: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = .{ fmt, options };
        try writer.writeAll("Signal(");
        if (self.digital < 0) try writer.writeByte('-') else try writer.writeByte('+');
        try writer.print("{d} / {d:0>1.4})", .{
            @abs(self.digital),
            self.analog,
        });
    }
};

pub const Battery = struct {
    component: Component,

    value: f32 = 1.0,

    pub fn init(allocator: std.mem.Allocator) !Battery {
        return .{
            .component = try Component.init(allocator, 0, 1, &process),
        };
    }

    pub fn deinit(self: *Battery, allocator: std.mem.Allocator) void {
        self.component.deinit(allocator);
    }

    pub fn process(component: *Component) void {
        const self: *Battery = @fieldParentPtr("component", component);
        component.outputs.items[0].signal.digital = @intFromFloat(std.math.sign(self.value));
        component.outputs.items[0].signal.analog = self.value;
    }
};

pub const Not = struct {
    component: Component,

    invert_output: bool = true,

    pub fn init(allocator: std.mem.Allocator) !Not {
        return .{
            .component = try Component.init(allocator, 1, 1, &process),
        };
    }

    pub fn deinit(self: *Not, allocator: std.mem.Allocator) void {
        self.component.deinit(allocator);
    }

    pub fn process(component: *Component) void {
        const self: *Not = @fieldParentPtr("component", component);
        if (self.invert_output) {
            component.outputs.items[0].signal.digital = 1 - @as(i2, @intCast(@abs(component.inputs.items[0].signal.digital)));
            component.outputs.items[0].signal.analog = 1.0 - @abs(component.inputs.items[0].signal.analog);
        } else {
            component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
            component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
        }
        component.outputs.items[0].signal.analog = std.math.clamp(component.outputs.items[0].signal.analog, -1.0, 1.0);
    }
};

pub const And = struct {
    component: Component,

    // if false, is in Minimum Input mode
    // if true, is in Multiply Inputs mode
    arithmetic_mode: bool = false,

    pub fn init(allocator: std.mem.Allocator) !And {
        return .{
            .component = try Component.init(allocator, 2, 1, &process),
        };
    }

    pub fn deinit(self: *And, allocator: std.mem.Allocator) void {
        self.component.deinit(allocator);
    }

    // TODO check implementation
    pub fn process(component: *Component) void {
        const self: *And = @fieldParentPtr("component", component);
        if (self.arithmetic_mode) {
            component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
            component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
            for (component.inputs.items[1..]) |input| {
                component.outputs.items[0].signal.digital = 0; // TODO
                component.outputs.items[0].signal.analog *= input.signal.analog;
            }
        } else {
            component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
            component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
            for (component.inputs.items[1..]) |input| {
                component.outputs.items[0].signal.digital = 0; // TODO
                component.outputs.items[0].signal.analog = switch (std.math.order(@abs(component.outputs.items[0].signal.analog), @abs(input.signal.analog))) {
                    .lt => component.outputs.items[0].signal.analog,
                    .eq => @min(component.outputs.items[0].signal.analog, input.signal.analog), // TODO what does this *actually* do?
                    .gt => input.signal.analog,
                };
            }
        }
        component.outputs.items[0].signal.analog = std.math.clamp(component.outputs.items[0].signal.analog, -1.0, 1.0);
    }
};

// TODO update test to use new Component interface
test "min" {
    var a = Signal{ .analog = 0.0 };
    var b = Signal{ .analog = 1.0 };

    var inputs = [_]*Signal{ &a, &b };
    var and1 = And{ .inputs = &inputs };

    and1.process();
    try std.testing.expectEqual(0.0, and1.output.analog);

    a.analog = -0.5;
    b.analog = -0.2;
    and1.process();
    try std.testing.expectEqual(-0.2, and1.output.analog);
}

pub const Or = struct {
    component: Component,

    // if false, is in Maximum Input mode
    // if true, is in Add Inputs mode
    arithmetic_mode: bool = false,

    pub fn init(allocator: std.mem.Allocator) !Or {
        return .{
            .component = try Component.init(allocator, 2, 1, &process),
        };
    }

    pub fn deinit(self: *Or, allocator: std.mem.Allocator) void {
        self.component.deinit(allocator);
    }

    // TODO check implementation
    pub fn process(component: *Component) void {
        const self: *Or = @fieldParentPtr("component", component);
        if (self.arithmetic_mode) {
            component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
            component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
            for (component.inputs.items[1..]) |input| {
                component.outputs.items[0].signal.digital = 0; // TODO
                component.outputs.items[0].signal.analog += input.signal.analog;
            }
        } else {
            component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
            component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
            for (component.inputs.items[1..]) |input| {
                component.outputs.items[0].signal.digital = 0; // TODO
                component.outputs.items[0].signal.analog = switch (std.math.order(@abs(component.outputs.items[0].signal.analog), @abs(input.signal.analog))) {
                    .lt => input.signal.analog,
                    .eq => @max(component.outputs.items[0].signal.analog, input.signal.analog), // TODO what does this *actually* do?
                    .gt => component.outputs.items[0].signal.analog,
                };
            }
        }
        component.outputs.items[0].signal.analog = std.math.clamp(component.outputs.items[0].signal.analog, -1.0, 1.0);
    }
};

// TODO update test to use new Component interface
test "max" {
    var a = Signal{ .analog = 0.0 };
    var b = Signal{ .analog = 1.0 };

    var inputs = [_]*Signal{ &a, &b };
    var or1 = Or{ .inputs = &inputs };

    or1.process();
    try std.testing.expectEqual(1.0, or1.output.analog);

    a.analog = -0.5;
    b.analog = -0.2;
    or1.process();
    try std.testing.expectEqual(-0.5, or1.output.analog);
}