/// Configuration errors window.
const ConfigErrors = @This();

const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;

const App = @import("App.zig");
const View = @import("View.zig");
const c = @import("c.zig").c;

const log = std.log.scoped(.gtk);

app: *App,
window: *c.GtkWindow,
view: PrimaryView,

pub fn create(app: *App) !void {
    if (app.config_errors_window != null) return error.InvalidOperation;

    const alloc = app.core_app.alloc;
    const self = try alloc.create(ConfigErrors);
    errdefer alloc.destroy(self);
    try self.init(app);

    app.config_errors_window = self;
}

pub fn update(self: *ConfigErrors) void {
    if (self.app.config._diagnostics.empty()) {
        c.gtk_window_destroy(@ptrCast(self.window));
        return;
    }

    self.view.update(&self.app.config);
    _ = c.gtk_window_present(self.window);
    _ = c.gtk_widget_grab_focus(@ptrCast(self.window));
}

/// Not public because this should be called by the GTK lifecycle.
fn destroy(self: *ConfigErrors) void {
    const alloc = self.app.core_app.alloc;
    self.app.config_errors_window = null;
    alloc.destroy(self);
}

fn init(self: *ConfigErrors, app: *App) !void {
    // Create the window
    const window = c.gtk_window_new();
    const gtk_window: *c.GtkWindow = @ptrCast(window);
    errdefer c.gtk_window_destroy(gtk_window);
    c.gtk_window_set_title(gtk_window, "Configuration Errors");
    c.gtk_window_set_default_size(gtk_window, 600, 275);
    c.gtk_window_set_resizable(gtk_window, 0);
    c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
    c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
    c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "config-errors-window");
    _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);

    // Set some state
    self.* = .{
        .app = app,
        .window = gtk_window,
        .view = undefined,
    };

    // Show the window
    const view = try PrimaryView.init(self);
    self.view = view;
    c.gtk_window_set_child(@ptrCast(window), view.root);
    c.gtk_widget_show(window);
}

fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
    const self = userdataSelf(ud.?);
    self.destroy();
}

fn userdataSelf(ud: *anyopaque) *ConfigErrors {
    return @ptrCast(@alignCast(ud));
}

const PrimaryView = struct {
    root: *c.GtkWidget,
    text: *c.GtkTextView,

    pub fn init(root: *ConfigErrors) !PrimaryView {
        // All our widgets
        const label = c.gtk_label_new(
            "One or more configuration errors were found while loading " ++
                "the configuration. Please review the errors below and reload " ++
                "your configuration or ignore the erroneous lines.",
        );
        const buf = contentsBuffer(&root.app.config);
        defer c.g_object_unref(buf);
        const buttons = try ButtonsView.init(root);
        const text_scroll = c.gtk_scrolled_window_new();
        errdefer c.g_object_unref(text_scroll);
        const text = c.gtk_text_view_new_with_buffer(buf);
        errdefer c.g_object_unref(text);
        c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text);

        // Create our view
        const view = try View.init(&.{
            .{ .name = "label", .widget = label },
            .{ .name = "text", .widget = text_scroll },
            .{ .name = "buttons", .widget = buttons.root },
        }, &vfl);
        errdefer view.deinit();

        // We can do additional settings once the layout is setup
        c.gtk_label_set_wrap(@ptrCast(label), 1);
        c.gtk_text_view_set_editable(@ptrCast(text), 0);
        c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0);
        c.gtk_text_view_set_top_margin(@ptrCast(text), 8);
        c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8);
        c.gtk_text_view_set_left_margin(@ptrCast(text), 8);
        c.gtk_text_view_set_right_margin(@ptrCast(text), 8);

        return .{ .root = view.root, .text = @ptrCast(text) };
    }

    pub fn update(self: *PrimaryView, config: *const Config) void {
        const buf = contentsBuffer(config);
        defer c.g_object_unref(buf);
        c.gtk_text_view_set_buffer(@ptrCast(self.text), buf);
    }

    /// Returns the GtkTextBuffer for the config errors that we want to show.
    fn contentsBuffer(config: *const Config) *c.GtkTextBuffer {
        const buf = c.gtk_text_buffer_new(null);
        errdefer c.g_object_unref(buf);

        var msg_buf: [4096]u8 = undefined;
        var fbs = std.io.fixedBufferStream(&msg_buf);

        for (config._diagnostics.items()) |diag| {
            fbs.reset();
            diag.write(fbs.writer()) catch |err| {
                log.warn(
                    "error writing diagnostic to buffer err={}",
                    .{err},
                );
                continue;
            };

            const msg = fbs.getWritten();
            c.gtk_text_buffer_insert_at_cursor(buf, msg.ptr, @intCast(msg.len));
            c.gtk_text_buffer_insert_at_cursor(buf, "\n", -1);
        }

        return buf;
    }

    const vfl = [_][*:0]const u8{
        "H:|-8-[label]-8-|",
        "H:|[text]|",
        "H:|[buttons]|",
        "V:|[label(<=80)][text(>=100)]-[buttons]-|",
    };
};

const ButtonsView = struct {
    root: *c.GtkWidget,

    pub fn init(root: *ConfigErrors) !ButtonsView {
        const ignore_button = c.gtk_button_new_with_label("Ignore");
        errdefer c.g_object_unref(ignore_button);

        const reload_button = c.gtk_button_new_with_label("Reload Configuration");
        errdefer c.g_object_unref(reload_button);

        // Create our view
        const view = try View.init(&.{
            .{ .name = "ignore", .widget = ignore_button },
            .{ .name = "reload", .widget = reload_button },
        }, &vfl);

        // Signals
        _ = c.g_signal_connect_data(
            ignore_button,
            "clicked",
            c.G_CALLBACK(&gtkIgnoreClick),
            root,
            null,
            c.G_CONNECT_DEFAULT,
        );
        _ = c.g_signal_connect_data(
            reload_button,
            "clicked",
            c.G_CALLBACK(&gtkReloadClick),
            root,
            null,
            c.G_CONNECT_DEFAULT,
        );

        return .{ .root = view.root };
    }

    fn gtkIgnoreClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
        const self: *ConfigErrors = @ptrCast(@alignCast(ud));
        c.gtk_window_destroy(@ptrCast(self.window));
    }

    fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
        const self: *ConfigErrors = @ptrCast(@alignCast(ud));
        self.app.reloadConfig(.app, .{}) catch |err| {
            log.warn("error reloading config error={}", .{err});
            return;
        };
    }

    const vfl = [_][*:0]const u8{
        "H:[ignore]-8-[reload]-8-|",
    };
};
