const std = @import("std");

pub fn main() !void {
    var input = Signal{ .digital = 1, .analog = 0.5 };

    var battery1 = Battery{ .value = -0.5 };
    var battery2 = Battery{ .value = 0.5 };
    var battery3 = Battery{ .value = -1.0 };

    var or1_inputs = [_]*Signal{ &input, &input, undefined };
    var or1 = Or{ .inputs = &or1_inputs, .arithmetic_mode = true };
    var or2_inputs = [_]*Signal{ &or1.output, &or1.output, undefined };
    var or2 = Or{ .inputs = &or2_inputs, .arithmetic_mode = true };

    var or3_inputs = [_]*Signal{ &input, &battery1.output };
    var or3 = Or{ .inputs = &or3_inputs };
    var or4_inputs = [_]*Signal{ &or1.output, &battery1.output };
    var or4 = Or{ .inputs = &or4_inputs };
    var or5_inputs = [_]*Signal{ &or2.output, &battery1.output };
    var or5 = Or{ .inputs = &or5_inputs };

    var or6_inputs = [_]*Signal{ &battery2.output, &or3.output };
    var or6 = Or{ .inputs = &or6_inputs, .arithmetic_mode = true };
    var and1_inputs = [_]*Signal{ &battery3.output, &or6.output };
    var and1 = And{ .inputs = &and1_inputs, .arithmetic_mode = true };
    var or7_inputs = [_]*Signal{ &battery2.output, &or4.output };
    var or7 = Or{ .inputs = &or7_inputs, .arithmetic_mode = true };
    var and2_inputs = [_]*Signal{ &battery3.output, &or7.output };
    var and2 = And{ .inputs = &and2_inputs, .arithmetic_mode = true };
    var or8_inputs = [_]*Signal{ &battery2.output, &or5.output };
    var or8 = Or{ .inputs = &or8_inputs, .arithmetic_mode = true };

    or1_inputs[2] = &and1.output;
    or2_inputs[2] = &and2.output;

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

    std.debug.print("Input:\n{}\n\n", .{input});
    std.debug.print("{}\n{}\n\n", .{ or1.output, or2.output });
    std.debug.print("{}\n{}\n{}\n\n", .{ or3.output, or4.output, or5.output });
    std.debug.print("{}\n{}\n{}\n{}\n{}\n\n", .{ or6.output, and1.output, or7.output, and2.output, or8.output });
}

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 {
    value: f32,

    output: Signal = .{},

    pub fn process(self: *Battery) void {
        self.output.digital = @intFromFloat(std.math.sign(self.value));
        self.output.analog = self.value;
    }
};

pub const Not = struct {
    input: *Signal,
    output: Signal = .{},

    invert_output: bool = true,

    pub fn process(self: *Not) void {
        if (self.invert_output) {
            self.output.digital = 1 - @as(i2, @intCast(@abs(self.input.digital)));
            self.output.analog = 1.0 - @abs(self.input.analog);
        } else {
            self.output.digital = self.input.digital;
            self.output.analog = self.input.analog;
        }
        self.output.analog = std.math.clamp(self.output.analog, -1.0, 1.0);
    }
};

pub const And = struct {
    inputs: []*Signal,
    output: Signal = .{},

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

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

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 {
    inputs: []*Signal,
    output: Signal = .{},

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

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

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);
}