/*
 * Copyright (C) 2021 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 "FileSystemSyncAccessHandle.h"

#include "BufferSource.h"
#include "FileSystemFileHandle.h"
#include "JSDOMPromiseDeferred.h"
#include "WorkerGlobalScope.h"
#include "WorkerThread.h"
#include <wtf/CompletionHandler.h>

namespace WebCore {

Ref<FileSystemSyncAccessHandle> FileSystemSyncAccessHandle::create(ScriptExecutionContext& context, FileSystemFileHandle& source, FileSystemSyncAccessHandleIdentifier identifier, FileHandle&& file)
{
    auto handle = adoptRef(*new FileSystemSyncAccessHandle(context, source, identifier, WTFMove(file)));
    handle->suspendIfNeeded();
    return handle;
}

FileSystemSyncAccessHandle::FileSystemSyncAccessHandle(ScriptExecutionContext& context, FileSystemFileHandle& source, FileSystemSyncAccessHandleIdentifier identifier, FileHandle&& file)
    : ActiveDOMObject(&context)
    , m_source(source)
    , m_identifier(identifier)
    , m_file(WTFMove(file))
{
    ASSERT(m_file);

    m_source->registerSyncAccessHandle(m_identifier, *this);
}

FileSystemSyncAccessHandle::~FileSystemSyncAccessHandle()
{
    ASSERT(isClosingOrClosed());

    m_source->unregisterSyncAccessHandle(m_identifier);

    if (m_closeResult)
        return;

    // Task replies may be scheduled to WorkerRunLoop but do not run.
    auto pendingPromises = std::exchange(m_pendingPromises, { });
    for (auto& promise : pendingPromises) {
        std::visit([](auto& promise) {
            promise.reject(Exception { UnknownError, "AccessHandle is about to be destroyed"_s });
        }, promise);
    }

    closeBackend(CloseMode::Sync);
}

bool FileSystemSyncAccessHandle::isClosingOrClosed() const
{
    return m_closeResult || !m_closeCallbacks.isEmpty();
}

void FileSystemSyncAccessHandle::truncate(unsigned long long size, DOMPromiseDeferred<void>&& promise)
{
    if (isClosingOrClosed())
        return promise.reject(Exception { InvalidStateError, "AccessHandle is closing or closed"_s });

    auto* scope = downcast<WorkerGlobalScope>(scriptExecutionContext());
    if (!scope)
        return promise.reject(Exception { InvalidStateError, "Context is invalid"_s });

    m_pendingPromises.append(WTFMove(promise));
    WorkerGlobalScope::postFileSystemStorageTask([weakThis = WeakPtr { *this }, file = m_file.handle(), size, workerThread = Ref { scope->thread() }]() mutable {
        workerThread->runLoop().postTask([weakThis = WTFMove(weakThis), success = FileSystem::truncateFile(file, size)](auto&) mutable {
            if (weakThis)
                weakThis->completePromise(success ? ExceptionOr<void> { } : Exception { UnknownError });
        });
    });
}

void FileSystemSyncAccessHandle::getSize(DOMPromiseDeferred<IDLUnsignedLongLong>&& promise)
{
    if (isClosingOrClosed())
        return promise.reject(Exception { InvalidStateError, "AccessHandle is closing or closed"_s });

    auto* scope = downcast<WorkerGlobalScope>(scriptExecutionContext());
    if (!scope)
        return promise.reject(Exception { InvalidStateError, "Context is invalid"_s });

    m_pendingPromises.append(WTFMove(promise));
    WorkerGlobalScope::postFileSystemStorageTask([weakThis = WeakPtr { *this }, file = m_file.handle(), workerThread = Ref { scope->thread() }]() mutable {
        workerThread->runLoop().postTask([weakThis = WTFMove(weakThis), success = FileSystem::fileSize(file)](auto&) mutable {
            if (weakThis)
                weakThis->completePromise(success ? ExceptionOr<uint64_t> { success.value() } : Exception { UnknownError });
        });
    });
}

void FileSystemSyncAccessHandle::flush(DOMPromiseDeferred<void>&& promise)
{
    if (isClosingOrClosed())
        return promise.reject(Exception { InvalidStateError, "AccessHandle is closing or closed"_s });

    auto* scope = downcast<WorkerGlobalScope>(scriptExecutionContext());
    if (!scope)
        return promise.reject(Exception { InvalidStateError, "Context is invalid"_s });

    m_pendingPromises.append(WTFMove(promise));
    WorkerGlobalScope::postFileSystemStorageTask([weakThis = WeakPtr { *this }, file = m_file.handle(), workerThread = Ref { scope->thread() }]() mutable {
        workerThread->runLoop().postTask([weakThis = WTFMove(weakThis), success = FileSystem::flushFile(file)](auto&) mutable {
            if (weakThis)
                weakThis->completePromise(success ? ExceptionOr<void> { } : Exception { UnknownError });
        });
    });
}

void FileSystemSyncAccessHandle::close(DOMPromiseDeferred<void>&& promise)
{
    closeInternal([weakThis = WeakPtr { *this }, promise = WTFMove(promise)](auto result) mutable {
        promise.settle(WTFMove(result));
    });
}

void FileSystemSyncAccessHandle::closeInternal(CloseCallback&& callback)
{
    if (m_closeResult)
        return callback(ExceptionOr<void> { *m_closeResult });

    auto isClosing = !m_closeCallbacks.isEmpty();
    m_closeCallbacks.append(WTFMove(callback));
    if (isClosing)
        return;

    ASSERT(m_file);
    closeFile();
}

void FileSystemSyncAccessHandle::closeFile()
{
    if (!m_file)
        return;

    auto* scope = downcast<WorkerGlobalScope>(scriptExecutionContext());
    ASSERT(scope);

    WorkerGlobalScope::postFileSystemStorageTask([weakThis = WeakPtr { *this }, file = std::exchange(m_file, { }), workerThread = Ref { scope->thread() }]() mutable {
        workerThread->runLoop().postTask([weakThis = WTFMove(weakThis)](auto&) mutable {
            if (weakThis)
                weakThis->didCloseFile();
        });
    });
}

void FileSystemSyncAccessHandle::didCloseFile()
{
    closeBackend(CloseMode::Async);
}

void FileSystemSyncAccessHandle::closeBackend(CloseMode mode)
{
    if (m_closeResult)
        return;

    if (mode == CloseMode::Async) {
        m_source->closeSyncAccessHandle(m_identifier, [this, protectedThis = Ref { *this }](auto result) mutable {
            didCloseBackend(WTFMove(result));
        });
        return;
    }

    m_source->closeSyncAccessHandle(m_identifier, [](auto) { });
    didCloseBackend({ });
}

void FileSystemSyncAccessHandle::didCloseBackend(ExceptionOr<void>&& result)
{
    if (m_closeResult)
        return;

    m_closeResult = WTFMove(result);
    auto callbacks = std::exchange(m_closeCallbacks, { });
    for (auto& callback : callbacks)
        callback(ExceptionOr<void> { *m_closeResult });
}

ExceptionOr<unsigned long long> FileSystemSyncAccessHandle::read(BufferSource&& buffer, FileSystemSyncAccessHandle::FilesystemReadWriteOptions options)
{
    ASSERT(!isMainThread());

    if (isClosingOrClosed())
        return Exception { InvalidStateError, "AccessHandle is closing or closed"_s };

    if (!m_pendingPromises.isEmpty())
        return Exception { InvalidStateError, "Access handle has unfinished operation"_s };

    int result = FileSystem::seekFile(m_file.handle(), options.at, FileSystem::FileSeekOrigin::Beginning);
    if (result == -1)
        return Exception { InvalidStateError, "Failed to read at offset"_s };

    result = FileSystem::readFromFile(m_file.handle(), buffer.mutableData(), buffer.length());
    if (result == -1)
        return Exception { InvalidStateError, "Failed to read from file"_s };

    return result;
}

ExceptionOr<unsigned long long> FileSystemSyncAccessHandle::write(BufferSource&& buffer, FileSystemSyncAccessHandle::FilesystemReadWriteOptions options)
{
    ASSERT(!isMainThread());

    if (isClosingOrClosed())
        return Exception { InvalidStateError, "AccessHandle is closing or closed"_s };

    if (!m_pendingPromises.isEmpty())
        return Exception { InvalidStateError, "Access handle has unfinished operation"_s };

    int result = FileSystem::seekFile(m_file.handle(), options.at, FileSystem::FileSeekOrigin::Beginning);
    if (result == -1)
        return Exception { InvalidStateError, "Failed to write at offset"_s };

    result = FileSystem::writeToFile(m_file.handle(), buffer.data(), buffer.length());
    if (result == -1)
        return Exception { InvalidStateError, "Failed to write to file"_s };

    return result;
}

void FileSystemSyncAccessHandle::completePromise(Result&& result)
{
    if (m_pendingPromises.isEmpty())
        return;

    auto pendingPromise = m_pendingPromises.takeFirst();
    WTF::switchOn(WTFMove(result), [&pendingPromise](ExceptionOr<void>&& result) {
        auto* promise = std::get_if<DOMPromiseDeferred<void>>(&pendingPromise);
        ASSERT(promise);
        promise->settle(WTFMove(result));
    }, [&pendingPromise](ExceptionOr<uint64_t>&& result) {
        auto* promise = std::get_if<DOMPromiseDeferred<IDLUnsignedLongLong>>(&pendingPromise);
        ASSERT(promise);
        promise->settle(WTFMove(result));
    });
}

const char* FileSystemSyncAccessHandle::activeDOMObjectName() const
{
    return "FileSystemSyncAccessHandle";
}

void FileSystemSyncAccessHandle::stop()
{
    closeInternal([](auto) { });
}

void FileSystemSyncAccessHandle::invalidate()
{
    closeFile();

    // Invalidation is initiated by backend.
    didCloseBackend({ });
}

} // namespace WebCore
