/*
 * Copyright (C) 2013 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. ``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
 * 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.
 */

#import "config.h"
#import "WebVideoFullscreenControllerAVKit.h"

#if PLATFORM(IOS_FAMILY)

#import "FrameView.h"
#import "HTMLVideoElement.h"
#import "Logging.h"
#import "MediaSelectionOption.h"
#import "PlaybackSessionInterfaceAVKit.h"
#import "PlaybackSessionModelMediaElement.h"
#import "RenderVideo.h"
#import "TimeRanges.h"
#import "VideoFullscreenChangeObserver.h"
#import "VideoFullscreenInterfaceAVKit.h"
#import "VideoFullscreenModelVideoElement.h"
#import "WebCoreThreadRun.h"
#import <QuartzCore/CoreAnimation.h>
#import <UIKit/UIView.h>
#import <pal/ios/UIKitSoftLink.h>
#import <pal/spi/cocoa/QuartzCoreSPI.h>

using namespace WebCore;

#if !(ENABLE(VIDEO_PRESENTATION_MODE) && HAVE(AVKIT))

@implementation WebVideoFullscreenController
- (void)setVideoElement:(NakedPtr<WebCore::HTMLVideoElement>)videoElement
{
    UNUSED_PARAM(videoElement);
}

- (NakedPtr<WebCore::HTMLVideoElement>)videoElement
{
    return nullptr;
}

- (void)enterFullscreen:(UIView *)view mode:(WebCore::HTMLMediaElementEnums::VideoFullscreenMode)mode
{
    UNUSED_PARAM(view);
    UNUSED_PARAM(mode);
}

- (void)requestHideAndExitFullscreen
{
}

- (void)exitFullscreen
{
}
@end

#else

static IntRect elementRectInWindow(HTMLVideoElement* videoElement)
{
    if (!videoElement)
        return { };
    auto* renderer = videoElement->renderer();
    auto* view = videoElement->document().view();
    if (!renderer || !view)
        return { };
    return view->convertToContainingWindow(renderer->absoluteBoundingBoxRect());
}

class VideoFullscreenControllerContext;

@interface WebVideoFullscreenController (delegate)
-(void)didFinishFullscreen:(VideoFullscreenControllerContext*)context;
@end

class VideoFullscreenControllerContext final
    : private VideoFullscreenModel
    , private VideoFullscreenModelClient
    , private VideoFullscreenChangeObserver
    , private PlaybackSessionModel
    , private PlaybackSessionModelClient
    , public ThreadSafeRefCounted<VideoFullscreenControllerContext> {

public:
    static Ref<VideoFullscreenControllerContext> create()
    {
        return adoptRef(*new VideoFullscreenControllerContext);
    }

    ~VideoFullscreenControllerContext();

    void setController(WebVideoFullscreenController* controller) { m_controller = controller; }
    void setUpFullscreen(HTMLVideoElement&, UIView *, HTMLMediaElementEnums::VideoFullscreenMode);
    void exitFullscreen();
    void requestHideAndExitFullscreen();
    void invalidate();

private:
    VideoFullscreenControllerContext() { }

    // VideoFullscreenChangeObserver
    void requestUpdateInlineRect() final;
    void requestVideoContentLayer() final;
    void returnVideoContentLayer() final;
    void didSetupFullscreen() final;
    void didEnterFullscreen(const FloatSize&) final { }
    void willExitFullscreen() final;
    void didExitFullscreen() final;
    void didCleanupFullscreen() final;
    void fullscreenMayReturnToInline() final;

    // VideoFullscreenModelClient
    void hasVideoChanged(bool) override;
    void videoDimensionsChanged(const FloatSize&) override;

    // PlaybackSessionModel
    void addClient(PlaybackSessionModelClient&) override;
    void removeClient(PlaybackSessionModelClient&) override;
    void play() override;
    void pause() override;
    void togglePlayState() override;
    void beginScrubbing() override;
    void endScrubbing() override;
    void seekToTime(double, double, double) override;
    void fastSeek(double time) override;
    void beginScanningForward() override;
    void beginScanningBackward() override;
    void endScanning() override;
    void selectAudioMediaOption(uint64_t) override;
    void selectLegibleMediaOption(uint64_t) override;
    double duration() const override;
    double playbackStartedTime() const override { return 0; }
    double currentTime() const override;
    double bufferedTime() const override;
    bool isPlaying() const override;
    bool isScrubbing() const override { return false; }
    float playbackRate() const override;
    Ref<TimeRanges> seekableRanges() const override;
    double seekableTimeRangesLastModifiedTime() const override;
    double liveUpdateInterval() const override;
    bool canPlayFastReverse() const override;
    Vector<MediaSelectionOption> audioMediaSelectionOptions() const override;
    uint64_t audioMediaSelectedIndex() const override;
    Vector<MediaSelectionOption> legibleMediaSelectionOptions() const override;
    uint64_t legibleMediaSelectedIndex() const override;
    bool externalPlaybackEnabled() const override;
    ExternalPlaybackTargetType externalPlaybackTargetType() const override;
    String externalPlaybackLocalizedDeviceName() const override;
    bool wirelessVideoPlaybackDisabled() const override;
    void togglePictureInPicture() override { }
    void toggleMuted() override;
    void setMuted(bool) final;
    void setVolume(double) final;
    void setPlayingOnSecondScreen(bool) final;

    // PlaybackSessionModelClient
    void durationChanged(double) override;
    void currentTimeChanged(double currentTime, double anchorTime) override;
    void bufferedTimeChanged(double) override;
    void rateChanged(bool isPlaying, float playbackRate) override;
    void seekableRangesChanged(const TimeRanges&, double lastModifiedTime, double liveUpdateInterval) override;
    void canPlayFastReverseChanged(bool) override;
    void audioMediaSelectionOptionsChanged(const Vector<MediaSelectionOption>& options, uint64_t selectedIndex) override;
    void legibleMediaSelectionOptionsChanged(const Vector<MediaSelectionOption>& options, uint64_t selectedIndex) override;
    void externalPlaybackChanged(bool enabled, PlaybackSessionModel::ExternalPlaybackTargetType, const String& localizedDeviceName) override;
    void wirelessVideoPlaybackDisabledChanged(bool) override;
    void mutedChanged(bool) override;
    void volumeChanged(double) override;

    // VideoFullscreenModel
    void addClient(VideoFullscreenModelClient&) override;
    void removeClient(VideoFullscreenModelClient&) override;
    void requestFullscreenMode(HTMLMediaElementEnums::VideoFullscreenMode, bool finishedWithMedia = false) override;
    void setVideoLayerFrame(FloatRect) override;
    void setVideoLayerGravity(MediaPlayerEnums::VideoGravity) override;
    void fullscreenModeChanged(HTMLMediaElementEnums::VideoFullscreenMode) override;
    bool hasVideo() const override;
    FloatSize videoDimensions() const override;
    bool isMuted() const override;
    double volume() const override;
    bool isPictureInPictureSupported() const override;
    bool isPictureInPictureActive() const override;
    void willEnterPictureInPicture() final;
    void didEnterPictureInPicture() final;
    void failedToEnterPictureInPicture() final;
    void willExitPictureInPicture() final;
    void didExitPictureInPicture() final;

    HashSet<PlaybackSessionModelClient*> m_playbackClients;
    HashSet<VideoFullscreenModelClient*> m_fullscreenClients;
    RefPtr<VideoFullscreenInterfaceAVKit> m_interface;
    RefPtr<VideoFullscreenModelVideoElement> m_fullscreenModel;
    RefPtr<PlaybackSessionModelMediaElement> m_playbackModel;
    RefPtr<HTMLVideoElement> m_videoElement;
    RetainPtr<UIView> m_videoFullscreenView;
    RetainPtr<WebVideoFullscreenController> m_controller;
};

VideoFullscreenControllerContext::~VideoFullscreenControllerContext()
{
    auto notifyClientsModelWasDestroyed = [this] {
        while (!m_playbackClients.isEmpty())
            (*m_playbackClients.begin())->modelDestroyed();
        while (!m_fullscreenClients.isEmpty())
            (*m_fullscreenClients.begin())->modelDestroyed();
    };
    if (isUIThread()) {
        WebThreadLock();
        notifyClientsModelWasDestroyed();
        m_playbackModel = nullptr;
        m_fullscreenModel = nullptr;
    } else
        dispatch_sync(dispatch_get_main_queue(), WTFMove(notifyClientsModelWasDestroyed));
}

#pragma mark VideoFullscreenChangeObserver

void VideoFullscreenControllerContext::requestUpdateInlineRect()
{
#if PLATFORM(IOS_FAMILY)
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] () mutable {
        IntRect clientRect = elementRectInWindow(m_videoElement.get());
        dispatch_async(dispatch_get_main_queue(), [protectedThis = WTFMove(protectedThis), this, clientRect] {
            m_interface->setInlineRect(clientRect, clientRect != IntRect(0, 0, 0, 0));
        });
    });
#else
    ASSERT_NOT_REACHED();
#endif
}

void VideoFullscreenControllerContext::requestVideoContentLayer()
{
#if PLATFORM(IOS_FAMILY)
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, videoFullscreenLayer = retainPtr([m_videoFullscreenView layer])] () mutable {
        [videoFullscreenLayer setBackgroundColor:cachedCGColor(WebCore::Color::transparentBlack)];
        m_fullscreenModel->setVideoFullscreenLayer(videoFullscreenLayer.get(), [protectedThis = WTFMove(protectedThis), this] () mutable {
            dispatch_async(dispatch_get_main_queue(), [protectedThis = WTFMove(protectedThis), this] {
                if (!m_interface)
                    return;

                m_interface->setHasVideoContentLayer(true);
            });
        });
    });
#else
    ASSERT_NOT_REACHED();
#endif
}

void VideoFullscreenControllerContext::returnVideoContentLayer()
{
#if PLATFORM(IOS_FAMILY)
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, videoFullscreenLayer = retainPtr([m_videoFullscreenView layer])] () mutable {
        [videoFullscreenLayer setBackgroundColor:cachedCGColor(WebCore::Color::transparentBlack)];
        m_fullscreenModel->setVideoFullscreenLayer(nil, [protectedThis = WTFMove(protectedThis), this] () mutable {
            dispatch_async(dispatch_get_main_queue(), [protectedThis = WTFMove(protectedThis), this] {
                if (!m_interface)
                    return;

                m_interface->setHasVideoContentLayer(false);
            });
        });
    });
#else
    ASSERT_NOT_REACHED();
#endif
}

void VideoFullscreenControllerContext::didSetupFullscreen()
{
    ASSERT(isUIThread());
#if PLATFORM(IOS_FAMILY)
    dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), this] {
        m_interface->enterFullscreen();
    });
#else
    WebThreadRun([protectedThis = makeRefPtr(this), this, videoFullscreenLayer = retainPtr([m_videoFullscreenView layer])] () mutable {
        [videoFullscreenLayer setBackgroundColor:cachedCGColor(WebCore::Color::transparentBlack)];
        m_fullscreenModel->setVideoFullscreenLayer(videoFullscreenLayer.get(), [protectedThis = WTFMove(protectedThis), this] () mutable {
            dispatch_async(dispatch_get_main_queue(), [protectedThis = WTFMove(protectedThis), this] {
                m_interface->enterFullscreen();
            });
        });
    });
#endif
}

void VideoFullscreenControllerContext::willExitFullscreen()
{
#if PLATFORM(WATCHOS)
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] () mutable {
        m_fullscreenModel->willExitFullscreen();
    });
#endif
}

void VideoFullscreenControllerContext::didExitFullscreen()
{
    ASSERT(isUIThread());
#if PLATFORM(IOS_FAMILY)
    dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), this] {
        m_interface->cleanupFullscreen();
    });
#else
    WebThreadRun([protectedThis = makeRefPtr(this), this] () mutable {
        m_fullscreenModel->setVideoFullscreenLayer(nil, [protectedThis = WTFMove(protectedThis), this] () mutable {
            dispatch_async(dispatch_get_main_queue(), [protectedThis = WTFMove(protectedThis), this] {
                m_interface->cleanupFullscreen();
            });
        });
    });
#endif
}

void VideoFullscreenControllerContext::didCleanupFullscreen()
{
    ASSERT(isUIThread());
    m_interface->setVideoFullscreenModel(nullptr);
    m_interface->setVideoFullscreenChangeObserver(nullptr);
    m_interface = nullptr;
    m_videoFullscreenView = nil;

    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        m_fullscreenModel->setVideoFullscreenLayer(nil);
        m_fullscreenModel->setVideoElement(nullptr);
        m_playbackModel->setMediaElement(nullptr);
        m_playbackModel->removeClient(*this);
        m_fullscreenModel->removeClient(*this);
        m_fullscreenModel = nullptr;
        m_videoElement = nullptr;

        [m_controller didFinishFullscreen:this];
    });
}

void VideoFullscreenControllerContext::fullscreenMayReturnToInline()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] () mutable {
        IntRect clientRect = elementRectInWindow(m_videoElement.get());
        dispatch_async(dispatch_get_main_queue(), [protectedThis = WTFMove(protectedThis), this, clientRect] {
            m_interface->preparedToReturnToInline(true, clientRect);
        });
    });
}

#pragma mark PlaybackSessionModelClient

void VideoFullscreenControllerContext::durationChanged(double duration)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), duration] {
            protectedThis->durationChanged(duration);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->durationChanged(duration);
}

void VideoFullscreenControllerContext::currentTimeChanged(double currentTime, double anchorTime)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), currentTime, anchorTime] {
            protectedThis->currentTimeChanged(currentTime, anchorTime);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->currentTimeChanged(currentTime, anchorTime);
}

void VideoFullscreenControllerContext::bufferedTimeChanged(double bufferedTime)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), bufferedTime] {
            protectedThis->bufferedTimeChanged(bufferedTime);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->bufferedTimeChanged(bufferedTime);
}

void VideoFullscreenControllerContext::rateChanged(bool isPlaying, float playbackRate)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), isPlaying, playbackRate] {
            protectedThis->rateChanged(isPlaying, playbackRate);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->rateChanged(isPlaying, playbackRate);
}

void VideoFullscreenControllerContext::hasVideoChanged(bool hasVideo)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), hasVideo] {
            protectedThis->hasVideoChanged(hasVideo);
        });
        return;
    }

    for (auto& client : m_fullscreenClients)
        client->hasVideoChanged(hasVideo);
}

void VideoFullscreenControllerContext::videoDimensionsChanged(const FloatSize& videoDimensions)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), videoDimensions = videoDimensions] {
            protectedThis->videoDimensionsChanged(videoDimensions);
        });
        return;
    }

    for (auto& client : m_fullscreenClients)
        client->videoDimensionsChanged(videoDimensions);
}

void VideoFullscreenControllerContext::seekableRangesChanged(const TimeRanges& timeRanges, double lastModifiedTime, double liveUpdateInterval)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), platformTimeRanges = timeRanges.ranges(), lastModifiedTime, liveUpdateInterval] {
            protectedThis->seekableRangesChanged(TimeRanges::create(platformTimeRanges), lastModifiedTime, liveUpdateInterval);
        });
        return;
    }

    for (auto &client : m_playbackClients)
        client->seekableRangesChanged(timeRanges, lastModifiedTime, liveUpdateInterval);
}

void VideoFullscreenControllerContext::canPlayFastReverseChanged(bool canPlayFastReverse)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), canPlayFastReverse] {
            protectedThis->canPlayFastReverseChanged(canPlayFastReverse);
        });
        return;
    }

    for (auto &client : m_playbackClients)
        client->canPlayFastReverseChanged(canPlayFastReverse);
}

static Vector<MediaSelectionOption> isolatedCopy(const Vector<MediaSelectionOption>& options)
{
    Vector<MediaSelectionOption> optionsCopy;
    optionsCopy.reserveInitialCapacity(options.size());
    for (auto& option : options)
        optionsCopy.uncheckedAppend({ option.displayName.isolatedCopy(), option.type });
    return optionsCopy;
}

void VideoFullscreenControllerContext::audioMediaSelectionOptionsChanged(const Vector<MediaSelectionOption>& options, uint64_t selectedIndex)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), options = isolatedCopy(options), selectedIndex] {
            protectedThis->audioMediaSelectionOptionsChanged(options, selectedIndex);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->audioMediaSelectionOptionsChanged(options, selectedIndex);
}

void VideoFullscreenControllerContext::legibleMediaSelectionOptionsChanged(const Vector<MediaSelectionOption>& options, uint64_t selectedIndex)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), options = isolatedCopy(options), selectedIndex] {
            protectedThis->legibleMediaSelectionOptionsChanged(options, selectedIndex);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->legibleMediaSelectionOptionsChanged(options, selectedIndex);
}

void VideoFullscreenControllerContext::externalPlaybackChanged(bool enabled, PlaybackSessionModel::ExternalPlaybackTargetType type, const String& localizedDeviceName)
{
    if (WebThreadIsCurrent()) {
        callOnMainThread([protectedThis = makeRef(*this), this, enabled, type, localizedDeviceName = localizedDeviceName.isolatedCopy()] {
            for (auto& client : m_playbackClients)
                client->externalPlaybackChanged(enabled, type, localizedDeviceName);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->externalPlaybackChanged(enabled, type, localizedDeviceName);
}

void VideoFullscreenControllerContext::wirelessVideoPlaybackDisabledChanged(bool disabled)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), disabled] {
            protectedThis->wirelessVideoPlaybackDisabledChanged(disabled);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->wirelessVideoPlaybackDisabledChanged(disabled);
}

void VideoFullscreenControllerContext::mutedChanged(bool muted)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), muted] {
            protectedThis->mutedChanged(muted);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->mutedChanged(muted);
}

void VideoFullscreenControllerContext::volumeChanged(double volume)
{
    if (WebThreadIsCurrent()) {
        dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), volume] {
            protectedThis->volumeChanged(volume);
        });
        return;
    }

    for (auto& client : m_playbackClients)
        client->volumeChanged(volume);
}
#pragma mark VideoFullscreenModel

void VideoFullscreenControllerContext::addClient(VideoFullscreenModelClient& client)
{
    ASSERT(isUIThread());
    ASSERT(!m_fullscreenClients.contains(&client));
    m_fullscreenClients.add(&client);
}

void VideoFullscreenControllerContext::removeClient(VideoFullscreenModelClient& client)
{
    ASSERT(isUIThread());
    ASSERT(m_fullscreenClients.contains(&client));
    m_fullscreenClients.remove(&client);
}

void VideoFullscreenControllerContext::requestFullscreenMode(HTMLMediaElementEnums::VideoFullscreenMode mode, bool finishedWithMedia)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, mode, finishedWithMedia] {
        if (m_fullscreenModel)
            m_fullscreenModel->requestFullscreenMode(mode, finishedWithMedia);
    });
}

void VideoFullscreenControllerContext::setVideoLayerFrame(FloatRect frame)
{
    ASSERT(isUIThread());
    RetainPtr<CALayer> videoFullscreenLayer = [m_videoFullscreenView layer];
    [videoFullscreenLayer setSublayerTransform:[videoFullscreenLayer transform]];

    dispatchAsyncOnMainThreadWithWebThreadLockIfNeeded([protectedThis = makeRefPtr(this), this, frame, videoFullscreenLayer = WTFMove(videoFullscreenLayer)] {
        [CATransaction begin];
        [CATransaction setDisableActions:YES];
        [CATransaction setAnimationDuration:0];

        [videoFullscreenLayer setSublayerTransform:CATransform3DIdentity];

        if (m_fullscreenModel)
            m_fullscreenModel->setVideoLayerFrame(frame);
        [CATransaction commit];
    });
}

void VideoFullscreenControllerContext::setVideoLayerGravity(MediaPlayerEnums::VideoGravity videoGravity)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, videoGravity] {
        if (m_fullscreenModel)
            m_fullscreenModel->setVideoLayerGravity(videoGravity);
    });
}

void VideoFullscreenControllerContext::fullscreenModeChanged(HTMLMediaElementEnums::VideoFullscreenMode mode)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, mode] {
        if (m_fullscreenModel)
            m_fullscreenModel->fullscreenModeChanged(mode);
    });
}

bool VideoFullscreenControllerContext::hasVideo() const
{
    ASSERT(isUIThread());
    return m_fullscreenModel ? m_fullscreenModel->hasVideo() : false;
}

bool VideoFullscreenControllerContext::isMuted() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->isMuted() : false;
}

double VideoFullscreenControllerContext::volume() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->volume() : 0;
}

bool VideoFullscreenControllerContext::isPictureInPictureActive() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->isPictureInPictureActive() : false;
}

bool VideoFullscreenControllerContext::isPictureInPictureSupported() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->isPictureInPictureSupported() : false;
}

void VideoFullscreenControllerContext::willEnterPictureInPicture()
{
    ASSERT(isUIThread());
    for (auto* client : m_fullscreenClients)
        client->willEnterPictureInPicture();
}

void VideoFullscreenControllerContext::didEnterPictureInPicture()
{
    ASSERT(isUIThread());
    for (auto* client : m_fullscreenClients)
        client->didEnterPictureInPicture();
}

void VideoFullscreenControllerContext::failedToEnterPictureInPicture()
{
    ASSERT(isUIThread());
    for (auto* client : m_fullscreenClients)
        client->failedToEnterPictureInPicture();
}

void VideoFullscreenControllerContext::willExitPictureInPicture()
{
    ASSERT(isUIThread());
    for (auto* client : m_fullscreenClients)
        client->willExitPictureInPicture();
}

void VideoFullscreenControllerContext::didExitPictureInPicture()
{
    ASSERT(isUIThread());
    for (auto* client : m_fullscreenClients)
        client->didExitPictureInPicture();
}

FloatSize VideoFullscreenControllerContext::videoDimensions() const
{
    ASSERT(isUIThread());
    return m_fullscreenModel ? m_fullscreenModel->videoDimensions() : FloatSize();
}

#pragma mark - PlaybackSessionModel

void VideoFullscreenControllerContext::addClient(PlaybackSessionModelClient& client)
{
    ASSERT(!m_playbackClients.contains(&client));
    m_playbackClients.add(&client);
}

void VideoFullscreenControllerContext::removeClient(PlaybackSessionModelClient& client)
{
    ASSERT(m_playbackClients.contains(&client));
    m_playbackClients.remove(&client);
}

void VideoFullscreenControllerContext::play()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->play();
    });
}

void VideoFullscreenControllerContext::pause()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->pause();
    });
}

void VideoFullscreenControllerContext::togglePlayState()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->togglePlayState();
    });
}

void VideoFullscreenControllerContext::toggleMuted()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->toggleMuted();
    });
}

void VideoFullscreenControllerContext::setMuted(bool muted)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, muted] {
        if (m_playbackModel)
            m_playbackModel->setMuted(muted);
    });
}

void VideoFullscreenControllerContext::setVolume(double volume)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, volume] {
        if (m_playbackModel)
            m_playbackModel->setVolume(volume);
    });
}

void VideoFullscreenControllerContext::setPlayingOnSecondScreen(bool value)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, value] {
        if (m_playbackModel)
            m_playbackModel->setPlayingOnSecondScreen(value);
    });
}

void VideoFullscreenControllerContext::beginScrubbing()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->beginScrubbing();
    });
}

void VideoFullscreenControllerContext::endScrubbing()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->endScrubbing();
    });
}

void VideoFullscreenControllerContext::seekToTime(double time, double toleranceBefore, double toleranceAfter)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, time, toleranceBefore, toleranceAfter] {
        if (m_playbackModel)
            m_playbackModel->seekToTime(time, toleranceBefore, toleranceAfter);
    });
}

void VideoFullscreenControllerContext::fastSeek(double time)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, time] {
        if (m_playbackModel)
            m_playbackModel->fastSeek(time);
    });
}

void VideoFullscreenControllerContext::beginScanningForward()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->beginScanningForward();
    });
}

void VideoFullscreenControllerContext::beginScanningBackward()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->beginScanningBackward();
    });
}

void VideoFullscreenControllerContext::endScanning()
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this] {
        if (m_playbackModel)
            m_playbackModel->endScanning();
    });
}

void VideoFullscreenControllerContext::selectAudioMediaOption(uint64_t index)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, index] {
        if (m_playbackModel)
            m_playbackModel->selectAudioMediaOption(index);
    });
}

void VideoFullscreenControllerContext::selectLegibleMediaOption(uint64_t index)
{
    ASSERT(isUIThread());
    WebThreadRun([protectedThis = makeRefPtr(this), this, index] {
        if (m_playbackModel)
            m_playbackModel->selectLegibleMediaOption(index);
    });
}

double VideoFullscreenControllerContext::duration() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->duration() : 0;
}

double VideoFullscreenControllerContext::currentTime() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->currentTime() : 0;
}

double VideoFullscreenControllerContext::bufferedTime() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->bufferedTime() : 0;
}

bool VideoFullscreenControllerContext::isPlaying() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->isPlaying() : false;
}

float VideoFullscreenControllerContext::playbackRate() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->playbackRate() : 0;
}

Ref<TimeRanges> VideoFullscreenControllerContext::seekableRanges() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->seekableRanges() : TimeRanges::create();
}

double VideoFullscreenControllerContext::seekableTimeRangesLastModifiedTime() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->seekableTimeRangesLastModifiedTime() : 0;
}

double VideoFullscreenControllerContext::liveUpdateInterval() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->liveUpdateInterval() : 0;
}

bool VideoFullscreenControllerContext::canPlayFastReverse() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->canPlayFastReverse() : false;
}

Vector<MediaSelectionOption> VideoFullscreenControllerContext::audioMediaSelectionOptions() const
{
    ASSERT(isUIThread());
    if (m_playbackModel)
        return m_playbackModel->audioMediaSelectionOptions();
    return { };
}

uint64_t VideoFullscreenControllerContext::audioMediaSelectedIndex() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->audioMediaSelectedIndex() : -1;
}

Vector<MediaSelectionOption> VideoFullscreenControllerContext::legibleMediaSelectionOptions() const
{
    ASSERT(isUIThread());
    if (m_playbackModel)
        return m_playbackModel->legibleMediaSelectionOptions();
    return { };
}

uint64_t VideoFullscreenControllerContext::legibleMediaSelectedIndex() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->legibleMediaSelectedIndex() : -1;
}

bool VideoFullscreenControllerContext::externalPlaybackEnabled() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->externalPlaybackEnabled() : false;
}

PlaybackSessionModel::ExternalPlaybackTargetType VideoFullscreenControllerContext::externalPlaybackTargetType() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->externalPlaybackTargetType() : TargetTypeNone;
}

String VideoFullscreenControllerContext::externalPlaybackLocalizedDeviceName() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->externalPlaybackLocalizedDeviceName() : String();
}

bool VideoFullscreenControllerContext::wirelessVideoPlaybackDisabled() const
{
    ASSERT(isUIThread());
    return m_playbackModel ? m_playbackModel->wirelessVideoPlaybackDisabled() : true;
}

#pragma mark Other

void VideoFullscreenControllerContext::setUpFullscreen(HTMLVideoElement& videoElement, UIView *view, HTMLMediaElementEnums::VideoFullscreenMode mode)
{
    ASSERT(isMainThread());
    RetainPtr<UIView> viewRef = view;
    m_videoElement = &videoElement;
    m_playbackModel = PlaybackSessionModelMediaElement::create();
    m_playbackModel->addClient(*this);
    m_playbackModel->setMediaElement(m_videoElement.get());

    m_fullscreenModel = VideoFullscreenModelVideoElement::create();
    m_fullscreenModel->addClient(*this);
    m_fullscreenModel->setVideoElement(m_videoElement.get());

    bool allowsPictureInPicture = m_videoElement->webkitSupportsPresentationMode(HTMLVideoElement::VideoPresentationMode::PictureInPicture);

    IntRect videoElementClientRect = elementRectInWindow(m_videoElement.get());
    FloatRect videoLayerFrame = FloatRect(FloatPoint(), videoElementClientRect.size());
    m_fullscreenModel->setVideoLayerFrame(videoLayerFrame);

    FloatSize videoDimensions = { (float)videoElement.videoWidth(), (float)videoElement.videoHeight() };

    dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), this, videoElementClientRect, videoDimensions, viewRef, mode, allowsPictureInPicture] {
        ASSERT(isUIThread());
        WebThreadLock();

        Ref<PlaybackSessionInterfaceAVKit> sessionInterface = PlaybackSessionInterfaceAVKit::create(*this);
        m_interface = VideoFullscreenInterfaceAVKit::create(sessionInterface.get());
        m_interface->setVideoFullscreenChangeObserver(this);
        m_interface->setVideoFullscreenModel(this);

        m_videoFullscreenView = adoptNS([PAL::allocUIViewInstance() init]);

        m_interface->setupFullscreen(*m_videoFullscreenView.get(), videoElementClientRect, videoDimensions, viewRef.get(), mode, allowsPictureInPicture, false, false);
    });
}

void VideoFullscreenControllerContext::exitFullscreen()
{
    ASSERT(WebThreadIsCurrent() || isMainThread());
    IntRect clientRect = elementRectInWindow(m_videoElement.get());
    dispatch_async(dispatch_get_main_queue(), [protectedThis = makeRefPtr(this), this, clientRect] {
        ASSERT(isUIThread());
        m_interface->exitFullscreen(clientRect);
    });
}

void VideoFullscreenControllerContext::requestHideAndExitFullscreen()
{
    ASSERT(isUIThread());
    m_interface->requestHideAndExitFullscreen();
}

@implementation WebVideoFullscreenController {
    RefPtr<VideoFullscreenControllerContext> _context;
    RefPtr<HTMLVideoElement> _videoElement;
}

- (instancetype)init
{
    if (!(self = [super init]))
        return nil;
    
    return self;
}

- (void)setVideoElement:(NakedPtr<HTMLVideoElement>)videoElement
{
    _videoElement = videoElement;
}

- (NakedPtr<HTMLVideoElement>)videoElement
{
    return _videoElement.get();
}

- (void)enterFullscreen:(UIView *)view mode:(HTMLMediaElementEnums::VideoFullscreenMode)mode
{
    ASSERT(isMainThread());
    _context = VideoFullscreenControllerContext::create();
    _context->setController(self);
    _context->setUpFullscreen(*_videoElement.get(), view, mode);
}

- (void)exitFullscreen
{
    ASSERT(WebThreadIsCurrent() || isMainThread());
    _context->exitFullscreen();
}

- (void)requestHideAndExitFullscreen
{
    ASSERT(isUIThread());
    if (_context)
        _context->requestHideAndExitFullscreen();
}

- (void)didFinishFullscreen:(VideoFullscreenControllerContext*)context
{
    ASSERT(WebThreadIsCurrent());
    ASSERT_UNUSED(context, context == _context);
    [[self retain] autorelease]; // retain self before breaking a retain cycle.
    _context->setController(nil);
    _context = nullptr;
    _videoElement = nullptr;
}

@end

#endif // !HAVE(AVKIT)

#endif // PLATFORM(IOS_FAMILY)
