/*
 * Copyright (C) 2008 Collabora Ltd.
 * Copyright (C) 2009 Gustavo Noronha Silva <gns@gnome.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include <glib/gi18n-lib.h>
#include "GRefPtr.h"
#include "Noncopyable.h"
#include "NotImplemented.h"
#include "ResourceHandleClient.h"
#include "ResourceHandleInternal.h"
#include "ResourceRequest.h"
#include "ResourceResponse.h"
#include "webkitdownload.h"
#include "webkitenumtypes.h"
#include "webkitmarshal.h"
#include "webkitnetworkresponse.h"
#include "webkitprivate.h"
#include <wtf/text/CString.h>

#include <glib/gstdio.h>

#ifdef ERROR
#undef ERROR
#endif

using namespace WebKit;
using namespace WebCore;

/**
 * SECTION:webkitdownload
 * @short_description: Object used to communicate with the application when downloading.
 *
 * #WebKitDownload carries information about a download request,
 * including a #WebKitNetworkRequest object. The application may use
 * this object to control the download process, or to simply figure
 * out what is to be downloaded, and do it itself.
 */

class DownloadClient : public Noncopyable, public ResourceHandleClient {
    public:
        DownloadClient(WebKitDownload*);

        virtual void didReceiveResponse(ResourceHandle*, const ResourceResponse&);
        virtual void didReceiveData(ResourceHandle*, const char*, int, int);
        virtual void didFinishLoading(ResourceHandle*);
        virtual void didFail(ResourceHandle*, const ResourceError&);
        virtual void wasBlocked(ResourceHandle*);
        virtual void cannotShowURL(ResourceHandle*);

    private:
        WebKitDownload* m_download;
};

#define WEBKIT_DOWNLOAD_GET_PRIVATE(obj)    (G_TYPE_INSTANCE_GET_PRIVATE((obj), WEBKIT_TYPE_DOWNLOAD, WebKitDownloadPrivate))

struct _WebKitDownloadPrivate {
    gchar* destinationURI;
    gchar* suggestedFilename;
    guint64 currentSize;
    GTimer* timer;
    WebKitDownloadStatus status;
    GFileOutputStream* outputStream;
    DownloadClient* downloadClient;
    WebKitNetworkRequest* networkRequest;
    WebKitNetworkResponse* networkResponse;
    RefPtr<ResourceHandle> resourceHandle;
};

enum {
    // Normal signals.
    ERROR,
    LAST_SIGNAL
};

static guint webkit_download_signals[LAST_SIGNAL] = { 0 };

enum {
    PROP_0,

    PROP_NETWORK_REQUEST,
    PROP_DESTINATION_URI,
    PROP_SUGGESTED_FILENAME,
    PROP_PROGRESS,
    PROP_STATUS,
    PROP_CURRENT_SIZE,
    PROP_TOTAL_SIZE,
    PROP_NETWORK_RESPONSE
};

G_DEFINE_TYPE(WebKitDownload, webkit_download, G_TYPE_OBJECT);


static void webkit_download_set_response(WebKitDownload* download, const ResourceResponse& response);
static void webkit_download_set_status(WebKitDownload* download, WebKitDownloadStatus status);

static void webkit_download_dispose(GObject* object)
{
    WebKitDownload* download = WEBKIT_DOWNLOAD(object);
    WebKitDownloadPrivate* priv = download->priv;

    if (priv->outputStream) {
        g_object_unref(priv->outputStream);
        priv->outputStream = NULL;
    }

    if (priv->networkRequest) {
        g_object_unref(priv->networkRequest);
        priv->networkRequest = NULL;
    }

    if (priv->networkResponse) {
        g_object_unref(priv->networkResponse);
        priv->networkResponse = NULL;
    }

    G_OBJECT_CLASS(webkit_download_parent_class)->dispose(object);
}

static void webkit_download_finalize(GObject* object)
{
    WebKitDownload* download = WEBKIT_DOWNLOAD(object);
    WebKitDownloadPrivate* priv = download->priv;

    // We don't call webkit_download_cancel() because we don't want to emit
    // signals when finalizing an object.
    if (priv->resourceHandle) {
        if (priv->status == WEBKIT_DOWNLOAD_STATUS_STARTED) {
            priv->resourceHandle->setClient(0);
            priv->resourceHandle->cancel();
        }
        priv->resourceHandle.release();
    }

    delete priv->downloadClient;

    // The download object may never have _start called on it, so we
    // need to make sure timer is non-NULL.
    if (priv->timer) {
        g_timer_destroy(priv->timer);
        priv->timer = NULL;
    }

    g_free(priv->destinationURI);
    g_free(priv->suggestedFilename);

    G_OBJECT_CLASS(webkit_download_parent_class)->finalize(object);
}

static void webkit_download_get_property(GObject* object, guint prop_id, GValue* value, GParamSpec* pspec)
{
    WebKitDownload* download = WEBKIT_DOWNLOAD(object);

    switch(prop_id) {
    case PROP_NETWORK_REQUEST:
        g_value_set_object(value, webkit_download_get_network_request(download));
        break;
    case PROP_NETWORK_RESPONSE:
        g_value_set_object(value, webkit_download_get_network_response(download));
        break;
    case PROP_DESTINATION_URI:
        g_value_set_string(value, webkit_download_get_destination_uri(download));
        break;
    case PROP_SUGGESTED_FILENAME:
        g_value_set_string(value, webkit_download_get_suggested_filename(download));
        break;
    case PROP_PROGRESS:
        g_value_set_double(value, webkit_download_get_progress(download));
        break;
    case PROP_STATUS:
        g_value_set_enum(value, webkit_download_get_status(download));
        break;
    case PROP_CURRENT_SIZE:
        g_value_set_uint64(value, webkit_download_get_current_size(download));
        break;
    case PROP_TOTAL_SIZE:
        g_value_set_uint64(value, webkit_download_get_total_size(download));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void webkit_download_set_property(GObject* object, guint prop_id, const GValue* value, GParamSpec *pspec)
{
    WebKitDownload* download = WEBKIT_DOWNLOAD(object);
    WebKitDownloadPrivate* priv = download->priv;

    switch(prop_id) {
    case PROP_NETWORK_REQUEST:
        priv->networkRequest = WEBKIT_NETWORK_REQUEST(g_value_dup_object(value));
        break;
    case PROP_NETWORK_RESPONSE:
        priv->networkResponse = WEBKIT_NETWORK_RESPONSE(g_value_dup_object(value));
        break;
    case PROP_DESTINATION_URI:
        webkit_download_set_destination_uri(download, g_value_get_string(value));
        break;
    case PROP_STATUS:
        webkit_download_set_status(download, static_cast<WebKitDownloadStatus>(g_value_get_enum(value)));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void webkit_download_class_init(WebKitDownloadClass* downloadClass)
{
    GObjectClass* objectClass = G_OBJECT_CLASS(downloadClass);
    objectClass->dispose = webkit_download_dispose;
    objectClass->finalize = webkit_download_finalize;
    objectClass->get_property = webkit_download_get_property;
    objectClass->set_property = webkit_download_set_property;

    webkit_init();

    /**
     * WebKitDownload::error:
     * @download: the object on which the signal is emitted
     * @error_code: the corresponding error code
     * @error_detail: detailed error code for the error, see
     * #WebKitDownloadError
     * @reason: a string describing the error
     *
     * Emitted when @download is interrupted either by user action or by
     * network errors, @error_detail will take any value of
     * #WebKitDownloadError.
     *
     * Since: 1.1.2
     */
    webkit_download_signals[ERROR] = g_signal_new("error",
            G_TYPE_FROM_CLASS(downloadClass),
            (GSignalFlags)G_SIGNAL_RUN_LAST,
            0,
            g_signal_accumulator_true_handled,
            NULL,
            webkit_marshal_BOOLEAN__INT_INT_STRING,
            G_TYPE_BOOLEAN, 3,
            G_TYPE_INT,
            G_TYPE_INT,
            G_TYPE_STRING);

    // Properties.

    /**
     * WebKitDownload:network-request
     *
     * The #WebKitNetworkRequest instance associated with the download.
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass,
                                    PROP_NETWORK_REQUEST,
                                    g_param_spec_object("network-request",
                                                        _("Network Request"),
                                                        _("The network request for the URI that should be downloaded"),
                                                        WEBKIT_TYPE_NETWORK_REQUEST,
                                                        (GParamFlags)(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));

    /**
     * WebKitDownload:network-response
     *
     * The #WebKitNetworkResponse instance associated with the download.
     *
     * Since: 1.1.16
     */
    g_object_class_install_property(objectClass,
                                    PROP_NETWORK_RESPONSE,
                                    g_param_spec_object("network-response",
                                                        _("Network Response"),
                                                        _("The network response for the URI that should be downloaded"),
                                                        WEBKIT_TYPE_NETWORK_RESPONSE,
                                                        (GParamFlags)(WEBKIT_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)));

    /**
     * WebKitDownload:destination-uri
     *
     * The URI of the save location for this download.
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass,
                                    PROP_DESTINATION_URI,
                                    g_param_spec_string("destination-uri",
                                                        _("Destination URI"),
                                                        _("The destination URI where to save the file"),
                                                        "",
                                                        WEBKIT_PARAM_READWRITE));

    /**
     * WebKitDownload:suggested-filename
     *
     * The file name suggested as default when saving
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass,
                                    PROP_SUGGESTED_FILENAME,
                                    g_param_spec_string("suggested-filename",
                                                        _("Suggested Filename"),
                                                        _("The filename suggested as default when saving"),
                                                        "",
                                                        WEBKIT_PARAM_READABLE));

    /**
     * WebKitDownload:progress:
     *
     * Determines the current progress of the download. Notice that,
     * although the progress changes are reported as soon as possible,
     * the emission of the notify signal for this property is
     * throttled, for the benefit of download managers. If you care
     * about every update, use WebKitDownload:current-size.
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass, PROP_PROGRESS,
                                    g_param_spec_double("progress",
                                                        _("Progress"),
                                                        _("Determines the current progress of the download"),
                                                        0.0, 1.0, 1.0,
                                                        WEBKIT_PARAM_READABLE));

    /**
     * WebKitDownload:status:
     *
     * Determines the current status of the download.
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass, PROP_STATUS,
                                    g_param_spec_enum("status",
                                                      _("Status"),
                                                      _("Determines the current status of the download"),
                                                      WEBKIT_TYPE_DOWNLOAD_STATUS,
                                                      WEBKIT_DOWNLOAD_STATUS_CREATED,
                                                      WEBKIT_PARAM_READABLE));

    /**
     * WebKitDownload:current-size
     *
     * The length of the data already downloaded
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass,
                                    PROP_CURRENT_SIZE,
                                    g_param_spec_uint64("current-size",
                                                        _("Current Size"),
                                                        _("The length of the data already downloaded"),
                                                        0, G_MAXUINT64, 0,
                                                        WEBKIT_PARAM_READABLE));

    /**
     * WebKitDownload:total-size
     *
     * The total size of the file
     *
     * Since: 1.1.2
     */
    g_object_class_install_property(objectClass,
                                    PROP_CURRENT_SIZE,
                                    g_param_spec_uint64("total-size",
                                                        _("Total Size"),
                                                        _("The total size of the file"),
                                                        0, G_MAXUINT64, 0,
                                                        WEBKIT_PARAM_READABLE));

    g_type_class_add_private(downloadClass, sizeof(WebKitDownloadPrivate));
}

static void webkit_download_init(WebKitDownload* download)
{
    WebKitDownloadPrivate* priv = WEBKIT_DOWNLOAD_GET_PRIVATE(download);
    download->priv = priv;

    priv->downloadClient = new DownloadClient(download);
    priv->currentSize = 0;
    priv->status = WEBKIT_DOWNLOAD_STATUS_CREATED;
}

/**
 * webkit_download_new:
 * @request: a #WebKitNetworkRequest
 *
 * Creates a new #WebKitDownload object for the given
 * #WebKitNetworkRequest object.
 *
 * Returns: the new #WebKitDownload
 *
 * Since: 1.1.2
 */
WebKitDownload* webkit_download_new(WebKitNetworkRequest* request)
{
    g_return_val_if_fail(request, NULL);

    return WEBKIT_DOWNLOAD(g_object_new(WEBKIT_TYPE_DOWNLOAD, "network-request", request, NULL));
}

// Internal usage only
WebKitDownload* webkit_download_new_with_handle(WebKitNetworkRequest* request, WebCore::ResourceHandle* handle, const WebCore::ResourceResponse& response)
{
    g_return_val_if_fail(request, NULL);

    ResourceHandleInternal* d = handle->getInternal();
    if (d->m_msg)
        soup_session_pause_message(webkit_get_default_session(), d->m_msg);

    WebKitDownload* download = WEBKIT_DOWNLOAD(g_object_new(WEBKIT_TYPE_DOWNLOAD, "network-request", request, NULL));
    WebKitDownloadPrivate* priv = download->priv;

    handle->ref();
    priv->resourceHandle = handle;

    webkit_download_set_response(download, response);

    return download;
}

static gboolean webkit_download_open_stream_for_uri(WebKitDownload* download, const gchar* uri, gboolean append=FALSE)
{
    g_return_val_if_fail(uri, FALSE);

    WebKitDownloadPrivate* priv = download->priv;
    GFile* file = g_file_new_for_uri(uri);
    GError* error = NULL;

    if (append)
        priv->outputStream = g_file_append_to(file, G_FILE_CREATE_NONE, NULL, &error);
    else
        priv->outputStream = g_file_replace(file, NULL, TRUE, G_FILE_CREATE_NONE, NULL, &error);

    g_object_unref(file);

    if (error) {
        gboolean handled;
        g_signal_emit_by_name(download, "error", 0, WEBKIT_DOWNLOAD_ERROR_DESTINATION, error->message, &handled);
        g_error_free(error);
        return FALSE;
    }

    return TRUE;
}

static void webkit_download_close_stream(WebKitDownload* download)
{
    WebKitDownloadPrivate* priv = download->priv;
    if (priv->outputStream) {
        g_object_unref(priv->outputStream);
        priv->outputStream = NULL;
    }
}

/**
 * webkit_download_start:
 * @download: the #WebKitDownload
 *
 * Initiates the download. Notice that you must have set the
 * destination-uri property before calling this method.
 *
 * Since: 1.1.2
 */
void webkit_download_start(WebKitDownload* download)
{
    g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));

    WebKitDownloadPrivate* priv = download->priv;
    g_return_if_fail(priv->destinationURI);
    g_return_if_fail(priv->status == WEBKIT_DOWNLOAD_STATUS_CREATED);
    g_return_if_fail(priv->timer == NULL);

    if (!priv->resourceHandle)
        priv->resourceHandle = ResourceHandle::create(core(priv->networkRequest), priv->downloadClient, 0, false, false);
    else {
        priv->resourceHandle->setClient(priv->downloadClient);

        ResourceHandleInternal* d = priv->resourceHandle->getInternal();
        if (d->m_msg)
            soup_session_unpause_message(webkit_get_default_session(), d->m_msg);
    }

    priv->timer = g_timer_new();
    webkit_download_open_stream_for_uri(download, priv->destinationURI);
}

/**
 * webkit_download_cancel:
 * @download: the #WebKitDownload
 *
 * Cancels the download. Calling this will not free the
 * #WebKitDownload object, so you still need to call
 * g_object_unref() on it, if you are the owner of a reference. Notice
 * that cancelling the download provokes the emission of the
 * WebKitDownload::error signal, reporting that the download was
 * cancelled.
 *
 * Since: 1.1.2
 */
void webkit_download_cancel(WebKitDownload* download)
{
    g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));

    WebKitDownloadPrivate* priv = download->priv;

    // Cancel may be called even if start was not called, so we need
    // to make sure timer is non-NULL.
    if (priv->timer)
        g_timer_stop(priv->timer);

    if (priv->resourceHandle)
        priv->resourceHandle->cancel();

    webkit_download_set_status(download, WEBKIT_DOWNLOAD_STATUS_CANCELLED);

    gboolean handled;
    g_signal_emit_by_name(download, "error", 0, WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER, _("User cancelled the download"), &handled);
}

/**
 * webkit_download_get_uri:
 * @download: the #WebKitDownload
 *
 * Convenience method to retrieve the URI from the
 * #WebKitNetworkRequest which is being downloaded.
 *
 * Returns: the uri
 *
 * Since: 1.1.2
 */
const gchar* webkit_download_get_uri(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), NULL);

    WebKitDownloadPrivate* priv = download->priv;
    return webkit_network_request_get_uri(priv->networkRequest);
}

/**
 * webkit_download_get_network_request:
 * @download: the #WebKitDownload
 *
 * Retrieves the #WebKitNetworkRequest object that backs the download
 * process.
 *
 * Returns: the #WebKitNetworkRequest instance
 *
 * Since: 1.1.2
 */
WebKitNetworkRequest* webkit_download_get_network_request(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), NULL);

    WebKitDownloadPrivate* priv = download->priv;
    return priv->networkRequest;
}

/**
 * webkit_download_get_network_response:
 * @download: the #WebKitDownload
 *
 * Retrieves the #WebKitNetworkResponse object that backs the download
 * process.
 *
 * Returns: the #WebKitNetworkResponse instance
 *
 * Since: 1.1.16
 */
WebKitNetworkResponse* webkit_download_get_network_response(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), NULL);

    WebKitDownloadPrivate* priv = download->priv;
    return priv->networkResponse;
}

static void webkit_download_set_response(WebKitDownload* download, const ResourceResponse& response)
{
    WebKitDownloadPrivate* priv = download->priv;
    priv->networkResponse = webkit_network_response_new_with_core_response(response);

    if (!response.isNull() && !response.suggestedFilename().isEmpty())
        webkit_download_set_suggested_filename(download, response.suggestedFilename().utf8().data());
}

/**
 * webkit_download_get_suggested_filename:
 * @download: the #WebKitDownload
 *
 * Retrieves the filename that was suggested by the server, or the one
 * derived by WebKit from the URI.
 *
 * Returns: the suggested filename
 *
 * Since: 1.1.2
 */
const gchar* webkit_download_get_suggested_filename(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), NULL);

    WebKitDownloadPrivate* priv = download->priv;
    if (priv->suggestedFilename)
        return priv->suggestedFilename;

    KURL url = KURL(KURL(), webkit_network_request_get_uri(priv->networkRequest));
    url.setQuery(String());
    url.removeFragmentIdentifier();
    priv->suggestedFilename = g_strdup(decodeURLEscapeSequences(url.lastPathComponent()).utf8().data());
    return priv->suggestedFilename;
}

// for internal use only
void webkit_download_set_suggested_filename(WebKitDownload* download, const gchar* suggestedFilename)
{
    WebKitDownloadPrivate* priv = download->priv;
    g_free(priv->suggestedFilename);
    priv->suggestedFilename = g_strdup(suggestedFilename);

    g_object_notify(G_OBJECT(download), "suggested-filename");
}


/**
 * webkit_download_get_destination_uri:
 * @download: the #WebKitDownload
 *
 * Obtains the URI to which the downloaded file will be written. This
 * must have been set by the application before calling
 * webkit_download_start(), and may be %NULL.
 *
 * Returns: the destination URI or %NULL
 *
 * Since: 1.1.2
 */
const gchar* webkit_download_get_destination_uri(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), NULL);

    WebKitDownloadPrivate* priv = download->priv;
    return priv->destinationURI;
}

/**
 * webkit_download_set_destination_uri:
 * @download: the #WebKitDownload
 * @destination_uri: the destination URI
 *
 * Defines the URI that should be used to save the downloaded file to.
 *
 * Since: 1.1.2
 */
void webkit_download_set_destination_uri(WebKitDownload* download, const gchar* destination_uri)
{
    g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));
    g_return_if_fail(destination_uri);

    WebKitDownloadPrivate* priv = download->priv;
    if (priv->destinationURI && !strcmp(priv->destinationURI, destination_uri))
        return;

    if (priv->status != WEBKIT_DOWNLOAD_STATUS_CREATED && priv->status != WEBKIT_DOWNLOAD_STATUS_CANCELLED) {
        ASSERT(priv->destinationURI);

        gboolean downloading = priv->outputStream != NULL;
        if (downloading)
            webkit_download_close_stream(download);

        GFile* src = g_file_new_for_uri(priv->destinationURI);
        GFile* dest = g_file_new_for_uri(destination_uri);
        GError* error = NULL;

        g_file_move(src, dest, G_FILE_COPY_BACKUP, NULL, NULL, NULL, &error);

        g_object_unref(src);
        g_object_unref(dest);

        g_free(priv->destinationURI);
        priv->destinationURI = g_strdup(destination_uri);

        if (error) {
            gboolean handled;
            g_signal_emit_by_name(download, "error", 0, WEBKIT_DOWNLOAD_ERROR_DESTINATION, error->message, &handled);
            g_error_free(error);
            return;
        }

        if (downloading) {
            if (!webkit_download_open_stream_for_uri(download, destination_uri, TRUE)) {
                webkit_download_cancel(download);
                return;
            }
        }
    } else {
        g_free(priv->destinationURI);
        priv->destinationURI = g_strdup(destination_uri);
    }

    // Only notify change if everything went fine.
    g_object_notify(G_OBJECT(download), "destination-uri");
}

/**
 * webkit_download_get_status:
 * @download: the #WebKitDownload
 *
 * Obtains the current status of the download, as a
 * #WebKitDownloadStatus.
 *
 * Returns: the current #WebKitDownloadStatus
 *
 * Since: 1.1.2
 */
WebKitDownloadStatus webkit_download_get_status(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), WEBKIT_DOWNLOAD_STATUS_ERROR);

    WebKitDownloadPrivate* priv = download->priv;
    return priv->status;
}

static void webkit_download_set_status(WebKitDownload* download, WebKitDownloadStatus status)
{
    g_return_if_fail(WEBKIT_IS_DOWNLOAD(download));

    WebKitDownloadPrivate* priv = download->priv;
    priv->status = status;

    g_object_notify(G_OBJECT(download), "status");
}

/**
 * webkit_download_get_total_size:
 * @download: the #WebKitDownload
 *
 * Returns the expected total size of the download. This is expected
 * because the server may provide incorrect or missing
 * Content-Length. Notice that this may grow over time, as it will be
 * always the same as current_size in the cases where current size
 * surpasses it.
 *
 * Returns: the expected total size of the downloaded file
 *
 * Since: 1.1.2
 */
guint64 webkit_download_get_total_size(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);

    WebKitDownloadPrivate* priv = download->priv;
    SoupMessage* message = priv->networkResponse ? webkit_network_response_get_message(priv->networkResponse) : NULL;

    if (!message)
        return 0;

    return MAX(priv->currentSize, static_cast<guint64>(soup_message_headers_get_content_length(message->response_headers)));
}

/**
 * webkit_download_get_current_size:
 * @download: the #WebKitDownload
 *
 * Current already downloaded size.
 *
 * Returns: the already downloaded size
 *
 * Since: 1.1.2
 */
guint64 webkit_download_get_current_size(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0);

    WebKitDownloadPrivate* priv = download->priv;
    return priv->currentSize;
}

/**
 * webkit_download_get_progress:
 * @download: a #WebKitDownload
 *
 * Determines the current progress of the download.
 *
 * Returns: a #gdouble ranging from 0.0 to 1.0.
 *
 * Since: 1.1.2
 */
gdouble webkit_download_get_progress(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 1.0);

    WebKitDownloadPrivate* priv = download->priv;
    if (!priv->networkResponse)
        return 0.0;

    gdouble total_size = static_cast<gdouble>(webkit_download_get_total_size(download));

    if (total_size == 0)
        return 1.0;

    return ((gdouble)priv->currentSize) / total_size;
}

/**
 * webkit_download_get_elapsed_time:
 * @download: a #WebKitDownload
 *
 * Elapsed time for the download in seconds, including any fractional
 * part. If the download is finished, had an error or was cancelled
 * this is the time between its start and the event.
 *
 * Returns: seconds since the download was started, as a #gdouble
 *
 * Since: 1.1.2
 */
gdouble webkit_download_get_elapsed_time(WebKitDownload* download)
{
    g_return_val_if_fail(WEBKIT_IS_DOWNLOAD(download), 0.0);

    WebKitDownloadPrivate* priv = download->priv;
    if (!priv->timer)
        return 0;

    return g_timer_elapsed(priv->timer, NULL);
}

static void webkit_download_received_data(WebKitDownload* download, const gchar* data, int length)
{
    WebKitDownloadPrivate* priv = download->priv;

    if (priv->currentSize == 0)
        webkit_download_set_status(download, WEBKIT_DOWNLOAD_STATUS_STARTED);

    ASSERT(priv->outputStream);

    gsize bytes_written;
    GError* error = NULL;

    g_output_stream_write_all(G_OUTPUT_STREAM(priv->outputStream),
                              data, length, &bytes_written, NULL, &error);

    if (error) {
        gboolean handled;
        g_signal_emit_by_name(download, "error", 0, WEBKIT_DOWNLOAD_ERROR_DESTINATION, error->message, &handled);
        g_error_free(error);
        return;
    }

    priv->currentSize += length;
    g_object_notify(G_OBJECT(download), "current-size");

    ASSERT(priv->networkResponse);
    if (priv->currentSize > webkit_download_get_total_size(download))
        g_object_notify(G_OBJECT(download), "total-size");

    // Throttle progress notification to not consume high amounts of
    // CPU on fast links, except when the last notification occured
    // in more then 0.7 secs from now, or the last notified progress
    // is passed in 1% or we reached the end.
    static gdouble lastProgress = 0;
    static gdouble lastElapsed = 0;
    gdouble currentElapsed = g_timer_elapsed(priv->timer, NULL);
    gdouble currentProgress = webkit_download_get_progress(download);

    if (lastElapsed
        && lastProgress
        && (currentElapsed - lastElapsed) < 0.7
        && (currentProgress - lastProgress) < 0.01
        && currentProgress < 1.0) {
        return;
    }
    lastElapsed = currentElapsed;
    lastProgress = currentProgress;

    g_object_notify(G_OBJECT(download), "progress");
}

static void webkit_download_finished_loading(WebKitDownload* download)
{
    webkit_download_close_stream(download);

    WebKitDownloadPrivate* priv = download->priv;

    g_timer_stop(priv->timer);

    g_object_notify(G_OBJECT(download), "progress");
    webkit_download_set_status(download, WEBKIT_DOWNLOAD_STATUS_FINISHED);
}

static void webkit_download_error(WebKitDownload* download, const ResourceError& error)
{
    webkit_download_close_stream(download);

    WebKitDownloadPrivate* priv = download->priv;
    GRefPtr<WebKitDownload> protect(download);

    g_timer_stop(priv->timer);
    webkit_download_set_status(download, WEBKIT_DOWNLOAD_STATUS_ERROR);

    gboolean handled;
    g_signal_emit_by_name(download, "error", 0, WEBKIT_DOWNLOAD_ERROR_NETWORK, error.localizedDescription().utf8().data(), &handled);
}

DownloadClient::DownloadClient(WebKitDownload* download)
    : m_download(download)
{
}

void DownloadClient::didReceiveResponse(ResourceHandle*, const ResourceResponse& response)
{
    webkit_download_set_response(m_download, response);
}

void DownloadClient::didReceiveData(ResourceHandle*, const char* data, int length, int lengthReceived)
{
    webkit_download_received_data(m_download, data, length);
}

void DownloadClient::didFinishLoading(ResourceHandle*)
{
    webkit_download_finished_loading(m_download);
}

void DownloadClient::didFail(ResourceHandle*, const ResourceError& error)
{
    webkit_download_error(m_download, error);
}

void DownloadClient::wasBlocked(ResourceHandle*)
{
    // FIXME: Implement this when we have the new frame loader signals
    // and error handling.
    notImplemented();
}

void DownloadClient::cannotShowURL(ResourceHandle*)
{
    // FIXME: Implement this when we have the new frame loader signals
    // and error handling.
    notImplemented();
}
