const std = @import("std");
const vaxis = @import("vaxis");
const srf = @import("srf");
const fmt = @import("../format.zig");

pub const Color = [3]u8;

pub const Theme = struct {
    // Backgrounds
    bg: Color,
    bg_panel: Color,
    bg_element: Color,

    // Tab bar
    tab_bg: Color,
    tab_fg: Color,
    tab_active_bg: Color,
    tab_active_fg: Color,

    // Content
    text: Color,
    text_muted: Color,
    text_dim: Color,

    // Status bar
    status_bg: Color,
    status_fg: Color,

    // Input prompt
    input_bg: Color,
    input_fg: Color,
    input_hint: Color,

    // Semantic
    accent: Color,
    positive: Color,
    negative: Color,
    warning: Color,
    info: Color,

    // Selection / cursor highlight
    select_bg: Color,
    select_fg: Color,

    // Border
    border: Color,

    // Chart / data visualization
    bar_fill: Color,

    pub fn vcolor(c: Color) vaxis.Cell.Color {
        return .{ .rgb = c };
    }

    pub fn style(_: Theme, fg_color: Color, bg_color: Color) vaxis.Style {
        return .{ .fg = vcolor(fg_color), .bg = vcolor(bg_color) };
    }

    pub fn contentStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.text), .bg = vcolor(self.bg) };
    }

    pub fn mutedStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.text_muted), .bg = vcolor(self.bg) };
    }

    pub fn dimStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.text_dim), .bg = vcolor(self.bg) };
    }

    pub fn statusStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.status_fg), .bg = vcolor(self.status_bg) };
    }

    pub fn tabStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.tab_fg), .bg = vcolor(self.tab_bg) };
    }

    pub fn tabActiveStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.tab_active_fg), .bg = vcolor(self.tab_active_bg), .bold = true };
    }

    pub fn tabDisabledStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.text_dim), .bg = vcolor(self.tab_bg) };
    }

    pub fn inputStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.input_fg), .bg = vcolor(self.input_bg), .bold = true };
    }

    pub fn inputHintStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.input_hint), .bg = vcolor(self.input_bg) };
    }

    pub fn selectStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.select_fg), .bg = vcolor(self.select_bg), .bold = true };
    }

    pub fn positiveStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.positive), .bg = vcolor(self.bg) };
    }

    pub fn negativeStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.negative), .bg = vcolor(self.bg) };
    }

    pub fn borderStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.border), .bg = vcolor(self.bg) };
    }

    pub fn headerStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.accent), .bg = vcolor(self.bg), .bold = true };
    }

    pub fn watchlistStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.text_dim), .bg = vcolor(self.bg) };
    }

    pub fn warningStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.warning), .bg = vcolor(self.bg) };
    }

    /// Cyan-ish style for informational / overlay content. Distinct
    /// from `accent` (purple — used by the projection-chart median
    /// line and bands) and `warning` (yellow). Used for the actuals
    /// overlay status-line indicator.
    pub fn infoStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.info), .bg = vcolor(self.bg) };
    }

    /// Map a semantic StyleIntent to a vaxis style.
    pub fn styleFor(self: Theme, intent: fmt.StyleIntent) vaxis.Style {
        return switch (intent) {
            .normal => self.contentStyle(),
            .muted => self.mutedStyle(),
            .positive => self.positiveStyle(),
            .negative => self.negativeStyle(),
            .warning => self.warningStyle(),
            .accent => .{ .fg = vcolor(self.accent), .bg = vcolor(self.bg) },
            .info => self.infoStyle(),
        };
    }

    pub fn barFillStyle(self: Theme) vaxis.Style {
        return .{ .fg = vcolor(self.bar_fill), .bg = vcolor(self.bg) };
    }
};

// Monokai-inspired dark theme, influenced by opencode color system.
// Backgrounds are near-black for transparent terminal compatibility.
// Accent colors draw from Monokai's iconic palette: orange, purple, green, pink, yellow, cyan.
pub const default_theme = Theme{
    .bg = .{ 0x0a, 0x0a, 0x0a }, // near-black (opencode darkStep1)
    .bg_panel = .{ 0x14, 0x14, 0x14 }, // slightly lighter (opencode darkStep2)
    .bg_element = .{ 0x1e, 0x1e, 0x1e }, // element bg (opencode darkStep3)

    .tab_bg = .{ 0x14, 0x14, 0x14 }, // panel bg
    .tab_fg = .{ 0x80, 0x80, 0x80 }, // muted gray
    .tab_active_bg = .{ 0xfa, 0xb2, 0x83 }, // warm orange (opencode primary/darkStep9)
    .tab_active_fg = .{ 0x0a, 0x0a, 0x0a }, // dark on orange

    .text = .{ 0xee, 0xee, 0xee }, // bright text (opencode darkStep12)
    .text_muted = .{ 0x80, 0x80, 0x80 }, // muted (opencode darkStep11)
    .text_dim = .{ 0x48, 0x48, 0x48 }, // dim (opencode darkStep7)

    .status_bg = .{ 0x14, 0x14, 0x14 }, // panel bg
    .status_fg = .{ 0x80, 0x80, 0x80 }, // muted gray

    .input_bg = .{ 0x28, 0x28, 0x28 }, // subtle element bg
    .input_fg = .{ 0xfa, 0xb2, 0x83 }, // warm orange prompt
    .input_hint = .{ 0x60, 0x60, 0x60 }, // dim hint

    .accent = .{ 0x9d, 0x7c, 0xd8 }, // purple (opencode darkAccent)
    .positive = .{ 0x7f, 0xd8, 0x8f }, // green (opencode darkGreen)
    .negative = .{ 0xe0, 0x6c, 0x75 }, // red (opencode darkRed)
    .warning = .{ 0xe5, 0xc0, 0x7b }, // yellow (opencode darkYellow)
    .info = .{ 0x56, 0xb6, 0xc2 }, // cyan (opencode darkCyan)

    .select_bg = .{ 0x32, 0x32, 0x32 }, // subtle highlight (opencode darkStep5)
    .select_fg = .{ 0xff, 0xc0, 0x9f }, // bright orange (opencode darkStep10)

    .border = .{ 0x3c, 0x3c, 0x3c }, // subtle border (opencode darkStep6)

    .bar_fill = .{ 0x89, 0xb4, 0xfa }, // blue (bar chart fill, matches CLI accent)
};

// ── SRF serialization ────────────────────────────────────────

const field_names = [_]struct { name: []const u8, offset: usize }{
    .{ .name = "bg", .offset = @offsetOf(Theme, "bg") },
    .{ .name = "bg_panel", .offset = @offsetOf(Theme, "bg_panel") },
    .{ .name = "bg_element", .offset = @offsetOf(Theme, "bg_element") },
    .{ .name = "tab_bg", .offset = @offsetOf(Theme, "tab_bg") },
    .{ .name = "tab_fg", .offset = @offsetOf(Theme, "tab_fg") },
    .{ .name = "tab_active_bg", .offset = @offsetOf(Theme, "tab_active_bg") },
    .{ .name = "tab_active_fg", .offset = @offsetOf(Theme, "tab_active_fg") },
    .{ .name = "text", .offset = @offsetOf(Theme, "text") },
    .{ .name = "text_muted", .offset = @offsetOf(Theme, "text_muted") },
    .{ .name = "text_dim", .offset = @offsetOf(Theme, "text_dim") },
    .{ .name = "status_bg", .offset = @offsetOf(Theme, "status_bg") },
    .{ .name = "status_fg", .offset = @offsetOf(Theme, "status_fg") },
    .{ .name = "input_bg", .offset = @offsetOf(Theme, "input_bg") },
    .{ .name = "input_fg", .offset = @offsetOf(Theme, "input_fg") },
    .{ .name = "input_hint", .offset = @offsetOf(Theme, "input_hint") },
    .{ .name = "accent", .offset = @offsetOf(Theme, "accent") },
    .{ .name = "positive", .offset = @offsetOf(Theme, "positive") },
    .{ .name = "negative", .offset = @offsetOf(Theme, "negative") },
    .{ .name = "warning", .offset = @offsetOf(Theme, "warning") },
    .{ .name = "info", .offset = @offsetOf(Theme, "info") },
    .{ .name = "select_bg", .offset = @offsetOf(Theme, "select_bg") },
    .{ .name = "select_fg", .offset = @offsetOf(Theme, "select_fg") },
    .{ .name = "border", .offset = @offsetOf(Theme, "border") },
    .{ .name = "bar_fill", .offset = @offsetOf(Theme, "bar_fill") },
};

fn colorPtr(theme: *Theme, offset: usize) *Color {
    const bytes: [*]u8 = @ptrCast(theme);
    return @ptrCast(@alignCast(bytes + offset));
}

fn colorPtrConst(theme: *const Theme, offset: usize) *const Color {
    const bytes: [*]const u8 = @ptrCast(theme);
    return @ptrCast(@alignCast(bytes + offset));
}

fn formatHex(c: Color) [7]u8 {
    var buf: [7]u8 = undefined;
    _ = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ c[0], c[1], c[2] }) catch
        @panic("formatHex: 7-byte buffer cannot hold #RRGGBB — unreachable");
    return buf;
}

fn parseHex(s: []const u8) ?Color {
    const hex = if (s.len > 0 and s[0] == '#') s[1..] else s;
    if (hex.len != 6) return null;
    const r = std.fmt.parseUnsigned(u8, hex[0..2], 16) catch return null;
    const g = std.fmt.parseUnsigned(u8, hex[2..4], 16) catch return null;
    const b = std.fmt.parseUnsigned(u8, hex[4..6], 16) catch return null;
    return .{ r, g, b };
}

pub fn printDefaults(io: std.Io) !void {
    var buf: [4096]u8 = undefined;
    var writer = std.Io.File.stdout().writer(io, &buf);
    const out = &writer.interface;

    try out.writeAll("#!srfv1\n");
    try out.writeAll("# zfin TUI theme\n");
    try out.writeAll("# This file is the sole source of colors when present.\n");
    try out.writeAll("# If removed, built-in defaults (monokai/opencode) are used.\n");
    try out.writeAll("# Regenerate: zfin interactive --default-theme > ~/.config/zfin/theme.srf\n");
    try out.writeAll("#\n");
    try out.writeAll("# All values are hex RGB: #rrggbb\n");

    for (field_names) |f| {
        const c = colorPtrConst(&default_theme, f.offset);
        const hex = formatHex(c.*);
        try out.print("{s}::{s}\n", .{ f.name, hex });
    }

    try out.flush();
}

pub fn loadFromFile(io: std.Io, allocator: std.mem.Allocator, path: []const u8) ?Theme {
    const data = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(64 * 1024)) catch return null;
    defer allocator.free(data);
    return loadFromData(data);
}

pub fn loadFromData(data: []const u8) ?Theme {
    // Use a stack allocator for parsing -- we don't need to keep parsed data
    var arena_buf: [32 * 1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&arena_buf);
    const alloc = fba.allocator();

    var reader = std.Io.Reader.fixed(data);
    var it = srf.iterator(&reader, alloc, .{ .parse_allocator = .none }) catch return null;
    // Don't deinit -- fba owns everything

    var theme = default_theme;

    while (it.next() catch return null) |fields| {
        while (fields.next() catch return null) |field| {
            if (field.value) |v| {
                const str = switch (v) {
                    .string => |s| s,
                    else => continue,
                };
                const color = parseHex(str) orelse continue;
                for (field_names) |f| {
                    if (std.mem.eql(u8, field.key, f.name)) {
                        colorPtr(&theme, f.offset).* = color;
                        break;
                    }
                }
            }
        }
    }

    return theme;
}

// ── Tests ────────────────────────────────────────────────────

test "parseHex" {
    const c = parseHex("#f8f8f2").?;
    try std.testing.expectEqual(@as(u8, 0xf8), c[0]);
    try std.testing.expectEqual(@as(u8, 0xf8), c[1]);
    try std.testing.expectEqual(@as(u8, 0xf2), c[2]);
}

test "parseHex no hash" {
    const c = parseHex("272822").?;
    try std.testing.expectEqual(@as(u8, 0x27), c[0]);
}

test "formatHex roundtrip" {
    const c = Color{ 0xae, 0x81, 0xff };
    const hex = formatHex(c);
    const parsed = parseHex(&hex).?;
    try std.testing.expectEqual(c[0], parsed[0]);
    try std.testing.expectEqual(c[1], parsed[1]);
    try std.testing.expectEqual(c[2], parsed[2]);
}

test "loadFromData" {
    const data =
        \\#!srfv1
        \\bg::#ff0000
        \\text::#00ff00
    ;
    const t = loadFromData(data).?;
    try std.testing.expectEqual(@as(u8, 0xff), t.bg[0]);
    try std.testing.expectEqual(@as(u8, 0x00), t.bg[1]);
    try std.testing.expectEqual(@as(u8, 0x00), t.text[0]);
    try std.testing.expectEqual(@as(u8, 0xff), t.text[1]);
}

test "default theme has valid colors" {
    const t = default_theme;
    // Background should be dark
    try std.testing.expect(t.bg[0] < 0x20);
    // Text should be bright
    try std.testing.expect(t.text[0] > 0xc0);
}
