const std = @import("std");
const os = std.os;
const io = std.io;
const fs = std.fs;
const mem = std.mem;
const heap = std.heap;
const fmt = std.fmt;
const posix = std.posix;

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

    var term = try Terminal.init(allocator);
    defer term.deinit();
    try term.clearScreen();

    var box1 = Terminal.Box.init(allocator);
    defer box1.deinit();
    box1.border_fg = try Terminal.Color.init("#ff8855");
    box1.top = 1;
    box1.bottom = 1;
    box1.left = 1;
    box1.right = 1;

    var box2 = Terminal.Box.init(allocator);
    defer box2.deinit();
    box2.border_fg = try Terminal.Color.init("#aaff55");
    box2.top = 1;
    box2.bottom = 1;
    box2.left = 1;
    box2.right = 1;

    var box3 = Terminal.Box.init(allocator);
    defer box3.deinit();
    box3.border_bg = try Terminal.Color.init("#aa33ff");
    box3.border_type = .bg;
    box3.top = 1;
    box3.bottom = 1;
    box3.left = 1;
    box3.right = 1;

    try term.box.addChild(&box1);
    try box1.addChild(&box2);
    try box2.addChild(&box3);

    while (true) {
        const events = try term.getEvents();
        _ = events;
        // for (events) |ev| if (ev.system == .winch) std.debug.print("hii\n", .{});
        try term.draw();
        std.time.sleep(1000);
    }

    // var box = Terminal.Box{
    //     .x = 0,
    //     .y = 0,
    //     .width = 13,
    //     .height = 1,

    //     .content = "hello world",
    //     .border = true,
    // };
    // try box.draw(&term);

    // try term.clearScreen();
    // try term.print("fooboo", .{});
    // try term.cursorLeft();
    // try term.cursorLeft();
    // try term.print("ar", .{});
    // // try term.cursorSet(12, 3);
    // try term.blinkOn();
    // try term.boldOn();
    // try term.underlineOn();
    // try term.italicsOn();
    // try term.print("ABCDEFGHIJKLMNOPQRSTUVWXYZ", .{});
    // try term.blinkOff();
    // try term.boldOff();
    // try term.underlineOff();
    // try term.italicsOff();
    // try term.print("eeheeveehee\n", .{});
}

pub const Terminal = struct {
    tty: fs.File,
    original_termios: os.linux.termios,
    info: Info,
    allocator: mem.Allocator,

    box: Box,

    events: std.ArrayList(Event),

    pub fn init(allocator: mem.Allocator) !Terminal {
        var term = Terminal{
            .tty = try fs.openFileAbsolute("/dev/tty", .{ .mode = .read_write }),
            .original_termios = undefined,
            .info = try Info.init(allocator),
            .allocator = allocator,
            .box = Box.init(allocator),
            .events = std.ArrayList(Event).init(allocator),
        };
        errdefer term.tty.close();
        errdefer term.info.deinit();
        try term.uncook();
        try attachSignalHandlers();
        try term.updateWinSize();
        return term;
    }

    pub fn deinit(self: *Terminal) void {
        self.events.deinit();
        self.box.deinit();
        self.cook() catch @panic("failed to restore termios");
        self.tty.close();
        self.info.deinit();
    }

    fn uncook(self: *Terminal) !void {
        self.original_termios = try posix.tcgetattr(self.tty.handle);
        var raw = self.original_termios;

        raw.lflag.ECHO = false;
        raw.lflag.ICANON = false;
        // raw.lflag.ISIG = false;
        raw.lflag.ISIG = true;
        raw.lflag.IEXTEN = false;

        raw.iflag.IXON = false;
        raw.iflag.ICRNL = false;
        raw.iflag.BRKINT = false;
        raw.iflag.INPCK = false;
        raw.iflag.ISTRIP = false;

        raw.oflag.OPOST = false;

        raw.cc[@intFromEnum(os.linux.V.TIME)] = 0;
        raw.cc[@intFromEnum(os.linux.V.MIN)] = 1;

        try posix.tcsetattr(self.tty.handle, .FLUSH, raw);
    }

    fn cook(self: *Terminal) !void {
        try posix.tcsetattr(self.tty.handle, .FLUSH, self.original_termios);
    }

    fn attachSignalHandlers() !void {
        var act = posix.Sigaction{
            .handler = .{ .handler = &S.handlerFn },
            .mask = posix.empty_sigset,
            .flags = 0,
        };
        // try posix.sigaction(posix.SIG.INT, &act, null);
        try posix.sigaction(posix.SIG.USR1, &act, null);
        try posix.sigaction(posix.SIG.USR2, &act, null);
        try posix.sigaction(posix.SIG.WINCH, &act, null);
    }

    pub fn getEvents(self: *Terminal) ![]Event {
        if (S.ev) |ev| {
            switch (ev) {
                Event.system => {
                    switch (ev.system) {
                        .winch => try self.updateWinSize(),
                        else => try self.events.append(ev),
                    }
                },
                // else => try self.events.append(ev);
            }
            S.ev = null;
        }
        return try self.events.toOwnedSlice();
    }

    const S = struct {
        var ev: ?Event = null;

        fn handlerFn(sig: i32) callconv(.C) void {
            switch (sig) {
                // posix.SIG.INT => ev = Event{ .system = .int },
                posix.SIG.USR1 => ev = Event{ .system = .usr1 },
                posix.SIG.USR2 => ev = Event{ .system = .usr2 },
                posix.SIG.WINCH => ev = Event{ .system = .winch },
                else => {},
            }
        }
    };

    pub const Event = union(enum) {
        system: enum {
            // int,
            usr1,
            usr2,
            winch,
        },
    };

    fn updateWinSize(self: *Terminal) !void {
        var sz: os.linux.winsize = undefined;
        const ret = os.linux.ioctl(0, os.linux.T.IOCGWINSZ, @intFromPtr(&sz));
        // std.debug.print("ret: {d}, {any}\r\n", .{ ret, sz });
        if (ret == 0) {
            self.box.rect = .{
                .x = 0,
                .y = 0,
                .h = @intCast(sz.ws_row),
                .w = @intCast(sz.ws_col),
            };
            self.box.makeDirty();
        } else unreachable; // TODO: handle else case
    }

    pub fn draw(self: *Terminal) !void {
        if (self.box.dirty) try self.clearScreen();
        try self.box.draw(self);
    }

    pub fn print(self: *Terminal, comptime format: []const u8, args: anytype) !void {
        const formatted = try fmt.allocPrint(self.allocator, format, args);
        defer self.allocator.free(formatted);
        try self.tty.writeAll(formatted);
    }

    pub fn cursorUp(self: *Terminal) !void {
        try self.info.writeString(.cursor_up, self.tty.writer(), &[_]u32{});
    }

    pub fn cursorDown(self: *Terminal) !void {
        try self.info.writeString(.cursor_down, self.tty.writer(), &[_]u32{});
    }

    pub fn cursorLeft(self: *Terminal) !void {
        try self.info.writeString(.cursor_left, self.tty.writer(), &[_]u32{});
    }

    pub fn cursorRight(self: *Terminal) !void {
        try self.info.writeString(.cursor_right, self.tty.writer(), &[_]u32{});
    }

    pub fn cursorSet(self: *Terminal, x: u32, y: u32) !void {
        try self.info.writeString(.cursor_address, self.tty.writer(), &[_]u32{ y, x });
    }

    pub fn blinkOn(self: *Terminal) !void {
        try self.info.writeString(.enter_blink_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn blinkOff(self: *Terminal) !void {
        try self.info.writeString(.exit_blink_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn boldOn(self: *Terminal) !void {
        try self.info.writeString(.enter_bold_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn boldOff(self: *Terminal) !void {
        try self.info.writeString(.exit_bold_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn italicsOn(self: *Terminal) !void {
        try self.info.writeString(.enter_italics_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn italicsOff(self: *Terminal) !void {
        try self.info.writeString(.exit_italics_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn underlineOn(self: *Terminal) !void {
        try self.info.writeString(.enter_underline_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn underlineOff(self: *Terminal) !void {
        try self.info.writeString(.exit_underline_mode, self.tty.writer(), &[_]u32{});
    }

    pub fn clearScreen(self: *Terminal) !void {
        try self.info.writeString(.clear_screen, self.tty.writer(), &[_]u32{});
    }

    // pub fn clearRegion(self: *Terminal, x: u32, y: u32, width: u32, height: u32) !void {
    //     var row = y;
    //     var i: u32 = 0;
    //     while (i < height) {

    //         i += 1;
    //         row += 1;
    //     }
    // }

    pub fn setFg(self: *Terminal, color: Color) !void {
        try self.info.writeString(.set_rgb_foreground, self.tty.writer(), &[_]u32{ @intCast(color.r), @intCast(color.g), @intCast(color.b) });
    }

    pub fn setBg(self: *Terminal, color: Color) !void {
        try self.info.writeString(.set_rgb_background, self.tty.writer(), &[_]u32{ @intCast(color.r), @intCast(color.g), @intCast(color.b) });
    }

    pub const Info = @import("terminfo.zig");
    pub const Color = @import("color.zig").RGB;

    pub const Box = struct {
        parent: ?*Box,
        children: std.ArrayList(*Box),

        rect: ?Rect = null, // TODO: maybe unionize this with .parent
        dirty: bool = true,

        left: u32,
        right: u32,
        top: u32,
        bottom: u32,

        border_type: enum { line, bg } = .line,
        border_char: u8 = ' ',
        border_bg: Color = Color{ .r = 0, .g = 0, .b = 0 },
        border_fg: Color = Color{ .r = 0xff, .g = 0xff, .b = 0xff },

        content: []const u8 = "",

        pub fn init(allocator: mem.Allocator) Box {
            return .{
                .parent = null,
                .children = std.ArrayList(*Box).init(allocator),
                .left = 2,
                .right = 2,
                .top = 1,
                .bottom = 1,
            };
        }

        pub fn deinit(self: *Box) void {
            for (self.children.items) |child| child.deinit();
            self.children.deinit();
        }

        pub fn addChild(self: *Box, child: *Box) !void {
            std.debug.assert(child.parent == null);
            child.parent = self;
            try self.children.append(child);
        }

        pub fn removeChild(self: *Box, child: *Box) !void {
            _ = self;
            _ = child;
        }

        pub fn removeChildByIndex(self: *Box, child: usize) !void {
            _ = self;
            _ = child;
        }

        pub fn moveChild(self: *Box, child: *Box, idx: usize) !void {
            _ = self;
            _ = child;
            _ = idx;
        }

        pub fn moveChildByIndex(self: *Box, child: usize, idx: usize) !void {
            _ = self;
            _ = child;
            _ = idx;
        }

        fn makeDirty(self: *Box) void {
            self.dirty = true;
            for (self.children.items) |child| child.makeDirty();
        }

        pub fn draw(self: *Box, term: *Terminal) !void {
            if (self.dirty) {
                // if (self != &term.box) self.calcRect();
                const rect = self.getRect();
                // std.debug.print("{any}\r\n", .{rect});
                switch (self.border_type) {
                    .line => {
                        try term.setFg(self.border_fg);
                        try term.setBg(self.border_bg);
                        var x: u32 = 0;
                        var y: u32 = 0;
                        try term.cursorSet(rect.x, rect.y);
                        try term.print("┌", .{});
                        while (x < rect.w - 2) : (x += 1) try term.print("─", .{});
                        try term.print("┐", .{});
                        y += 1;
                        while (y < rect.h - 1) : (y += 1) {
                            try term.cursorSet(rect.x, rect.y + y);
                            try term.print("│", .{});
                            try term.cursorSet(rect.x + rect.w - 1, rect.y + y);
                            try term.print("│", .{});
                        }
                        x = 0;
                        try term.cursorSet(rect.x, rect.y + rect.h - 1);
                        try term.print("└", .{});
                        while (x < rect.w - 2) : (x += 1) try term.print("─", .{});
                        try term.print("┘", .{});
                    },
                    .bg => {
                        try term.setBg(self.border_bg);
                        var y: u32 = 0;
                        try term.cursorSet(rect.x, rect.y);
                        while (y < rect.h) : (y += 1) {
                            try term.cursorSet(rect.x, rect.y + y);
                            var x: u32 = 0;
                            while (x < rect.w) : (x += 1) try term.print(" ", .{});
                        }
                    },
                }
                self.dirty = false;
            }
            for (self.children.items) |child| try child.draw(term);
        }

        fn getRect(self: *Box) Rect {
            var rect: Rect = undefined;
            if (self.parent) |parent| {
                const parent_rect = parent.getRect();
                rect.x = parent_rect.x + self.left;
                rect.y = parent_rect.y + self.top;
                rect.w = parent_rect.w + parent_rect.x - rect.x - self.right;
                rect.h = parent_rect.h + parent_rect.y - rect.y - self.bottom;
            } else return self.rect.?;
            return rect;
        }
        const Rect = struct { x: u32, y: u32, w: u32, h: u32 };
    };
};