/*
 * Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2020-2021, the SerenityOS developers.
 * Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
 * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
 * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/CSS/CSSFunctionDeclarations.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSNestedDeclarations.h>
#include <LibWeb/CSS/ContainerQuery.h>
#include <LibWeb/CSS/MediaList.h>
#include <LibWeb/CSS/MediaQuery.h>
#include <LibWeb/CSS/Parser/ErrorReporter.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/QueryValueType.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>

namespace Web::CSS::Parser {

Vector<NonnullRefPtr<MediaQuery>> Parser::parse_as_media_query_list()
{
    return parse_a_media_query_list(m_token_stream);
}

template<typename T>
Vector<NonnullRefPtr<MediaQuery>> Parser::parse_a_media_query_list(TokenStream<T>& tokens)
{
    // https://www.w3.org/TR/mediaqueries-4/#mq-list

    // AD-HOC: Ignore whitespace-only queries
    // to make `@media {..}` equivalent to `@media all {..}`
    tokens.discard_whitespace();
    if (!tokens.has_next_token())
        return {};

    auto comma_separated_lists = parse_a_comma_separated_list_of_component_values(tokens);

    AK::Vector<NonnullRefPtr<MediaQuery>> media_queries;
    for (auto& media_query_parts : comma_separated_lists) {
        auto stream = TokenStream(media_query_parts);
        media_queries.append(parse_media_query(stream));
    }

    return media_queries;
}

RefPtr<MediaQuery> Parser::parse_as_media_query()
{
    // https://www.w3.org/TR/cssom-1/#parse-a-media-query
    auto media_query_list = parse_as_media_query_list();
    if (media_query_list.is_empty())
        return MediaQuery::create_not_all();
    if (media_query_list.size() == 1)
        return media_query_list.first();
    return nullptr;
}

// `<media-query>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-query
NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<ComponentValue>& tokens)
{
    // `<media-query> = <media-condition>
    //                | [ not | only ]? <media-type> [ and <media-condition-without-or> ]?`

    // `[ not | only ]?`, Returns whether to negate the query
    auto parse_initial_modifier = [](auto& tokens) -> Optional<bool> {
        auto transaction = tokens.begin_transaction();
        tokens.discard_whitespace();
        auto& token = tokens.consume_a_token();
        if (!token.is(Token::Type::Ident))
            return {};

        auto ident = token.token().ident();
        if (ident.equals_ignoring_ascii_case("not"sv)) {
            transaction.commit();
            return true;
        }
        if (ident.equals_ignoring_ascii_case("only"sv)) {
            transaction.commit();
            return false;
        }
        return {};
    };

    auto invalid_media_query = [&](String&& description) {
        // "A media query that does not match the grammar in the previous section must be replaced by `not all`
        // during parsing." - https://www.w3.org/TR/mediaqueries-5/#error-handling
        ErrorReporter::the().report(InvalidQueryError {
            .query_type = "@media"_fly_string,
            .value_string = tokens.dump_string(),
            .description = move(description),
        });
        return MediaQuery::create_not_all();
    };

    auto media_query = MediaQuery::create();
    tokens.discard_whitespace();

    // `<media-condition>`
    if (auto media_condition = parse_media_condition(tokens)) {
        tokens.discard_whitespace();
        if (tokens.has_next_token())
            return invalid_media_query("Trailing tokens after <media-condition>"_string);
        media_query->m_media_condition = media_condition.release_nonnull();
        return media_query;
    }

    // `[ not | only ]?`
    if (auto modifier = parse_initial_modifier(tokens); modifier.has_value()) {
        media_query->m_negated = modifier.value();
        tokens.discard_whitespace();
    }

    // `<media-type>`
    if (auto media_type = parse_media_type(tokens); media_type.has_value()) {
        media_query->m_media_type = media_type.release_value();
        tokens.discard_whitespace();
    } else {
        // https://drafts.csswg.org/mediaqueries-4/#error-handling
        // A media query that does not match the grammar in the previous section must be replaced by not all during parsing.
        return invalid_media_query("Doesn't match `<media-query>`"_string);
    }

    if (!tokens.has_next_token())
        return media_query;

    // `[ and <media-condition-without-or> ]?`
    if (auto const& maybe_and = tokens.consume_a_token(); maybe_and.is_ident("and"sv)) {
        if (auto media_condition = parse_media_condition(tokens)) {
            // "or" is disallowed at the top level
            if (is<BooleanOrExpression>(*media_condition))
                return invalid_media_query("Contains top-level `or`"_string);

            tokens.discard_whitespace();
            if (tokens.has_next_token())
                return invalid_media_query("Trailing tokens after `<media-condition-without-or>`"_string);
            media_query->m_media_condition = move(media_condition);
            return media_query;
        }
        return invalid_media_query("Missing `<media-condition>` after `and`"_string);
    }

    return invalid_media_query("Trailing tokens after `<media-query>`"_string);
}

// `<media-condition>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition
OwnPtr<BooleanExpression> Parser::parse_media_condition(TokenStream<ComponentValue>& tokens)
{
    return parse_boolean_expression(tokens, MatchResult::Unknown, [this](TokenStream<ComponentValue>& outer_tokens) -> OwnPtr<BooleanExpression> {
        auto transaction = outer_tokens.begin_transaction();

        outer_tokens.discard_whitespace();

        if (!(outer_tokens.next_token().is_block() && outer_tokens.next_token().block().is_paren()))
            return nullptr;

        auto const& block = outer_tokens.consume_a_token().block();

        TokenStream inner_tokens { block.value };

        if (auto maybe_media_feature = parse_media_feature(inner_tokens)) {
            transaction.commit();
            return maybe_media_feature;
        }

        return nullptr;
    });
}

enum class FeatureNameType : u8 {
    Normal,
    Min,
    Max,
};

template<typename FeatureID>
struct FeatureName {
    FeatureNameType type;
    FeatureID id;
};

// `<mf-lt> = '<' '='?
//  <mf-gt> = '>' '='?
//  <mf-eq> = '='
//  <mf-comparison> = <mf-lt> | <mf-gt> | <mf-eq>`
static Optional<FeatureComparison> parse_feature_comparison(TokenStream<ComponentValue>& tokens)
{
    auto transaction = tokens.begin_transaction();
    tokens.discard_whitespace();

    auto& first = tokens.consume_a_token();
    if (first.is(Token::Type::Delim)) {
        auto first_delim = first.token().delim();
        if (first_delim == '=') {
            transaction.commit();
            return FeatureComparison::Equal;
        }
        if (first_delim == '<') {
            auto& second = tokens.next_token();
            if (second.is_delim('=')) {
                tokens.discard_a_token();
                transaction.commit();
                return FeatureComparison::LessThanOrEqual;
            }
            transaction.commit();
            return FeatureComparison::LessThan;
        }
        if (first_delim == '>') {
            auto& second = tokens.next_token();
            if (second.is_delim('=')) {
                tokens.discard_a_token();
                transaction.commit();
                return FeatureComparison::GreaterThanOrEqual;
            }
            transaction.commit();
            return FeatureComparison::GreaterThan;
        }
    }

    return {};
}

template<typename Feature, typename FeatureID, typename FeatureNameFromString, typename ParseFeatureValue, typename AllowsRangeSyntax>
static OwnPtr<Feature> parse_query_feature(TokenStream<ComponentValue>& inner_tokens, FeatureNameFromString feature_name_from_string, ParseFeatureValue parse_feature_value, AllowsRangeSyntax allows_range_syntax)
{
    auto transaction = inner_tokens.begin_transaction();

    // `<mf-name> = <ident>`
    auto parse_feature_name = [&](auto& tokens, bool allow_min_max_prefix) -> Optional<FeatureName<FeatureID>> {
        auto transaction = tokens.begin_transaction();
        auto& token = tokens.consume_a_token();
        if (token.is(Token::Type::Ident)) {
            auto name = token.token().ident();
            if (auto id = feature_name_from_string(name); id.has_value()) {
                transaction.commit();
                return FeatureName<FeatureID> { FeatureNameType::Normal, id.value() };
            }

            if (allow_min_max_prefix && (name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) || name.starts_with_bytes("max-"sv, CaseSensitivity::CaseInsensitive))) {
                auto adjusted_name = name.bytes_as_string_view().substring_view(4);
                if (auto id = feature_name_from_string(adjusted_name); id.has_value() && allows_range_syntax(id.value())) {
                    transaction.commit();
                    return FeatureName<FeatureID> {
                        name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) ? FeatureNameType::Min : FeatureNameType::Max,
                        id.value()
                    };
                }
            }
        }
        return {};
    };

    auto parse_feature_boolean = [&](auto& tokens) -> OwnPtr<Feature> {
        auto transaction = tokens.begin_transaction();
        tokens.discard_whitespace();

        if (auto maybe_name = parse_feature_name(tokens, false); maybe_name.has_value()) {
            tokens.discard_whitespace();
            if (!tokens.has_next_token()) {
                transaction.commit();
                return Feature::boolean(maybe_name->id);
            }
        }

        return {};
    };

    auto parse_feature_plain = [&](auto& tokens) -> OwnPtr<Feature> {
        auto transaction = tokens.begin_transaction();
        tokens.discard_whitespace();

        if (auto maybe_name = parse_feature_name(tokens, true); maybe_name.has_value()) {
            tokens.discard_whitespace();
            if (tokens.consume_a_token().is(Token::Type::Colon)) {
                tokens.discard_whitespace();
                if (auto maybe_value = parse_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
                    tokens.discard_whitespace();
                    if (!tokens.has_next_token()) {
                        transaction.commit();
                        switch (maybe_name->type) {
                        case FeatureNameType::Normal:
                            return Feature::plain(maybe_name->id, maybe_value.release_value());
                        case FeatureNameType::Min:
                            return Feature::min(maybe_name->id, maybe_value.release_value());
                        case FeatureNameType::Max:
                            return Feature::max(maybe_name->id, maybe_value.release_value());
                        }
                        VERIFY_NOT_REACHED();
                    }
                }
            }
        }

        return {};
    };

    // `<mf-range> = <mf-name> <mf-comparison> <mf-value>
    //             | <mf-value> <mf-comparison> <mf-name>
    //             | <mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>
    //             | <mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>`
    auto parse_feature_range = [&](auto& tokens) -> OwnPtr<Feature> {
        auto transaction = tokens.begin_transaction();
        tokens.discard_whitespace();

        // `<mf-name> <mf-comparison> <mf-value>`
        // NOTE: We have to check for <mf-name> first, since all <mf-name>s will also parse as <mf-value>.
        if (auto maybe_name = parse_feature_name(tokens, false); maybe_name.has_value() && allows_range_syntax(maybe_name->id)) {
            tokens.discard_whitespace();
            if (auto maybe_comparison = parse_feature_comparison(tokens); maybe_comparison.has_value()) {
                tokens.discard_whitespace();
                if (auto maybe_value = parse_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
                    tokens.discard_whitespace();
                    if (!tokens.has_next_token() && !maybe_value->is_ident()) {
                        transaction.commit();
                        return Feature::half_range(maybe_name->id, maybe_comparison.release_value(), maybe_value.release_value());
                    }
                }
            }
        }

        //  `<mf-value> <mf-comparison> <mf-name>
        // | <mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>
        // | <mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>`
        // NOTE: To parse the first value, we need to first find and parse the <mf-name> so we know what value types to parse.
        //       To allow for <mf-value> to be any number of tokens long, we scan forward until we find a comparison, and then
        //       treat the next non-whitespace token as the <mf-name>, which should be correct as long as they don't add a value
        //       type that can include a comparison in it. :^)
        Optional<FeatureName<FeatureID>> maybe_name;
        {
            // This transaction is never committed, we just use it to rewind automatically.
            auto temp_transaction = tokens.begin_transaction();
            while (tokens.has_next_token() && !maybe_name.has_value()) {
                if (auto maybe_comparison = parse_feature_comparison(tokens); maybe_comparison.has_value()) {
                    // We found a comparison, so the next non-whitespace token should be the <mf-name>
                    tokens.discard_whitespace();
                    maybe_name = parse_feature_name(tokens, false);
                    break;
                }
                tokens.discard_a_token();
                tokens.discard_whitespace();
            }
        }

        if (maybe_name.has_value() && allows_range_syntax(maybe_name->id)) {
            if (auto maybe_left_value = parse_feature_value(maybe_name->id, tokens); maybe_left_value.has_value()) {
                tokens.discard_whitespace();
                if (auto maybe_left_comparison = parse_feature_comparison(tokens); maybe_left_comparison.has_value()) {
                    tokens.discard_whitespace();
                    tokens.discard_a_token(); // The <mf-name> which we already parsed above.
                    tokens.discard_whitespace();

                    if (!tokens.has_next_token()) {
                        transaction.commit();
                        return Feature::half_range(maybe_left_value.release_value(), maybe_left_comparison.release_value(), maybe_name->id);
                    }

                    if (auto maybe_right_comparison = parse_feature_comparison(tokens); maybe_right_comparison.has_value()) {
                        tokens.discard_whitespace();
                        if (auto maybe_right_value = parse_feature_value(maybe_name->id, tokens); maybe_right_value.has_value()) {
                            tokens.discard_whitespace();
                            // For this to be valid, the following must be true:
                            // - Comparisons must either both be >/>= or both be </<=.
                            // - Neither comparison can be `=`.
                            // - Neither value can be an ident.
                            auto left_comparison = maybe_left_comparison.release_value();
                            auto right_comparison = maybe_right_comparison.release_value();

                            if (!tokens.has_next_token()
                                && feature_comparisons_match(left_comparison, right_comparison)
                                && left_comparison != FeatureComparison::Equal
                                && !maybe_left_value->is_ident() && !maybe_right_value->is_ident()) {
                                transaction.commit();
                                return Feature::range(maybe_left_value.release_value(), left_comparison, maybe_name->id, right_comparison, maybe_right_value.release_value());
                            }
                        }
                    }
                }
            }
        }

        return {};
    };

    if (auto maybe_feature_boolean = parse_feature_boolean(inner_tokens)) {
        inner_tokens.discard_whitespace();
        if (inner_tokens.has_next_token())
            return nullptr;
        transaction.commit();
        return maybe_feature_boolean.release_nonnull();
    }

    if (auto maybe_feature_plain = parse_feature_plain(inner_tokens)) {
        inner_tokens.discard_whitespace();
        if (inner_tokens.has_next_token())
            return nullptr;
        transaction.commit();
        return maybe_feature_plain.release_nonnull();
    }

    if (auto maybe_feature_range = parse_feature_range(inner_tokens)) {
        inner_tokens.discard_whitespace();
        if (inner_tokens.has_next_token())
            return nullptr;
        transaction.commit();
        return maybe_feature_range.release_nonnull();
    }

    return {};
}

// `<media-feature>`, https://drafts.csswg.org/mediaqueries-5/#typedef-media-feature
OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& inner_tokens)
{
    return parse_query_feature<MediaFeature, MediaFeatureID>(
        inner_tokens,
        [](StringView name) { return media_feature_id_from_string(name); },
        [this](MediaFeatureID id, auto& tokens) { return parse_media_feature_value(id, tokens); },
        [](MediaFeatureID id) { return media_feature_type_is_range(id); });
}

// `<size-feature>`, https://drafts.csswg.org/css-conditional-5/#size-container
OwnPtr<SizeFeature> Parser::parse_size_feature(TokenStream<ComponentValue>& inner_tokens)
{
    return parse_query_feature<SizeFeature, SizeFeatureID>(
        inner_tokens,
        [](StringView name) { return size_feature_id_from_string(name); },
        [this](SizeFeatureID id, auto& tokens) { return parse_size_feature_value(id, tokens); },
        [](SizeFeatureID id) { return size_feature_type_is_range(id); });
}

Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<ComponentValue>& tokens)
{
    auto transaction = tokens.begin_transaction();
    tokens.discard_whitespace();
    auto const& token = tokens.consume_a_token();

    if (!token.is(Token::Type::Ident))
        return {};

    // https://drafts.csswg.org/mediaqueries-3/#error-handling
    // "However, an exception is made for media types ‘layer’, ‘not’, ‘and’, ‘only’, and ‘or’. Even though they do match
    // the IDENT production, they must not be treated as unknown media types, but rather trigger the malformed query clause."
    if (token.is_ident("layer"sv) || token.is_ident("not"sv) || token.is_ident("and"sv) || token.is_ident("only"sv) || token.is_ident("or"sv))
        return {};

    transaction.commit();

    auto const& ident = token.token().ident();
    return MediaQuery::MediaType {
        .name = ident,
        .known_type = media_type_from_string(ident),
    };
}

static bool is_feature_value_token(ComponentValue const& component_value)
{
    if (!component_value.is_token())
        return true;
    switch (component_value.token().type()) {
    case Token::Type::Ident:
    case Token::Type::Function:
    case Token::Type::AtKeyword:
    case Token::Type::Hash:
    case Token::Type::String:
    case Token::Type::BadString:
    case Token::Type::Url:
    case Token::Type::BadUrl:
    case Token::Type::Number:
    case Token::Type::Percentage:
    case Token::Type::Dimension:
    case Token::Type::Whitespace:
    case Token::Type::Comma:
        return true;
    case Token::Type::Delim:
        // FIXME: What list of delimiters should we actually allow here?
        return !first_is_one_of(component_value.token().delim(), static_cast<u32>('<'), static_cast<u32>('>'), static_cast<u32>('='));
    case Token::Type::Invalid:
    case Token::Type::EndOfFile:
    case Token::Type::CDO:
    case Token::Type::CDC:
    case Token::Type::Colon:
    case Token::Type::Semicolon:
    case Token::Type::OpenSquare:
    case Token::Type::CloseSquare:
    case Token::Type::OpenParen:
    case Token::Type::CloseParen:
    case Token::Type::OpenCurly:
    case Token::Type::CloseCurly:
        return false;
    }
    VERIFY_NOT_REACHED();
}

template<typename FeatureID, typename FeatureAcceptsKeyword, typename FeatureAcceptsType>
Optional<FeatureValue> Parser::parse_feature_value(FeatureID feature, TokenStream<ComponentValue>& tokens, FeatureAcceptsKeyword feature_accepts_keyword, FeatureAcceptsType feature_accepts_type)
{
    {
        auto transaction = tokens.begin_transaction();
        auto value = [&](FeatureID feature, TokenStream<ComponentValue>& tokens) -> Optional<FeatureValue> {
            auto context_guard = push_temporary_value_parsing_context(SpecialContext::MediaCondition);

            // One branch for each member of the QueryValueType enum:
            // Identifiers
            if (tokens.next_token().is(Token::Type::Ident)) {
                auto transaction = tokens.begin_transaction();
                tokens.discard_whitespace();
                auto keyword = parse_keyword_value(tokens);
                if (keyword && feature_accepts_keyword(feature, keyword->to_keyword())) {
                    transaction.commit();
                    return FeatureValue(FeatureValue::Type::Ident, keyword.release_nonnull());
                }
            }

            // Boolean (<mq-boolean> in the spec: a 1 or 0)
            if (feature_accepts_type(feature, QueryValueType::Boolean)) {
                auto transaction = tokens.begin_transaction();
                tokens.discard_whitespace();
                if (auto integer = parse_integer_value(tokens, infinite_integer_range)) {
                    if (integer->is_calculated() || first_is_one_of(integer->as_integer().integer(), 0, 1)) {
                        transaction.commit();
                        return FeatureValue(FeatureValue::Type::Integer, integer.release_nonnull());
                    }
                }
            }

            // Integer
            if (feature_accepts_type(feature, QueryValueType::Integer)) {
                auto transaction = tokens.begin_transaction();
                if (auto integer = parse_integer_value(tokens, infinite_integer_range)) {
                    transaction.commit();
                    return FeatureValue(FeatureValue::Type::Integer, integer.release_nonnull());
                }
            }

            // Length
            if (feature_accepts_type(feature, QueryValueType::Length)) {
                auto transaction = tokens.begin_transaction();
                tokens.discard_whitespace();
                if (auto length = parse_length_value(tokens, infinite_range)) {
                    transaction.commit();
                    return FeatureValue(FeatureValue::Type::Length, length.release_nonnull());
                }

                // https://drafts.csswg.org/mediaqueries-5/#typedef-mf-value
                // <mf-value> = <number> | <dimension> | <ident> | <ratio>
                //
                // https://drafts.csswg.org/css-values-4/#lengths
                // "For zero lengths the unit identifier is optional"
                //
                // https://drafts.csswg.org/css-values-4/#zero-value
                // "Values of '0' can be written without units, even if the
                // value type doesn't allow 'unitless zeroes'."
                if (tokens.has_next_token()) {
                    auto const& token = tokens.next_token();
                    if (auto calc = parse_calculated_value(token, { .accepted_ranges_by_type = { { ValueType::Number, infinite_range } } }); calc && calc->as_calculated().resolves_to_number()) {
                        if (auto resolved_number = calc->as_calculated().resolve_number({}); resolved_number.has_value() && *resolved_number == 0) {
                            tokens.discard_a_token();
                            transaction.commit();
                            return FeatureValue(FeatureValue::Type::Length, LengthStyleValue::create(Length::make_px(0)));
                        }
                    }
                }
            }

            // Ratio
            if (feature_accepts_type(feature, QueryValueType::Ratio)) {
                auto transaction = tokens.begin_transaction();
                tokens.discard_whitespace();
                if (auto ratio = parse_ratio_value(tokens)) {
                    transaction.commit();
                    return FeatureValue(FeatureValue::Type::Ratio, ratio.release_nonnull());
                }
            }

            // Resolution
            if (feature_accepts_type(feature, QueryValueType::Resolution)) {
                auto transaction = tokens.begin_transaction();
                tokens.discard_whitespace();
                if (auto resolution = parse_resolution_value(tokens, infinite_range)) {
                    transaction.commit();
                    return FeatureValue(FeatureValue::Type::Resolution, resolution.release_nonnull());
                }
            }

            return {};
        }(feature, tokens);

        if (value.has_value()) {
            tokens.discard_whitespace();

            // Only returned the value if there are no trailing tokens.
            // Otherwise, the transaction gets reverted and we consume all the value tokens below.
            if (!is_feature_value_token(tokens.next_token())) {
                transaction.commit();
                return value.release_value();
            }
        }
    }

    // Parsing failed somehow, so wrap all the tokens into an "unknown" FeatureValue if possible.

    auto transaction = tokens.begin_transaction();
    tokens.discard_whitespace();
    Vector<ComponentValue> unknown_tokens;

    // Consume any tokens that could be part of a value.
    while (tokens.has_next_token()) {
        if (is_feature_value_token(tokens.next_token())) {
            unknown_tokens.append(tokens.consume_a_token());
        } else {
            break;
        }
    }

    if (!unknown_tokens.is_empty()) {
        transaction.commit();
        ErrorReporter::the().report(InvalidValueError {
            .value_type = "<mf-value>"_fly_string,
            .value_string = MUST(String::join(""sv, unknown_tokens)),
            .description = "Unrecognized type"_string,
        });
        // NB: We only use this for serialization so the substitution function presence is irrelevant and we can just
        //     set it to empty.
        return FeatureValue(FeatureValue::Type::Unknown, move(UnresolvedStyleValue::create(move(unknown_tokens), {})));
    }

    return {};
}

// `<mf-value>`, https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value
Optional<FeatureValue> Parser::parse_media_feature_value(MediaFeatureID feature, TokenStream<ComponentValue>& tokens)
{
    return parse_feature_value(
        feature,
        tokens,
        [](MediaFeatureID feature, Keyword keyword) { return media_feature_accepts_keyword(feature, keyword); },
        [](MediaFeatureID feature, QueryValueType type) { return media_feature_accepts_type(feature, type); });
}

Optional<FeatureValue> Parser::parse_size_feature_value(SizeFeatureID feature, TokenStream<ComponentValue>& tokens)
{
    auto size_feature_accepts_keyword = [](SizeFeatureID feature, Keyword keyword) {
        return feature == SizeFeatureID::Orientation && first_is_one_of(keyword, Keyword::Landscape, Keyword::Portrait);
    };
    auto size_feature_accepts_type = [](SizeFeatureID feature, QueryValueType type) {
        switch (type) {
        case QueryValueType::Length:
            return first_is_one_of(feature,
                SizeFeatureID::BlockSize,
                SizeFeatureID::Height,
                SizeFeatureID::InlineSize,
                SizeFeatureID::Width);
        case QueryValueType::Ratio:
            return feature == SizeFeatureID::AspectRatio;
        default:
            return false;
        }
    };
    return parse_feature_value(feature, tokens, size_feature_accepts_keyword, size_feature_accepts_type);
}

template<typename NestedDeclarationsRule>
GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule(AtRule const& rule, Nested nested)
{
    m_rule_context.append(RuleContext::AtMedia);
    ScopeGuard guard = [&] {
        [[maybe_unused]] auto last = m_rule_context.take_last();
        VERIFY(last == RuleContext::AtMedia);
    };

    // https://drafts.csswg.org/css-conditional-3/#at-media
    // @media <media-query-list> {
    // <rule-list>
    // }
    if (!rule.is_block_rule) {
        ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
            .rule_name = "@media"_fly_string,
            .prelude = MUST(String::join(""sv, rule.prelude)),
            .description = "Expected a block."_string,
        });
        return nullptr;
    }

    auto media_query_tokens = TokenStream { rule.prelude };
    auto media_query_list = parse_a_media_query_list(media_query_tokens);
    auto media_list = MediaList::create(realm(), move(media_query_list));

    GC::RootVector<GC::Ref<CSSRule>> child_rules;
    for (auto const& child : rule.child_rules_and_lists_of_declarations) {
        child.visit(
            [&](Rule const& rule) {
                if (auto child_rule = convert_to_rule<NestedDeclarationsRule>(rule, nested))
                    child_rules.append(*child_rule);
            },
            [&](Vector<Declaration> const& declarations) {
                child_rules.append(NestedDeclarationsRule::create(realm(), *this, declarations));
            });
    }
    auto rule_list = CSSRuleList::create(realm(), child_rules);
    return CSSMediaRule::create(realm(), media_list, rule_list);
}

template GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule<CSSNestedDeclarations>(AtRule const&, Parser::Nested);
template GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule<CSSFunctionDeclarations>(AtRule const&, Parser::Nested);

}
