const std = @import("std");
const Api = @import("../../api/schema.zig").Api;
const FilesystemRouter = @import("../../router.zig");
const http = @import("../../http.zig");
const JavaScript = @import("../javascript.zig");
const QueryStringMap = @import("../../url.zig").QueryStringMap;
const CombinedScanner = @import("../../url.zig").CombinedScanner;
const bun = @import("../../global.zig");
const string = bun.string;
const JSC = @import("../../jsc.zig");
const js = JSC.C;
const WebCore = @import("../webcore/response.zig");
const Router = @This();
const Bundler = @import("../../bundler.zig");
const VirtualMachine = JavaScript.VirtualMachine;
const ScriptSrcStream = std.io.FixedBufferStream([]u8);
const ZigString = JSC.ZigString;
const Fs = @import("../../fs.zig");
const Base = @import("../base.zig");
const getAllocator = Base.getAllocator;
const JSObject = JSC.JSObject;
const JSError = Base.JSError;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const strings = @import("strings");
const NewClass = Base.NewClass;
const To = Base.To;
const Request = WebCore.Request;
const d = Base.d;
const FetchEvent = WebCore.FetchEvent;
const Response = WebCore.Response;
const LOLHTML = @import("lolhtml");

const SelectorMap = std.ArrayListUnmanaged(*LOLHTML.HTMLSelector);
pub const LOLHTMLContext = struct {
    selectors: SelectorMap = .{},
    element_handlers: std.ArrayListUnmanaged(*ElementHandler) = .{},
    document_handlers: std.ArrayListUnmanaged(*DocumentHandler) = .{},

    pub fn deinit(this: *LOLHTMLContext, allocator: std.mem.Allocator) void {
        for (this.selectors.items) |selector| {
            selector.deinit();
        }
        this.selectors.deinit(allocator);
        this.selectors = .{};

        for (this.element_handlers.items) |handler| {
            handler.deinit();
        }
        this.element_handlers.deinit(allocator);
        this.element_handlers = .{};

        for (this.document_handlers.items) |handler| {
            handler.deinit();
        }
        this.document_handlers.deinit(allocator);
        this.document_handlers = .{};
    }
};
pub const HTMLRewriter = struct {
    builder: *LOLHTML.HTMLRewriter.Builder,
    context: LOLHTMLContext,

    pub const Constructor = JSC.NewConstructor(HTMLRewriter, .{ .constructor = constructor }, .{});

    pub const Class = NewClass(
        HTMLRewriter,
        .{ .name = "HTMLRewriter" },
        .{
            .finalize = finalize,
            .on = .{
                .rfn = wrap(HTMLRewriter, "on"),
            },
            .onDocument = .{
                .rfn = wrap(HTMLRewriter, "onDocument"),
            },
            .transform = .{
                .rfn = wrap(HTMLRewriter, "transform"),
            },
        },
        .{},
    );

    pub fn constructor(
        ctx: js.JSContextRef,
        _: js.JSObjectRef,
        _: []const js.JSValueRef,
        _: js.ExceptionRef,
    ) js.JSObjectRef {
        var rewriter = bun.default_allocator.create(HTMLRewriter) catch unreachable;
        rewriter.* = HTMLRewriter{
            .builder = LOLHTML.HTMLRewriter.Builder.init(),
            .context = .{},
        };
        return HTMLRewriter.Class.make(ctx, rewriter);
    }

    pub fn on(
        this: *HTMLRewriter,
        global: *JSGlobalObject,
        selector_name: ZigString,
        thisObject: JSC.C.JSObjectRef,
        listener: JSValue,
        exception: JSC.C.ExceptionRef,
    ) JSValue {
        var selector_slice = std.fmt.allocPrint(bun.default_allocator, "{}", .{selector_name}) catch unreachable;

        var selector = LOLHTML.HTMLSelector.parse(selector_slice) catch
            return throwLOLHTMLError(global);
        var handler_ = ElementHandler.init(global, listener, exception);
        if (exception.* != null) {
            selector.deinit();
            return JSValue.fromRef(exception.*);
        }
        var handler = getAllocator(global).create(ElementHandler) catch unreachable;
        handler.* = handler_;

        this.builder.addElementContentHandlers(
            selector,

            ElementHandler,
            ElementHandler.onElement,
            if (handler.onElementCallback != null)
                handler
            else
                null,

            ElementHandler,
            ElementHandler.onComment,
            if (handler.onCommentCallback != null)
                handler
            else
                null,

            ElementHandler,
            ElementHandler.onText,
            if (handler.onTextCallback != null)
                handler
            else
                null,
        ) catch {
            selector.deinit();
            return throwLOLHTMLError(global);
        };

        this.context.selectors.append(bun.default_allocator, selector) catch unreachable;
        this.context.element_handlers.append(bun.default_allocator, handler) catch unreachable;
        return JSValue.fromRef(thisObject);
    }

    pub fn onDocument(
        this: *HTMLRewriter,
        global: *JSGlobalObject,
        listener: JSValue,
        thisObject: JSC.C.JSObjectRef,
        exception: JSC.C.ExceptionRef,
    ) JSValue {
        var handler_ = DocumentHandler.init(global, listener, exception);
        if (exception.* != null) {
            return JSValue.fromRef(exception.*);
        }

        var handler = getAllocator(global).create(DocumentHandler) catch unreachable;
        handler.* = handler_;

        this.builder.addDocumentContentHandlers(
            DocumentHandler,
            DocumentHandler.onDocType,
            if (handler.onDocTypeCallback != null)
                handler
            else
                null,

            DocumentHandler,
            DocumentHandler.onComment,
            if (handler.onCommentCallback != null)
                handler
            else
                null,

            DocumentHandler,
            DocumentHandler.onText,
            if (handler.onTextCallback != null)
                handler
            else
                null,

            DocumentHandler,
            DocumentHandler.onEnd,
            if (handler.onEndCallback != null)
                handler
            else
                null,
        ) catch {
            return throwLOLHTMLError(global);
        };

        this.context.document_handlers.append(bun.default_allocator, handler) catch unreachable;
        return JSValue.fromRef(thisObject);
    }

    pub fn finalize(this: *HTMLRewriter) void {
        this.finalizeWithoutDestroy();
        bun.default_allocator.destroy(this);
    }

    pub fn finalizeWithoutDestroy(this: *HTMLRewriter) void {
        this.context.deinit(bun.default_allocator);
    }

    pub fn beginTransform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue {
        const new_context = this.context;
        this.context = .{};
        return BufferOutputSink.init(new_context, global, response, this.builder);
    }

    pub fn returnEmptyResponse(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue {
        var result = bun.default_allocator.create(Response) catch unreachable;

        response.cloneInto(result, getAllocator(global), global);
        this.finalizeWithoutDestroy();
        return result.toJS(global);
    }

    pub fn transform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue {
        if (response.body.len() == 0 and !(response.body.value == .Blob and response.body.value.Blob.needsToReadFile())) {
            return this.returnEmptyResponse(global, response);
        }

        return this.beginTransform(global, response);
    }

    pub const HTMLRewriterLoader = struct {
        rewriter: *LOLHTML.HTMLRewriter,
        finalized: bool = false,
        context: LOLHTMLContext,
        chunk_size: usize = 0,
        failed: bool = false,
        output: JSC.WebCore.Sink,
        signal: JSC.WebCore.Signal = .{},
        backpressure: std.fifo.LinearFifo(u8, .Dynamic) = std.fifo.LinearFifo(u8, .Dynamic).init(bun.default_allocator),

        pub fn finalize(this: *HTMLRewriterLoader) void {
            if (this.finalized) return;
            this.rewriter.deinit();
            this.backpressure.deinit();
            this.backpressure = std.fifo.LinearFifo(u8, .Dynamic).init(bun.default_allocator);
            this.finalized = true;
        }

        pub fn fail(this: *HTMLRewriterLoader, err: JSC.Node.Syscall.Error) void {
            this.signal.close(err);
            this.output.end(err);
            this.failed = true;
            this.finalize();
        }

        pub fn connect(this: *HTMLRewriterLoader, signal: JSC.WebCore.Signal) void {
            this.signal = signal;
        }

        pub fn writeToDestination(this: *HTMLRewriterLoader, bytes: []const u8) void {
            if (this.backpressure.count > 0) {
                this.backpressure.write(bytes) catch {
                    this.fail(JSC.Node.Syscall.Error.oom);
                    this.finalize();
                };
                return;
            }

            const write_result = this.output.write(.{ .temporary = bun.ByteList.init(bytes) });

            switch (write_result) {
                .err => |err| {
                    this.fail(err);
                },
                .owned_and_done, .temporary_and_done, .into_array_and_done => {
                    this.done();
                },
                .pending => |pending| {
                    pending.applyBackpressure(bun.default_allocator, &this.output, pending, bytes);
                },
                .into_array, .owned, .temporary => {
                    this.signal.ready(if (this.chunk_size > 0) this.chunk_size else null, null);
                },
            }
        }

        pub fn done(
            this: *HTMLRewriterLoader,
        ) void {
            this.output.end(null);
            this.signal.close(null);
            this.finalize();
        }

        pub fn setup(
            this: *HTMLRewriterLoader,
            builder: *LOLHTML.HTMLRewriter.Builder,
            context: LOLHTMLContext,
            size_hint: ?usize,
            output: JSC.WebCore.Sink,
        ) ?[]const u8 {
            for (context.document_handlers.items) |doc| {
                doc.ctx = this;
            }
            for (context.element_handlers.items) |doc| {
                doc.ctx = this;
            }

            const chunk_size = @maximum(size_hint orelse 16384, 1024);
            this.rewriter = builder.build(
                .UTF8,
                .{
                    .preallocated_parsing_buffer_size = chunk_size,
                    .max_allowed_memory_usage = std.math.maxInt(u32),
                },
                false,
                HTMLRewriterLoader,
                this,
                HTMLRewriterLoader.writeToDestination,
                HTMLRewriterLoader.done,
            ) catch {
                output.end();
                return LOLHTML.HTMLString.lastError().slice();
            };

            this.chunk_size = chunk_size;
            this.context = context;
            this.output = output;

            return null;
        }

        pub fn sink(this: *HTMLRewriterLoader) JSC.WebCore.Sink {
            return JSC.WebCore.Sink.init(this);
        }

        fn writeBytes(this: *HTMLRewriterLoader, bytes: bun.ByteList, comptime deinit_: bool) ?JSC.Node.Syscall.Error {
            this.rewriter.write(bytes.slice()) catch {
                return JSC.Node.Syscall.Error{
                    .errno = 1,
                    // TODO: make this a union
                    .path = bun.default_allocator.dupe(u8, LOLHTML.HTMLString.lastError().slice()) catch unreachable,
                };
            };
            if (comptime deinit_) bytes.listManaged(bun.default_allocator).deinit();
            return null;
        }

        pub fn write(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable {
            switch (data) {
                .owned => |bytes| {
                    if (this.writeBytes(bytes, true)) |err| {
                        return .{ .err = err };
                    }
                    return .{ .owned = bytes.len };
                },
                .owned_and_done => |bytes| {
                    if (this.writeBytes(bytes, true)) |err| {
                        return .{ .err = err };
                    }
                    return .{ .owned_and_done = bytes.len };
                },
                .temporary_and_done => |bytes| {
                    if (this.writeBytes(bytes, false)) |err| {
                        return .{ .err = err };
                    }
                    return .{ .temporary_and_done = bytes.len };
                },
                .temporary => |bytes| {
                    if (this.writeBytes(bytes, false)) |err| {
                        return .{ .err = err };
                    }
                    return .{ .temporary = bytes.len };
                },
                else => unreachable,
            }
        }

        pub fn writeUTF16(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable {
            return JSC.WebCore.Sink.UTF8Fallback.writeUTF16(HTMLRewriterLoader, this, data, write);
        }

        pub fn writeLatin1(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable {
            return JSC.WebCore.Sink.UTF8Fallback.writeLatin1(HTMLRewriterLoader, this, data, write);
        }
    };

    pub const BufferOutputSink = struct {
        global: *JSGlobalObject,
        bytes: bun.MutableString,
        rewriter: *LOLHTML.HTMLRewriter,
        context: LOLHTMLContext,
        response: *Response,
        input: JSC.WebCore.AnyBlob = undefined,
        pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue {
            var result = bun.default_allocator.create(Response) catch unreachable;
            var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable;
            sink.* = BufferOutputSink{
                .global = global,
                .bytes = bun.MutableString.initEmpty(bun.default_allocator),
                .rewriter = undefined,
                .context = context,
                .response = result,
            };

            for (sink.context.document_handlers.items) |doc| {
                doc.ctx = sink;
            }
            for (sink.context.element_handlers.items) |doc| {
                doc.ctx = sink;
            }
            const input_size = original.body.len();
            sink.rewriter = builder.build(
                .UTF8,
                .{
                    .preallocated_parsing_buffer_size = if (input_size == JSC.WebCore.Blob.max_size)
                        1024
                    else
                        @maximum(input_size, 1024),
                    .max_allowed_memory_usage = std.math.maxInt(u32),
                },
                false,
                BufferOutputSink,
                sink,
                BufferOutputSink.write,
                BufferOutputSink.done,
            ) catch {
                sink.deinit();
                bun.default_allocator.destroy(result);

                return throwLOLHTMLError(global);
            };

            result.* = Response{
                .allocator = bun.default_allocator,
                .body = .{
                    .init = .{
                        .status_code = 200,
                    },
                    .value = .{
                        .Locked = .{
                            .global = global,
                            .task = sink,
                        },
                    },
                },
            };

            result.body.init.headers = original.body.init.headers;
            result.body.init.method = original.body.init.method;
            result.body.init.status_code = original.body.init.status_code;

            result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable;
            result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable;

            var input = original.body.value.useAsAnyBlob();
            sink.input = input;

            const is_pending = input.needsToReadFile();
            defer if (!is_pending) input.detach();

            if (is_pending) {
                sink.input.Blob.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoading, global);
            } else if (sink.runOutputSink(input.slice(), false, false)) |error_value| {
                return error_value;
            }

            // Hold off on cloning until we're actually done.
            return sink.response.toJS(sink.global);
        }

        pub fn onFinishedLoading(sink: *BufferOutputSink, bytes: JSC.WebCore.Blob.Store.ReadFile.ResultType) void {
            switch (bytes) {
                .err => |err| {
                    if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink) and
                        sink.response.body.value.Locked.promise == null)
                    {
                        sink.response.body.value = .{ .Empty = .{} };
                        // is there a pending promise?
                        // we will need to reject it
                    } else if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink) and
                        sink.response.body.value.Locked.promise != null)
                    {
                        sink.response.body.value.Locked.onReceiveValue = null;
                        sink.response.body.value.Locked.task = null;
                    }

                    sink.response.body.value.toErrorInstance(err.toErrorInstance(sink.global), sink.global);
                    sink.rewriter.end() catch {};
                    sink.deinit();
                    return;
                },
                .result => |data| {
                    _ = sink.runOutputSink(data.buf, true, data.is_temporary);
                },
            }
        }

        pub fn runOutputSink(
            sink: *BufferOutputSink,
            bytes: []const u8,
            is_async: bool,
            free_bytes_on_end: bool,
        ) ?JSValue {
            defer if (free_bytes_on_end)
                bun.default_allocator.free(bun.constStrToU8(bytes));

            sink.bytes.growBy(bytes.len) catch unreachable;
            var global = sink.global;
            var response = sink.response;

            sink.rewriter.write(bytes) catch {
                sink.deinit();
                bun.default_allocator.destroy(sink);

                if (is_async) {
                    response.body.value.toErrorInstance(throwLOLHTMLError(global), global);

                    return null;
                } else {
                    return throwLOLHTMLError(global);
                }
            };

            sink.rewriter.end() catch {
                if (!is_async) response.finalize();
                sink.response = undefined;
                sink.deinit();

                if (is_async) {
                    response.body.value.toErrorInstance(throwLOLHTMLError(global), global);
                    return null;
                } else {
                    return throwLOLHTMLError(global);
                }
            };

            return null;
        }

        pub const Sync = enum { suspended, pending, done };

        pub fn done(this: *BufferOutputSink) void {
            var prev_value = this.response.body.value;
            var bytes = this.bytes.toOwnedSliceLeaky();
            this.response.body.value = JSC.WebCore.Body.Value.createBlobValue(
                bytes,
                bun.default_allocator,

                true,
            );
            prev_value.resolve(
                &this.response.body.value,
                this.global,
            );
        }

        pub fn write(this: *BufferOutputSink, bytes: []const u8) void {
            this.bytes.append(bytes) catch unreachable;
        }

        pub fn deinit(this: *BufferOutputSink) void {
            this.bytes.deinit();

            this.context.deinit(bun.default_allocator);
        }
    };

    // pub const StreamOutputSink = struct {
    //     global: *JSGlobalObject,
    //     rewriter: *LOLHTML.HTMLRewriter,
    //     context: LOLHTMLContext,
    //     response: *Response,
    //     input: JSC.WebCore.Blob = undefined,
    //     pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue {
    //         var result = bun.default_allocator.create(Response) catch unreachable;
    //         var sink = bun.default_allocator.create(StreamOutputSink) catch unreachable;
    //         sink.* = StreamOutputSink{
    //             .global = global,
    //             .rewriter = undefined,
    //             .context = context,
    //             .response = result,
    //         };

    //         for (sink.context.document_handlers.items) |doc| {
    //             doc.ctx = sink;
    //         }
    //         for (sink.context.element_handlers.items) |doc| {
    //             doc.ctx = sink;
    //         }

    //         sink.rewriter = builder.build(
    //             .UTF8,
    //             .{
    //                 .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024),
    //                 .max_allowed_memory_usage = std.math.maxInt(u32),
    //             },
    //             false,
    //             StreamOutputSink,
    //             sink,
    //             StreamOutputSink.write,
    //             StreamOutputSink.done,
    //         ) catch {
    //             sink.deinit();
    //             bun.default_allocator.destroy(result);

    //             return throwLOLHTMLError(global);
    //         };

    //         result.* = Response{
    //             .allocator = bun.default_allocator,
    //             .body = .{
    //                 .init = .{
    //                     .status_code = 200,
    //                 },
    //                 .value = .{
    //                     .Locked = .{
    //                         .global = global,
    //                         .task = sink,
    //                     },
    //                 },
    //             },
    //         };

    //         result.body.init.headers = original.body.init.headers;
    //         result.body.init.method = original.body.init.method;
    //         result.body.init.status_code = original.body.init.status_code;

    //         result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable;
    //         result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable;

    //         var input: JSC.WebCore.Blob = original.body.value.use();

    //         const is_pending = input.needsToReadFile();
    //         defer if (!is_pending) input.detach();

    //         if (is_pending) {
    //             input.doReadFileInternal(*StreamOutputSink, sink, onFinishedLoading, global);
    //         } else if (sink.runOutputSink(input.sharedView(), false, false)) |error_value| {
    //             return error_value;
    //         }

    //         // Hold off on cloning until we're actually done.

    //         return JSC.JSValue.fromRef(
    //             Response.makeMaybePooled(sink.global, sink.response),
    //         );
    //     }

    //     pub fn runOutputSink(
    //         sink: *StreamOutputSink,
    //         bytes: []const u8,
    //         is_async: bool,
    //         free_bytes_on_end: bool,
    //     ) ?JSValue {
    //         defer if (free_bytes_on_end)
    //             bun.default_allocator.free(bun.constStrToU8(bytes));

    //         return null;
    //     }

    //     pub const Sync = enum { suspended, pending, done };

    //     pub fn done(this: *StreamOutputSink) void {
    //         var prev_value = this.response.body.value;
    //         var bytes = this.bytes.toOwnedSliceLeaky();
    //         this.response.body.value = .{
    //             .Blob = JSC.WebCore.Blob.init(bytes, this.bytes.allocator, this.global),
    //         };
    //         prev_value.resolve(
    //             &this.response.body.value,
    //             this.global,
    //         );
    //     }

    //     pub fn write(this: *StreamOutputSink, bytes: []const u8) void {
    //         this.bytes.append(bytes) catch unreachable;
    //     }

    //     pub fn deinit(this: *StreamOutputSink) void {
    //         this.bytes.deinit();

    //         this.context.deinit(bun.default_allocator);
    //     }
    // };
};

const DocumentHandler = struct {
    onDocTypeCallback: ?JSValue = null,
    onCommentCallback: ?JSValue = null,
    onTextCallback: ?JSValue = null,
    onEndCallback: ?JSValue = null,
    thisObject: JSValue,
    global: *JSGlobalObject,
    ctx: ?*HTMLRewriter.BufferOutputSink = null,

    pub const onDocType = HandlerCallback(
        DocumentHandler,
        DocType,
        LOLHTML.DocType,
        "doctype",
        "onDocTypeCallback",
    );
    pub const onComment = HandlerCallback(
        DocumentHandler,
        Comment,
        LOLHTML.Comment,
        "comment",
        "onCommentCallback",
    );
    pub const onText = HandlerCallback(
        DocumentHandler,
        TextChunk,
        LOLHTML.TextChunk,
        "text_chunk",
        "onTextCallback",
    );
    pub const onEnd = HandlerCallback(
        DocumentHandler,
        DocEnd,
        LOLHTML.DocEnd,
        "doc_end",
        "onEndCallback",
    );

    pub fn init(global: *JSGlobalObject, thisObject: JSValue, exception: JSC.C.ExceptionRef) DocumentHandler {
        var handler = DocumentHandler{
            .thisObject = thisObject,
            .global = global,
        };

        switch (thisObject.jsType()) {
            .Object, .ProxyObject, .Cell, .FinalObject => {},
            else => |kind| {
                JSC.throwInvalidArguments(
                    "Expected object but received {s}",
                    .{@as(string, @tagName(kind))},
                    global,
                    exception,
                );
                return undefined;
            },
        }

        if (thisObject.get(global, "doctype")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("doctype must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onDocTypeCallback = val;
        }

        if (thisObject.get(global, "comments")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("comments must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onCommentCallback = val;
        }

        if (thisObject.get(global, "text")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("text must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onTextCallback = val;
        }

        if (thisObject.get(global, "end")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("end must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onEndCallback = val;
        }

        JSC.C.JSValueProtect(global, thisObject.asObjectRef());
        return handler;
    }

    pub fn deinit(this: *DocumentHandler) void {
        if (this.onDocTypeCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onDocTypeCallback = null;
        }

        if (this.onCommentCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onCommentCallback = null;
        }

        if (this.onTextCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onTextCallback = null;
        }

        if (this.onEndCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onEndCallback = null;
        }

        JSC.C.JSValueUnprotect(this.global, this.thisObject.asObjectRef());
    }
};

fn HandlerCallback(
    comptime HandlerType: type,
    comptime ZigType: type,
    comptime LOLHTMLType: type,
    comptime field_name: string,
    comptime callback_name: string,
) (fn (*HandlerType, *LOLHTMLType) bool) {
    return struct {
        pub fn callback(this: *HandlerType, value: *LOLHTMLType) bool {
            if (comptime JSC.is_bindgen)
                unreachable;
            var zig_element = bun.default_allocator.create(ZigType) catch unreachable;
            @field(zig_element, field_name) = value;
            // At the end of this scope, the value is no longer valid
            var args = [1]JSC.C.JSObjectRef{
                ZigType.Class.make(this.global, zig_element),
            };
            var result = JSC.C.JSObjectCallAsFunctionReturnValue(
                this.global,
                @field(this, callback_name).?.asObjectRef(),
                if (comptime @hasField(HandlerType, "thisObject"))
                    @field(this, "thisObject").asObjectRef()
                else
                    null,
                1,
                &args,
            );
            var promise_: ?*JSC.JSInternalPromise = null;
            while (!result.isUndefinedOrNull()) {
                if (result.isError() or result.isAggregateError(this.global)) {
                    @field(zig_element, field_name) = null;
                    return true;
                }

                var promise = promise_ orelse JSC.JSInternalPromise.resolvedPromise(this.global, result);
                promise_ = promise;
                JavaScript.VirtualMachine.vm.event_loop.waitForPromise(promise);

                switch (promise.status(this.global.vm())) {
                    JSC.JSPromise.Status.Pending => unreachable,
                    JSC.JSPromise.Status.Rejected => {
                        JavaScript.VirtualMachine.vm.runErrorHandler(promise.result(this.global.vm()), null);
                        @field(zig_element, field_name) = null;
                        return false;
                    },
                    JSC.JSPromise.Status.Fulfilled => {
                        result = promise.result(this.global.vm());
                        break;
                    },
                }

                break;
            }
            @field(zig_element, field_name) = null;
            return false;
        }
    }.callback;
}

const ElementHandler = struct {
    onElementCallback: ?JSValue = null,
    onCommentCallback: ?JSValue = null,
    onTextCallback: ?JSValue = null,
    thisObject: JSValue,
    global: *JSGlobalObject,
    ctx: ?*HTMLRewriter.BufferOutputSink = null,

    pub fn init(global: *JSGlobalObject, thisObject: JSValue, exception: JSC.C.ExceptionRef) ElementHandler {
        var handler = ElementHandler{
            .thisObject = thisObject,
            .global = global,
        };

        switch (thisObject.jsType()) {
            .Object, .ProxyObject, .Cell, .FinalObject => {},
            else => |kind| {
                JSC.throwInvalidArguments(
                    "Expected object but received {s}",
                    .{@as(string, @tagName(kind))},
                    global,
                    exception,
                );
                return undefined;
            },
        }

        if (thisObject.get(global, "element")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("element must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onElementCallback = val;
        }

        if (thisObject.get(global, "comments")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("comments must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onCommentCallback = val;
        }

        if (thisObject.get(global, "text")) |val| {
            if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) {
                JSC.throwInvalidArguments("text must be a function", .{}, global, exception);
                return undefined;
            }
            JSC.C.JSValueProtect(global, val.asObjectRef());
            handler.onTextCallback = val;
        }

        JSC.C.JSValueProtect(global, thisObject.asObjectRef());
        return handler;
    }

    pub fn deinit(this: *ElementHandler) void {
        if (this.onElementCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onElementCallback = null;
        }

        if (this.onCommentCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onCommentCallback = null;
        }

        if (this.onTextCallback) |cb| {
            JSC.C.JSValueUnprotect(this.global, cb.asObjectRef());
            this.onTextCallback = null;
        }

        JSC.C.JSValueUnprotect(this.global, this.thisObject.asObjectRef());
    }

    pub fn onElement(this: *ElementHandler, value: *LOLHTML.Element) bool {
        return HandlerCallback(
            ElementHandler,
            Element,
            LOLHTML.Element,
            "element",
            "onElementCallback",
        )(this, value);
    }

    pub const onComment = HandlerCallback(
        ElementHandler,
        Comment,
        LOLHTML.Comment,
        "comment",
        "onCommentCallback",
    );

    pub const onText = HandlerCallback(
        ElementHandler,
        TextChunk,
        LOLHTML.TextChunk,
        "text_chunk",
        "onTextCallback",
    );
};

pub const ContentOptions = struct {
    html: bool = false,
};

const getterWrap = JSC.getterWrap;
const setterWrap = JSC.setterWrap;
const wrap = JSC.wrapAsync;

pub fn free_html_writer_string(_: ?*anyopaque, ptr: ?*anyopaque, len: usize) callconv(.C) void {
    var str = LOLHTML.HTMLString{ .ptr = bun.cast([*]const u8, ptr.?), .len = len };
    str.deinit();
}

fn throwLOLHTMLError(global: *JSGlobalObject) JSValue {
    var err = LOLHTML.HTMLString.lastError();
    return ZigString.init(err.slice()).toErrorInstance(global);
}

fn htmlStringValue(input: LOLHTML.HTMLString, globalObject: *JSGlobalObject) JSValue {
    var str = ZigString.init(
        input.slice(),
    );
    str.detectEncoding();

    return str.toExternalValueWithCallback(
        globalObject,
        free_html_writer_string,
    );
}

pub const TextChunk = struct {
    text_chunk: ?*LOLHTML.TextChunk = null,

    pub const Class = NewClass(
        TextChunk,
        .{ .name = "TextChunk" },
        .{
            .before = .{
                .rfn = wrap(TextChunk, "before"),
            },
            .after = .{
                .rfn = wrap(TextChunk, "after"),
            },

            .replace = .{
                .rfn = wrap(TextChunk, "replace"),
            },

            .remove = .{
                .rfn = wrap(TextChunk, "remove"),
            },
            .finalize = finalize,
        },
        .{
            .removed = .{
                .get = getterWrap(TextChunk, "removed"),
            },
            .text = .{
                .get = getterWrap(TextChunk, "getText"),
            },
        },
    );

    fn contentHandler(this: *TextChunk, comptime Callback: (fn (*LOLHTML.TextChunk, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        if (this.text_chunk == null)
            return JSC.JSValue.jsUndefined();
        var content_slice = content.toSlice(bun.default_allocator);
        defer content_slice.deinit();

        Callback(
            this.text_chunk.?,
            content_slice.slice(),
            contentOptions != null and contentOptions.?.html,
        ) catch return throwLOLHTMLError(globalObject);

        return JSValue.fromRef(thisObject);
    }

    pub fn before(
        this: *TextChunk,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.TextChunk.before, thisObject, globalObject, content, contentOptions);
    }

    pub fn after(
        this: *TextChunk,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.TextChunk.after, thisObject, globalObject, content, contentOptions);
    }

    pub fn replace(
        this: *TextChunk,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.TextChunk.replace, thisObject, globalObject, content, contentOptions);
    }

    pub fn remove(this: *TextChunk, thisObject: js.JSObjectRef) JSValue {
        if (this.text_chunk == null)
            return JSC.JSValue.jsUndefined();
        this.text_chunk.?.remove();
        return JSValue.fromRef(thisObject);
    }

    pub fn getText(this: *TextChunk, global: *JSGlobalObject) JSValue {
        if (this.text_chunk == null)
            return JSC.JSValue.jsUndefined();
        return ZigString.init(this.text_chunk.?.getContent().slice()).withEncoding().toValue(global);
    }

    pub fn removed(this: *TextChunk, _: *JSGlobalObject) JSValue {
        return JSC.JSValue.jsBoolean(this.text_chunk.?.isRemoved());
    }

    pub fn finalize(this: *TextChunk) void {
        this.text_chunk = null;
        bun.default_allocator.destroy(this);
    }
};

pub const DocType = struct {
    doctype: ?*LOLHTML.DocType = null,

    pub fn finalize(this: *DocType) void {
        this.doctype = null;
        bun.default_allocator.destroy(this);
    }

    pub const Class = NewClass(
        DocType,
        .{
            .name = "DocType",
        },
        .{
            .finalize = finalize,
        },
        .{
            .name = .{
                .get = getterWrap(DocType, "name"),
            },
            .systemId = .{
                .get = getterWrap(DocType, "systemId"),
            },

            .publicId = .{
                .get = getterWrap(DocType, "publicId"),
            },
        },
    );

    /// The doctype name.
    pub fn name(this: *DocType, global: *JSGlobalObject) JSValue {
        if (this.doctype == null)
            return JSC.JSValue.jsUndefined();
        const str = this.doctype.?.getName().slice();
        if (str.len == 0)
            return JSValue.jsNull();
        return ZigString.init(str).toValue(global);
    }

    pub fn systemId(this: *DocType, global: *JSGlobalObject) JSValue {
        if (this.doctype == null)
            return JSC.JSValue.jsUndefined();

        const str = this.doctype.?.getSystemId().slice();
        if (str.len == 0)
            return JSValue.jsNull();
        return ZigString.init(str).toValue(global);
    }

    pub fn publicId(this: *DocType, global: *JSGlobalObject) JSValue {
        if (this.doctype == null)
            return JSC.JSValue.jsUndefined();

        const str = this.doctype.?.getPublicId().slice();
        if (str.len == 0)
            return JSValue.jsNull();
        return ZigString.init(str).toValue(global);
    }
};

pub const DocEnd = struct {
    doc_end: ?*LOLHTML.DocEnd,

    pub fn finalize(this: *DocEnd) void {
        this.doc_end = null;
        bun.default_allocator.destroy(this);
    }

    pub const Class = NewClass(
        DocEnd,
        .{ .name = "DocEnd" },
        .{
            .finalize = finalize,
            .append = .{
                .rfn = wrap(DocEnd, "append"),
            },
        },
        .{},
    );

    fn contentHandler(this: *DocEnd, comptime Callback: (fn (*LOLHTML.DocEnd, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        if (this.doc_end == null)
            return JSValue.jsNull();

        var content_slice = content.toSlice(bun.default_allocator);
        defer content_slice.deinit();

        Callback(
            this.doc_end.?,
            content_slice.slice(),
            contentOptions != null and contentOptions.?.html,
        ) catch return throwLOLHTMLError(globalObject);

        return JSValue.fromRef(thisObject);
    }

    pub fn append(
        this: *DocEnd,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.DocEnd.append, thisObject, globalObject, content, contentOptions);
    }
};

pub const Comment = struct {
    comment: ?*LOLHTML.Comment = null,

    pub fn finalize(this: *Comment) void {
        this.comment = null;
        bun.default_allocator.destroy(this);
    }

    pub const Class = NewClass(
        Comment,
        .{ .name = "Comment" },
        .{
            .before = .{
                .rfn = wrap(Comment, "before"),
            },
            .after = .{
                .rfn = wrap(Comment, "after"),
            },

            .replace = .{
                .rfn = wrap(Comment, "replace"),
            },

            .remove = .{
                .rfn = wrap(Comment, "remove"),
            },
            .finalize = finalize,
        },
        .{
            .removed = .{
                .get = getterWrap(Comment, "removed"),
            },
            .text = .{
                .get = getterWrap(Comment, "getText"),
                .set = setterWrap(Comment, "setText"),
            },
        },
    );

    fn contentHandler(this: *Comment, comptime Callback: (fn (*LOLHTML.Comment, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        if (this.comment == null)
            return JSValue.jsNull();
        var content_slice = content.toSlice(bun.default_allocator);
        defer content_slice.deinit();

        Callback(
            this.comment.?,
            content_slice.slice(),
            contentOptions != null and contentOptions.?.html,
        ) catch return throwLOLHTMLError(globalObject);

        return JSValue.fromRef(thisObject);
    }

    pub fn before(
        this: *Comment,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.Comment.before, thisObject, globalObject, content, contentOptions);
    }

    pub fn after(
        this: *Comment,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.Comment.after, thisObject, globalObject, content, contentOptions);
    }

    pub fn replace(
        this: *Comment,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.Comment.replace, thisObject, globalObject, content, contentOptions);
    }

    pub fn remove(this: *Comment, thisObject: js.JSObjectRef) JSValue {
        if (this.comment == null)
            return JSValue.jsNull();
        this.comment.?.remove();
        return JSValue.fromRef(thisObject);
    }

    pub fn getText(this: *Comment, global: *JSGlobalObject) JSValue {
        if (this.comment == null)
            return JSValue.jsNull();
        return ZigString.init(this.comment.?.getText().slice()).withEncoding().toValue(global);
    }

    pub fn setText(
        this: *Comment,
        value: JSValue,
        exception: JSC.C.ExceptionRef,
        global: *JSGlobalObject,
    ) void {
        if (this.comment == null)
            return;
        var text = value.toSlice(global, bun.default_allocator);
        defer text.deinit();
        this.comment.?.setText(text.slice()) catch {
            exception.* = throwLOLHTMLError(global).asObjectRef();
        };
    }

    pub fn removed(this: *Comment, _: *JSGlobalObject) JSValue {
        if (this.comment == null)
            return JSC.JSValue.jsUndefined();
        return JSC.JSValue.jsBoolean(this.comment.?.isRemoved());
    }
};

pub const EndTag = struct {
    end_tag: ?*LOLHTML.EndTag,

    pub fn finalize(this: *EndTag) void {
        this.end_tag = null;
        bun.default_allocator.destroy(this);
    }

    pub const Handler = struct {
        callback: ?JSC.JSValue,
        global: *JSGlobalObject,

        pub const onEndTag = HandlerCallback(
            Handler,
            EndTag,
            LOLHTML.EndTag,
            "end_tag",
            "callback",
        );

        pub const onEndTagHandler = LOLHTML.DirectiveHandler(LOLHTML.EndTag, Handler, onEndTag);
    };

    pub const Class = NewClass(
        EndTag,
        .{ .name = "EndTag" },
        .{
            .before = .{
                .rfn = wrap(EndTag, "before"),
            },
            .after = .{
                .rfn = wrap(EndTag, "after"),
            },

            .remove = .{
                .rfn = wrap(EndTag, "remove"),
            },
            .finalize = finalize,
        },
        .{
            .name = .{
                .get = getterWrap(EndTag, "getName"),
                .set = setterWrap(EndTag, "setName"),
            },
        },
    );

    fn contentHandler(this: *EndTag, comptime Callback: (fn (*LOLHTML.EndTag, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        if (this.end_tag == null)
            return JSValue.jsNull();

        var content_slice = content.toSlice(bun.default_allocator);
        defer content_slice.deinit();

        Callback(
            this.end_tag.?,
            content_slice.slice(),
            contentOptions != null and contentOptions.?.html,
        ) catch return throwLOLHTMLError(globalObject);

        return JSValue.fromRef(thisObject);
    }

    pub fn before(
        this: *EndTag,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.EndTag.before, thisObject, globalObject, content, contentOptions);
    }

    pub fn after(
        this: *EndTag,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.EndTag.after, thisObject, globalObject, content, contentOptions);
    }

    pub fn replace(
        this: *EndTag,
        thisObject: js.JSObjectRef,
        globalObject: *JSGlobalObject,
        content: ZigString,
        contentOptions: ?ContentOptions,
    ) JSValue {
        return this.contentHandler(LOLHTML.EndTag.replace, thisObject, globalObject, content, contentOptions);
    }

    pub fn remove(this: *EndTag, thisObject: js.JSObjectRef) JSValue {
        if (this.end_tag == null)
            return JSC.JSValue.jsUndefined();

        this.end_tag.?.remove();
        return JSValue.fromRef(thisObject);
    }

    pub fn getName(this: *EndTag, global: *JSGlobalObject) JSValue {
        if (this.end_tag == null)
            return JSC.JSValue.jsUndefined();

        return ZigString.init(this.end_tag.?.getName().slice()).withEncoding().toValue(global);
    }

    pub fn setName(
        this: *EndTag,
        value: JSValue,
        exception: JSC.C.ExceptionRef,
        global: *JSGlobalObject,
    ) void {
        if (this.end_tag == null)
            return;
        var text = value.toSlice(global, bun.default_allocator);
        defer text.deinit();
        this.end_tag.?.setName(text.slice()) catch {
            exception.* = throwLOLHTMLError(global).asObjectRef();
        };
    }
};

pub const AttributeIterator = struct {
    iterator: ?*LOLHTML.Attribute.Iterator = null,

    const attribute_iterator_path: string = "file:///bun-vfs/lolhtml/AttributeIterator.js";
    const attribute_iterator_code: string =
        \\"use strict";
        \\
        \\class AttributeIterator {
        \\  constructor(internal) {
        \\    this.#iterator = internal;
        \\  }
        \\
        \\  #iterator;
        \\  
        \\  [Symbol.iterator]() {
        \\     return this;
        \\  }
        \\  
        \\  next() {
        \\     if (this.#iterator === null) 
        \\          return {done: true};
        \\     var value = this.#iterator.next();
        \\     if (!value) {
        \\         this.#iterator = null;
        \\         return {done: true};
        \\     }
        \\     return {done: false, value: value};
        \\  }
        \\}
        \\
        \\return new AttributeIterator(internal1);
    ;
    threadlocal var attribute_iterator_class: JSC.C.JSObjectRef = undefined;
    threadlocal var attribute_iterator_loaded: bool = false;

    pub fn getAttributeIteratorJSClass(global: *JSGlobalObject) JSValue {
        if (attribute_iterator_loaded)
            return JSC.JSValue.fromRef(attribute_iterator_class);
        attribute_iterator_loaded = true;
        var exception_ptr: ?[*]JSC.JSValueRef = null;
        var name = JSC.C.JSStringCreateStatic("AttributeIteratorGetter", "AttributeIteratorGetter".len);
        var param_name = JSC.C.JSStringCreateStatic("internal1", "internal1".len);
        var attribute_iterator_class_ = JSC.C.JSObjectMakeFunction(
            global,
            name,
            1,
            &[_]JSC.C.JSStringRef{param_name},
            JSC.C.JSStringCreateStatic(attribute_iterator_code.ptr, attribute_iterator_code.len),
            JSC.C.JSStringCreateStatic(attribute_iterator_path.ptr, attribute_iterator_path.len),
            0,
            exception_ptr,
        );
        JSC.C.JSValueProtect(global, attribute_iterator_class_);
        attribute_iterator_class = attribute_iterator_class_;
        return JSC.JSValue.fromRef(attribute_iterator_class);
    }

    pub fn finalize(this: *AttributeIterator) void {
        if (this.iterator) |iter| {
            iter.deinit();
            this.iterator = null;
        }
        bun.default_allocator.destroy(this);
    }

    pub const Class = NewClass(
        AttributeIterator,
        .{ .name = "AttributeIterator" },
        .{
            .next = .{
                .rfn = wrap(AttributeIterator, "next"),
            },
            .finalize = finalize,
        },
        .{},
    );

    const value_ = ZigString.init("value");
    const done_ = ZigString.init("done");
    pub fn next(
        this: *AttributeIterator,
        globalObject: *JSGlobalObject,
    ) JSValue {
        if (this.iterator == null) {
            return JSC.JSValue.jsNull();
        }

        var attribute = this.iterator.?.next() orelse {
            this.iterator.?.deinit();
            this.iterator = null;
            return JSC.JSValue.jsNull();
        };

        // TODO: don't clone here
        const value = attribute.value();
        const name = attribute.name();
        defer name.deinit();
        defer value.deinit();

        var strs = [2]ZigString{
            ZigString.init(name.slice()),
            ZigString.init(value.slice()),
        };

        var valid_strs: []ZigString = strs[0..2];

        var array = JSC.JSValue.createStringArray(
            globalObject,
            valid_strs.ptr,
            valid_strs.len,
            true,
        );

        return array;
    }
};
pub const Element = struct {
    element: ?*LOLHTML.Element = null,

    pub const Class = NewClass(
        Element,
        .{ .name = "Element" },
        .{
            .getAttribute = .{
                .rfn = wrap(Element, "getAttribute"),
            },
            .hasAttribute = .{
                .rfn = wrap(Element, "hasAttribute"),
            },
            .setAttribute = .{
                .rfn = wrap(Element, "setAttribute"),
            },
            .removeAttribute = .{
                .rfn = wrap(Element, "removeAttribute"),
            },
            .before = .{
                .rfn = wrap(Element, "before"),
            },
            .after = .{
                .rfn = wrap(Element, "after"),
            },
            .prepend = .{
                .rfn = wrap(Element, "prepend"),
            },
            .append = .{
                .rfn = wrap(Element, "append"),
            },
            .replace = .{
                .rfn = wrap(Element, "replace"),
            },
            .setInnerContent = .{
                .rfn = wrap(Element, "setInnerContent"),
            },
            .remove = .{
                .rfn = wrap(Element, "remove"),
            },
            .removeAndKeepContent = .{
                .rfn = wrap(Element, "removeAndKeepContent"),
            },
            .onEndTag = .{
                .rfn = wrap(Element, "onEndTag"),
            },
            .finalize = finalize,
        },
        .{
            .tagName = .{
                .get = getterWrap(Element, "getTagName"),
                .set = setterWrap(Element, "setTagName"),
            },
            .removed = .{
                .get = getterWrap(Element, "getRemoved"),
            },
            .namespaceURI = .{
                .get = getterWrap(Element, "getNamespaceURI"),
            },
            .attributes = .{
                .get = getterWrap(Element, "getAttributes"),
            },
        },
    );

    pub fn finalize(this: *Element) void {
        this.element = null;
        bun.default_allocator.destroy(this);
    }

    pub fn onEndTag(
        this: *Element,
        globalObject: *JSGlobalObject,
        function: JSValue,
        thisObject: JSC.C.JSObjectRef,
    ) JSValue {
        if (this.element == null)
            return JSValue.jsNull();
        if (function.isUndefinedOrNull() or !function.isCallable(globalObject.vm())) {
            return ZigString.init("Expected a function").withEncoding().toValue(globalObject);
        }

        var end_tag_handler = bun.default_allocator.create(EndTag.Handler) catch unreachable;
        end_tag_handler.* = .{ .global = globalObject, .callback = function };

        this.element.?.onEndTag(EndTag.Handler.onEndTagHandler, end_tag_handler) catch {
            bun.default_allocator.destroy(end_tag_handler);
            return throwLOLHTMLError(globalObject);
        };

        JSC.C.JSValueProtect(globalObject.ref(), function.asObjectRef());
        return JSValue.fromRef(thisObject);
    }

    //     // fn wrap(comptime name: string)

    ///  Returns the value for a given attribute name: ZigString on the element, or null if it is not found.
    pub fn getAttribute(this: *Element, globalObject: *JSGlobalObject, name: ZigString) JSValue {
        if (this.element == null)
            return JSValue.jsNull();

        var slice = name.toSlice(bun.default_allocator);
        defer slice.deinit();
        var attr = this.element.?.getAttribute(slice.slice()).slice();

        if (attr.len == 0)
            return JSC.JSValue.jsNull();

        var str = ZigString.init(
            attr,
        );

        return str.toExternalValueWithCallback(
            globalObject,
            free_html_writer_string,
        );
    }

    /// Returns a boolean indicating whether an attribute exists on the element.
    pub fn hasAttribute(this: *Element, global: *JSGlobalObject, name: ZigString) JSValue {
        if (this.element == null)
            return JSValue.jsBoolean(false);

        var slice = name.toSlice(bun.default_allocator);
        defer slice.deinit();
        return JSValue.jsBoolean(this.element.?.hasAttribute(slice.slice()) catch return throwLOLHTMLError(global));
    }

    /// Sets an attribute to a provided value, creating the attribute if it does not exist.
    pub fn setAttribute(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, name_: ZigString, value_: ZigString) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        var name_slice = name_.toSlice(bun.default_allocator);
        defer name_slice.deinit();

        var value_slice = value_.toSlice(bun.default_allocator);
        defer value_slice.deinit();
        this.element.?.setAttribute(name_slice.slice(), value_slice.slice()) catch return throwLOLHTMLError(globalObject);
        return JSValue.fromRef(thisObject);
    }

    ///  Removes the attribute.
    pub fn removeAttribute(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, name: ZigString) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        var name_slice = name.toSlice(bun.default_allocator);
        defer name_slice.deinit();

        this.element.?.removeAttribute(
            name_slice.slice(),
        ) catch return throwLOLHTMLError(globalObject);
        return JSValue.fromRef(thisObject);
    }

    fn contentHandler(this: *Element, comptime Callback: (fn (*LOLHTML.Element, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        var content_slice = content.toSlice(bun.default_allocator);
        defer content_slice.deinit();

        Callback(
            this.element.?,
            content_slice.slice(),
            contentOptions != null and contentOptions.?.html,
        ) catch return throwLOLHTMLError(globalObject);

        return JSValue.fromRef(thisObject);
    }

    ///  Inserts content before the element.
    pub fn before(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        return contentHandler(
            this,
            LOLHTML.Element.before,
            thisObject,
            globalObject,
            content,
            contentOptions,
        );
    }

    ///  Inserts content right after the element.
    pub fn after(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        return contentHandler(
            this,
            LOLHTML.Element.after,
            thisObject,
            globalObject,
            content,
            contentOptions,
        );
    }

    /// Inserts content right after the start tag of the element.
    pub fn prepend(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        return contentHandler(
            this,
            LOLHTML.Element.prepend,
            thisObject,
            globalObject,
            content,
            contentOptions,
        );
    }

    ///  Inserts content right before the end tag of the element.
    pub fn append(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        return contentHandler(
            this,
            LOLHTML.Element.append,
            thisObject,
            globalObject,
            content,
            contentOptions,
        );
    }

    /// Removes the element and inserts content in place of it.
    pub fn replace(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        return contentHandler(
            this,
            LOLHTML.Element.replace,
            thisObject,
            globalObject,
            content,
            contentOptions,
        );
    }

    ///  Replaces content of the element.
    pub fn setInnerContent(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
        return contentHandler(
            this,
            LOLHTML.Element.setInnerContent,
            thisObject,
            globalObject,
            content,
            contentOptions,
        );
    }

    ///  Removes the element with all its content.
    pub fn remove(this: *Element, thisObject: js.JSObjectRef) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        this.element.?.remove();
        return JSValue.fromRef(thisObject);
    }

    ///  Removes the start tag and end tag of the element but keeps its inner content intact.
    pub fn removeAndKeepContent(this: *Element, thisObject: js.JSObjectRef) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        this.element.?.removeAndKeepContent();
        return JSValue.fromRef(thisObject);
    }

    pub fn getTagName(this: *Element, globalObject: *JSGlobalObject) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        return htmlStringValue(this.element.?.tagName(), globalObject);
    }

    pub fn setTagName(this: *Element, value: JSValue, exception: JSC.C.ExceptionRef, global: *JSGlobalObject) void {
        if (this.element == null)
            return;

        var text = value.toSlice(global, bun.default_allocator);
        defer text.deinit();

        this.element.?.setTagName(text.slice()) catch {
            exception.* = throwLOLHTMLError(global).asObjectRef();
        };
    }

    pub fn getRemoved(this: *Element, _: *JSGlobalObject) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();
        return JSC.JSValue.jsBoolean(this.element.?.isRemoved());
    }

    pub fn getNamespaceURI(this: *Element, globalObject: *JSGlobalObject) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        return ZigString.init(std.mem.span(this.element.?.namespaceURI())).toValue(globalObject);
    }

    pub fn getAttributes(this: *Element, globalObject: *JSGlobalObject) JSValue {
        if (this.element == null)
            return JSValue.jsUndefined();

        var iter = this.element.?.attributes() orelse return throwLOLHTMLError(globalObject);
        var attr_iter = bun.default_allocator.create(AttributeIterator) catch unreachable;
        attr_iter.* = .{ .iterator = iter };
        var attr = AttributeIterator.Class.make(globalObject.ref(), attr_iter);
        JSC.C.JSValueProtect(globalObject.ref(), attr);
        defer JSC.C.JSValueUnprotect(globalObject.ref(), attr);
        return JSC.JSValue.fromRef(
            JSC.C.JSObjectCallAsFunction(
                globalObject.ref(),
                AttributeIterator.getAttributeIteratorJSClass(globalObject).asObjectRef(),
                null,
                1,
                @ptrCast([*]JSC.C.JSObjectRef, &attr),
                null,
            ),
        );
    }
};
