# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause

from dataclasses import dataclass
from dataclasses import field
from typing import List
from typing import Optional
from typing import Set
from typing import TextIO

from Generators.libweb_bindings.named_and_indexed_properties import interface_supports_named_properties
from Utils.utils import title_case_to_snake_case
from Utils.webidl_parser import Interface
from Utils.webidl_parser import Module

ALL_WORKERS_EXPOSURE = {
    "DedicatedWorker",
    "SharedWorker",
    "ServiceWorker",
    "AudioWorklet",
}

ALL_EXPOSURE = ALL_WORKERS_EXPOSURE | {
    "Window",
    "Worklet",
}


@dataclass
class InterfaceSets:
    intrinsics: List[Interface] = field(default_factory=list)
    window_exposed: List[Interface] = field(default_factory=list)
    dedicated_worker_exposed: List[Interface] = field(default_factory=list)
    shared_worker_exposed: List[Interface] = field(default_factory=list)

    def add_interface(self, interface: Interface) -> None:
        exposed_value = interface.extended_attributes.get("Exposed")
        if exposed_value is None:
            raise RuntimeError(f"Interface {interface.name} is missing extended attribute Exposed")

        exposures = parse_exposure_set(interface.name, exposed_value)

        self.intrinsics.append(interface)

        if "Window" in exposures:
            self.window_exposed.append(interface)

        if "DedicatedWorker" in exposures:
            self.dedicated_worker_exposed.append(interface)

        if "SharedWorker" in exposures:
            self.shared_worker_exposed.append(interface)


@dataclass
class LegacyConstructor:
    name: str
    constructor_class: str


def collect_interface_sets(modules: List[Module]) -> InterfaceSets:
    interface_sets = InterfaceSets()

    for module in modules:
        interface = module.interface
        if interface is None:
            continue
        if not should_have_interface_object(interface):
            continue
        interface_sets.add_interface(interface)

    return interface_sets


def parse_exposure_set(interface_name: str, exposed_value: str) -> Set[str]:
    exposed_value = exposed_value.strip()

    if exposed_value == "*":
        return set(ALL_EXPOSURE)
    if exposed_value == "Nobody":
        return set()

    def parse_single_candidate(candidate: str) -> Optional[Set[str]]:
        if candidate == "Window":
            return {"Window"}
        if candidate == "Worker":
            return set(ALL_WORKERS_EXPOSURE)
        if candidate == "DedicatedWorker":
            return {"DedicatedWorker"}
        if candidate == "SharedWorker":
            return {"SharedWorker"}
        if candidate == "ServiceWorker":
            return {"ServiceWorker"}
        if candidate == "AudioWorklet":
            return {"AudioWorklet"}
        if candidate == "LayoutWorklet":
            return {"LayoutWorklet"}
        if candidate == "PaintWorklet":
            return {"PaintWorklet"}
        if candidate == "Worklet":
            return {"Worklet"}
        return None

    if (parsed_candidate := parse_single_candidate(exposed_value)) is not None:
        return parsed_candidate

    if exposed_value.startswith("(") and exposed_value.endswith(")"):
        exposures: Set[str] = set()
        for candidate in exposed_value[1:-1].split(","):
            candidate_name = candidate.strip()
            if (parsed_candidate := parse_single_candidate(candidate_name)) is None:
                raise RuntimeError(
                    f"Unknown Exposed attribute candidate {candidate_name} in {exposed_value!r} in {interface_name}"
                )
            exposures.update(parsed_candidate)

        if exposures:
            return exposures

    raise RuntimeError(f"Unknown Exposed attribute {exposed_value!r} in {interface_name}")


def should_have_interface_object(interface: Interface) -> bool:
    if interface.is_callback_interface:
        return bool(interface.constants)
    return True


def can_use_shared_interface_constructor(interface: Interface) -> bool:
    return not interface.is_namespace and not interface.is_callback_interface


def can_use_shared_interface_prototype(interface: Interface) -> bool:
    return (
        not interface.is_namespace
        and not interface.is_callback_interface
        and "Global" not in interface.extended_attributes
        and not interface.has_special_member
        and interface.named_property_getter is None
        and interface.indexed_property_getter is None
        and interface.named_property_setter is None
        and interface.named_property_deleter is None
        and interface.indexed_property_setter is None
    )


def interface_prototype_has_immutable_prototype(interface: Interface) -> bool:
    # NB: This currently assumes only Workers and Window can be globals.
    return (
        "Global" in interface.extended_attributes
        or interface.name == "WorkerGlobalScope"
        or interface.name == "EventTarget"
    )


def lookup_legacy_constructor(interface: Interface) -> Optional[LegacyConstructor]:
    legacy_factory_function = interface.extended_attributes.get("LegacyFactoryFunction")
    if not legacy_factory_function:
        return None

    legacy_factory_function = legacy_factory_function.lstrip()
    name = []
    for character in legacy_factory_function:
        if character.isspace() or character == "(":
            break
        name.append(character)

    constructor_name = "".join(name)
    return LegacyConstructor(
        name=constructor_name,
        constructor_class=f"{constructor_name}Constructor",
    )


def write_intrinsic_definitions_header(out: TextIO, interface_sets: InterfaceSets) -> None:
    out.write(
        """#pragma once

#include <AK/Types.h>

namespace Web::Bindings {

enum class InterfaceName : u16 {
    Unknown = 0,
"""
    )

    for index, interface in enumerate(interface_sets.intrinsics, start=1):
        out.write(f"    {interface.name} = {index},\n")

    out.write(
        """};

bool is_exposed(InterfaceName, JS::Realm&);

}
"""
    )


def write_intrinsic_definitions_implementation(out: TextIO, interface_sets: InterfaceSets) -> None:
    out.write(
        """#include <LibGC/DeferGC.h>
#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/InterfaceObject.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/Export.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/DedicatedWorkerGlobalScope.h>
#include <LibWeb/HTML/SharedWorkerGlobalScope.h>
"""
    )

    for interface in interface_sets.intrinsics:
        out.write(f"#include <LibWeb/Bindings/{interface.implemented_name}.h>\n")

        legacy_constructor = lookup_legacy_constructor(interface)
        if legacy_constructor is not None:
            out.write(f"#include <LibWeb/Bindings/{legacy_constructor.constructor_class}.h>\n")

    out.write(
        """
namespace Web::Bindings {
"""
    )

    write_secure_context_switch(out, interface_sets.intrinsics)
    write_experimental_switch(out, interface_sets.intrinsics)
    write_global_exposed_switch(out, "window", interface_sets.window_exposed)
    write_global_exposed_switch(out, "dedicated_worker", interface_sets.dedicated_worker_exposed)
    write_global_exposed_switch(out, "shared_worker", interface_sets.shared_worker_exposed)

    out.write(
        """
// An interface, callback interface, namespace, or member construct is exposed in a given realm realm if the following steps return true:
bool is_exposed(InterfaceName name, JS::Realm& realm)
{
    auto const& global_object = realm.global_object();

    // 1. If construct’s exposure set is not *, and realm.[[GlobalObject]] does not implement an interface that is in construct’s exposure set, then return false.
    if (is<HTML::Window>(global_object)) {
       if (!is_window_exposed(name))
           return false;
    } else if (is<HTML::DedicatedWorkerGlobalScope>(global_object)) {
       if (!is_dedicated_worker_exposed(name))
           return false;
    } else if (is<HTML::SharedWorkerGlobalScope>(global_object)) {
        if (!is_shared_worker_exposed(name))
            return false;
    } else {
        TODO(); // FIXME: ServiceWorkerGlobalScope and WorkletGlobalScope.
    }

    // 2. If realm’s settings object is not a secure context, and construct is conditionally exposed on
    //    [SecureContext], then return false.
    if (is_secure_context_interface(name) && HTML::is_non_secure_context(principal_host_defined_environment_settings_object(realm)))
        return false;

    // AD-HOC: Do not expose experimental interfaces unless instructed to do so.
    if (!HTML::UniversalGlobalScopeMixin::expose_experimental_interfaces() && is_experimental_interface(name))
        return false;

    // FIXME: 3. If realm’s settings object’s cross-origin isolated capability is false, and construct is
    //           conditionally exposed on [CrossOriginIsolated], then return false.

    // 4. Return true.
    return true;
}

"""
    )

    for interface in interface_sets.intrinsics:
        if interface.is_namespace:
            write_namespace_creation(out, interface, interface_sets.intrinsics)
        else:
            write_interface_creation(out, interface)

    out.write(
        """}
"""
    )


def write_secure_context_switch(out: TextIO, interfaces: List[Interface]) -> None:
    out.write(
        """static constexpr bool is_secure_context_interface(InterfaceName name)
{
    switch (name) {
"""
    )
    for interface in interfaces:
        if "SecureContext" not in interface.extended_attributes:
            continue
        out.write(f"    case InterfaceName::{interface.name}:\n")

    out.write(
        """        return true;
    default:
        return false;
    }
}
"""
    )


def write_experimental_switch(out: TextIO, interfaces: List[Interface]) -> None:
    out.write(
        """static constexpr bool is_experimental_interface(InterfaceName name)
{
    switch (name) {
"""
    )
    for interface in interfaces:
        if "Experimental" not in interface.extended_attributes:
            continue
        out.write(f"    case InterfaceName::{interface.name}:\n")

    out.write(
        """        return true;
    default:
        return false;
    }
}
"""
    )


def write_global_exposed_switch(out: TextIO, global_name: str, interfaces: List[Interface]) -> None:
    out.write(
        f"""static constexpr bool is_{global_name}_exposed(InterfaceName name)
{{
    switch (name) {{
"""
    )

    for interface in interfaces:
        out.write(f"    case InterfaceName::{interface.name}:\n")

    out.write(
        """        return true;
    default:
        return false;
    }
}
"""
    )


def write_namespace_creation(out: TextIO, interface: Interface, interfaces: List[Interface]) -> None:
    out.write(
        f"""template<>
void Intrinsics::create_web_namespace<{interface.namespace_class}>(JS::Realm& realm)
{{
    auto namespace_object = realm.create<{interface.namespace_class}>(realm);
    m_namespaces.set("{interface.name}"_fly_string, namespace_object);

    [[maybe_unused]] static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;
"""
    )

    for owned_interface in interfaces:
        if owned_interface.extended_attributes.get("LegacyNamespace") != interface.name:
            continue

        out.write(
            f"""    namespace_object->define_intrinsic_accessor("{owned_interface.name}"_utf16_fly_string, attr, [](auto& realm) -> JS::Value {{ return &Bindings::ensure_web_constructor<{owned_interface.prototype_class}>(realm, "{interface.name}.{owned_interface.name}"_fly_string); }});
"""
        )

    out.write(
        """}
"""
    )


def write_interface_creation(out: TextIO, interface: Interface) -> None:
    if interface.is_callback_interface:
        out.write(
            f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
    static constexpr InterfaceObjectMetadata metadata {{
        .name = "{interface.name}"sv,
        .namespaced_name = "{interface.namespaced_name}"sv,
        .initialize_constructor = &{interface.constructor_class}::initialize,
        .initialize_prototype = &{interface.prototype_class}::initialize,
    }};
    create_web_prototype_and_constructor(realm, metadata);
}}
"""
        )
        return

    if can_use_shared_interface_prototype(interface):
        out.write(
            f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
"""
        )

        ensure_parent_prototype = "nullptr"
        ensure_parent_constructor = "nullptr"
        if interface.parent_name:
            ensure_parent_prototype = f"""[](JS::Realm& realm) -> JS::Object& {{ return Web::Bindings::ensure_web_prototype<{interface.parent_name}Prototype>(realm, "{interface.parent_name}"_fly_string); }}"""
            ensure_parent_constructor = f"""[](JS::Realm& realm) -> JS::NativeFunction& {{ return Web::Bindings::ensure_web_constructor<{interface.parent_name}Prototype>(realm, "{interface.parent_name}"_fly_string); }}"""

        out.write(
            f"""    static constexpr InterfaceObjectMetadata metadata {{
        .name = "{interface.name}"sv,
        .namespaced_name = "{interface.namespaced_name}"sv,
        .ensure_parent_prototype = {ensure_parent_prototype},
        .ensure_parent_constructor = {ensure_parent_constructor},
        .initialize_constructor = &{interface.constructor_class}::initialize,
        .initialize_prototype = &{interface.prototype_class}::initialize,
        .define_unforgeable_attributes = &{interface.prototype_class}::define_unforgeable_attributes,
        .construct = &{interface.constructor_class}::construct,
        .has_immutable_prototype = {str(interface_prototype_has_immutable_prototype(interface)).lower()},
    }};
    create_web_prototype_and_constructor(realm, metadata);
"""
        )

        legacy_constructor = lookup_legacy_constructor(interface)
        if legacy_constructor is not None:
            out.write(
                f"""    auto legacy_constructor = realm.create<{legacy_constructor.constructor_class}>(realm);
    m_constructors.set("{legacy_constructor.name}"_fly_string, legacy_constructor.ptr());
"""
            )

        out.write(
            """}
"""
        )
        return

    if can_use_shared_interface_constructor(interface):
        out.write(
            f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
"""
        )

        ensure_parent_constructor = "nullptr"
        if interface.parent_name:
            ensure_parent_constructor = f"""[](JS::Realm& realm) -> JS::NativeFunction& {{ return Web::Bindings::ensure_web_constructor<{interface.parent_name}Prototype>(realm, "{interface.parent_name}"_fly_string); }}"""

        out.write(
            f"""    static constexpr InterfaceObjectMetadata metadata {{
        .name = "{interface.name}"sv,
        .namespaced_name = "{interface.namespaced_name}"sv,
        .ensure_parent_constructor = {ensure_parent_constructor},
        .initialize_constructor = &{interface.constructor_class}::initialize,
        .construct = &{interface.constructor_class}::construct,
    }};

"""
        )

        if interface_supports_named_properties(interface):
            out.write(
                f"""    auto named_properties_object = realm.create<{interface.name}Properties>(realm);
    m_prototypes.set("{interface.name}Properties"_fly_string, named_properties_object);

"""
            )

        out.write(
            f"""    auto prototype = realm.create<{interface.prototype_class}>(realm);
    m_prototypes.set("{interface.namespaced_name}"_fly_string, prototype);

    create_web_constructor(realm, metadata, prototype);
"""
        )

        legacy_constructor = lookup_legacy_constructor(interface)
        if legacy_constructor is not None:
            out.write(
                f"""    auto legacy_constructor = realm.create<{legacy_constructor.constructor_class}>(realm);
    m_constructors.set("{legacy_constructor.name}"_fly_string, legacy_constructor.ptr());
"""
            )

        out.write(
            """}
"""
        )
        return

    out.write(
        f"""template<>
WEB_API void Intrinsics::create_web_prototype_and_constructor<{interface.prototype_class}>(JS::Realm& realm)
{{
    auto& vm = realm.vm();

"""
    )

    if interface_supports_named_properties(interface):
        out.write(
            f"""    auto named_properties_object = realm.create<{interface.name}Properties>(realm);
    m_prototypes.set("{interface.name}Properties"_fly_string, named_properties_object);

"""
        )

    out.write(
        f"""    auto prototype = realm.create<{interface.prototype_class}>(realm);
    m_prototypes.set("{interface.namespaced_name}"_fly_string, prototype);

    auto constructor = realm.create<{interface.constructor_class}>(realm);
    m_constructors.set("{interface.namespaced_name}"_fly_string, constructor);

    prototype->define_direct_property(vm.names.constructor, constructor.ptr(), JS::Attribute::Writable | JS::Attribute::Configurable);
"""
    )

    legacy_constructor = lookup_legacy_constructor(interface)
    if legacy_constructor is not None:
        out.write(
            f"""    auto legacy_constructor = realm.create<{legacy_constructor.constructor_class}>(realm);
    m_constructors.set("{legacy_constructor.name}"_fly_string, legacy_constructor.ptr());
"""
        )

    out.write(
        """}
"""
    )


def write_exposed_interface_header(out: TextIO, class_name: str) -> None:
    snake_name = title_case_to_snake_case(class_name)
    out.write(
        f"""#pragma once

#include <LibJS/Forward.h>

namespace Web::Bindings {{

void add_{snake_name}_exposed_interfaces(JS::Object&);

}}
"""
    )


def write_exposed_interface_implementation(out: TextIO, class_name: str, exposed_interfaces: List[Interface]) -> None:
    snake_name = title_case_to_snake_case(class_name)
    out.write(
        f"""#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/{class_name}ExposedInterfaces.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/UniversalGlobalScope.h>
"""
    )

    for interface in exposed_interfaces:
        out.write(f"#include <LibWeb/Bindings/{interface.implemented_name}.h>\n")

    out.write(
        f"""
namespace Web::Bindings {{

void add_{snake_name}_exposed_interfaces(JS::Object& global)
{{
    static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;

    [[maybe_unused]] bool is_secure_context = HTML::is_secure_context(HTML::relevant_settings_object(global));
    [[maybe_unused]] bool expose_experimental_interfaces = HTML::UniversalGlobalScopeMixin::expose_experimental_interfaces();
"""
    )

    for interface in exposed_interfaces:
        if interface.is_namespace:
            write_namespace_global_accessor(out, interface)
            continue

        if "LegacyNamespace" in interface.extended_attributes:
            continue

        if "LegacyNoInterfaceObject" in interface.extended_attributes:
            continue

        if not should_have_interface_object(interface):
            continue

        write_interface_global_accessor(out, class_name, interface)

    out.write(
        """}

}
"""
    )


def write_interface_global_accessor(out: TextIO, class_name: str, interface: Interface) -> None:
    indentation = "    "

    def write_constructor_accessor(name: str, constructor_name: str) -> None:
        out.write(
            f'{indentation}global.define_intrinsic_accessor("{name}"_utf16_fly_string, attr, [](auto& realm) -> JS::Value {{ return &ensure_web_constructor<{interface.prototype_class}>(realm, "{constructor_name}"_fly_string); }});\n'
        )

    if "SecureContext" in interface.extended_attributes:
        out.write(
            """    if (is_secure_context) {
"""
        )
        indentation += "    "

    if "Experimental" in interface.extended_attributes:
        out.write(
            f"""{indentation}if (expose_experimental_interfaces) {{
"""
        )
        indentation += "    "

    write_constructor_accessor(interface.namespaced_name, interface.namespaced_name)

    if class_name == "Window":
        legacy_window_alias = interface.extended_attributes.get("LegacyWindowAlias")
        if legacy_window_alias:
            if legacy_window_alias.startswith("(") and legacy_window_alias.endswith(")"):
                aliases = [alias.strip() for alias in legacy_window_alias[1:-1].split(",")]
            else:
                aliases = [legacy_window_alias]

            for alias in aliases:
                write_constructor_accessor(alias, interface.namespaced_name)

    legacy_constructor = lookup_legacy_constructor(interface)
    if legacy_constructor is not None:
        write_constructor_accessor(legacy_constructor.name, legacy_constructor.name)

    if "Experimental" in interface.extended_attributes:
        indentation = indentation[:-4]
        out.write(
            f"""{indentation}}}
"""
        )

    if "SecureContext" in interface.extended_attributes:
        indentation = indentation[:-4]
        out.write(
            f"""{indentation}}}
"""
        )


def write_namespace_global_accessor(out: TextIO, interface: Interface) -> None:
    out.write(
        f"""    global.define_intrinsic_accessor("{interface.name}"_utf16_fly_string, attr, [](auto& realm) -> JS::Value {{ return &ensure_web_namespace<{interface.namespace_class}>(realm, "{interface.name}"_fly_string); }});
"""
    )
