/*
 * Copyright (C) 2016 Canon Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted, provided that the following conditions
 * are required to be 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 Canon Inc. 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 CANON 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 CANON INC. AND 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 "FetchResponse.h"

#if ENABLE(FETCH_API)

#include "ExceptionCode.h"
#include "FetchRequest.h"
#include "HTTPParsers.h"
#include "JSBlob.h"
#include "JSDOMPromise.h"
#include "JSFetchResponse.h"
#include "ScriptExecutionContext.h"

namespace WebCore {

static inline bool isRedirectStatus(int status)
{
    return status == 301 || status == 302 || status == 303 || status == 307 || status == 308;
}

PassRefPtr<FetchResponse> FetchResponse::error(ScriptExecutionContext* context)
{
	RefPtr<FetchResponse> response = adoptRef(new FetchResponse(context, PassOwnPtr<FetchBody>(), FetchHeaders::create(FetchHeaders::Guard::Immutable), ResourceResponse()));
	response->m_response.setType(Type::Error);
	return response;
}

RefPtr<FetchResponse> FetchResponse::redirect(ScriptExecutionContext* context, const String& url, int status, ExceptionCode& ec)
{
    // FIXME: Tighten the URL parsing algorithm according https://url.spec.whatwg.org/#concept-url-parser.
    KURL requestURL = context->completeURL(url);
    if (!requestURL.isValid() || !requestURL.user().isEmpty() || !requestURL.pass().isEmpty()) {
        ec = TypeError;
        return nullptr;
    }
    if (!isRedirectStatus(status)) {
        ec = RangeError;
        return nullptr;
    }
    RefPtr<FetchResponse> redirectResponse = adoptRef(new FetchResponse(context, PassOwnPtr<FetchBody>(), FetchHeaders::create(FetchHeaders::Guard::Immutable), ResourceResponse()));
    redirectResponse->m_response.setHTTPStatusCode(status);
    redirectResponse->m_headers->fastSet(HTTPHeaderName::Location, requestURL.string());
    return redirectResponse;
}

void FetchResponse::setStatus(int status, const String& statusText, ExceptionCode& ec)
{
    if (!isValidReasonPhrase(statusText)) {
        ec = TypeError;
        return;
    }
    m_response.setHTTPStatusCode(status);
    m_response.setHTTPStatusText(statusText);
}

void FetchResponse::initializeWith(JSC::ExecState* execState, JSC::JSValue body)
{
    ASSERT(scriptExecutionContext());
    extractBody(*scriptExecutionContext(), *execState, body);
    updateContentType();
}

FetchResponse::FetchResponse(ScriptExecutionContext* context, PassOwnPtr<FetchBody> body, RefPtr<FetchHeaders> headers, const ResourceResponse& response)
    : FetchBodyOwner(context, body, headers)
    , m_response(response)
	, m_consumer(FetchBodyConsumer::Type::ArrayBuffer)
{
}

RefPtr<FetchResponse> FetchResponse::cloneForJS()
{
    ASSERT(scriptExecutionContext());
    ASSERT(!isDisturbedOrLocked());

    RefPtr<FetchResponse> clone = adoptRef(new FetchResponse(scriptExecutionContext(), PassOwnPtr<FetchBody>(), FetchHeaders::create(*headers()), ResourceResponse(m_response)));
    clone->cloneBody(*this);
    return clone.release();
}

void FetchResponse::fetch(ScriptExecutionContext& context, FetchRequest& request, RefPtr<DeferredPromise> promise)
{
    RefPtr<FetchResponse> response = adoptRef(new FetchResponse(&context, FetchBody::loadingBody(), FetchHeaders::create(FetchHeaders::Guard::Immutable), ResourceResponse()));

    response->m_bodyLoader = adoptPtr(new BodyLoader(*response, promise));
    if (!response->m_bodyLoader->start(context, request))
        response->m_bodyLoader.clear();
}

const String& FetchResponse::url() const
{
    if (m_responseURL.isNull())
        m_responseURL = m_response.url().serialize(true);
    return m_responseURL;
}

void FetchResponse::BodyLoader::didSucceed()
{
    ASSERT(m_response.hasPendingActivity());
    m_response.m_body->loadingSucceeded();

#if ENABLE(STREAMS_API)
    if (m_response.m_readableStreamSource && !m_response.body().consumer().hasData())
        m_response.closeStream();
#endif

    if (m_loader->isStarted()) {
        RefPtr<FetchResponse> protector(&m_response);
    	m_response.m_bodyLoader.clear();
    }
}

void FetchResponse::BodyLoader::didFail()
{
    ASSERT(m_response.hasPendingActivity());
    if (m_promise)
    	m_promise.release()->reject<ExceptionCode>(TypeError);

#if ENABLE(STREAMS_API)
    if (m_response.m_readableStreamSource) {
        if (!m_response.m_readableStreamSource->isCancelling())
            m_response.m_readableStreamSource->error(ASCIILiteral("Loading failed"));
        m_response.m_readableStreamSource.clear();
    }
#endif

    // Check whether didFail is called as part of FetchLoader::start.
    if (m_loader->isStarted()){
        RefPtr<FetchResponse> protector(&m_response);
        m_response.m_bodyLoader.clear();
    }
}

FetchResponse::BodyLoader::BodyLoader(FetchResponse& response, RefPtr<DeferredPromise> promise)
    : m_response(response)
    , m_promise(promise)
{
    m_response.setPendingActivity(&m_response);
}

FetchResponse::BodyLoader::~BodyLoader()
{
    m_response.unsetPendingActivity(&m_response);
}

void FetchResponse::BodyLoader::didReceiveResponse(const ResourceResponse& resourceResponse)
{
    ASSERT(m_promise);

    m_response.m_response = resourceResponse;
    m_response.m_headers->filterAndFill(resourceResponse.httpHeaderFields(), FetchHeaders::Guard::Response);
    m_response.updateContentType();

    m_promise.release()->resolve(&m_response);
}

void FetchResponse::BodyLoader::didReceiveData(const char* data, size_t size)
{
#if ENABLE(STREAMS_API)
    ASSERT(m_response.m_readableStreamSource);
    FetchResponseSource& source = *m_response.m_readableStreamSource;

    if (!source.isPulling()) {
        m_response.body().consumer().append(data, size);
        return;
    }

    if (m_response.body().consumer().hasData() && !source.enqueue(m_response.body().consumer().takeAsArrayBuffer())) {
        stop();
        return;
    }
    if (!source.enqueue(ArrayBuffer::tryCreate(data, size))) {
        stop();
        return;
    }
    source.resolvePullPromise();
#else
    UNUSED_PARAM(data);
    UNUSED_PARAM(size);
#endif
}

bool FetchResponse::BodyLoader::start(ScriptExecutionContext& context, const FetchRequest& request)
{
    m_loader = adoptPtr(new FetchLoader(*this, &m_response.m_body->consumer()));
    m_loader->start(context, request);
    return m_loader->isStarted();
}

void FetchResponse::BodyLoader::stop()
{
    m_promise.clear();
    if (m_loader)
        m_loader->stop();
}

void FetchResponse::consume(unsigned type, RefPtr<DeferredPromise> promise)
{
    ASSERT(type <= static_cast<unsigned>(FetchBodyConsumer::Type::Text));

    FetchBodyConsumer::Type consumerType = static_cast<FetchBodyConsumer::Type>(type);

    if (isLoading()) {
        consumeOnceLoadingFinished(consumerType, promise);
        return;
    }

    switch (consumerType) {
    case FetchBodyConsumer::Type::ArrayBuffer:
        arrayBuffer(promise);
        return;
    case FetchBodyConsumer::Type::Blob:
        blob(promise);
        return;
    case FetchBodyConsumer::Type::JSON:
        json(promise);
        return;
    case FetchBodyConsumer::Type::Text:
        text(promise);
        return;
    case FetchBodyConsumer::Type::None:
        ASSERT_NOT_REACHED();
        return;
    }
}

#if ENABLE(STREAMS_API)
void FetchResponse::startConsumingStream(unsigned type)
{
    m_isDisturbed = true;
    m_consumer.setType(static_cast<FetchBodyConsumer::Type>(type));
}

void FetchResponse::consumeChunk(RefPtr<JSC::Uint8Array> chunk)
{
    m_consumer.append(chunk->data(), chunk->byteLength());
}

void FetchResponse::finishConsumingStream(RefPtr<DeferredPromise> promise)
{
    m_consumer.resolve(promise);
}

void FetchResponse::consumeBodyAsStream()
{
    ASSERT(m_readableStreamSource);
    m_isDisturbed = true;
    if (!isLoading()) {
        body().consumeAsStream(*this, *m_readableStreamSource);
        if (!m_readableStreamSource->isPulling())
            m_readableStreamSource = 0;
        return;
    }

    ASSERT(m_bodyLoader);

    RefPtr<SharedBuffer> data = m_bodyLoader->startStreaming();
    if (data) {
    	unsigned size = data->size();
        if (!m_readableStreamSource->enqueue(JSC::ArrayBuffer::createAdopted(data->releaseBuffer(), size))) {
            stop();
            return;
        }
        m_readableStreamSource->resolvePullPromise();
    }
}

void FetchResponse::closeStream()
{
    ASSERT(m_readableStreamSource);
    m_readableStreamSource->close();
    m_readableStreamSource = 0;
}

void FetchResponse::feedStream()
{
    ASSERT(m_readableStreamSource);
    bool shouldCloseStream = !m_bodyLoader;

    if (body().consumer().hasData()) {
        if (!m_readableStreamSource->enqueue(body().consumer().takeAsArrayBuffer())) {
            stop();
            return;
        }
        if (!shouldCloseStream) {
            m_readableStreamSource->resolvePullPromise();
            return;
        }
    } else if (!shouldCloseStream)
        return;

    closeStream();
}

ReadableStreamSource* FetchResponse::createReadableStreamSource()
{
    ASSERT(!m_readableStreamSource);
    ASSERT(!isDisturbedOrLocked());

    if (isBodyNull())
        return 0;

    m_readableStreamSource = adoptRef(new FetchResponseSource(*this));
    return m_readableStreamSource.get();
}

RefPtr<SharedBuffer> FetchResponse::BodyLoader::startStreaming()
{
    ASSERT(m_loader);
    return m_loader->startStreaming();
}

void FetchResponse::cancel()
{
    m_isDisturbed = true;
    stop();
}

#endif

void FetchResponse::stop()
{
    RefPtr<FetchResponse> protect(this);
    FetchBodyOwner::stop();
    if (m_bodyLoader) {
        m_bodyLoader->stop();
        m_bodyLoader = nullptr;
    }
}

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

bool FetchResponse::canSuspendForDocumentSuspension() const
{
    // FIXME: We can probably do the same strategy as XHR.
    return !isActive();
}

} // namespace WebCore

#endif // ENABLE(FETCH_API)
