// 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");

pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
    const R = reflect(Inner, field);

    return struct {
        _inner: Inner,
        _rc: lp.RC(u8) = .{},

        const Self = @This();

        const Result = struct {
            done: bool,
            value: ?R.ValueType,

            pub const js_as_object = true;
        };

        pub fn init(inner: Inner, executor: R.Executor) !*Self {
            const self = try executor._factory.create(Self{ ._inner = inner });

            if (@hasDecl(Inner, "acquireRef")) {
                self._inner.acquireRef();
            }
            return self;
        }

        pub fn deinit(self: *Self, page: *Page) void {
            if (@hasDecl(Inner, "releaseRef")) {
                self._inner.releaseRef(page);
            }
            page.factory.destroy(self);
        }

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

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

        pub fn next(self: *Self, executor: R.Executor) if (R.has_error_return) anyerror!Result else Result {
            const entry = (if (comptime R.has_error_return) try self._inner.next(executor) else self._inner.next(executor)) orelse {
                return .{ .done = true, .value = null };
            };

            if (comptime field == null) {
                return .{ .done = false, .value = entry };
            }

            return .{
                .done = false,
                .value = @field(entry, field.?),
            };
        }

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

            pub const Meta = struct {
                pub const prototype_chain = bridge.prototypeChain();
                pub var class_id: bridge.ClassId = undefined;
            };

            pub const next = bridge.function(Self.next, .{ .null_as_undefined = true });
            pub const symbol_iterator = bridge.iterator(Self, .{});
        };
    };
}

fn reflect(comptime Inner: type, comptime field: ?[]const u8) Reflect {
    const fn_info = @typeInfo(@TypeOf(Inner.next)).@"fn";
    const R = fn_info.return_type.?;
    const has_error_return = @typeInfo(R) == .error_union;
    // The executor type is the last parameter of inner.next (after self)
    const Executor = fn_info.params[1].type.?;
    return .{
        .has_error_return = has_error_return,
        .ValueType = ValueType(unwrapOptional(unwrapError(R)), field),
        .Executor = Executor,
    };
}

const Reflect = struct {
    has_error_return: bool,
    ValueType: type,
    Executor: type,
};

fn unwrapError(comptime T: type) type {
    if (@typeInfo(T) == .error_union) {
        return @typeInfo(T).error_union.payload;
    }
    return T;
}

fn unwrapOptional(comptime T: type) type {
    return @typeInfo(T).optional.child;
}

fn ValueType(comptime R: type, comptime field_: ?[]const u8) type {
    const field = field_ orelse return R;
    inline for (@typeInfo(R).@"struct".fields) |f| {
        if (comptime std.mem.eql(u8, f.name, field)) {
            return f.type;
        }
    }
    @compileError("Unknown EntryIterator field " ++ @typeName(R) ++ "." ++ field);
}
