/*
 * Copyright (C) 2019 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "PropertyCascade.h"

#include "CSSCustomPropertyValue.h"
#include "CSSKeywordValueInlines.h"
#include "CSSPaintImageValue.h"
#include "CSSValuePool.h"
#include "ComputedStyleDependencies.h"
#include "PaintWorkletGlobalScope.h"
#include "PropertyAllowlist.h"
#include "StyleBuilderGenerated.h"
#include "StyleKeyword+Mappings.h"
#include "StylePropertyShorthand.h"
#include <ranges>
#include <wtf/TZoneMallocInlines.h>

namespace WebCore {
namespace Style {

WTF_MAKE_TZONE_ALLOCATED_IMPL(PropertyCascade);

PropertyCascade::PropertyCascade(const MatchResult& matchResult, IncludedProperties&& includedProperties, const HashMap<AnimatableCSSProperty, EnumSet<AnimationSource>>* animatedProperties, const StyleProperties* positionTryFallbackProperties)
    : m_matchResult(matchResult)
    , m_includedProperties(WTF::move(includedProperties))
    , m_maximumOrigin(positionTryFallbackProperties ? PropertyCascade::Origin::PositionFallback : PropertyCascade::Origin::Author)
    , m_animationLayer(animatedProperties ? std::optional { AnimationLayer { *animatedProperties } } : std::nullopt)
{
    ASSERT(!m_includedProperties.isEmpty());

    if (positionTryFallbackProperties)
        m_positionTryFallbackProperties = MatchedProperties { *positionTryFallbackProperties };

    buildCascade();
}

PropertyCascade::PropertyCascade(const PropertyCascade& parent, Origin maximumOrigin, std::optional<ScopeOrdinal> rollbackScope, std::optional<CascadeLayerPriority> maximumCascadeLayerPriorityForRollback)
    : m_matchResult(parent.m_matchResult)
    , m_includedProperties(normalProperties()) // Include all properties to the rollback cascade, lower prority layers may not get included otherwise.
    , m_maximumOrigin(maximumOrigin)
    , m_rollbackScope(rollbackScope)
    , m_maximumCascadeLayerPriorityForRollback(maximumCascadeLayerPriorityForRollback)
    , m_animationLayer(parent.m_animationLayer)
    , m_positionTryFallbackProperties(parent.m_positionTryFallbackProperties)
{
    buildCascade();
}

// Constructs a cascade where all properties in the parent cascade have been reverted.
PropertyCascade::PropertyCascade(const PropertyCascade& parent, RevertRuleTag)
    : m_matchResult(parent.m_matchResult)
    , m_includedProperties(normalProperties())
    , m_maximumOrigin(parent.m_maximumOrigin)
    , m_rollbackScope(parent.m_rollbackScope)
    , m_maximumCascadeLayerPriorityForRollback(parent.m_maximumCascadeLayerPriorityForRollback)
    , m_ruleRollbackDepth(parent.m_ruleRollbackDepth + 1) // Increment rollback depth.
    , m_animationLayer(parent.m_animationLayer)
    , m_positionTryFallbackProperties(parent.m_positionTryFallbackProperties)
{
    buildCascade();
}

PropertyCascade::~PropertyCascade() = default;

PropertyCascade::AnimationLayer::AnimationLayer(const HashMap<AnimatableCSSProperty, EnumSet<AnimationSource>>& properties)
    : properties(properties)
{
    for (auto& key : properties.keys()) {
        if (std::holds_alternative<AtomString>(key)) {
            hasCustomProperties = true;
            break;
        }
    }

    hasFontSize = properties.contains(CSSPropertyFontSize);
    hasLineHeight = properties.contains(CSSPropertyLineHeight);
}

void PropertyCascade::buildCascade()
{
    std::array<bool, 3> originsWithImportant = { };

    for (auto origin : { Origin::UserAgent, Origin::User, Origin::Author }) {
        if (origin > m_maximumOrigin)
            break;
        bool hasImportant = addNormalMatches(origin);
        if (hasImportant)
            originsWithImportant[std::to_underlying(origin)] = true;
    }

    if (m_positionTryFallbackProperties)
        addPositionTryFallbackProperties();

    for (auto origin : { Origin::Author, Origin::User, Origin::UserAgent }) {
        if (!originsWithImportant[std::to_underlying(origin)])
            continue;
        addImportantMatches(origin);
    }

    sortLogicalGroupPropertyIDs();

    m_delayedRollbackProperties.clear();
}

void PropertyCascade::setPropertyInternal(Property& property, CSSPropertyID id, CSSValue& cssValue, const MatchedProperties& matchedProperties, Origin origin)
{
    ASSERT(matchedProperties.linkMatchType <= SelectorChecker::MatchAll);
    property.id = id;
    property.origin = origin;
    property.styleScopeOrdinal = matchedProperties.styleScopeOrdinal;
    property.cascadeLayerPriority = matchedProperties.cascadeLayerPriority;
    property.fromStyleAttribute = matchedProperties.fromStyleAttribute;

    if (matchedProperties.linkMatchType == SelectorChecker::MatchAll) {
        property.origins[SelectorChecker::MatchDefault] = origin;
        property.cssValue[SelectorChecker::MatchDefault] = &cssValue;

        property.origins[SelectorChecker::MatchLink] = origin;
        property.cssValue[SelectorChecker::MatchLink] = &cssValue;

        property.origins[SelectorChecker::MatchVisited] = origin;
        property.cssValue[SelectorChecker::MatchVisited] = &cssValue;
    } else {
        property.origins[matchedProperties.linkMatchType] = origin;
        property.cssValue[matchedProperties.linkMatchType] = &cssValue;
    }
}

void PropertyCascade::set(CSSPropertyID id, CSSValue& cssValue, const MatchedProperties& matchedProperties, Origin origin)
{
    if (id >= firstLogicalGroupProperty) {
        setLogicalGroupProperty(id, cssValue, matchedProperties, origin);
        return;
    }

    ASSERT(id < m_propertyIsPresent.size());
    if (id == CSSPropertyCustom) {
        m_propertyIsPresent.set(id);
        const auto& customValue = downcast<CSSCustomPropertyValue>(cssValue);
        auto result = m_customProperties.ensure(customValue.name(), [&]() {
            Property property;
            property.cssValue = { };
            setPropertyInternal(property, id, cssValue, matchedProperties, origin);
            return property;
        });
        if (!result.isNewEntry)
            setPropertyInternal(result.iterator->value, id, cssValue, matchedProperties, origin);
        return;
    }

    auto& property = m_properties[id];
    if (!m_propertyIsPresent.testAndSet(id))
        property.cssValue = { };
    setPropertyInternal(property, id, cssValue, matchedProperties, origin);
}

void PropertyCascade::setLogicalGroupProperty(CSSPropertyID id, CSSValue& cssValue, const MatchedProperties& matchedProperties, Origin origin)
{
    ASSERT(id >= firstLogicalGroupProperty);
    ASSERT(id <= lastLogicalGroupProperty);

    auto& property = m_properties[id];
    if (!hasLogicalGroupProperty(id)) {
        property.cssValue = { };
        m_lowestSeenLogicalGroupProperty = std::min(m_lowestSeenLogicalGroupProperty, id);
        m_highestSeenLogicalGroupProperty = std::max(m_highestSeenLogicalGroupProperty, id);
    }
    setLogicalGroupPropertyIndex(id, ++m_lastIndexForLogicalGroup);
    setPropertyInternal(property, id, cssValue, matchedProperties, origin);
}

void PropertyCascade::setDelayingForRuleRollback(CSSPropertyID propertyID, CSSValue& cssValue, const MatchedProperties& matchedProperties, Origin origin)
{
    ASSERT(m_ruleRollbackDepth);

    auto key = [&] -> std::pair<unsigned, AtomString>  {
        if (propertyID == CSSPropertyCustom)
            return { propertyID, downcast<CSSCustomPropertyValue>(cssValue).name() };
        return { propertyID, emptyAtom() };
    }();
    auto& delayedValues = m_delayedRollbackProperties.ensure(key, [&] {
        return Deque<DelayedRollbackProperty>();
    }).iterator->value;

    delayedValues.prepend(DelayedRollbackProperty { cssValue, matchedProperties, origin });

    // Ignore the last m_ruleRollbackDepth values for each property.
    if (delayedValues.size() <= m_ruleRollbackDepth)
        return;

    auto last = delayedValues.takeLast();
    SUPPRESS_UNCOUNTED_ARG set(propertyID, last.value, last.properties, last.origin);
}

bool PropertyCascade::hasProperty(CSSPropertyID propertyID, const CSSValue& value)
{
    if (propertyID == CSSPropertyCustom)
        return hasCustomProperty(downcast<CSSCustomPropertyValue>(value).name());
    return propertyID < firstLogicalGroupProperty ? hasNormalProperty(propertyID) : hasLogicalGroupProperty(propertyID);
}

bool PropertyCascade::mayOverrideExistingProperty(CSSPropertyID propertyID, const CSSValue& value)
{
    if (propertyID == CSSPropertyCustom)
        return hasCustomProperty(downcast<CSSCustomPropertyValue>(value).name());
    if (propertyID < firstLogicalGroupProperty)
        return hasNormalProperty(propertyID);

    // Apply all logical group properties if we have applied any. They may override the ones we already applied.
    // FIXME: This could check if any existing properties are actually in the same group.
    return !!m_lastIndexForLogicalGroup;
}

const PropertyCascade::Property& PropertyCascade::functionResultProperty() const
{
    ASSERT(hasNormalProperty(CSSPropertyResult));
    return normalProperty(CSSPropertyResult);
}

const PropertyCascade::Property* PropertyCascade::lastPropertyResolvingLogicalPropertyPair(CSSPropertyID propertyID, WritingMode writingMode) const
{
    ASSERT(CSSProperty::isInLogicalPropertyGroup(propertyID));

    auto pairID = [&] {
        if (CSSProperty::isDirectionAwareProperty(propertyID))
            return CSSProperty::resolveDirectionAwareProperty(propertyID, writingMode);
        return CSSProperty::unresolvePhysicalProperty(propertyID, writingMode);
    }();
    ASSERT(pairID != CSSPropertyInvalid);

    auto indexForPropertyID = logicalGroupPropertyIndex(propertyID);
    auto indexForPairID = logicalGroupPropertyIndex(pairID);
    if (indexForPropertyID > indexForPairID)
        return &logicalGroupProperty(propertyID);
    if (indexForPropertyID < indexForPairID)
        return &logicalGroupProperty(pairID);
    ASSERT(!hasLogicalGroupProperty(propertyID));
    ASSERT(!hasLogicalGroupProperty(pairID));
    return nullptr;
}

bool PropertyCascade::addMatch(const MatchedProperties& matchedProperties, Origin origin, IsImportant important)
{
    auto includePropertiesForRollback = [&] {
        if (m_rollbackScope && matchedProperties.styleScopeOrdinal > *m_rollbackScope)
            return true;
        if (origin < m_maximumOrigin)
            return true;
        if (matchedProperties.fromStyleAttribute == FromStyleAttribute::Yes)
            return false;
        return matchedProperties.cascadeLayerPriority <= *m_maximumCascadeLayerPriorityForRollback;
    };
    if (m_maximumCascadeLayerPriorityForRollback && !includePropertiesForRollback())
        return false;

    if (matchedProperties.isStartingStyle == IsStartingStyle::Yes && !m_includedProperties.types.contains(PropertyType::StartingStyle))
        return false;

    auto propertyAllowlist = matchedProperties.allowlistType;
    bool hasImportantProperties = false;

    for (auto current : matchedProperties.properties.get()) {
        if (current.isImportant())
            hasImportantProperties = true;
        if ((important == IsImportant::No && current.isImportant()) || (important == IsImportant::Yes && !current.isImportant()))
            continue;

        auto propertyID = cascadeAliasProperty(current.id());

        auto shouldIncludeProperty = [&] {
#if ENABLE(VIDEO)
            if (propertyAllowlist == PropertyAllowlist::Cue && !isValidCueStyleProperty(propertyID))
                return false;
            if (propertyAllowlist == PropertyAllowlist::CueSelector && !isValidCueSelectorStyleProperty(propertyID))
                return false;
            if (propertyAllowlist == PropertyAllowlist::CueBackground && !isValidCueBackgroundStyleProperty(propertyID))
                return false;
#endif
            if (propertyAllowlist == PropertyAllowlist::Marker && !isValidMarkerStyleProperty(propertyID))
                return false;
            if (propertyAllowlist == PropertyAllowlist::Highlight && !isValidHighlightStyleProperty(propertyID))
                return false;

            if (m_includedProperties.types.containsAll(normalPropertyTypes()))
                return true;

            if (m_includedProperties.ids.contains(propertyID))
                return true;

            if (matchedProperties.isCacheable == IsCacheable::Partially && m_includedProperties.types.contains(PropertyType::NonCacheable))
                return true;

            // If we have applied this property for some reason already we must apply anything that overrides it.
            if (mayOverrideExistingProperty(propertyID, *current.value()))
                return true;

            if (m_includedProperties.types.contains(PropertyType::AfterAnimation)) {
                if (shouldApplyAfterAnimation(current)) {
                    m_animationLayer->overriddenProperties.add(propertyID);
                    return true;
                }
                return false;
            }

            bool currentIsInherited = CSSProperty::isInheritedProperty(current.id());
            if (m_includedProperties.types.contains(PropertyType::Inherited) && currentIsInherited)
                return true;
            if (m_includedProperties.types.contains(PropertyType::ExplicitlyInherited) && isValueID(*current.value(), CSSValueInherit))
                return true;
            if (m_includedProperties.types.contains(PropertyType::NonInherited) && !currentIsInherited)
                return true;

            return false;
        }();

        if (!shouldIncludeProperty)
            continue;

        if (m_ruleRollbackDepth) {
            setDelayingForRuleRollback(propertyID, protect(*current.value()), matchedProperties, origin);
            continue;
        }

        SUPPRESS_UNCOUNTED_ARG set(propertyID, *current.value(), matchedProperties, origin);
    }

    return hasImportantProperties;
}

bool PropertyCascade::shouldApplyAfterAnimation(const StyleProperties::PropertyReference& property)
{
    ASSERT(m_animationLayer);

    auto id = property.id();
    RefPtr customProperty = dynamicDowncast<CSSCustomPropertyValue>(*property.value());

    auto animationSource = m_animationLayer->properties.getOptional(customProperty ? AnimatableCSSProperty { customProperty->name() } : id);
    if (animationSource) {
        // "Important declarations from all origins take precedence over animations."
        // https://drafts.csswg.org/css-cascade-5/#importance
        // But transitions take precedence over !important, so only allow the
        // override for properties not animated by a transition.
        return !animationSource->contains(AnimationSource::CSSTransition) && property.isImportant();
    }

    // If we are animating custom properties they may affect other properties so we need to re-resolve them.
    if (m_animationLayer->hasCustomProperties) {
        // We could check if the we are actually animating the referenced variable. Indirect cases would need to be taken into account.
        if (customProperty && customProperty->isVariableReference())
            return true;
        if (property.value()->hasSubstitutionFunctions())
            return true;
    }

    // Check for 'em' units and similar property dependencies.
    if (m_animationLayer->hasFontSize || m_animationLayer->hasLineHeight) {
        auto dependencies = protect(property.value())->computedStyleDependencies();
        if (m_animationLayer->hasFontSize && dependencies.properties.contains(CSSPropertyFontSize))
            return true;
        if (m_animationLayer->hasLineHeight && dependencies.properties.contains(CSSPropertyLineHeight))
            return true;
    }

    return false;
}

void PropertyCascade::addPositionTryFallbackProperties()
{
    ASSERT(m_positionTryFallbackProperties);

    // "All of the properties in a @position-try are applied to the box as part of the Position Fallback Origin,
    // a new cascade origin that lies between the Author Origin and the Animation Origin"
    // https://drafts.csswg.org/css-anchor-position-1/#fallback-rule
    if (m_maximumOrigin < Origin::PositionFallback)
        return;

    addMatch(*m_positionTryFallbackProperties, Origin::PositionFallback, IsImportant::No);
}

static auto& NODELETE declarationsForOrigin(const MatchResult& matchResult, PropertyCascade::Origin origin)
{
    switch (origin) {
    case PropertyCascade::Origin::UserAgent: return matchResult.userAgentDeclarations;
    case PropertyCascade::Origin::User: return matchResult.userDeclarations;
    case PropertyCascade::Origin::Author: return matchResult.authorDeclarations;
    case PropertyCascade::Origin::PositionFallback: break;
    }
    ASSERT_NOT_REACHED();
    return matchResult.authorDeclarations;
}

bool PropertyCascade::addNormalMatches(Origin origin)
{
    bool hasImportant = false;
    for (auto& matchedDeclarations : declarationsForOrigin(m_matchResult, origin))
        hasImportant |= addMatch(matchedDeclarations, origin, IsImportant::No);

    return hasImportant;
}

static bool hasImportantProperties(const StyleProperties& properties)
{
    for (auto property : properties) {
        if (property.isImportant())
            return true;
    }
    return false;
}

void PropertyCascade::addImportantMatches(Origin origin)
{
    struct ImportantMatch {
        unsigned index;
        ScopeOrdinal ordinal;
        CascadeLayerPriority layerPriority;
        FromStyleAttribute fromStyleAttribute;
    };
    Vector<ImportantMatch> importantMatches;
    bool hasMatchesFromOtherScopesOrLayers = false;

    auto& matchedDeclarations = declarationsForOrigin(m_matchResult, origin);

    for (unsigned i = 0; i < matchedDeclarations.size(); ++i) {
        const MatchedProperties& matchedProperties = matchedDeclarations[i];

        if (!hasImportantProperties(matchedProperties.properties))
            continue;

        importantMatches.append({ i, matchedProperties.styleScopeOrdinal, matchedProperties.cascadeLayerPriority, matchedProperties.fromStyleAttribute });

        if (matchedProperties.styleScopeOrdinal != ScopeOrdinal::Element || matchedProperties.cascadeLayerPriority != RuleSet::cascadeLayerPriorityForUnlayered)
            hasMatchesFromOtherScopesOrLayers = true;
    }

    if (importantMatches.isEmpty())
        return;

    if (hasMatchesFromOtherScopesOrLayers) {
        // Match results are sorted in reverse tree context order so this is not needed for normal properties.
        std::ranges::stable_sort(importantMatches, [](auto& a, auto& b) {
            // For !important properties a later shadow tree wins.
            if (a.ordinal != b.ordinal)
                return a.ordinal < b.ordinal;
            // Lower priority layer wins, except if style attribute is involved.
            if (a.fromStyleAttribute != b.fromStyleAttribute)
                return a.fromStyleAttribute == FromStyleAttribute::No;
            return a.layerPriority > b.layerPriority;
        });
    }

    for (auto& match : importantMatches)
        addMatch(matchedDeclarations[match.index], origin, IsImportant::Yes);
}

void PropertyCascade::sortLogicalGroupPropertyIDs()
{
    size_t endIndex = 0;
    for (uint16_t id = m_lowestSeenLogicalGroupProperty; id <= m_highestSeenLogicalGroupProperty; ++id) {
        auto propertyID = static_cast<CSSPropertyID>(id);
        if (hasLogicalGroupProperty(propertyID))
            m_logicalGroupPropertyIDs[endIndex++] = propertyID;
    }
    m_seenLogicalGroupPropertyCount = endIndex;
    auto logicalGroupPropertyIDs = std::span { m_logicalGroupPropertyIDs }.first(endIndex);
    std::ranges::sort(logicalGroupPropertyIDs, [&](auto id1, auto id2) {
        return logicalGroupPropertyIndex(id1) < logicalGroupPropertyIndex(id2);
    });
}

ValueOrReference<HashSet<AnimatableCSSProperty>> PropertyCascade::overriddenAnimatedProperties() const
{
    if (m_animationLayer)
        return m_animationLayer->overriddenProperties;
    return { };
}

}
}
