const std = @import("std");
const dvui = @import("../dvui.zig");

const Event = dvui.Event;
const Options = dvui.Options;
const Rect = dvui.Rect;
const RectScale = dvui.RectScale;
const Size = dvui.Size;
const Widget = dvui.Widget;
const WidgetData = dvui.WidgetData;
const MenuWidget = dvui.MenuWidget;
const ScrollAreaWidget = dvui.ScrollAreaWidget;

const FloatingMenuWidget = @This();

pub const FloatingMenuAvoid = enum {
    none,
    horizontal,
    vertical,

    /// Pick horizontal or vertical based on the direction of the current
    /// parent menu (if any).
    auto,
};

// this lets us maintain a chain of all the nested FloatingMenuWidgets without
// forcing the user to manually do it
var current: ?*FloatingMenuWidget = null;

fn currentSet(p: ?*FloatingMenuWidget) ?*FloatingMenuWidget {
    const ret = current;
    current = p;
    return ret;
}

pub fn currentGet() ?*FloatingMenuWidget {
    return current;
}

pub var defaults: Options = .{
    .name = "FloatingMenu",
    .corners = .default,
    .border = Rect.all(1),
    .padding = Rect.all(4),
    .background = true,
    .style = .window,
};

pub const InitOptions = struct {
    from: Rect.Natural,
    avoid: FloatingMenuAvoid = .auto,
};

render_ftb: dvui.RenderFrontToBack,
wd: WidgetData,
prev_windowInfo: dvui.subwindowCurrentSetReturn = undefined,
prev_scroll: ?*dvui.ScrollContainerWidget = undefined,
prev_last_focus: dvui.Id,
parent_fmw: ?*FloatingMenuWidget = null,
have_popup_child: bool = false,
prevClip: Rect.Physical,
scale_val: f32,
menu: MenuWidget,
scaler: dvui.ScaleWidget,
scroll: ScrollAreaWidget,

/// It's expected to call this when `self` is `undefined`
pub fn init(self: *FloatingMenuWidget, src: std.builtin.SourceLocation, init_opts: InitOptions, opts: Options) void {
    self.* = .{
        // the widget itself doesn't have any styling, it comes from the
        // embedded MenuWidget
        // passing options.rect will stop WidgetData.init from calling
        // rectFor/minSizeForChild which is important because we are outside
        // normal layout
        .wd = .init(src, .{ .subwindow = true }, .{ .id_extra = opts.id_extra, .rect = .{} }),
        // get scale from parent
        .scale_val = dvui.parentGet().screenRectScale(Rect{}).s / dvui.windowNaturalScale(),

        .render_ftb = undefined,
        .prev_last_focus = undefined,
        .prevClip = undefined,
        .menu = undefined,
        .scaler = undefined,
        .scroll = undefined,
    };

    const options = defaults.override(opts);
    // NOTE: options is really for our embedded ScrollAreaWidget

    const avoid: dvui.PlaceOnScreenAvoid = switch (init_opts.avoid) {
        .none => .none,
        .horizontal => .horizontal,
        .vertical => .vertical,
        .auto => if (dvui.MenuWidget.current()) |pm| switch (pm.init_opts.dir) {
            .horizontal => .vertical,
            .vertical => .horizontal,
        } else .none,
    };

    self.data().rect = Rect.fromPoint(.cast(init_opts.from.topLeft()));
    if (dvui.minSizeGet(self.data().id)) |_| {
        const ms = dvui.minSize(self.data().id, options.min_sizeGet());
        self.data().rect = self.data().rect.toSize(ms);
        self.data().rect = .cast(dvui.placeOnScreen(dvui.windowRect(), init_opts.from, avoid, .cast(self.data().rect)));
        if (dvui.dataGet(null, self.data().id, "_check_focus", void) != null) {
            dvui.dataRemove(null, self.data().id, "_check_focus");
            if (dvui.MenuWidget.current() == null or !dvui.MenuWidget.current().?.mouse_mode or self.data().rectScale().r.contains(dvui.currentWindow().mouse_pt)) {
                dvui.focusSubwindow(self.data().id, null);
            }
        }
    } else {
        self.data().rect = .cast(dvui.placeOnScreen(dvui.windowRect(), init_opts.from, avoid, .cast(self.data().rect)));
        dvui.dataSet(null, self.data().id, "_check_focus", {});

        // need a second frame to fit contents (FocusWindow calls refresh but
        // here for clarity)
        dvui.refresh(null, @src(), self.data().id);
    }

    self.data().register();
    dvui.parentSet(self.widget());

    // standard subwindow stuff
    {
        const rs = self.data().rectScale();
        self.render_ftb.initReset();
        self.prev_windowInfo = dvui.subwindowCurrentSet(self.data().id, null);
        dvui.subwindowAdd(self.data().id, self.data().rect, rs.r, false, null, true);
        dvui.captureMouseMaintain(.{ .id = self.data().id, .rect = rs.r, .subwindow_id = self.data().id });
        self.prevClip = dvui.clipGet();
        dvui.clipSet(dvui.windowRectPixels()); // break out of whatever clipping we were in
        self.prev_scroll = dvui.ScrollContainerWidget.scrollSet(null);
    }

    // prevents parents from processing key events if focus is inside the floating window
    self.prev_last_focus = dvui.lastFocusedIdInFrame();

    self.parent_fmw = currentSet(self);

    const rs = self.data().rectScale();
    const evts = dvui.events();
    for (evts) |*e| {
        if (!dvui.eventMatch(e, .{ .id = self.data().id, .r = rs.r }))
            continue;

        if (e.evt == .mouse and e.evt.mouse.action == .focus) {
            // focus but let the focus event propagate to widgets
            dvui.focusSubwindow(self.data().id, e.num);
        }
    }

    self.scaler.init(@src(), .{ .scale = &self.scale_val }, .{ .expand = .both });

    // we are using scroll to do border/background but floating windows
    // don't have margin, so turn that off
    self.scroll.init(@src(), .{ .horizontal = .none }, options.override(.{ .margin = .{}, .expand = .both }));

    if (dvui.MenuWidget.current()) |pm| {
        pm.child_popup_rect = rs.r;
    }

    self.menu.init(@src(), .{ .dir = .vertical, .parentSubwindowId = self.prev_windowInfo.id }, options.strip().override(.{ .role = .none, .expand = .horizontal }));
}

pub fn close(self: *FloatingMenuWidget) void {
    self.menu.close();
}

pub fn widget(self: *FloatingMenuWidget) Widget {
    return Widget.init(self, data, rectFor, screenRectScale, minSizeForChild);
}

pub fn data(self: *FloatingMenuWidget) *WidgetData {
    return self.wd.validate();
}

pub fn rectFor(self: *FloatingMenuWidget, id: dvui.Id, min_size: Size, e: Options.Expand, g: Options.Gravity) Rect {
    _ = id;
    return dvui.placeIn(self.data().contentRect().justSize(), min_size, e, g);
}

pub fn screenRectScale(self: *FloatingMenuWidget, rect: Rect) RectScale {
    return self.data().contentRectScale().rectToRectScale(rect);
}

pub fn minSizeForChild(self: *FloatingMenuWidget, s: Size) void {
    self.data().minSizeMax(self.data().options.padSize(s));
}

pub fn chainFocused(self: *FloatingMenuWidget, self_call: bool) bool {
    if (!self_call) {
        // if we got called by someone else, then we have a popup child
        self.have_popup_child = true;
    }

    var ret: bool = false;

    // we have to call chainFocused on our parent if we have one so we
    // can't return early

    if (self.data().id == dvui.focusedSubwindowId()) {
        // we are focused
        ret = true;
    }

    if (self.parent_fmw) |pp| {
        // we had a parent popup, is that focused
        if (pp.chainFocused(false)) {
            ret = true;
        }
    } else if (self.prev_windowInfo.id == dvui.focusedSubwindowId()) {
        // no parent popup, is our parent window focused
        ret = true;
    }

    return ret;
}

pub fn deinit(self: *FloatingMenuWidget) void {
    defer if (dvui.widgetIsAllocated(self)) dvui.widgetFree(self);
    defer self.* = undefined;

    const evts = dvui.events();
    const rs = self.data().rectScale();
    for (evts) |*e| {
        if (!dvui.eventMatch(e, .{ .id = self.data().id, .r = rs.r, .cleanup = true }))
            continue;

        if (e.evt == .mouse) {
            if (e.evt.mouse.action == .focus) {
                // unhandled click, clear focus
                e.handle(@src(), self.data());
                dvui.focusWidget(null, null, null);
            }
        } else if (e.evt == .key) {
            // catch any tabs that weren't handled by widgets
            if (e.evt.key.action == .down and e.evt.key.matchBind("next_widget")) {
                e.handle(@src(), self.data());
                dvui.tabIndexNext(e.num);
            }

            if (e.evt.key.action == .down and e.evt.key.matchBind("prev_widget")) {
                e.handle(@src(), self.data());
                dvui.tabIndexPrev(e.num);
            }
        }
    }

    if (!self.have_popup_child and !self.chainFocused(true)) {
        // if a popup chain is open and the user focuses a different window
        // (not the parent of the popups), then we want to close the popups

        // only the last popup can do the check, you can't query the focus
        // status of children, only parents
        self.menu.close_chain(.unintentional);
        dvui.refresh(null, @src(), self.data().id);
    }

    self.menu.deinit();
    self.scroll.deinit();
    self.scaler.deinit();

    // in case no children ever show up, this will provide a visual indication
    // that there is an empty floating menu
    self.data().minSizeMax(self.data().options.padSize(.{ .w = 20, .h = 20 }));

    self.data().minSizeSetAndRefresh();

    // outside normal layout, don't call minSizeForChild or self.data().minSizeReportToParent();

    _ = currentSet(self.parent_fmw);
    dvui.parentReset(self.data().id, self.data().parent);
    dvui.currentWindow().last_focused_id_this_frame = self.prev_last_focus;

    // standard subwindow stuff
    {
        _ = dvui.ScrollContainerWidget.scrollSet(self.prev_scroll);
        _ = dvui.subwindowCurrentSet(self.prev_windowInfo.id, self.prev_windowInfo.rect);
        dvui.clipSet(self.prevClip);
        self.render_ftb.deinit();
    }
}

test {
    @import("std").testing.refAllDecls(@This());
}
