/*
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/Parser/Parser.h>

namespace Web::CSS {

CSSStyleRule::CSSStyleRule(NonnullRefPtrVector<Selector>&& selectors, NonnullRefPtr<CSSStyleDeclaration>&& declaration)
    : m_selectors(move(selectors))
    , m_declaration(move(declaration))
{
}

CSSStyleRule::~CSSStyleRule()
{
}

// https://www.w3.org/TR/cssom/#dom-cssstylerule-style
CSSStyleDeclaration* CSSStyleRule::style()
{
    return m_declaration;
}

static StringView to_string(Selector::SimpleSelector::PseudoElement pseudo_element)
{
    switch (pseudo_element) {
    case Selector::SimpleSelector::PseudoElement::Before:
        return "before"sv;
    case Selector::SimpleSelector::PseudoElement::After:
        return "after"sv;
    case Selector::SimpleSelector::PseudoElement::FirstLine:
        return "first-line"sv;
    case Selector::SimpleSelector::PseudoElement::FirstLetter:
        return "first-letter"sv;
    case Selector::SimpleSelector::PseudoElement::None:
        break;
    }
    VERIFY_NOT_REACHED();
}

static StringView to_string(Selector::SimpleSelector::PseudoClass::Type pseudo_class)
{
    switch (pseudo_class) {
    case Selector::SimpleSelector::PseudoClass::Type::Link:
        return "link"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Visited:
        return "visited"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Hover:
        return "hover"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Focus:
        return "focus"sv;
    case Selector::SimpleSelector::PseudoClass::Type::FirstChild:
        return "first-child"sv;
    case Selector::SimpleSelector::PseudoClass::Type::LastChild:
        return "last-child"sv;
    case Selector::SimpleSelector::PseudoClass::Type::OnlyChild:
        return "only-child"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Empty:
        return "empty"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Root:
        return "root"sv;
    case Selector::SimpleSelector::PseudoClass::Type::FirstOfType:
        return "first-of-pseudo_class"sv;
    case Selector::SimpleSelector::PseudoClass::Type::LastOfType:
        return "last-of-pseudo_class"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Disabled:
        return "disabled"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Enabled:
        return "enabled"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Checked:
        return "checked"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Active:
        return "active"sv;
    case Selector::SimpleSelector::PseudoClass::Type::NthChild:
        return "nth-child"sv;
    case Selector::SimpleSelector::PseudoClass::Type::NthLastChild:
        return "nth-last-child"sv;
    case Selector::SimpleSelector::PseudoClass::Type::Not:
        return "not"sv;
    case Selector::SimpleSelector::PseudoClass::Type::None:
        break;
    }
    VERIFY_NOT_REACHED();
}

static String serialize_a_group_of_selectors(NonnullRefPtrVector<Selector> const&);

// https://www.w3.org/TR/cssom/#serialize-a-simple-selector
static String serialize_a_simple_selector(Selector::SimpleSelector const& simple_selector)
{
    StringBuilder builder;
    switch (simple_selector.type) {
    case Selector::SimpleSelector::Type::TagName:
    case Selector::SimpleSelector::Type::Universal:
        // FIXME: 1. If the namespace prefix maps to a namespace that is not the default namespace and is not the null namespace (not in a namespace) append the serialization of the namespace prefix as an identifier, followed by a "|" (U+007C) to s.
        // FIXME: 2. If the namespace prefix maps to a namespace that is the null namespace (not in a namespace) append "|" (U+007C) to s.
        // 3. If this is a type selector append the serialization of the element name as an identifier to s.
        if (simple_selector.type == Selector::SimpleSelector::Type::TagName) {
            // FIXME: Use the "serialize an identifier" algorithm.
            builder.append(simple_selector.value);
        }
        // 4. If this is a universal selector append "*" (U+002A) to s.
        if (simple_selector.type == Selector::SimpleSelector::Type::Universal)
            builder.append('*');
        break;
    case Selector::SimpleSelector::Type::Attribute:
        // 1. Append "[" (U+005B) to s.
        builder.append('[');

        // FIXME: 2. If the namespace prefix maps to a namespace that is not the null namespace (not in a namespace) append the serialization of the namespace prefix as an identifier, followed by a "|" (U+007C) to s.

        // 3. Append the serialization of the attribute name as an identifier to s.
        // FIXME: Use the "serialize an identifier" algorithm.
        builder.append(simple_selector.attribute.name);

        // 4. If there is an attribute value specified, append "=", "~=", "|=", "^=", "$=", or "*=" as appropriate (depending on the type of attribute selector),
        //    followed by the serialization of the attribute value as a string, to s.
        if (!simple_selector.attribute.value.is_null()) {
            switch (simple_selector.attribute.match_type) {
            case Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch:
                builder.append("=");
                break;
            case Selector::SimpleSelector::Attribute::MatchType::ContainsWord:
                builder.append("~=");
                break;
            case Selector::SimpleSelector::Attribute::MatchType::ContainsString:
                builder.append("*=");
                break;
            case Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment:
                builder.append("|=");
                break;
            case Selector::SimpleSelector::Attribute::MatchType::StartsWithString:
                builder.append("^=");
                break;
            case Selector::SimpleSelector::Attribute::MatchType::EndsWithString:
                builder.append("$=");
                break;
            default:
                break;
            }
        }
        // FIXME: 5. If the attribute selector has the case-sensitivity flag present, append " i" (U+0020 U+0069) to s.

        // 6. Append "]" (U+005D) to s.
        builder.append(']');
        break;

    case Selector::SimpleSelector::Type::Class:
        // Append a "." (U+002E), followed by the serialization of the class name as an identifier to s.
        builder.append('.');
        // FIXME: Use the "serialize an identifier" algorithm.
        builder.append(simple_selector.value);
        break;

    case Selector::SimpleSelector::Type::Id:
        // Append a "#" (U+0023), followed by the serialization of the ID as an identifier to s.
        builder.append('#');
        // FIXME: Use the "serialize an identifier" algorithm.
        builder.append(simple_selector.value);
        break;

    case Selector::SimpleSelector::Type::PseudoClass:
        switch (simple_selector.pseudo_class.type) {
        case Selector::SimpleSelector::PseudoClass::Type::Link:
        case Selector::SimpleSelector::PseudoClass::Type::Visited:
        case Selector::SimpleSelector::PseudoClass::Type::Hover:
        case Selector::SimpleSelector::PseudoClass::Type::Focus:
        case Selector::SimpleSelector::PseudoClass::Type::FirstChild:
        case Selector::SimpleSelector::PseudoClass::Type::LastChild:
        case Selector::SimpleSelector::PseudoClass::Type::OnlyChild:
        case Selector::SimpleSelector::PseudoClass::Type::Empty:
        case Selector::SimpleSelector::PseudoClass::Type::Root:
        case Selector::SimpleSelector::PseudoClass::Type::FirstOfType:
        case Selector::SimpleSelector::PseudoClass::Type::LastOfType:
        case Selector::SimpleSelector::PseudoClass::Type::Disabled:
        case Selector::SimpleSelector::PseudoClass::Type::Enabled:
        case Selector::SimpleSelector::PseudoClass::Type::Checked:
        case Selector::SimpleSelector::PseudoClass::Type::Active:
            // If the pseudo-class does not accept arguments append ":" (U+003A), followed by the name of the pseudo-class, to s.
            builder.append(':');
            builder.append(to_string(simple_selector.pseudo_class.type));
            break;
        case Selector::SimpleSelector::PseudoClass::Type::NthChild:
        case Selector::SimpleSelector::PseudoClass::Type::NthLastChild:
        case Selector::SimpleSelector::PseudoClass::Type::Not:
            // Otherwise, append ":" (U+003A), followed by the name of the pseudo-class, followed by "(" (U+0028),
            // followed by the value of the pseudo-class argument(s) determined as per below, followed by ")" (U+0029), to s.
            builder.append(':');
            builder.append(to_string(simple_selector.pseudo_class.type));
            builder.append('(');
            if (simple_selector.pseudo_class.type == Selector::SimpleSelector::PseudoClass::Type::NthChild
                || simple_selector.pseudo_class.type == Selector::SimpleSelector::PseudoClass::Type::NthLastChild) {
                // FIXME: The result of serializing the value using the rules to serialize an <an+b> value.
                TODO();
            } else if (simple_selector.pseudo_class.type == Selector::SimpleSelector::PseudoClass::Type::Not) {
                // The result of serializing the value using the rules for serializing a group of selectors.
                builder.append(serialize_a_group_of_selectors(simple_selector.pseudo_class.not_selector));
            }
            builder.append(')');
            break;
        default:
            VERIFY_NOT_REACHED();
        }
        break;
    default:
        dbgln("FIXME: Unsupported simple selector serialization for type {}", to_underlying(simple_selector.type));
        break;
    }
    return builder.to_string();
}

// https://www.w3.org/TR/cssom/#serialize-a-selector
static String serialize_a_selector(Selector const& selector)
{
    StringBuilder builder;

    // To serialize a selector let s be the empty string, run the steps below for each part of the chain of the selector, and finally return s:
    for (size_t i = 0; i < selector.compound_selectors().size(); ++i) {
        auto const& compound_selector = selector.compound_selectors()[i];
        // 1. If there is only one simple selector in the compound selectors which is a universal selector, append the result of serializing the universal selector to s.
        if (compound_selector.simple_selectors.size() == 1
            && compound_selector.simple_selectors.first().type == Selector::SimpleSelector::Type::Universal) {
            builder.append(serialize_a_simple_selector(selector.compound_selectors().first().simple_selectors.first()));
        }
        // 2. Otherwise, for each simple selector in the compound selectors...
        //    FIXME: ...that is not a universal selector of which the namespace prefix maps to a namespace that is not the default namespace...
        //    ...serialize the simple selector and append the result to s.
        else {
            for (auto& simple_selector : compound_selector.simple_selectors) {
                builder.append(serialize_a_simple_selector(simple_selector));
            }
        }

        // 3. If this is not the last part of the chain of the selector append a single SPACE (U+0020),
        //    followed by the combinator ">", "+", "~", ">>", "||", as appropriate, followed by another
        //    single SPACE (U+0020) if the combinator was not whitespace, to s.
        if (i != selector.compound_selectors().size() - 1) {
            builder.append(' ');
            switch (compound_selector.combinator) {
            case Selector::Combinator::ImmediateChild:
                builder.append('>');
                break;
            case Selector::Combinator::NextSibling:
                builder.append('+');
                break;
            case Selector::Combinator::SubsequentSibling:
                builder.append('~');
                break;
            case Selector::Combinator::Column:
                builder.append("||");
                break;
            default:
                break;
            }
        } else {
            // 4. If this is the last part of the chain of the selector and there is a pseudo-element, append "::" followed by the name of the pseudo-element, to s.
            // FIXME: This doesn't feel entirely correct. Our model of pseudo-elements seems off.
            if (!compound_selector.simple_selectors.is_empty()
                && compound_selector.simple_selectors.first().type == Selector::SimpleSelector::Type::PseudoElement) {
                builder.append("::");
                builder.append(to_string(compound_selector.simple_selectors.first().pseudo_element));
            }
        }
    }

    return builder.to_string();
}

// https://www.w3.org/TR/cssom/#serialize-a-group-of-selectors
static String serialize_a_group_of_selectors(NonnullRefPtrVector<Selector> const& selectors)
{
    // To serialize a group of selectors serialize each selector in the group of selectors and then serialize a comma-separated list of these serializations.
    StringBuilder builder;
    for (auto& selector : selectors)
        builder.append(serialize_a_selector(selector));
    return builder.to_string();
}

// https://www.w3.org/TR/cssom/#serialize-a-css-rule
String CSSStyleRule::serialized() const
{
    StringBuilder builder;

    // 1. Let s initially be the result of performing serialize a group of selectors on the rule’s associated selectors,
    //    followed by the string " {", i.e., a single SPACE (U+0020), followed by LEFT CURLY BRACKET (U+007B).
    builder.append(serialize_a_group_of_selectors(selectors()));
    builder.append(" {"sv);

    // 2. Let decls be the result of performing serialize a CSS declaration block on the rule’s associated declarations, or null if there are no such declarations.
    auto decls = declaration().serialized();

    // FIXME: 3. Let rules be the result of performing serialize a CSS rule on each rule in the rule’s cssRules list, or null if there are no such rules.
    String rules;

    // 4. If decls and rules are both null, append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)) and return s.
    if (decls.is_null() && rules.is_null()) {
        builder.append(" }"sv);
        return builder.to_string();
    }

    // 5. If rules is null:
    if (rules.is_null()) {
        //    1. Append a single SPACE (U+0020) to s
        builder.append(' ');
        //    2. Append decls to s
        builder.append(decls);
        //    3. Append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)).
        builder.append(" }"sv);
        //    4. Return s.
        return builder.to_string();
    }

    // FIXME: 6. Otherwise:
    // FIXME:    1. If decls is not null, prepend it to rules.
    // FIXME:    2. For each rule in rules:
    // FIXME:       1. Append a newline followed by two spaces to s.
    // FIXME:       2. Append rule to s.
    // FIXME:    3. Append a newline followed by RIGHT CURLY BRACKET (U+007D) to s.
    // FIXME:    4. Return s.
    TODO();
}

// https://www.w3.org/TR/cssom/#dom-cssstylerule-selectortext
String CSSStyleRule::selector_text() const
{
    // The selectorText attribute, on getting, must return the result of serializing the associated group of selectors.
    return serialized();
}

// https://www.w3.org/TR/cssom/#dom-cssstylerule-selectortext
void CSSStyleRule::set_selector_text(StringView selector_text)
{
    // 1. Run the parse a group of selectors algorithm on the given value.
    auto parsed_selectors = parse_selector({}, selector_text);

    // 2. If the algorithm returns a non-null value replace the associated group of selectors with the returned value.
    if (parsed_selectors.has_value())
        m_selectors = parsed_selectors.release_value();

    // 3. Otherwise, if the algorithm returns a null value, do nothing.
}

}