/*
 * Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/CSS/CSSDescriptors.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/ShorthandStyleValue.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::CSS {

CSSDescriptors::CSSDescriptors(JS::Realm& realm, AtRuleID at_rule_id, Vector<Descriptor> descriptors)
    : CSSStyleDeclaration(realm, Computed::No, Readonly::No)
    , m_at_rule_id(at_rule_id)
    , m_descriptors(move(descriptors))
{
}

CSSDescriptors::~CSSDescriptors() = default;

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-length
size_t CSSDescriptors::length() const
{
    // The length attribute must return the number of CSS declarations in the declarations.
    return m_descriptors.size();
}

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-item
String CSSDescriptors::item(size_t index) const
{
    // The item(index) method must return the property name of the CSS declaration at position index.
    // If there is no indexth object in the collection, then the method must return the empty string.
    if (index >= length())
        return {};

    return m_descriptors[index].descriptor_name_and_id.name().to_string();
}

// https://drafts.csswg.org/cssom/#set-a-css-declaration
bool CSSDescriptors::set_a_css_declaration(DescriptorNameAndID const& descriptor_name_and_id, NonnullRefPtr<StyleValue const> value, Important)
{
    VERIFY(!is_computed());

    for (auto& descriptor : m_descriptors) {
        if (descriptor.descriptor_name_and_id == descriptor_name_and_id) {
            if (*descriptor.value == *value)
                return false;
            descriptor.value = move(value);
            return true;
        }
    }

    m_descriptors.append(Descriptor {
        .descriptor_name_and_id = descriptor_name_and_id,
        .value = move(value),
    });
    return true;
}

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty
WebIDL::ExceptionOr<void> CSSDescriptors::set_property(FlyString const& property, StringView value, StringView priority)
{
    // 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
    if (is_readonly())
        return WebIDL::NoModificationAllowedError::create(realm(), "Cannot modify properties of readonly CSSStyleDeclaration"_utf16);

    // 2. If property is not a custom property, follow these substeps:
    Optional<DescriptorNameAndID> descriptor_name_and_id;
    {
        // 1. Let property be property converted to ASCII lowercase.
        // 2. If property is not a case-sensitive match for a supported CSS property, then return.
        descriptor_name_and_id = DescriptorNameAndID::from_name(m_at_rule_id, property);
        if (!descriptor_name_and_id.has_value())
            return {};
    }

    // 3. If value is the empty string, invoke removeProperty() with property as argument and return.
    if (value.is_empty()) {
        MUST(remove_property(property));
        return {};
    }

    // 4. If priority is not the empty string and is not an ASCII case-insensitive match for the string "important", then return.
    if (!priority.is_empty() && !priority.equals_ignoring_ascii_case("important"sv))
        return {};

    // 5. Let component value list be the result of parsing value for property property.
    RefPtr<StyleValue const> component_value_list = parse_css_descriptor(Parser::ParsingParams {}, m_at_rule_id, *descriptor_name_and_id, value);

    // 6. If component value list is null, then return.
    if (!component_value_list)
        return {};

    // 7. Let updated be false.
    auto updated = false;

    // 8. If property is a shorthand property, then for each longhand property longhand that property maps to, in canonical order, follow these substeps:
    if (is_shorthand(m_at_rule_id, *descriptor_name_and_id)) {
        for_each_expanded_longhand(m_at_rule_id, *descriptor_name_and_id, component_value_list, [this, &updated, priority](DescriptorNameAndID const& longhand_name_and_id, auto longhand_value) {
            VERIFY(longhand_value);

            // 1. Let longhand result be the result of set the CSS declaration longhand with the appropriate value(s)
            //    from component value list, with the important flag set if priority is not the empty string, and unset
            //    otherwise, and with the list of declarations being the declarations.
            auto longhand_result = set_a_css_declaration(longhand_name_and_id, longhand_value.release_nonnull(), priority.is_empty() ? Important::No : Important::Yes);

            // 2. If longhand result is true, let updated be true.
            if (longhand_result)
                updated = true;
        });
    }
    // 9. Otherwise, let updated be the result of set the CSS declaration property with value component value list,
    //    with the important flag set if priority is not the empty string, and unset otherwise, and with the list of
    //    declarations being the declarations.
    else {
        updated = set_a_css_declaration(*descriptor_name_and_id, *component_value_list, !priority.is_empty() ? Important::Yes : Important::No);
    }

    // 10. If updated is true, update style attribute for the CSS declaration block.
    if (updated)
        update_style_attribute();

    return {};
}

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-removeproperty
WebIDL::ExceptionOr<String> CSSDescriptors::remove_property(FlyString const& property)
{
    // 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
    if (is_readonly())
        return WebIDL::NoModificationAllowedError::create(realm(), "Cannot modify properties of readonly CSSStyleDeclaration"_utf16);

    // 2. If property is not a custom property, let property be property converted to ASCII lowercase.
    // AD-HOC: We compare names case-insensitively instead.

    // 3. Let value be the return value of invoking getPropertyValue() with property as argument.
    auto value = get_property_value(property);

    // 4. Let removed be false.
    bool removed = false;
    auto descriptor_name_and_id = DescriptorNameAndID::from_name(m_at_rule_id, property);

    // 5. If property is a shorthand property, for each longhand property longhand that property maps to:
    if (descriptor_name_and_id.has_value() && is_shorthand(m_at_rule_id, *descriptor_name_and_id)) {
        for_each_expanded_longhand(m_at_rule_id, *descriptor_name_and_id, nullptr, [this, &removed](DescriptorNameAndID const& longhand_name_and_id, auto const&) {
            // 1. If longhand is not a property name of a CSS declaration in the declarations, continue.
            // 2. Remove that CSS declaration and let removed be true.
            if (m_descriptors.remove_first_matching([longhand_name_and_id](Descriptor const& entry) { return entry.descriptor_name_and_id == longhand_name_and_id; })) {
                removed = true;
            }
        });
    }
    // 6. Otherwise, if property is a case-sensitive match for a property name of a CSS declaration in the
    //    declarations, remove that CSS declaration and let removed be true.
    else if (descriptor_name_and_id.has_value()) {
        removed = m_descriptors.remove_first_matching([descriptor_name_and_id](Descriptor const& entry) { return entry.descriptor_name_and_id == descriptor_name_and_id; });
    }

    // 7. If removed is true, Update style attribute for the CSS declaration block.
    if (removed)
        update_style_attribute();

    // 8. Return value.
    return value;
}

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue
String CSSDescriptors::get_property_value(FlyString const& property) const
{
    // 1. If property is not a custom property, follow these substeps: ...
    // NB: These substeps only apply to shorthands, and descriptors cannot be shorthands.

    // 2. If property is a case-sensitive match for a property name of a CSS declaration in the declarations, then
    //    return the result of invoking serialize a CSS value of that declaration.
    auto descriptor_name_and_id = DescriptorNameAndID::from_name(m_at_rule_id, property);
    if (descriptor_name_and_id.has_value()) {
        auto match = m_descriptors.first_matching([descriptor_name_and_id](Descriptor const& entry) { return entry.descriptor_name_and_id == descriptor_name_and_id; });
        if (match.has_value())
            return match->value->to_string(SerializationMode::Normal);
    }

    // 3. Return the empty string.
    return {};
}

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertypriority
StringView CSSDescriptors::get_property_priority(FlyString const&) const
{
    // AD-HOC: It's not valid for descriptors to be !important.
    return {};
}

// https://drafts.csswg.org/cssom/#serialize-a-css-declaration-block
String CSSDescriptors::serialized() const
{
    // 1. Let list be an empty array.
    Vector<String> list;
    list.ensure_capacity(m_descriptors.size());

    // 2. Let already serialized be an empty array.
    // AD-HOC: Not needed as we don't have shorthands.

    // 3. Declaration loop: For each CSS declaration declaration in declaration block’s declarations, follow these substeps:
    for (auto const& descriptor : m_descriptors) {
        // 1. Let property be declaration’s property name.
        auto property = descriptor.descriptor_name_and_id.name();

        // 2. If property is in already serialized, continue with the steps labeled declaration loop.
        // AD-HOC: Not needed as we don't have shorthands.

        // 3. If property maps to one or more shorthand properties, let shorthands be an array of those shorthand properties, in preferred order.
        // 4. Shorthand loop: For each shorthand in shorthands, follow these substeps: ...
        // NB: Descriptors can't be shorthands.

        // 5. Let value be the result of invoking serialize a CSS value of declaration.
        auto value = descriptor.value->to_string(SerializationMode::Normal);

        // 6. Let serialized declaration be the result of invoking serialize a CSS declaration with property name property, value value, and the important flag set if declaration has its important flag set.
        auto serialized_declaration = serialize_a_css_declaration(property, value, Important::No);

        // 7. Append serialized declaration to list.
        list.append(serialized_declaration);

        // 8. Append property to already serialized.
        // AD-HOC: Not needed as we don't have shorthands.
    }

    // 4. Return list joined with " " (U+0020).
    return MUST(String::join(' ', list));
}

// https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-csstext
WebIDL::ExceptionOr<void> CSSDescriptors::set_css_text(StringView value)
{
    // 1. If the readonly flag is set, then throw a NoModificationAllowedError exception.
    if (is_readonly())
        return WebIDL::NoModificationAllowedError::create(realm(), "Cannot modify properties of readonly CSSStyleDeclaration"_utf16);

    // 2. Empty the declarations.
    m_descriptors.clear();

    // 3. Parse the given value and, if the return value is not the empty list, insert the items in the list into the
    //    declarations, in specified order.
    auto descriptors = parse_css_descriptor_declaration_block(Parser::ParsingParams {}, m_at_rule_id, value);
    if (!descriptors.is_empty())
        m_descriptors = move(descriptors);

    // 4. Update style attribute for the CSS declaration block.
    update_style_attribute();

    return {};
}

void CSSDescriptors::visit_edges(Visitor& visitor)
{
    Base::visit_edges(visitor);
    for (auto& descriptor : m_descriptors) {
        descriptor.value->visit_edges(visitor);
    }
}

RefPtr<StyleValue const> CSSDescriptors::descriptor(DescriptorNameAndID const& descriptor_name_and_id) const
{
    auto match = m_descriptors.first_matching([descriptor_name_and_id](Descriptor const& descriptor) {
        return descriptor.descriptor_name_and_id == descriptor_name_and_id;
    });
    if (match.has_value())
        return match->value;
    return nullptr;
}

RefPtr<StyleValue const> CSSDescriptors::descriptor_or_initial_value(DescriptorNameAndID const& descriptor_name_and_id) const
{
    if (auto value = descriptor(descriptor_name_and_id))
        return value.release_nonnull();

    return descriptor_initial_value(m_at_rule_id, descriptor_name_and_id.id());
}

bool is_shorthand(AtRuleID at_rule, DescriptorNameAndID const& descriptor)
{
    if (at_rule == AtRuleID::Page && descriptor.id() == DescriptorID::Margin)
        return true;

    return false;
}

void for_each_expanded_longhand(AtRuleID at_rule, DescriptorNameAndID const& descriptor, RefPtr<StyleValue const> value, Function<void(DescriptorNameAndID const&, RefPtr<StyleValue const>)> callback)
{
    if (at_rule == AtRuleID::Page && descriptor.id() == DescriptorID::Margin) {
        if (!value) {
            callback(DescriptorNameAndID::from_id(DescriptorID::MarginTop), nullptr);
            callback(DescriptorNameAndID::from_id(DescriptorID::MarginRight), nullptr);
            callback(DescriptorNameAndID::from_id(DescriptorID::MarginBottom), nullptr);
            callback(DescriptorNameAndID::from_id(DescriptorID::MarginLeft), nullptr);
            return;
        }

        auto const& shorthand_value = value->as_shorthand();

        callback(DescriptorNameAndID::from_id(DescriptorID::MarginTop), shorthand_value.longhand(PropertyID::MarginTop));
        callback(DescriptorNameAndID::from_id(DescriptorID::MarginRight), shorthand_value.longhand(PropertyID::MarginRight));
        callback(DescriptorNameAndID::from_id(DescriptorID::MarginBottom), shorthand_value.longhand(PropertyID::MarginBottom));
        callback(DescriptorNameAndID::from_id(DescriptorID::MarginLeft), shorthand_value.longhand(PropertyID::MarginLeft));
    }
}

}
