// 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 log = @import("../../log.zig");
const String = @import("../../string.zig").String;

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

const Node = @import("Node.zig");
const CSS = @import("CSS.zig");
const ShadowRoot = @import("ShadowRoot.zig");
const EventTarget = @import("EventTarget.zig");
const collections = @import("collections.zig");
const Selector = @import("selector/Selector.zig");
const Animation = @import("animation/Animation.zig");
const DOMStringMap = @import("element/DOMStringMap.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");

pub const DOMRect = @import("DOMRect.zig");
pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.zig");
pub const Attribute = @import("element/Attribute.zig");

const Element = @This();

pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
pub const NamespaceUriLookup = std.AutoHashMapUnmanaged(*Element, []const u8);

pub const ScrollPosition = struct {
    x: u32 = 0,
    y: u32 = 0,
};
pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);

pub const Namespace = enum(u8) {
    html,
    svg,
    mathml,
    xml,
    // We should keep the original value, but don't.  If this becomes important
    // consider storing it in a page lookup, like `_element_class_lists`, rather
    // that adding a slice directly here (directly in every element).
    unknown,
    null,

    pub fn toUri(self: Namespace) ?[]const u8 {
        return switch (self) {
            .html => "http://www.w3.org/1999/xhtml",
            .svg => "http://www.w3.org/2000/svg",
            .mathml => "http://www.w3.org/1998/Math/MathML",
            .xml => "http://www.w3.org/XML/1998/namespace",
            .unknown => "http://lightpanda.io/unsupported/namespace",
            .null => null,
        };
    }

    pub fn parse(namespace_: ?[]const u8) Namespace {
        const namespace = namespace_ orelse return .null;
        if (namespace.len == "http://www.w3.org/1999/xhtml".len) {
            // Common case, avoid the string comparion. Recklessly
            @branchHint(.likely);
            return .html;
        }
        if (std.mem.eql(u8, namespace, "http://www.w3.org/XML/1998/namespace")) {
            return .xml;
        }
        if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/svg")) {
            return .svg;
        }
        if (std.mem.eql(u8, namespace, "http://www.w3.org/1998/Math/MathML")) {
            return .mathml;
        }
        return .unknown;
    }
};

_type: Type,
_proto: *Node,
_namespace: Namespace = .html,
_attributes: ?*Attribute.List = null,

pub const Type = union(enum) {
    html: *Html,
    svg: *Svg,
};

pub fn is(self: *Element, comptime T: type) ?*T {
    const type_name = @typeName(T);
    switch (self._type) {
        .html => |el| {
            if (T == Html) {
                return el;
            }
            if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.")) {
                return el.is(T);
            }
        },
        .svg => |svg| {
            if (T == Svg) {
                return svg;
            }
            if (comptime std.mem.startsWith(u8, type_name, "webapi.element.svg.")) {
                return svg.is(T);
            }
        },
    }
    return null;
}

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

pub fn asNode(self: *Element) *Node {
    return self._proto;
}

pub fn asEventTarget(self: *Element) *EventTarget {
    return self._proto.asEventTarget();
}

pub fn asConstNode(self: *const Element) *const Node {
    return self._proto;
}

pub fn attributesEql(self: *const Element, other: *Element) bool {
    if (self._attributes) |attr_list| {
        const other_list = other._attributes orelse return false;
        return attr_list.eql(other_list);
    }
    // Make sure no attrs in both sides.
    return other._attributes == null;
}

/// TODO: localName and prefix comparison.
pub fn isEqualNode(self: *Element, other: *Element) bool {
    const self_tag = self.getTagNameDump();
    const other_tag = other.getTagNameDump();
    // Compare namespaces and tags.
    const dirty = self._namespace != other._namespace or !std.mem.eql(u8, self_tag, other_tag);
    if (dirty) {
        return false;
    }

    // Compare attributes.
    if (!self.attributesEql(other)) {
        return false;
    }

    // Compare children.
    var self_iter = self.asNode().childrenIterator();
    var other_iter = other.asNode().childrenIterator();
    var self_count: usize = 0;
    var other_count: usize = 0;
    while (self_iter.next()) |self_node| : (self_count += 1) {
        const other_node = other_iter.next() orelse return false;
        other_count += 1;
        if (self_node.isEqualNode(other_node)) {
            continue;
        }

        return false;
    }

    // Make sure both have equal number of children.
    return self_count == other_count;
}

pub fn getTagNameLower(self: *const Element) []const u8 {
    switch (self._type) {
        .html => |he| switch (he._type) {
            .custom => |ce| {
                @branchHint(.unlikely);
                return ce._tag_name.str();
            },
            else => return switch (he._type) {
                .anchor => "a",
                .area => "area",
                .base => "base",
                .body => "body",
                .br => "br",
                .button => "button",
                .canvas => "canvas",
                .custom => |e| e._tag_name.str(),
                .data => "data",
                .datalist => "datalist",
                .details => "details",
                .dialog => "dialog",
                .directory => "dir",
                .div => "div",
                .dl => "dl",
                .embed => "embed",
                .fieldset => "fieldset",
                .font => "font",
                .form => "form",
                .generic => |e| e._tag_name.str(),
                .heading => |e| e._tag_name.str(),
                .head => "head",
                .html => "html",
                .hr => "hr",
                .iframe => "iframe",
                .img => "img",
                .input => "input",
                .label => "label",
                .legend => "legend",
                .li => "li",
                .link => "link",
                .map => "map",
                .media => |m| switch (m._type) {
                    .audio => "audio",
                    .video => "video",
                    .generic => "media",
                },
                .meta => "meta",
                .meter => "meter",
                .mod => |e| e._tag_name.str(),
                .object => "object",
                .ol => "ol",
                .optgroup => "optgroup",
                .option => "option",
                .output => "output",
                .p => "p",
                .picture => "picture",
                .param => "param",
                .pre => "pre",
                .progress => "progress",
                .quote => |e| e._tag_name.str(),
                .script => "script",
                .select => "select",
                .slot => "slot",
                .source => "source",
                .span => "span",
                .style => "style",
                .table => "table",
                .table_caption => "caption",
                .table_cell => |e| e._tag_name.str(),
                .table_col => |e| e._tag_name.str(),
                .table_row => "tr",
                .table_section => |e| e._tag_name.str(),
                .template => "template",
                .textarea => "textarea",
                .time => "time",
                .title => "title",
                .track => "track",
                .ul => "ul",
                .unknown => |e| e._tag_name.str(),
            },
        },
        .svg => |svg| return svg._tag_name.str(),
    }
}

pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
    return switch (self._type) {
        .html => |he| switch (he._type) {
            .anchor => "A",
            .area => "AREA",
            .base => "BASE",
            .body => "BODY",
            .br => "BR",
            .button => "BUTTON",
            .canvas => "CANVAS",
            .custom => |e| upperTagName(&e._tag_name, buf),
            .data => "DATA",
            .datalist => "DATALIST",
            .details => "DETAILS",
            .dialog => "DIALOG",
            .directory => "DIR",
            .div => "DIV",
            .dl => "DL",
            .embed => "EMBED",
            .fieldset => "FIELDSET",
            .font => "FONT",
            .form => "FORM",
            .generic => |e| upperTagName(&e._tag_name, buf),
            .heading => |e| upperTagName(&e._tag_name, buf),
            .head => "HEAD",
            .html => "HTML",
            .hr => "HR",
            .iframe => "IFRAME",
            .img => "IMG",
            .input => "INPUT",
            .label => "LABEL",
            .legend => "LEGEND",
            .li => "LI",
            .link => "LINK",
            .map => "MAP",
            .meta => "META",
            .media => |m| switch (m._type) {
                .audio => "AUDIO",
                .video => "VIDEO",
                .generic => "MEDIA",
            },
            .meter => "METER",
            .mod => |e| upperTagName(&e._tag_name, buf),
            .object => "OBJECT",
            .ol => "OL",
            .optgroup => "OPTGROUP",
            .option => "OPTION",
            .output => "OUTPUT",
            .p => "P",
            .picture => "PICTURE",
            .param => "PARAM",
            .pre => "PRE",
            .progress => "PROGRESS",
            .quote => |e| upperTagName(&e._tag_name, buf),
            .script => "SCRIPT",
            .select => "SELECT",
            .slot => "SLOT",
            .source => "SOURCE",
            .span => "SPAN",
            .style => "STYLE",
            .table => "TABLE",
            .table_caption => "CAPTION",
            .table_cell => |e| upperTagName(&e._tag_name, buf),
            .table_col => |e| upperTagName(&e._tag_name, buf),
            .table_row => "TR",
            .table_section => |e| upperTagName(&e._tag_name, buf),
            .template => "TEMPLATE",
            .textarea => "TEXTAREA",
            .time => "TIME",
            .title => "TITLE",
            .track => "TRACK",
            .ul => "UL",
            .unknown => |e| switch (self._namespace) {
                .html => upperTagName(&e._tag_name, buf),
                .svg, .xml, .mathml, .unknown, .null => e._tag_name.str(),
            },
        },
        .svg => |svg| svg._tag_name.str(),
    };
}

pub fn getTagNameDump(self: *const Element) []const u8 {
    switch (self._type) {
        .html => return self.getTagNameLower(),
        .svg => |svg| return svg._tag_name.str(),
    }
}

pub fn getNamespaceURI(self: *const Element) ?[]const u8 {
    return self._namespace.toUri();
}

pub fn getNamespaceUri(self: *Element, page: *Page) ?[]const u8 {
    if (self._namespace != .unknown) return self._namespace.toUri();
    return page._element_namespace_uris.get(self);
}

pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, page: *Page) ?[]const u8 {
    // Hardcoded reserved prefixes
    if (prefix) |p| {
        if (std.mem.eql(u8, p, "xml")) return "http://www.w3.org/XML/1998/namespace";
        if (std.mem.eql(u8, p, "xmlns")) return "http://www.w3.org/2000/xmlns/";
    }

    // Step 1: check element's own namespace/prefix
    if (self.getNamespaceUri(page)) |ns_uri| {
        const el_prefix = self._prefix();
        const match = if (prefix == null and el_prefix == null)
            true
        else if (prefix != null and el_prefix != null)
            std.mem.eql(u8, prefix.?, el_prefix.?)
        else
            false;
        if (match) return ns_uri;
    }

    // Step 2: search xmlns attributes
    if (self._attributes) |attrs| {
        var iter = attrs.iterator();
        while (iter.next()) |entry| {
            if (prefix == null) {
                if (entry._name.eql(comptime .wrap("xmlns"))) {
                    const val = entry._value.str();
                    return if (val.len == 0) null else val;
                }
            } else {
                const name = entry._name.str();
                if (std.mem.startsWith(u8, name, "xmlns:")) {
                    if (std.mem.eql(u8, name["xmlns:".len..], prefix.?)) {
                        const val = entry._value.str();
                        return if (val.len == 0) null else val;
                    }
                }
            }
        }
    }

    // Step 3: recurse to parent element
    const parent = self.asNode().parentElement() orelse return null;
    return parent.lookupNamespaceURIForElement(prefix, page);
}

fn _prefix(self: *const Element) ?[]const u8 {
    const name = self.getTagNameLower();
    if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
        return name[0..pos];
    }
    return null;
}

pub fn getLocalName(self: *Element) []const u8 {
    const name = self.getTagNameLower();
    if (std.mem.indexOfPos(u8, name, 0, ":")) |pos| {
        return name[pos + 1 ..];
    }

    return name;
}

// Wrapper methods that delegate to Html implementations
pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
    const he = self.is(Html) orelse return error.NotHtmlElement;
    return he.getInnerText(writer);
}

pub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void {
    const he = self.is(Html) orelse return error.NotHtmlElement;
    return he.setInnerText(text, page);
}

pub fn insertAdjacentHTML(
    self: *Element,
    position: []const u8,
    html_or_xml: []const u8,
    page: *Page,
) !void {
    const he = self.is(Html) orelse return error.NotHtmlElement;
    return he.insertAdjacentHTML(position, html_or_xml, page);
}

pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
    const dump = @import("../dump.zig");
    return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);
}

pub fn setOuterHTML(self: *Element, html: []const u8, page: *Page) !void {
    const node = self.asNode();
    const parent = node._parent orelse return;

    page.domChanged();
    if (html.len > 0) {
        const fragment = (try Node.DocumentFragment.init(page)).asNode();
        try page.parseHtmlAsChildren(fragment, html);
        try page.insertAllChildrenBefore(fragment, parent, node);
    }

    page.removeNode(parent, node, .{ .will_be_reconnected = false });
}

pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
    const dump = @import("../dump.zig");
    return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page);
}

pub fn setInnerHTML(self: *Element, html: []const u8, page: *Page) !void {
    const parent = self.asNode();

    // Remove all existing children
    page.domChanged();
    var it = parent.childrenIterator();
    while (it.next()) |child| {
        page.removeNode(parent, child, .{ .will_be_reconnected = false });
    }

    // Fast path: skip parsing if html is empty
    if (html.len == 0) {
        return;
    }

    // Parse and add new children
    try page.parseHtmlAsChildren(parent, html);
}

pub fn getId(self: *const Element) []const u8 {
    return self.getAttributeSafe(comptime .wrap("id")) orelse "";
}

pub fn setId(self: *Element, value: []const u8, page: *Page) !void {
    return self.setAttributeSafe(comptime .wrap("id"), .wrap(value), page);
}

pub fn getSlot(self: *const Element) []const u8 {
    return self.getAttributeSafe(comptime .wrap("slot")) orelse "";
}

pub fn setSlot(self: *Element, value: []const u8, page: *Page) !void {
    return self.setAttributeSafe(comptime .wrap("slot"), .wrap(value), page);
}

pub fn getDir(self: *const Element) []const u8 {
    return self.getAttributeSafe(comptime .wrap("dir")) orelse "";
}

pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
    return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
}

// ARIAMixin - ARIA attribute reflection
pub fn getAriaAtomic(self: *const Element) ?[]const u8 {
    return self.getAttributeSafe(comptime .wrap("aria-atomic"));
}

pub fn setAriaAtomic(self: *Element, value: ?[]const u8, page: *Page) !void {
    if (value) |v| {
        try self.setAttributeSafe(comptime .wrap("aria-atomic"), .wrap(v), page);
    } else {
        try self.removeAttribute(comptime .wrap("aria-atomic"), page);
    }
}

pub fn getAriaLive(self: *const Element) ?[]const u8 {
    return self.getAttributeSafe(comptime .wrap("aria-live"));
}

pub fn setAriaLive(self: *Element, value: ?[]const u8, page: *Page) !void {
    if (value) |v| {
        try self.setAttributeSafe(comptime .wrap("aria-live"), .wrap(v), page);
    } else {
        try self.removeAttribute(comptime .wrap("aria-live"), page);
    }
}

pub fn getClassName(self: *const Element) []const u8 {
    return self.getAttributeSafe(comptime .wrap("class")) orelse "";
}

pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
    return self.setAttributeSafe(comptime .wrap("class"), .wrap(value), page);
}

pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
    const attributes = self._attributes orelse return .{};
    return attributes.iterator();
}

pub fn getAttribute(self: *const Element, name: String, page: *Page) !?String {
    const attributes = self._attributes orelse return null;
    return attributes.get(name, page);
}

/// For simplicity, the namespace is currently ignored and only the local name is used.
pub fn getAttributeNS(
    self: *const Element,
    maybe_namespace: ?[]const u8,
    local_name: String,
    page: *Page,
) !?String {
    if (maybe_namespace) |namespace| {
        if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
            log.warn(.not_implemented, "Element.getAttributeNS", .{ .namespace = namespace });
        }
    }

    return self.getAttribute(local_name, page);
}

pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
    const attributes = self._attributes orelse return null;
    return attributes.getSafe(name);
}

pub fn hasAttribute(self: *const Element, name: String, page: *Page) !bool {
    const attributes = self._attributes orelse return false;
    const value = try attributes.get(name, page);
    return value != null;
}

pub fn hasAttributeSafe(self: *const Element, name: String) bool {
    const attributes = self._attributes orelse return false;
    return attributes.hasSafe(name);
}

pub fn isDisabled(self: *Element) bool {
    if (self.getAttributeSafe(comptime .wrap("disabled")) != null) {
        return true;
    }

    const element_node = self.asNode();
    var current: ?*Node = element_node._parent;
    while (current) |node| {
        current = node._parent;
        const ancestor = node.is(Element) orelse continue;

        if (ancestor.getTag() == .fieldset and ancestor.getAttributeSafe(comptime .wrap("disabled")) != null) {
            var child = ancestor.firstElementChild();
            while (child) |c| {
                if (c.getTag() == .legend) {
                    if (c.asNode().contains(element_node)) return false;
                    break;
                }
                child = c.nextElementSibling();
            }
            return true;
        }
    }
    return false;
}

pub fn hasAttributes(self: *const Element) bool {
    const attributes = self._attributes orelse return false;
    return attributes.isEmpty() == false;
}

pub fn getAttributeNode(self: *Element, name: String, page: *Page) !?*Attribute {
    const attributes = self._attributes orelse return null;
    return attributes.getAttribute(name, self, page);
}

pub fn setAttribute(self: *Element, name: String, value: String, page: *Page) !void {
    try Attribute.validateAttributeName(name);
    const attributes = try self.getOrCreateAttributeList(page);
    _ = try attributes.put(name, value, self, page);
}

pub fn setAttributeNS(
    self: *Element,
    maybe_namespace: ?[]const u8,
    qualified_name: []const u8,
    value: String,
    page: *Page,
) !void {
    const attr_name = if (maybe_namespace) |namespace| blk: {
        // For xmlns namespace, store the full qualified name (e.g. "xmlns:bar")
        // so lookupNamespaceURI can find namespace declarations.
        if (std.mem.eql(u8, namespace, "http://www.w3.org/2000/xmlns/")) {
            break :blk qualified_name;
        }
        if (!std.mem.eql(u8, namespace, "http://www.w3.org/1999/xhtml")) {
            log.warn(.not_implemented, "Element.setAttributeNS", .{ .namespace = namespace });
        }
        break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
            qualified_name[idx + 1 ..]
        else
            qualified_name;
    } else blk: {
        break :blk if (std.mem.indexOfScalarPos(u8, qualified_name, 0, ':')) |idx|
            qualified_name[idx + 1 ..]
        else
            qualified_name;
    };
    return self.setAttribute(.wrap(attr_name), value, page);
}

pub fn setAttributeSafe(self: *Element, name: String, value: String, page: *Page) !void {
    const attributes = try self.getOrCreateAttributeList(page);
    _ = try attributes.putSafe(name, value, self, page);
}

pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List {
    return self._attributes orelse return self.createAttributeList(page);
}

pub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List {
    lp.assert(self._attributes == null, "Element.createAttributeList non-null _attributes", .{});
    const a = try page.arena.create(Attribute.List);
    a.* = .{ .normalize = self._namespace == .html };
    self._attributes = a;
    return a;
}

pub fn getShadowRoot(self: *Element, page: *Page) ?*ShadowRoot {
    const shadow_root = page._element_shadow_roots.get(self) orelse return null;
    if (shadow_root._mode == .closed) return null;
    return shadow_root;
}

pub fn getAssignedSlot(self: *Element, page: *Page) ?*Html.Slot {
    return page._element_assigned_slots.get(self);
}

pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowRoot {
    if (page._element_shadow_roots.get(self)) |_| {
        return error.AlreadyHasShadowRoot;
    }
    const mode = try ShadowRoot.Mode.fromString(mode_str);
    const shadow_root = try ShadowRoot.init(self, mode, page);
    try page._element_shadow_roots.put(page.arena, self, shadow_root);
    return shadow_root;
}

pub fn insertAdjacentElement(
    self: *Element,
    position: []const u8,
    element: *Element,
    page: *Page,
) !void {
    const target_node, const prev_node = try self.asNode().findAdjacentNodes(position);
    _ = try target_node.insertBefore(element.asNode(), prev_node, page);
}

pub fn insertAdjacentText(
    self: *Element,
    where: []const u8,
    data: []const u8,
    page: *Page,
) !void {
    const text_node = try page.createTextNode(data);
    const target_node, const prev_node = try self.asNode().findAdjacentNodes(where);
    _ = try target_node.insertBefore(text_node, prev_node, page);
}

pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attribute {
    if (attr._element) |el| {
        if (el == self) {
            return attr;
        }
        attr._element = null;
        _ = try el.removeAttributeNode(attr, page);
    }

    const attributes = try self.getOrCreateAttributeList(page);
    return attributes.putAttribute(attr, self, page);
}

pub fn removeAttribute(self: *Element, name: String, page: *Page) !void {
    const attributes = self._attributes orelse return;
    return attributes.delete(name, self, page);
}

pub fn toggleAttribute(self: *Element, name: String, force: ?bool, page: *Page) !bool {
    try Attribute.validateAttributeName(name);
    const has = try self.hasAttribute(name, page);

    const should_add = force orelse !has;

    if (should_add and !has) {
        try self.setAttribute(name, String.empty, page);
        return true;
    } else if (!should_add and has) {
        try self.removeAttribute(name, page);
        return false;
    }

    return should_add;
}

pub fn removeAttributeNode(self: *Element, attr: *Attribute, page: *Page) !*Attribute {
    if (attr._element == null or attr._element.? != self) {
        return error.NotFound;
    }
    try self.removeAttribute(attr._name, page);
    attr._element = null;
    return attr;
}

pub fn getAttributeNames(self: *const Element, page: *Page) ![][]const u8 {
    const attributes = self._attributes orelse return &.{};
    return attributes.getNames(page);
}

pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNodeMap {
    const gop = try page._attribute_named_node_map_lookup.getOrPut(page.arena, @intFromPtr(self));
    if (!gop.found_existing) {
        const attributes = try self.getOrCreateAttributeList(page);
        const named_node_map = try page._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });
        gop.value_ptr.* = named_node_map;
    }
    return gop.value_ptr.*;
}

pub fn getOrCreateStyle(self: *Element, page: *Page) !*CSSStyleProperties {
    const gop = try page._element_styles.getOrPut(page.arena, self);
    if (!gop.found_existing) {
        gop.value_ptr.* = try CSSStyleProperties.init(self, false, page);
    }
    return gop.value_ptr.*;
}

fn getStyle(self: *Element, page: *Page) ?*CSSStyleProperties {
    return page._element_styles.get(self);
}

pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
    const gop = try page._element_class_lists.getOrPut(page.arena, self);
    if (!gop.found_existing) {
        gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
            ._element = self,
            ._attribute_name = comptime .wrap("class"),
        });
    }
    return gop.value_ptr.*;
}

pub fn setClassList(self: *Element, value: String, page: *Page) !void {
    const class_list = try self.getClassList(page);
    try class_list.setValue(value, page);
}

pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
    const gop = try page._element_rel_lists.getOrPut(page.arena, self);
    if (!gop.found_existing) {
        gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
            ._element = self,
            ._attribute_name = comptime .wrap("rel"),
        });
    }
    return gop.value_ptr.*;
}

pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
    const gop = try page._element_datasets.getOrPut(page.arena, self);
    if (!gop.found_existing) {
        gop.value_ptr.* = try page._factory.create(DOMStringMap{
            ._element = self,
        });
    }
    return gop.value_ptr.*;
}

pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
    return self.asNode().replaceChildren(nodes, page);
}

pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
    page.domChanged();

    const ref_node = self.asNode();
    const parent = ref_node._parent orelse return;

    const parent_is_connected = parent.isConnected();

    // Detect if the ref_node must be removed (byt default) or kept.
    // We kept it when ref_node is present into the nodes list.
    var rm_ref_node = true;

    for (nodes) |node_or_text| {
        const child = try node_or_text.toNode(page);

        // If a child is the ref node. We keep it at its own current position.
        if (child == ref_node) {
            rm_ref_node = false;
            continue;
        }

        if (child._parent) |current_parent| {
            page.removeNode(current_parent, child, .{ .will_be_reconnected = parent_is_connected });
        }

        try page.insertNodeRelative(
            parent,
            child,
            .{ .before = ref_node },
            .{ .child_already_connected = child.isConnected() },
        );
    }

    if (rm_ref_node) {
        page.removeNode(parent, ref_node, .{ .will_be_reconnected = false });
    }
}

pub fn remove(self: *Element, page: *Page) void {
    page.domChanged();
    const node = self.asNode();
    const parent = node._parent orelse return;
    page.removeNode(parent, node, .{ .will_be_reconnected = false });
}

pub fn focus(self: *Element, page: *Page) !void {
    if (self.asNode().isConnected() == false) {
        // a disconnected node cannot take focus
        return;
    }

    const FocusEvent = @import("event/FocusEvent.zig");

    const new_target = self.asEventTarget();
    const old_active = page.document._active_element;
    page.document._active_element = self;

    if (old_active) |old| {
        if (old == self) {
            return;
        }

        const old_target = old.asEventTarget();

        // Dispatch blur on old element (no bubble, composed)
        const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, page);
        try page._event_manager.dispatch(old_target, blur_event.asEvent());

        // Dispatch focusout on old element (bubbles, composed)
        const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);
        try page._event_manager.dispatch(old_target, focusout_event.asEvent());
    }

    const old_related: ?*EventTarget = if (old_active) |old| old.asEventTarget() else null;

    // Dispatch focus on new element (no bubble, composed)
    const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, page);
    try page._event_manager.dispatch(new_target, focus_event.asEvent());

    // Dispatch focusin on new element (bubbles, composed)
    const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);
    try page._event_manager.dispatch(new_target, focusin_event.asEvent());
}

pub fn blur(self: *Element, page: *Page) !void {
    if (page.document._active_element != self) return;

    page.document._active_element = null;

    const FocusEvent = @import("event/FocusEvent.zig");
    const old_target = self.asEventTarget();

    // Dispatch blur (no bubble, composed)
    const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, page);
    try page._event_manager.dispatch(old_target, blur_event.asEvent());

    // Dispatch focusout (bubbles, composed)
    const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, page);
    try page._event_manager.dispatch(old_target, focusout_event.asEvent());
}

pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {
    return collections.NodeLive(.child_elements).init(self.asNode(), {}, page);
}

pub fn append(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
    const parent = self.asNode();
    for (nodes) |node_or_text| {
        const child = try node_or_text.toNode(page);
        _ = try parent.appendChild(child, page);
    }
}

pub fn prepend(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
    const parent = self.asNode();
    var i = nodes.len;
    while (i > 0) {
        i -= 1;
        const child = try nodes[i].toNode(page);
        _ = try parent.insertBefore(child, parent.firstChild(), page);
    }
}

pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
    const node = self.asNode();
    const parent = node.parentNode() orelse return;

    for (nodes) |node_or_text| {
        const child = try node_or_text.toNode(page);
        _ = try parent.insertBefore(child, node, page);
    }
}

pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
    const node = self.asNode();
    const parent = node.parentNode() orelse return;
    const viable_next = Node.NodeOrText.viableNextSibling(node, nodes);

    for (nodes) |node_or_text| {
        const child = try node_or_text.toNode(page);
        _ = try parent.insertBefore(child, viable_next, page);
    }
}

pub fn firstElementChild(self: *Element) ?*Element {
    var maybe_child = self.asNode().firstChild();
    while (maybe_child) |child| {
        if (child.is(Element)) |el| return el;
        maybe_child = child.nextSibling();
    }
    return null;
}

pub fn lastElementChild(self: *Element) ?*Element {
    var maybe_child = self.asNode().lastChild();
    while (maybe_child) |child| {
        if (child.is(Element)) |el| return el;
        maybe_child = child.previousSibling();
    }
    return null;
}

pub fn nextElementSibling(self: *Element) ?*Element {
    var maybe_sibling = self.asNode().nextSibling();
    while (maybe_sibling) |sibling| {
        if (sibling.is(Element)) |el| return el;
        maybe_sibling = sibling.nextSibling();
    }
    return null;
}

pub fn previousElementSibling(self: *Element) ?*Element {
    var maybe_sibling = self.asNode().previousSibling();
    while (maybe_sibling) |sibling| {
        if (sibling.is(Element)) |el| return el;
        maybe_sibling = sibling.previousSibling();
    }
    return null;
}

pub fn getChildElementCount(self: *Element) usize {
    var count: usize = 0;
    var it = self.asNode().childrenIterator();
    while (it.next()) |node| {
        if (node.is(Element) != null) {
            count += 1;
        }
    }
    return count;
}

pub fn matches(self: *Element, selector: []const u8, page: *Page) !bool {
    return Selector.matches(self, selector, page);
}

pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element {
    return Selector.querySelector(self.asNode(), selector, page);
}

pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Selector.List {
    return Selector.querySelectorAll(self.asNode(), input, page);
}

pub fn getAnimations(_: *const Element) []*Animation {
    return &.{};
}

pub fn animate(_: *Element, _: ?js.Object, _: ?js.Object, page: *Page) !*Animation {
    return Animation.init(page);
}

pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element {
    if (selector.len == 0) {
        return error.SyntaxError;
    }

    var current: ?*Element = self;
    while (current) |el| {
        if (try Selector.matchesWithScope(el, selector, self, page)) {
            return el;
        }

        const parent = el._proto._parent orelse break;

        if (parent.is(ShadowRoot) != null) {
            break;
        }

        current = parent.is(Element);
    }
    return null;
}

pub fn parentElement(self: *Element) ?*Element {
    return self._proto.parentElement();
}

/// Cache for visibility checks - re-exported from StyleManager for convenience.
pub const VisibilityCache = StyleManager.VisibilityCache;

/// Cache for pointer-events checks - re-exported from StyleManager for convenience.
pub const PointerEventsCache = StyleManager.PointerEventsCache;

pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, page: *Page) bool {
    return page._style_manager.hasPointerEventsNone(self, cache);
}

pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, page: *Page) bool {
    return !page._style_manager.isHidden(self, cache, .{});
}

const CheckVisibilityOpts = struct {
    checkOpacity: bool = false,
    opacityProperty: bool = false,
    checkVisibilityCSS: bool = false,
    visibilityProperty: bool = false,
};
pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, page: *Page) bool {
    const opts = opts_ orelse CheckVisibilityOpts{};
    return !page._style_manager.isHidden(self, null, .{
        .check_opacity = opts.checkOpacity or opts.opacityProperty,
        .check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
    });
}

fn getElementDimensions(self: *Element, page: *Page) struct { width: f64, height: f64 } {
    var width: f64 = 5.0;
    var height: f64 = 5.0;

    if (self.getStyle(page)) |style| {
        const decl = style.asCSSStyleDeclaration();
        width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 5.0;
        height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 5.0;
    }

    if (width == 5.0 or height == 5.0) {
        const tag = self.getTag();

        // Root containers get large default size to contain descendant positions.
        // With calculateDocumentPosition using linear depth scaling (100px per level),
        // even very deep trees (100 levels) stay within 10,000px.
        // 100M pixels is plausible for very long documents.
        if (tag == .html or tag == .body) {
            if (width == 5.0) width = 1920.0;
            if (height == 5.0) height = 100_000_000.0;
        } else if (tag == .img or tag == .iframe) {
            if (self.getAttributeSafe(comptime .wrap("width"))) |w| {
                width = std.fmt.parseFloat(f64, w) catch width;
            }
            if (self.getAttributeSafe(comptime .wrap("height"))) |h| {
                height = std.fmt.parseFloat(f64, h) catch height;
            }
        }
    }

    return .{ .width = width, .height = height };
}

pub fn getClientWidth(self: *Element, page: *Page) f64 {
    if (!self.checkVisibilityCached(null, page)) {
        return 0.0;
    }
    const dims = self.getElementDimensions(page);
    return dims.width;
}

pub fn getClientHeight(self: *Element, page: *Page) f64 {
    if (!self.checkVisibilityCached(null, page)) {
        return 0.0;
    }
    const dims = self.getElementDimensions(page);
    return dims.height;
}

pub fn getBoundingClientRect(self: *Element, page: *Page) DOMRect {
    if (!self.checkVisibilityCached(null, page)) {
        return .{
            ._x = 0.0,
            ._y = 0.0,
            ._width = 0.0,
            ._height = 0.0,
        };
    }

    return self.getBoundingClientRectForVisible(page);
}

// Some cases need a the BoundingClientRect but have already done the
// visibility check.
pub fn getBoundingClientRectForVisible(self: *Element, page: *Page) DOMRect {
    const y = calculateDocumentPosition(self.asNode());
    const dims = self.getElementDimensions(page);

    // Use sibling position for x coordinate to ensure siblings have different x values
    const x = calculateSiblingPosition(self.asNode());

    return .{
        ._x = x,
        ._y = y,
        ._width = dims.width,
        ._height = dims.height,
    };
}

pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
    if (!self.checkVisibilityCached(null, page)) {
        return &.{};
    }
    const rects = try page.call_arena.alloc(DOMRect, 1);
    rects[0] = self.getBoundingClientRectForVisible(page);
    return rects;
}

pub fn getScrollTop(self: *Element, page: *Page) u32 {
    const pos = page._element_scroll_positions.get(self) orelse return 0;
    return pos.y;
}

pub fn setScrollTop(self: *Element, value: i32, page: *Page) !void {
    const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
    if (!gop.found_existing) {
        gop.value_ptr.* = .{};
    }
    gop.value_ptr.y = @intCast(@max(0, value));
}

pub fn getScrollLeft(self: *Element, page: *Page) u32 {
    const pos = page._element_scroll_positions.get(self) orelse return 0;
    return pos.x;
}

pub fn setScrollLeft(self: *Element, value: i32, page: *Page) !void {
    const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
    if (!gop.found_existing) {
        gop.value_ptr.* = .{};
    }
    gop.value_ptr.x = @intCast(@max(0, value));
}

pub fn getScrollHeight(self: *Element, page: *Page) f64 {
    // In our dummy layout engine, content doesn't overflow
    return self.getClientHeight(page);
}

pub fn getScrollWidth(self: *Element, page: *Page) f64 {
    // In our dummy layout engine, content doesn't overflow
    return self.getClientWidth(page);
}

pub fn getOffsetHeight(self: *Element, page: *Page) f64 {
    if (!self.checkVisibilityCached(null, page)) {
        return 0.0;
    }
    const dims = self.getElementDimensions(page);
    return dims.height;
}

pub fn getOffsetWidth(self: *Element, page: *Page) f64 {
    if (!self.checkVisibilityCached(null, page)) {
        return 0.0;
    }
    const dims = self.getElementDimensions(page);
    return dims.width;
}

pub fn getOffsetTop(self: *Element, page: *Page) f64 {
    if (!self.checkVisibilityCached(null, page)) {
        return 0.0;
    }
    return calculateDocumentPosition(self.asNode());
}

pub fn getOffsetLeft(self: *Element, page: *Page) f64 {
    if (!self.checkVisibilityCached(null, page)) {
        return 0.0;
    }
    return calculateSiblingPosition(self.asNode());
}

pub fn getClientTop(_: *Element) f64 {
    // Border width - in our dummy layout, we don't apply borders to layout
    return 0.0;
}

pub fn getClientLeft(_: *Element) f64 {
    // Border width - in our dummy layout, we don't apply borders to layout
    return 0.0;
}

// Calculates document position by counting all nodes that appear before this one
// in tree order, but only traversing the "left side" of the tree.
//
// This walks up from the target node to the root, and at each level counts:
// 1. All previous siblings and their descendants
// 2. The parent itself
//
// Example:
//   <body>              → y=0
//     <h1>Text</h1>     → y=1    (body=1)
//     <h2>              → y=2    (body=1 + h1=1)
//       <a>Link1</a>    → y=3    (body=1 + h1=1 + h2=1)
//     </h2>
//     <p>Text</p>       → y=5    (body=1 + h1=1 + h2=2)
//     <h2>              → y=6    (body=1 + h1=1 + h2=2 + p=1)
//       <a>Link2</a>    → y=7    (body=1 + h1=1 + h2=2 + p=1 + h2=1)
//     </h2>
//   </body>
//
// Trade-offs:
// - O(depth × siblings × subtree_height) - only left-side traversal
// - Linear scaling: 5px per node
// - Perfect document order, guaranteed unique positions
// - Compact coordinates (1000 nodes ≈ 5,000px)
fn calculateDocumentPosition(node: *Node) f64 {
    var position: f64 = 0.0;
    var current = node;

    // Walk up to root, counting preceding nodes
    while (current.parentNode()) |parent| {
        // Count all previous siblings and their descendants
        var sibling = parent.firstChild();
        while (sibling) |s| {
            if (s == current) break;
            position += countSubtreeNodes(s);
            sibling = s.nextSibling();
        }

        // Count the parent itself
        position += 1.0;
        current = parent;
    }

    return position * 5.0; // 5px per node
}

// Counts total nodes in a subtree (node + all descendants)
fn countSubtreeNodes(node: *Node) f64 {
    var count: f64 = 1.0; // Count this node

    var child = node.firstChild();
    while (child) |c| {
        count += countSubtreeNodes(c);
        child = c.nextSibling();
    }

    return count;
}

// Calculates horizontal position using the same approach as y,
// just scaled differently for visual distinction
fn calculateSiblingPosition(node: *Node) f64 {
    var position: f64 = 0.0;
    var current = node;

    // Walk up to root, counting preceding nodes (same as y)
    while (current.parentNode()) |parent| {
        // Count all previous siblings and their descendants
        var sibling = parent.firstChild();
        while (sibling) |s| {
            if (s == current) break;
            position += countSubtreeNodes(s);
            sibling = s.nextSibling();
        }

        // Count the parent itself
        position += 1.0;
        current = parent;
    }

    return position * 5.0; // 5px per node
}

pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
    return self.asNode().getElementsByTagName(tag_name, page);
}

pub fn getElementsByTagNameNS(self: *Element, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
    return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
}

pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
    return self.asNode().getElementsByClassName(class_name, page);
}

pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
    const tag_name = self.getTagNameDump();
    const node = try page.createElementNS(self._namespace, tag_name, self._attributes);

    // Allow element-specific types to copy their runtime state
    _ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), page }) catch |err| {
        log.err(.dom, "element.clone.failed", .{ .err = err });
    };

    if (deep) {
        var child_it = self.asNode().childrenIterator();
        while (child_it.next()) |child| {
            if (try child.cloneNodeForAppending(true, page)) |cloned_child| {
                // We pass `true` to `child_already_connected` as a hacky optimization
                // We _know_ this child isn't connected (Because the parent isn't connected)
                // setting this to `true` skips all connection checks.
                try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
            }
        }
    }

    return node;
}

pub fn scrollIntoViewIfNeeded(_: *const Element, center_if_needed: ?bool) void {
    _ = center_if_needed;
}

const ScrollIntoViewOpts = union {
    align_to_top: bool,
    obj: js.Object,
};
pub fn scrollIntoView(_: *const Element, opts: ?ScrollIntoViewOpts) void {
    _ = opts;
}

pub fn format(self: *Element, writer: *std.Io.Writer) !void {
    try writer.writeByte('<');
    try writer.writeAll(self.getTagNameDump());

    if (self._attributes) |attributes| {
        var it = attributes.iterator();
        while (it.next()) |attr| {
            try writer.print(" {f}", .{attr});
        }
    }
    try writer.writeByte('>');
}

fn upperTagName(tag_name: *String, buf: []u8) []const u8 {
    if (tag_name.len > buf.len) {
        log.info(.dom, "tag.long.name", .{ .name = tag_name.str() });
        return tag_name.str();
    }
    const tag = tag_name.str();
    return std.ascii.upperString(buf, tag);
}

pub fn getTag(self: *const Element) Tag {
    return switch (self._type) {
        .html => |he| switch (he._type) {
            .anchor => .anchor,
            .area => .area,
            .base => .base,
            .div => .div,
            .dl => .dl,
            .embed => .embed,
            .form => .form,
            .p => .p,
            .custom => .custom,
            .data => .data,
            .datalist => .datalist,
            .details => .details,
            .dialog => .dialog,
            .directory => .directory,
            .iframe => .iframe,
            .img => .img,
            .br => .br,
            .button => .button,
            .canvas => .canvas,
            .fieldset => .fieldset,
            .font => .font,
            .heading => |h| h._tag,
            .label => .label,
            .legend => .legend,
            .li => .li,
            .map => .map,
            .ul => .ul,
            .ol => .ol,
            .object => .object,
            .optgroup => .optgroup,
            .output => .output,
            .picture => .picture,
            .param => .param,
            .pre => .pre,
            .generic => |g| g._tag,
            .media => |m| switch (m._type) {
                .audio => .audio,
                .video => .video,
                .generic => .media,
            },
            .meter => .meter,
            .mod => |m| m._tag,
            .progress => .progress,
            .quote => |q| q._tag,
            .script => .script,
            .select => .select,
            .slot => .slot,
            .source => .source,
            .span => .span,
            .option => .option,
            .table => .table,
            .table_caption => .caption,
            .table_cell => |tc| tc._tag,
            .table_col => |tc| tc._tag,
            .table_row => .tr,
            .table_section => |ts| ts._tag,
            .template => .template,
            .textarea => .textarea,
            .time => .time,
            .track => .track,
            .input => .input,
            .link => .link,
            .meta => .meta,
            .hr => .hr,
            .style => .style,
            .title => .title,
            .body => .body,
            .html => .html,
            .head => .head,
            .unknown => .unknown,
        },
        .svg => |se| switch (se._type) {
            .svg => .svg,
            .generic => |g| g._tag,
        },
    };
}

pub const Tag = enum {
    address,
    anchor,
    audio,
    area,
    aside,
    article,
    b,
    blockquote,
    body,
    br,
    button,
    base,
    canvas,
    caption,
    circle,
    code,
    col,
    colgroup,
    custom,
    data,
    datalist,
    dd,
    details,
    del,
    dfn,
    dialog,
    div,
    directory,
    dl,
    dt,
    embed,
    ellipse,
    em,
    fieldset,
    figure,
    form,
    font,
    footer,
    g,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6,
    head,
    header,
    heading,
    hgroup,
    hr,
    html,
    i,
    iframe,
    img,
    input,
    ins,
    label,
    legend,
    li,
    line,
    link,
    main,
    map,
    marquee,
    media,
    menu,
    meta,
    meter,
    nav,
    noscript,
    object,
    ol,
    optgroup,
    option,
    output,
    p,
    path,
    param,
    picture,
    polygon,
    polyline,
    pre,
    progress,
    quote,
    rect,
    s,
    script,
    section,
    select,
    slot,
    source,
    span,
    strong,
    style,
    sub,
    summary,
    sup,
    svg,
    table,
    time,
    tbody,
    td,
    text,
    template,
    textarea,
    tfoot,
    th,
    thead,
    title,
    tr,
    track,
    ul,
    video,
    unknown,

    // If the tag is "unknown", we can't use the optimized tag matching, but
    // need to fallback to the actual tag name
    pub fn parseForMatch(lower: []const u8) ?Tag {
        const tag = std.meta.stringToEnum(Tag, lower) orelse return null;
        return switch (tag) {
            .unknown, .custom => null,
            else => tag,
        };
    }

    pub fn isBlock(self: Tag) bool {
        // zig fmt: off
        return switch (self) {
            // Semantic Layout
            .article, .aside, .footer, .header, .main, .nav, .section,
            // Grouping / Containers
            .address, .div, .fieldset, .figure, .p,
            // Headings
            .h1, .h2, .h3, .h4, .h5, .h6,
            // Lists
            .dl, .ol, .ul,
            // Preformatted / Quotes
            .blockquote, .pre,
            // Tables
            .table,
            // Other
            .hr,
            => true,
            else => false,
        };
        // zig fmt: on
    }

    pub fn isMetadata(self: Tag) bool {
        return switch (self) {
            .base, .head, .link, .meta, .noscript, .script, .style, .template, .title => true,
            else => false,
        };
    }
};

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

    pub const Meta = struct {
        pub const name = "Element";
        pub const prototype_chain = bridge.prototypeChain();
        pub var class_id: bridge.ClassId = undefined;
        pub const enumerable = false;
    };

    pub const tagName = bridge.accessor(_tagName, null, .{});
    fn _tagName(self: *Element, page: *Page) []const u8 {
        return self.getTagNameSpec(&page.buf);
    }
    pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});

    pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{});
    fn _innerText(self: *Element, page: *const Page) ![]const u8 {
        var buf = std.Io.Writer.Allocating.init(page.call_arena);
        try self.getInnerText(&buf.writer);
        return buf.written();
    }

    pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{});
    fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
        var buf = std.Io.Writer.Allocating.init(page.call_arena);
        try self.getOuterHTML(&buf.writer, page);
        return buf.written();
    }

    pub const innerHTML = bridge.accessor(_innerHTML, Element.setInnerHTML, .{});
    fn _innerHTML(self: *Element, page: *Page) ![]const u8 {
        var buf = std.Io.Writer.Allocating.init(page.call_arena);
        try self.getInnerHTML(&buf.writer, page);
        return buf.written();
    }

    pub const prefix = bridge.accessor(Element._prefix, null, .{});

    pub const setAttribute = bridge.function(_setAttribute, .{ .dom_exception = true });
    fn _setAttribute(self: *Element, name: String, value: js.Value, page: *Page) !void {
        return self.setAttribute(name, .wrap(try value.toStringSlice()), page);
    }

    pub const setAttributeNS = bridge.function(_setAttributeNS, .{ .dom_exception = true });
    fn _setAttributeNS(self: *Element, maybe_ns: ?[]const u8, qn: []const u8, value: js.Value, page: *Page) !void {
        return self.setAttributeNS(maybe_ns, qn, .wrap(try value.toStringSlice()), page);
    }

    pub const localName = bridge.accessor(Element.getLocalName, null, .{});
    pub const id = bridge.accessor(Element.getId, Element.setId, .{});
    pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
    pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{});
    pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{});
    pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
    pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
    pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});
    pub const dataset = bridge.accessor(Element.getDataset, null, .{});
    pub const style = bridge.accessor(Element.getOrCreateStyle, null, .{});
    pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
    pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
    pub const hasAttributes = bridge.function(Element.hasAttributes, .{});
    pub const getAttribute = bridge.function(Element.getAttribute, .{});
    pub const getAttributeNS = bridge.function(Element.getAttributeNS, .{});
    pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
    pub const setAttributeNode = bridge.function(Element.setAttributeNode, .{});
    pub const removeAttribute = bridge.function(Element.removeAttribute, .{});
    pub const toggleAttribute = bridge.function(Element.toggleAttribute, .{ .dom_exception = true });
    pub const getAttributeNames = bridge.function(Element.getAttributeNames, .{});
    pub const removeAttributeNode = bridge.function(Element.removeAttributeNode, .{ .dom_exception = true });
    pub const shadowRoot = bridge.accessor(Element.getShadowRoot, null, .{});
    pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
    pub const attachShadow = bridge.function(_attachShadow, .{ .dom_exception = true });
    pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .dom_exception = true });
    pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .dom_exception = true });
    pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .dom_exception = true });

    const ShadowRootInit = struct {
        mode: []const u8,
    };
    fn _attachShadow(self: *Element, init: ShadowRootInit, page: *Page) !*ShadowRoot {
        return self.attachShadow(init.mode, page);
    }
    pub const replaceChildren = bridge.function(Element.replaceChildren, .{ .dom_exception = true });
    pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true });
    pub const remove = bridge.function(Element.remove, .{});
    pub const append = bridge.function(Element.append, .{ .dom_exception = true });
    pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true });
    pub const before = bridge.function(Element.before, .{ .dom_exception = true });
    pub const after = bridge.function(Element.after, .{ .dom_exception = true });
    pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
    pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
    pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
    pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
    pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});
    pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
    pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
    pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
    pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });
    pub const getAnimations = bridge.function(Element.getAnimations, .{});
    pub const animate = bridge.function(Element.animate, .{});
    pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
    pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{});
    pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{});
    pub const clientTop = bridge.accessor(Element.getClientTop, null, .{});
    pub const clientLeft = bridge.accessor(Element.getClientLeft, null, .{});
    pub const scrollTop = bridge.accessor(Element.getScrollTop, Element.setScrollTop, .{});
    pub const scrollLeft = bridge.accessor(Element.getScrollLeft, Element.setScrollLeft, .{});
    pub const scrollHeight = bridge.accessor(Element.getScrollHeight, null, .{});
    pub const scrollWidth = bridge.accessor(Element.getScrollWidth, null, .{});
    pub const offsetTop = bridge.accessor(Element.getOffsetTop, null, .{});
    pub const offsetLeft = bridge.accessor(Element.getOffsetLeft, null, .{});
    pub const offsetWidth = bridge.accessor(Element.getOffsetWidth, null, .{});
    pub const offsetHeight = bridge.accessor(Element.getOffsetHeight, null, .{});
    pub const getClientRects = bridge.function(Element.getClientRects, .{});
    pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
    pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
    pub const getElementsByTagNameNS = bridge.function(Element.getElementsByTagNameNS, .{});
    pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
    pub const children = bridge.accessor(Element.getChildren, null, .{});
    pub const focus = bridge.function(Element.focus, .{});
    pub const blur = bridge.function(Element.blur, .{});
    pub const scrollIntoView = bridge.function(Element.scrollIntoView, .{});
    pub const scrollIntoViewIfNeeded = bridge.function(Element.scrollIntoViewIfNeeded, .{});
};

pub const Build = struct {
    // Calls `func_name` with `args` on the most specific type where it is
    // implement. This could be on the Element itself.
    pub fn call(self: *const Element, comptime func_name: []const u8, args: anytype) !bool {
        inline for (@typeInfo(Element.Type).@"union".fields) |f| {
            if (@field(Element.Type, f.name) == self._type) {
                // The inner type implements this function. Call it and we're done.
                const S = reflect.Struct(f.type);
                if (@hasDecl(S, "Build")) {
                    if (@hasDecl(S.Build, "call")) {
                        const sub = @field(self._type, f.name);
                        return S.Build.call(sub, func_name, args);
                    }

                    // The inner type implements this function. Call it and we're done.
                    if (@hasDecl(f.type, func_name)) {
                        return @call(.auto, @field(f.type, func_name), args);
                    }
                }
            }
        }

        if (@hasDecl(Element.Build, func_name)) {
            // Our last resort - the element implements this function.
            try @call(.auto, @field(Element.Build, func_name), args);
            return true;
        }

        // inform our caller (the Node) that we didn't find anything that implemented
        // func_name and it should keep searching for a match.
        return false;
    }
};

const testing = @import("../../testing.zig");
test "WebApi: Element" {
    try testing.htmlRunner("element", .{});
}
