/*
 * Copyright (C) 2012-2018 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.
 */

#include "config.h"
#include "JSRunLoopTimer.h"

#include "IncrementalSweeper.h"
#include "JSCInlines.h"
#include "JSObject.h"
#include "JSString.h"

#include <wtf/MainThread.h>
#include <wtf/NoTailCalls.h>
#include <wtf/Threading.h>

#if USE(GLIB_EVENT_LOOP)
#include <glib.h>
#include <wtf/glib/RunLoopSourcePriority.h>
#endif

#include <mutex>

namespace JSC {

const Seconds JSRunLoopTimer::s_decade { 60 * 60 * 24 * 365 * 10 };

static inline JSRunLoopTimer::Manager::EpochTime epochTime(Seconds delay)
{
#if USE(CF)
    return Seconds { CFAbsoluteTimeGetCurrent() + delay.value() };
#else
    return MonotonicTime::now().secondsSinceEpoch() + delay;
#endif
}

#if USE(CF)
void JSRunLoopTimer::Manager::timerDidFireCallback(CFRunLoopTimerRef, void* contextPtr)
{
    static_cast<JSRunLoopTimer::Manager*>(contextPtr)->timerDidFire();
}

void JSRunLoopTimer::Manager::PerVMData::setRunLoop(Manager* manager, CFRunLoopRef newRunLoop)
{
    if (runLoop) {
        CFRunLoopRemoveTimer(runLoop.get(), timer.get(), kCFRunLoopCommonModes);
        CFRunLoopTimerInvalidate(timer.get());
        runLoop.clear();
        timer.clear();
    }

    if (newRunLoop) {
        runLoop = newRunLoop;
        memset(&context, 0, sizeof(CFRunLoopTimerContext));
        RELEASE_ASSERT(manager);
        context.info = manager;
        timer = adoptCF(CFRunLoopTimerCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + s_decade.seconds(), CFAbsoluteTimeGetCurrent() + s_decade.seconds(), 0, 0, JSRunLoopTimer::Manager::timerDidFireCallback, &context));
        CFRunLoopAddTimer(runLoop.get(), timer.get(), kCFRunLoopCommonModes);

        EpochTime scheduleTime = epochTime(s_decade);
        for (auto& pair : timers)
            scheduleTime = std::min(pair.second, scheduleTime);
        CFRunLoopTimerSetNextFireDate(timer.get(), scheduleTime.value());
    }
}
#else
JSRunLoopTimer::Manager::PerVMData::PerVMData(Manager& manager)
    : runLoop(&RunLoop::current())
    , timer(std::make_unique<RunLoop::Timer<Manager>>(*runLoop, &manager, &JSRunLoopTimer::Manager::timerDidFireCallback))
{
#if USE(GLIB_EVENT_LOOP)
    timer->setPriority(RunLoopSourcePriority::JavascriptTimer);
    timer->setName("[JavaScriptCore] JSRunLoopTimer");
#endif
}

void JSRunLoopTimer::Manager::timerDidFireCallback()
{
    timerDidFire();
}
#endif

JSRunLoopTimer::Manager::PerVMData::~PerVMData()
{
#if USE(CF)
    setRunLoop(nullptr, nullptr);
#endif
}

void JSRunLoopTimer::Manager::timerDidFire()
{
    Vector<Ref<JSRunLoopTimer>> timersToFire;

    {
        auto locker = holdLock(m_lock);
#if USE(CF)
        CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
#else
        RunLoop* currentRunLoop = &RunLoop::current();
#endif
        EpochTime nowEpochTime = epochTime(0_s);
        for (auto& entry : m_mapping) {
            PerVMData& data = entry.value;
#if USE(CF)
            if (data.runLoop.get() != currentRunLoop)
                continue;
#else
            if (data.runLoop != currentRunLoop)
                continue;
#endif
            
            EpochTime scheduleTime = epochTime(s_decade);
            for (size_t i = 0; i < data.timers.size(); ++i) {
                {
                    auto& pair = data.timers[i];
                    if (pair.second > nowEpochTime) {
                        scheduleTime = std::min(pair.second, scheduleTime);
                        continue;
                    }
                    auto& last = data.timers.last();
                    if (&last != &pair)
                        std::swap(pair, last);
                    --i;
                }

                auto pair = data.timers.takeLast();
                timersToFire.append(WTFMove(pair.first));
            }

#if USE(CF)
            CFRunLoopTimerSetNextFireDate(data.timer.get(), scheduleTime.value());
#else
            data.timer->startOneShot(std::max(0_s, scheduleTime - MonotonicTime::now().secondsSinceEpoch()));
#endif
        }
    }

    for (auto& timer : timersToFire)
        timer->timerDidFire();
}

JSRunLoopTimer::Manager& JSRunLoopTimer::Manager::shared()
{
    static Manager* manager;
    static std::once_flag once;
    std::call_once(once, [&] {
        manager = new Manager;
    });
    return *manager;
}

void JSRunLoopTimer::Manager::registerVM(VM& vm)
{
    PerVMData data { *this };
#if USE(CF)
    data.setRunLoop(this, vm.runLoop());
#endif

    auto locker = holdLock(m_lock);
    auto addResult = m_mapping.add({ vm.apiLock() }, WTFMove(data));
    RELEASE_ASSERT(addResult.isNewEntry);
}

void JSRunLoopTimer::Manager::unregisterVM(VM& vm)
{
    auto locker = holdLock(m_lock);

    auto iter = m_mapping.find({ vm.apiLock() });
    RELEASE_ASSERT(iter != m_mapping.end());
    m_mapping.remove(iter);
}

void JSRunLoopTimer::Manager::scheduleTimer(JSRunLoopTimer& timer, Seconds delay)
{
    EpochTime fireEpochTime = epochTime(delay);

    auto locker = holdLock(m_lock);
    auto iter = m_mapping.find(timer.m_apiLock);
    RELEASE_ASSERT(iter != m_mapping.end()); // We don't allow calling this after the VM dies.

    PerVMData& data = iter->value;
    EpochTime scheduleTime = fireEpochTime;
    bool found = false;
    for (auto& entry : data.timers) {
        if (entry.first.ptr() == &timer) {
            entry.second = fireEpochTime;
            found = true;
        }
        scheduleTime = std::min(scheduleTime, entry.second);
    }

    if (!found)
        data.timers.append({ timer, fireEpochTime });

#if USE(CF)
    CFRunLoopTimerSetNextFireDate(data.timer.get(), scheduleTime.value());
#else
    data.timer->startOneShot(std::max(0_s, scheduleTime - MonotonicTime::now().secondsSinceEpoch()));
#endif
}

void JSRunLoopTimer::Manager::cancelTimer(JSRunLoopTimer& timer)
{
    auto locker = holdLock(m_lock);
    auto iter = m_mapping.find(timer.m_apiLock);
    if (iter == m_mapping.end()) {
        // It's trivial to allow this to be called after the VM dies, so we allow for it.
        return;
    }

    PerVMData& data = iter->value;
    EpochTime scheduleTime = epochTime(s_decade);
    for (unsigned i = 0; i < data.timers.size(); ++i) {
        {
            auto& entry = data.timers[i];
            if (entry.first.ptr() == &timer) {
                RELEASE_ASSERT(timer.refCount() >= 2); // If we remove it from the entry below, we should not be the last thing pointing to it!
                auto& last = data.timers.last();
                if (&last != &entry)
                    std::swap(entry, last);
                data.timers.removeLast();
                i--;
                continue;
            }
        }

        scheduleTime = std::min(scheduleTime, data.timers[i].second);
    }

#if USE(CF)
    CFRunLoopTimerSetNextFireDate(data.timer.get(), scheduleTime.value());
#else
    data.timer->startOneShot(std::max(0_s, scheduleTime - MonotonicTime::now().secondsSinceEpoch()));
#endif
}

Optional<Seconds> JSRunLoopTimer::Manager::timeUntilFire(JSRunLoopTimer& timer)
{
    auto locker = holdLock(m_lock);
    auto iter = m_mapping.find(timer.m_apiLock);
    RELEASE_ASSERT(iter != m_mapping.end()); // We only allow this to be called with a live VM.

    PerVMData& data = iter->value;
    for (auto& entry : data.timers) {
        if (entry.first.ptr() == &timer) {
            EpochTime nowEpochTime = epochTime(0_s);
            return entry.second - nowEpochTime;
        }
    }

    return WTF::nullopt;
}

#if USE(CF)
void JSRunLoopTimer::Manager::didChangeRunLoop(VM& vm, CFRunLoopRef newRunLoop)
{
    auto locker = holdLock(m_lock);
    auto iter = m_mapping.find({ vm.apiLock() });
    RELEASE_ASSERT(iter != m_mapping.end());

    PerVMData& data = iter->value;
    data.setRunLoop(this, newRunLoop);
}
#endif

void JSRunLoopTimer::timerDidFire()
{
    NO_TAIL_CALLS();

    {
        auto locker = holdLock(m_lock);
        if (!m_isScheduled) {
            // We raced between this callback being called and cancel() being called.
            // That's fine, we just don't do anything here.
            return;
        }
    }

    std::lock_guard<JSLock> lock(m_apiLock.get());
    RefPtr<VM> vm = m_apiLock->vm();
    if (!vm) {
        // The VM has been destroyed, so we should just give up.
        return;
    }

    doWork(*vm);
}

JSRunLoopTimer::JSRunLoopTimer(VM* vm)
    : m_apiLock(vm->apiLock())
{
}

JSRunLoopTimer::~JSRunLoopTimer()
{
}

Optional<Seconds> JSRunLoopTimer::timeUntilFire()
{
    return Manager::shared().timeUntilFire(*this);
}

void JSRunLoopTimer::setTimeUntilFire(Seconds intervalInSeconds)
{
    {
        auto locker = holdLock(m_lock);
        m_isScheduled = true;
        Manager::shared().scheduleTimer(*this, intervalInSeconds);
    }

    auto locker = holdLock(m_timerCallbacksLock);
    for (auto& task : m_timerSetCallbacks)
        task->run();
}

void JSRunLoopTimer::cancelTimer()
{
    auto locker = holdLock(m_lock);
    m_isScheduled = false;
    Manager::shared().cancelTimer(*this);
}

void JSRunLoopTimer::addTimerSetNotification(TimerNotificationCallback callback)
{
    auto locker = holdLock(m_timerCallbacksLock);
    m_timerSetCallbacks.add(callback);
}

void JSRunLoopTimer::removeTimerSetNotification(TimerNotificationCallback callback)
{
    auto locker = holdLock(m_timerCallbacksLock);
    m_timerSetCallbacks.remove(callback);
}

} // namespace JSC
