// Copyright (C) 2023-2025  Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const lp = @import("lightpanda");

const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");

const Node = @import("Node.zig");
const EventTarget = @import("EventTarget.zig");

const String = lp.String;
const Allocator = std.mem.Allocator;

pub const Event = @This();

pub const _prototype_root = true;
_type: Type,
_arena: Allocator,
_bubbles: bool = false,
_cancelable: bool = false,
_composed: bool = false,
_type_string: String,
_target: ?*EventTarget = null,
_current_target: ?*EventTarget = null,
_dispatch_target: ?*EventTarget = null, // Original target for composedPath()
_prevent_default: bool = false,
_stop_propagation: bool = false,
_stop_immediate_propagation: bool = false,
_event_phase: EventPhase = .none,
_time_stamp: u64,
_needs_retargeting: bool = false,
_is_trusted: bool = false,

// There's a period of time between creating an event and handing it off to v8
// where things can fail. If it does fail, we need to deinit the event. The timing
// window can be difficult to capture, so we use a reference count.
// should be 0, 1, or 2. 0
// - 0: no reference, always a transient state going to either 1 or about to be deinit'd
// - 1: either zig or v8 have a reference
// - 2: both zig and v8 have a reference
_rc: lp.RC(u8) = .{},

pub const EventPhase = enum(u8) {
    none = 0,
    capturing_phase = 1,
    at_target = 2,
    bubbling_phase = 3,
};

pub const Type = union(enum) {
    generic,
    error_event: *@import("event/ErrorEvent.zig"),
    custom_event: *@import("event/CustomEvent.zig"),
    message_event: *@import("event/MessageEvent.zig"),
    progress_event: *@import("event/ProgressEvent.zig"),
    navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"),
    page_transition_event: *@import("event/PageTransitionEvent.zig"),
    pop_state_event: *@import("event/PopStateEvent.zig"),
    ui_event: *@import("event/UIEvent.zig"),
    promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
    submit_event: *@import("event/SubmitEvent.zig"),
    form_data_event: *@import("event/FormDataEvent.zig"),
    close_event: *@import("event/CloseEvent.zig"),
};

pub const Options = struct {
    bubbles: bool = false,
    cancelable: bool = false,
    composed: bool = false,
};

pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
    const arena = try page.getArena(.tiny, "Event");
    errdefer page.releaseArena(arena);
    const str = try String.init(arena, typ, .{});
    return initWithTrusted(arena, str, opts_, false);
}

pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
    const arena = try page.getArena(.tiny, "Event.trusted");
    errdefer page.releaseArena(arena);
    return initWithTrusted(arena, typ, opts_, true);
}

fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, comptime trusted: bool) !*Event {
    const opts = opts_ orelse Options{};

    // Round to 2ms for privacy (browsers do this)
    const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
    const time_stamp = (raw_timestamp / 2) * 2;

    const event = try arena.create(Event);
    event.* = .{
        ._arena = arena,
        ._type = .generic,
        ._bubbles = opts.bubbles,
        ._time_stamp = time_stamp,
        ._cancelable = opts.cancelable,
        ._composed = opts.composed,
        ._type_string = typ,
        ._is_trusted = trusted,
    };
    return event;
}

pub fn initEvent(
    self: *Event,
    event_string: []const u8,
    bubbles: ?bool,
    cancelable: ?bool,
) !void {
    if (self._event_phase != .none) {
        return;
    }

    self._type_string = try String.init(self._arena, event_string, .{});
    self._bubbles = bubbles orelse false;
    self._cancelable = cancelable orelse false;
    self._stop_propagation = false;
    self._stop_immediate_propagation = false;
    self._prevent_default = false;
}

pub fn acquireRef(self: *Event) void {
    self._rc.acquire();
}

pub fn deinit(self: *Event, page: *Page) void {
    page.releaseArena(self._arena);
}

pub fn releaseRef(self: *Event, page: *Page) void {
    self._rc.release(self, page);
}

pub fn as(self: *Event, comptime T: type) *T {
    return self.is(T).?;
}

pub fn is(self: *Event, comptime T: type) ?*T {
    switch (self._type) {
        .generic => return if (T == Event) self else null,
        .error_event => |e| return if (T == @import("event/ErrorEvent.zig")) e else null,
        .custom_event => |e| return if (T == @import("event/CustomEvent.zig")) e else null,
        .message_event => |e| return if (T == @import("event/MessageEvent.zig")) e else null,
        .progress_event => |e| return if (T == @import("event/ProgressEvent.zig")) e else null,
        .navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
        .page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
        .pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
        .promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
        .submit_event => |e| return if (T == @import("event/SubmitEvent.zig")) e else null,
        .form_data_event => |e| return if (T == @import("event/FormDataEvent.zig")) e else null,
        .close_event => |e| return if (T == @import("event/CloseEvent.zig")) e else null,
        .ui_event => |e| {
            if (T == @import("event/UIEvent.zig")) {
                return e;
            }
            return e.is(T);
        },
    }
    return null;
}

pub fn getType(self: *const Event) []const u8 {
    return self._type_string.str();
}

pub fn getBubbles(self: *const Event) bool {
    return self._bubbles;
}

pub fn getCancelable(self: *const Event) bool {
    return self._cancelable;
}

pub fn getComposed(self: *const Event) bool {
    return self._composed;
}

pub fn getTarget(self: *const Event) ?*EventTarget {
    return self._target;
}

pub fn getCurrentTarget(self: *const Event) ?*EventTarget {
    return self._current_target;
}

pub fn preventDefault(self: *Event) void {
    if (self._cancelable) {
        self._prevent_default = true;
    }
}

pub fn stopPropagation(self: *Event) void {
    self._stop_propagation = true;
}

pub fn stopImmediatePropagation(self: *Event) void {
    self._stop_immediate_propagation = true;
    self._stop_propagation = true;
}

pub fn getDefaultPrevented(self: *const Event) bool {
    return self._prevent_default;
}

pub fn getReturnValue(self: *const Event) bool {
    return !self._prevent_default;
}

pub fn setReturnValue(self: *Event, v: bool) void {
    if (!v) {
        // Setting returnValue=false is equivalent to preventDefault()
        if (self._cancelable) {
            self._prevent_default = true;
        }
    }
}

pub fn getCancelBubble(self: *const Event) bool {
    return self._stop_propagation;
}

pub fn setCancelBubble(self: *Event) void {
    self.stopPropagation();
}

pub fn getEventPhase(self: *const Event) u8 {
    return @intFromEnum(self._event_phase);
}

pub fn getTimeStamp(self: *const Event) u64 {
    return self._time_stamp;
}

pub fn setTrusted(self: *Event) void {
    self._is_trusted = true;
}

pub fn setUntrusted(self: *Event) void {
    self._is_trusted = false;
}

pub fn getIsTrusted(self: *const Event) bool {
    return self._is_trusted;
}

pub fn composedPath(self: *Event, frame: *Frame) ![]const *EventTarget {
    // Return empty array if event is not being dispatched
    if (self._event_phase == .none) {
        return &.{};
    }

    // Use dispatch_target (original target) if available, otherwise fall back to target
    // This is important because _target gets retargeted during event dispatch
    const target = self._dispatch_target orelse self._target orelse return &.{};

    // Only nodes have a propagation path
    const target_node = switch (target._type) {
        .node => |n| n,
        else => return &.{},
    };

    // Build the path by walking up from target
    var path_len: usize = 0;
    var path_buffer: [128]*EventTarget = undefined;
    var stopped_at_shadow_boundary = false;

    // Track closed shadow boundaries (position in path and host position)
    var closed_shadow_boundary: ?struct { shadow_end: usize, host_start: usize } = null;

    var node: ?*Node = target_node;
    while (node) |n| {
        if (path_len >= path_buffer.len) {
            break;
        }
        path_buffer[path_len] = n.asEventTarget();
        path_len += 1;

        // Check if this node is a shadow root
        if (n._type == .document_fragment) {
            if (n._type.document_fragment._type == .shadow_root) {
                const shadow = n._type.document_fragment._type.shadow_root;

                // If event is not composed, stop at shadow boundary
                if (!self._composed) {
                    stopped_at_shadow_boundary = true;
                    break;
                }

                // Track the first closed shadow boundary we encounter
                if (shadow._mode == .closed and closed_shadow_boundary == null) {
                    // Mark where the shadow root is in the path
                    // The next element will be the host
                    closed_shadow_boundary = .{
                        .shadow_end = path_len - 1, // index of shadow root
                        .host_start = path_len, // index where host will be
                    };
                }

                // Jump to the shadow host and continue
                node = shadow._host.asNode();
                continue;
            }
        }

        node = n._parent;
    }

    // Add window at the end (unless we stopped at shadow boundary)
    if (!stopped_at_shadow_boundary) {
        if (path_len < path_buffer.len) {
            path_buffer[path_len] = frame.window.asEventTarget();
            path_len += 1;
        }
    }

    // Determine visible path based on current_target and closed shadow boundaries
    var visible_start_index: usize = 0;

    if (closed_shadow_boundary) |boundary| {
        // Check if current_target is outside the closed shadow
        // If current_target is null or is at/after the host position, hide shadow internals
        const current_target = self._current_target;

        if (current_target) |ct| {
            // Find current_target in the path
            var ct_index: ?usize = null;
            for (path_buffer[0..path_len], 0..) |elem, i| {
                if (elem == ct) {
                    ct_index = i;
                    break;
                }
            }

            // If current_target is at or after the host (outside the closed shadow),
            // hide everything from target up to the host
            if (ct_index) |idx| {
                if (idx >= boundary.host_start) {
                    visible_start_index = boundary.host_start;
                }
            }
        }
    }

    // Calculate the visible portion of the path
    const visible_path_len = if (path_len > visible_start_index) path_len - visible_start_index else 0;

    // Allocate and return the visible path using call_arena (short-lived)
    const path = try frame.call_arena.alloc(*EventTarget, visible_path_len);
    @memcpy(path, path_buffer[visible_start_index..path_len]);
    return path;
}

pub fn populateFromOptions(self: *Event, opts: anytype) void {
    self._bubbles = opts.bubbles;
    self._cancelable = opts.cancelable;
    self._composed = opts.composed;
}

pub fn inheritOptions(comptime T: type, comptime additions: anytype) type {
    var all_fields: []const std.builtin.Type.StructField = &.{};

    if (@hasField(T, "_proto")) {
        const t_fields = @typeInfo(T).@"struct".fields;

        inline for (t_fields) |field| {
            if (std.mem.eql(u8, field.name, "_proto")) {
                const ProtoType = @typeInfo(field.type).pointer.child;
                if (@hasDecl(ProtoType, "Options")) {
                    const parent_options = @typeInfo(ProtoType.Options);
                    all_fields = all_fields ++ parent_options.@"struct".fields;
                }
            }
        }
    }

    const additions_info = @typeInfo(additions);
    all_fields = all_fields ++ additions_info.@"struct".fields;

    return @Type(.{
        .@"struct" = .{
            .layout = .auto,
            .fields = all_fields,
            .decls = &.{},
            .is_tuple = false,
        },
    });
}

pub fn populatePrototypes(self: anytype, opts: anytype, trusted: bool) void {
    const T = @TypeOf(self.*);

    if (@hasField(T, "_proto")) {
        populatePrototypes(self._proto, opts, trusted);
    }

    if (@hasDecl(T, "populateFromOptions")) {
        T.populateFromOptions(self, opts);
    }

    // Set isTrusted at the Event level (base of prototype chain)
    if (T == Event or @hasField(T, "is_trusted")) {
        self._is_trusted = trusted;
    }
}

pub const JsApi = struct {
    pub const bridge = js.Bridge(Event);

    pub const Meta = struct {
        pub const name = "Event";

        pub const prototype_chain = bridge.prototypeChain();
        pub var class_id: bridge.ClassId = undefined;
        pub const enumerable = false;
    };

    pub const constructor = bridge.constructor(Event.init, .{});
    pub const @"type" = bridge.accessor(Event.getType, null, .{});
    pub const bubbles = bridge.accessor(Event.getBubbles, null, .{});
    pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});
    pub const composed = bridge.accessor(Event.getComposed, null, .{});
    pub const target = bridge.accessor(Event.getTarget, null, .{});
    pub const srcElement = bridge.accessor(Event.getTarget, null, .{});
    pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
    pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
    pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});
    pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{});
    pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{});
    pub const preventDefault = bridge.function(Event.preventDefault, .{});
    pub const stopPropagation = bridge.function(Event.stopPropagation, .{});
    pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
    pub const composedPath = bridge.function(Event.composedPath, .{});
    pub const initEvent = bridge.function(Event.initEvent, .{});
    // deprecated
    pub const returnValue = bridge.accessor(Event.getReturnValue, Event.setReturnValue, .{});
    // deprecated
    pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});

    // Event phase constants
    pub const NONE = bridge.property(@intFromEnum(EventPhase.none), .{ .template = true });
    pub const CAPTURING_PHASE = bridge.property(@intFromEnum(EventPhase.capturing_phase), .{ .template = true });
    pub const AT_TARGET = bridge.property(@intFromEnum(EventPhase.at_target), .{ .template = true });
    pub const BUBBLING_PHASE = bridge.property(@intFromEnum(EventPhase.bubbling_phase), .{ .template = true });
};

// tested in event_target
