/*
 * Copyright (C) 2007 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.
 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "ImplicitAnimation.h"

#include "CSSAnimationControllerPrivate.h"
#include "CSSPropertyAnimation.h"
#include "CompositeAnimation.h"
#if PLATFORM(IOS_FAMILY)
#include "ContentChangeObserver.h"
#endif
#include "EventNames.h"
#include "GeometryUtilities.h"
#include "KeyframeAnimation.h"
#include "RenderBox.h"
#include "StylePendingResources.h"

namespace WebCore {

ImplicitAnimation::ImplicitAnimation(const Animation& transition, CSSPropertyID animatingProperty, Element& element, CompositeAnimation& compositeAnimation, const RenderStyle& fromStyle)
    : AnimationBase(transition, element, compositeAnimation)
    , m_fromStyle(RenderStyle::clonePtr(fromStyle))
    , m_transitionProperty(transition.property())
    , m_animatingProperty(animatingProperty)
{
#if PLATFORM(IOS_FAMILY)
    element.document().contentChangeObserver().didAddTransition(element, transition);
#endif
    ASSERT(animatingProperty != CSSPropertyInvalid);
}

ImplicitAnimation::~ImplicitAnimation()
{
#if PLATFORM(IOS_FAMILY)
    if (auto* element = this->element())
        element->document().contentChangeObserver().didRemoveTransition(*element, m_animatingProperty);
#endif
    // // Make sure to tell the renderer that we are ending. This will make sure any accelerated animations are removed.
    if (!postActive())
        endAnimation();
}

bool ImplicitAnimation::shouldSendEventForListener(Document::ListenerType inListenerType) const
{
    return element()->document().hasListenerType(inListenerType);
}

OptionSet<AnimateChange> ImplicitAnimation::animate(CompositeAnimation& compositeAnimation, const RenderStyle& targetStyle, std::unique_ptr<RenderStyle>& animatedStyle)
{
    // If we get this far and the animation is done, it means we are cleaning up a just finished animation.
    // So just return. Everything is already all cleaned up.
    if (postActive())
        return { };

    AnimationState oldState = state();

    // Reset to start the transition if we are new
    if (isNew())
        reset(targetStyle, compositeAnimation);

    // Run a cycle of animation.
    // We know we will need a new render style, so make one if needed
    if (!animatedStyle)
        animatedStyle = RenderStyle::clonePtr(targetStyle);

    CSSPropertyAnimation::blendProperties(this, m_animatingProperty, animatedStyle.get(), m_fromStyle.get(), m_toStyle.get(), progress());
    // FIXME: we also need to detect cases where we have to software animate for other reasons,
    // such as a child using inheriting the transform. https://bugs.webkit.org/show_bug.cgi?id=23902

    // Fire the start timeout if needed
    fireAnimationEventsIfNeeded();

    OptionSet<AnimateChange> change(AnimateChange::StyleBlended);
    if (state() != oldState)
        change.add(AnimateChange::StateChange);

    if ((isPausedState(oldState) || isRunningState(oldState)) != (isPausedState(state()) || isRunningState(state())))
        change.add(AnimateChange::RunningStateChange);

    return change;
}

void ImplicitAnimation::getAnimatedStyle(std::unique_ptr<RenderStyle>& animatedStyle)
{
    if (!animatedStyle)
        animatedStyle = RenderStyle::clonePtr(*m_toStyle);

    CSSPropertyAnimation::blendProperties(this, m_animatingProperty, animatedStyle.get(), m_fromStyle.get(), m_toStyle.get(), progress());
}

bool ImplicitAnimation::computeExtentOfTransformAnimation(LayoutRect& bounds) const
{
    ASSERT(hasStyle());

    if (!is<RenderBox>(renderer()))
        return true; // Non-boxes don't get transformed;

    ASSERT(m_animatingProperty == CSSPropertyTransform);

    RenderBox& box = downcast<RenderBox>(*renderer());
    FloatRect rendererBox = snapRectToDevicePixels(box.borderBoxRect(), box.document().deviceScaleFactor());

    LayoutRect startBounds = bounds;
    LayoutRect endBounds = bounds;

    if (transformFunctionListsMatch()) {
        if (!computeTransformedExtentViaTransformList(rendererBox, *m_fromStyle, startBounds))
            return false;

        if (!computeTransformedExtentViaTransformList(rendererBox, *m_toStyle, endBounds))
            return false;
    } else {
        if (!computeTransformedExtentViaMatrix(rendererBox, *m_fromStyle, startBounds))
            return false;

        if (!computeTransformedExtentViaMatrix(rendererBox, *m_toStyle, endBounds))
            return false;
    }

    bounds = unionRect(startBounds, endBounds);
    return true;
}

bool ImplicitAnimation::affectsAcceleratedProperty() const
{
    return CSSPropertyAnimation::animationOfPropertyIsAccelerated(m_animatingProperty);
}

bool ImplicitAnimation::startAnimation(double timeOffset)
{
    if (auto* renderer = this->renderer())
        return renderer->startTransition(timeOffset, m_animatingProperty, m_fromStyle.get(), m_toStyle.get());
    return false;
}

void ImplicitAnimation::pauseAnimation(double timeOffset)
{
    if (auto* renderer = this->renderer())
        renderer->transitionPaused(timeOffset, m_animatingProperty);
    // Restore the original (unanimated) style
    if (!paused())
        setNeedsStyleRecalc(element());
}

void ImplicitAnimation::clear()
{
#if PLATFORM(IOS_FAMILY)
    if (auto* element = this->element())
        element->document().contentChangeObserver().didRemoveTransition(*element, m_animatingProperty);
#endif
    AnimationBase::clear();
}

void ImplicitAnimation::endAnimation(bool)
{
    if (auto* renderer = this->renderer())
        renderer->transitionFinished(m_animatingProperty);
}

void ImplicitAnimation::onAnimationEnd(double elapsedTime)
{
#if PLATFORM(IOS_FAMILY)
    if (auto* element = this->element())
        element->document().contentChangeObserver().didFinishTransition(*element, m_animatingProperty);
#endif
    // If we have a keyframe animation on this property, this transition is being overridden. The keyframe
    // animation keeps an unanimated style in case a transition starts while the keyframe animation is
    // running. But now that the transition has completed, we need to update this style with its new
    // destination. If we didn't, the next time through we would think a transition had started
    // (comparing the old unanimated style with the new final style of the transition).
    if (auto* animation = m_compositeAnimation->animationForProperty(m_animatingProperty))
        animation->setUnanimatedStyle(RenderStyle::clonePtr(*m_toStyle));

    sendTransitionEvent(eventNames().transitionendEvent, elapsedTime);
    endAnimation();
}

bool ImplicitAnimation::sendTransitionEvent(const AtomString& eventType, double elapsedTime)
{
    if (eventType == eventNames().transitionendEvent) {
        Document::ListenerType listenerType = Document::TRANSITIONEND_LISTENER;

        if (shouldSendEventForListener(listenerType)) {
            String propertyName = getPropertyNameString(m_animatingProperty);

            // Dispatch the event
            auto element = makeRefPtr(this->element());

            ASSERT(!element || element->document().pageCacheState() == Document::NotInPageCache);
            if (!element)
                return false;

            // Schedule event handling
            m_compositeAnimation->animationController().addEventToDispatch(*element, eventType, propertyName, elapsedTime);

            // Restore the original (unanimated) style
            if (eventType == eventNames().transitionendEvent && element->renderer())
                setNeedsStyleRecalc(element.get());

            return true; // Did dispatch an event
        }
    }

    return false; // Didn't dispatch an event
}

void ImplicitAnimation::reset(const RenderStyle& to, CompositeAnimation& compositeAnimation)
{
    ASSERT(m_fromStyle);

    m_toStyle = RenderStyle::clonePtr(to);

    if (element())
        Style::loadPendingResources(*m_toStyle, element()->document(), element());

    // Restart the transition.
    if (m_fromStyle && m_toStyle && !compositeAnimation.isSuspended())
        updateStateMachine(AnimationStateInput::RestartAnimation, -1);

    // Set the transform animation list.
    validateTransformFunctionList();
    checkForMatchingFilterFunctionLists();
#if ENABLE(FILTERS_LEVEL_2)
    checkForMatchingBackdropFilterFunctionLists();
#endif
    checkForMatchingColorFilterFunctionLists();
}

void ImplicitAnimation::setOverridden(bool b)
{
    if (b == m_overridden)
        return;

    m_overridden = b;
    updateStateMachine(m_overridden ? AnimationStateInput::PauseOverride : AnimationStateInput::ResumeOverride, -1);
}

bool ImplicitAnimation::affectsProperty(CSSPropertyID property) const
{
    return (m_animatingProperty == property);
}

bool ImplicitAnimation::isTargetPropertyEqual(CSSPropertyID prop, const RenderStyle* targetStyle)
{
    // We can get here for a transition that has not started yet. This would make m_toStyle unset and null.
    // So we check that here (see <https://bugs.webkit.org/show_bug.cgi?id=26706>)
    if (!m_toStyle)
        return false;
    return CSSPropertyAnimation::propertiesEqual(prop, m_toStyle.get(), targetStyle);
}

void ImplicitAnimation::blendPropertyValueInStyle(CSSPropertyID prop, RenderStyle* currentStyle)
{
    // We should never add a transition with a 0 duration and delay. But if we ever did
    // it would have a null toStyle. So just in case, let's check that here. (See
    // <https://bugs.webkit.org/show_bug.cgi?id=24787>
    if (!m_toStyle)
        return;

    CSSPropertyAnimation::blendProperties(this, prop, currentStyle, m_fromStyle.get(), m_toStyle.get(), progress());
}

void ImplicitAnimation::validateTransformFunctionList()
{
    m_transformFunctionListsMatch = false;

    if (!m_fromStyle || !m_toStyle)
        return;

    const TransformOperations* val = &m_fromStyle->transform();
    const TransformOperations* toVal = &m_toStyle->transform();

    if (val->operations().isEmpty())
        val = toVal;

    if (val->operations().isEmpty())
        return;

    // An empty transform list matches anything.
    if (val != toVal && !toVal->operations().isEmpty() && !val->operationsMatch(*toVal))
        return;

    // Transform lists match.
    m_transformFunctionListsMatch = true;
}

static bool filterOperationsMatch(const FilterOperations* fromOperations, const FilterOperations& toOperations)
{
    if (fromOperations->operations().isEmpty())
        fromOperations = &toOperations;

    if (fromOperations->operations().isEmpty())
        return false;

    if (fromOperations != &toOperations && !toOperations.operations().isEmpty() && !fromOperations->operationsMatch(toOperations))
        return false;

    return true;
}

void ImplicitAnimation::checkForMatchingFilterFunctionLists()
{
    m_filterFunctionListsMatch = false;

    if (!m_fromStyle || !m_toStyle)
        return;

    m_filterFunctionListsMatch = filterOperationsMatch(&m_fromStyle->filter(), m_toStyle->filter());
}

#if ENABLE(FILTERS_LEVEL_2)
void ImplicitAnimation::checkForMatchingBackdropFilterFunctionLists()
{
    m_backdropFilterFunctionListsMatch = false;

    if (!m_fromStyle || !m_toStyle)
        return;

    m_backdropFilterFunctionListsMatch = filterOperationsMatch(&m_fromStyle->backdropFilter(), m_toStyle->backdropFilter());
}
#endif

void ImplicitAnimation::checkForMatchingColorFilterFunctionLists()
{
    m_filterFunctionListsMatch = false;

    if (!m_fromStyle || !m_toStyle)
        return;

    m_colorFilterFunctionListsMatch = filterOperationsMatch(&m_fromStyle->appleColorFilter(), m_toStyle->appleColorFilter());
}

Optional<Seconds> ImplicitAnimation::timeToNextService()
{
    Optional<Seconds> t = AnimationBase::timeToNextService();
    if (!t || t.value() != 0_s || preActive())
        return t;

    // A return value of 0 means we need service. But if this is an accelerated animation we
    // only need service at the end of the transition.
    if (CSSPropertyAnimation::animationOfPropertyIsAccelerated(m_animatingProperty) && isAccelerated()) {
        bool isLooping;
        getTimeToNextEvent(t.value(), isLooping);
    }
    return t;
}

} // namespace WebCore
