/*
 * Copyright (C) 2011 Christian Dywan <christian@lanedo.com>
 * Copyright (C) 2012 Igalia S.L.
 *
 * 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 "webkitfavicondatabase.h"

#include "DatabaseDetails.h"
#include "DatabaseTracker.h"
#include "FileSystem.h"
#include "GRefPtr.h"
#include "IconDatabase.h"
#include "IconDatabaseClient.h"
#include "Image.h"
#include "IntSize.h"
#include "webkitfavicondatabaseprivate.h"
#include "webkitglobals.h"
#include "webkitglobalsprivate.h"
#include "webkitmarshal.h"
#include "webkitsecurityoriginprivate.h"
#include "webkitwebframe.h"
#include <glib/gi18n-lib.h>
#include <wtf/MainThread.h>
#include <wtf/gobject/GOwnPtr.h>
#include <wtf/text/CString.h>

/**
 * SECTION:webkitfavicondatabase
 * @short_description: A WebKit favicon database
 * @Title: WebKitFaviconDatabase
 *
 * #WebKitFaviconDatabase provides access to the icons associated with
 * web sites.
 *
 * WebKit will automatically look for available icons in link elements
 * on opened pages as well as an existing favicon.ico and load the
 * images found into a memory cache if possible. That cache is frozen
 * to an on-disk database for persistence.
 *
 * The database is disabled by default. In order for icons to be
 * stored and accessed, you will need to set an icon database path
 * using webkit_favicon_database_set_path(). Disable the database
 * again passing %NULL to the previous call.
 *
 * If WebKitWebSettings::enable-private-browsing is %TRUE new icons
 * won't be added to the on-disk database and no existing icons will
 * be deleted from it. Nevertheless, WebKit will still store them in
 * the in-memory cache during the current execution.
 *
 * Since: 1.8
 */

using namespace WebKit;
using namespace WebCore;

class PendingIconRequest;

static void webkitFaviconDatabaseProcessPendingIconsForURI(WebKitFaviconDatabase*, const String& pageURI);
static void webkitFaviconDatabaseImportFinished(WebKitFaviconDatabase*);
static void webkitFaviconDatabaseGetIconPixbufCancelled(GCancellable*, PendingIconRequest*);
static void webkitFaviconDatabaseClose(WebKitFaviconDatabase* database);

class IconDatabaseClientGtk : public IconDatabaseClient {
public:
    // IconDatabaseClient interface
    virtual bool performImport() { return true; }
    virtual void didRemoveAllIcons() { };

    // Called when an icon is requested while the initial import is
    // going on.
    virtual void didImportIconURLForPageURL(const String& URL) { };

    // Called whenever a retained icon is read from database.
    virtual void didImportIconDataForPageURL(const String& URL)
    {
        WebKitFaviconDatabase* database = webkit_get_favicon_database();
        // We need to emit this here because webkitFaviconDatabaseDispatchDidReceiveIcon()
        // is only called for icons that have just being downloaded, and this is called
        // when icon data is imported from the database.
        g_signal_emit_by_name(database, "icon-loaded", URL.utf8().data());

        webkitFaviconDatabaseProcessPendingIconsForURI(database, URL);
    }

    virtual void didChangeIconForPageURL(const String& URL)
    {
        // Called when the the favicon for a particular URL changes.
        // It does not mean that the new icon data is available yet.
    }

    virtual void didFinishURLImport()
    {
        webkitFaviconDatabaseImportFinished(webkit_get_favicon_database());

        // Now that everything is imported enable pruning of old
        // icons. No icon will be removed during the import process
        // because we disable cleanups before opening the database.
        IconDatabase::allowDatabaseCleanup();
    }
};

class PendingIconRequest {
public:
    PendingIconRequest(const String& pageURL, GSimpleAsyncResult* result, GCancellable* cancellable, IntSize iconSize)
        : m_pageURL(pageURL)
        , m_asyncResult(result)
        , m_cancellable(cancellable)
        , m_cancelledId(0)
        , m_iconSize(iconSize)
    {
        if (cancellable) {
            m_cancelledId = g_cancellable_connect(cancellable, G_CALLBACK(webkitFaviconDatabaseGetIconPixbufCancelled), this, 0);
            g_object_set_data_full(G_OBJECT(result), "cancellable", g_object_ref(cancellable), static_cast<GDestroyNotify>(g_object_unref));
        }
    }

    ~PendingIconRequest()
    {
        if (m_cancelledId > 0)
            g_cancellable_disconnect(m_cancellable.get(), m_cancelledId);
    }

    const String& pageURL() { return m_pageURL; }
    GSimpleAsyncResult* asyncResult() { return m_asyncResult.get(); }
    const IntSize& iconSize() { return m_iconSize; }

    void asyncResultCancel()
    {
        ASSERT(m_asyncResult);
        g_simple_async_result_set_error(m_asyncResult.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED, "%s", _("Operation was cancelled"));
        g_simple_async_result_complete(m_asyncResult.get());
    }

    void asyncResultCompleteInIdle(GdkPixbuf* icon)
    {
        ASSERT(m_asyncResult);
        g_simple_async_result_set_op_res_gpointer(m_asyncResult.get(), icon, 0);
        g_simple_async_result_complete_in_idle(m_asyncResult.get());
    }

    void asyncResultComplete(GdkPixbuf* icon)
    {
        ASSERT(m_asyncResult);
        g_simple_async_result_set_op_res_gpointer(m_asyncResult.get(), icon, 0);
        g_simple_async_result_complete(m_asyncResult.get());
    }

private:
    String m_pageURL;
    GRefPtr<GSimpleAsyncResult> m_asyncResult;
    GRefPtr<GCancellable> m_cancellable;
    gulong m_cancelledId;
    IntSize m_iconSize;
};


enum {
    PROP_0,

    PROP_PATH,
};

enum {
    ICON_LOADED,

    LAST_SIGNAL
};

static guint webkit_favicon_database_signals[LAST_SIGNAL] = { 0, };

G_DEFINE_TYPE(WebKitFaviconDatabase, webkit_favicon_database, G_TYPE_OBJECT)

typedef Vector<OwnPtr<PendingIconRequest> > PendingIconRequestVector;
typedef HashMap<String, PendingIconRequestVector*> PendingIconRequestMap;

struct _WebKitFaviconDatabasePrivate {
    GOwnPtr<gchar> path;
    IconDatabaseClientGtk iconDatabaseClient;
    PendingIconRequestMap pendingIconRequests;
    bool importFinished;
};

static void webkit_favicon_database_finalize(GObject* object)
{
    WebKitFaviconDatabase* database = WEBKIT_FAVICON_DATABASE(object);

    webkitFaviconDatabaseClose(database);
    database->priv->~WebKitFaviconDatabasePrivate();

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

static void webkit_favicon_database_set_property(GObject* object, guint propId, const GValue* value, GParamSpec* pspec)
{
    WebKitFaviconDatabase* database = WEBKIT_FAVICON_DATABASE(object);

    switch (propId) {
    case PROP_PATH:
        webkit_favicon_database_set_path(database, g_value_get_string(value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec);
        break;
    }
}

static void webkit_favicon_database_get_property(GObject* object, guint propId, GValue* value, GParamSpec* pspec)
{
    WebKitFaviconDatabase* database = WEBKIT_FAVICON_DATABASE(object);

    switch (propId) {
    case PROP_PATH:
        g_value_set_string(value, webkit_favicon_database_get_path(database));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec);
        break;
    }
}

static void webkit_favicon_database_class_init(WebKitFaviconDatabaseClass* klass)
{
    webkitInit();

    GObjectClass* gobjectClass = G_OBJECT_CLASS(klass);
    gobjectClass->finalize = webkit_favicon_database_finalize;
    gobjectClass->set_property = webkit_favicon_database_set_property;
    gobjectClass->get_property = webkit_favicon_database_get_property;

     /**
      * WebKitFaviconDatabase:path:
      *
      * The absolute path of the icon database folder.
      *
      * Since: 1.8
      */
     g_object_class_install_property(gobjectClass, PROP_PATH,
                                     g_param_spec_string("path",
                                                         _("Path"),
                                                         _("The absolute path of the icon database folder"),
                                                         NULL,
                                                         WEBKIT_PARAM_READWRITE));


     /**
      * WebKitFaviconDatabase::icon-loaded:
      * @database: the object on which the signal is emitted
      * @frame_uri: the URI of the main frame of a Web page containing
      * the icon
      *
      * This signal is fired if an icon is loaded on any
      * #WebKitWebView. If you are only interested in a particular
      * #WebKitWebView see #WebKitWebView::icon-loaded.
      *
      * Note that this signal carries the URI of the frame that loads
      * the icon, while #WebKitWebView::icon-loaded provides the URI
      * of the favicon.
      *
      * Since: 1.8
      */
     webkit_favicon_database_signals[ICON_LOADED] = g_signal_new("icon-loaded",
                                                              G_TYPE_FROM_CLASS(klass),
                                                              (GSignalFlags)G_SIGNAL_RUN_LAST,
                                                              0, 0, 0,
                                                              webkit_marshal_VOID__STRING,
                                                              G_TYPE_NONE, 1,
                                                              G_TYPE_STRING);

    g_type_class_add_private(klass, sizeof(WebKitFaviconDatabasePrivate));
}

static void webkit_favicon_database_init(WebKitFaviconDatabase* database)
{
    database->priv = G_TYPE_INSTANCE_GET_PRIVATE(database, WEBKIT_TYPE_FAVICON_DATABASE, WebKitFaviconDatabasePrivate);
    new (database->priv) WebKitFaviconDatabasePrivate();
}

// Called from FrameLoaderClient::dispatchDidReceiveIcon()
void webkitFaviconDatabaseDispatchDidReceiveIcon(WebKitFaviconDatabase* database, const char* frameURI)
{
    g_signal_emit(database, webkit_favicon_database_signals[ICON_LOADED], 0, frameURI);

    // Retain the new icon.
    iconDatabase().retainIconForPageURL(String::fromUTF8(frameURI));
}

/**
 * webkit_favicon_database_get_path:
 * @database: a #WebKitFaviconDatabase
 *
 * Determines the absolute path to the database folder on disk.
 *
 * Returns: the absolute path of the database folder, or %NULL
 *
 * Since: 1.8
 */
const gchar* webkit_favicon_database_get_path(WebKitFaviconDatabase* database)
{
    g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), 0);

    return database->priv->path.get();
}

static void webkitFaviconDatabaseClose(WebKitFaviconDatabase* database)
{
    if (iconDatabase().isEnabled()) {
        iconDatabase().setEnabled(false);
        iconDatabase().close();
    }
}

/**
 * webkit_favicon_database_set_path:
 * @database: a #WebKitFaviconDatabase
 * @path: (allow-none): an absolute path to the icon database folder
 * or %NULL to disable the database
 *
 * Specifies the absolute path to the database folder on disk. The
 * icon database will only be enabled after a call to this method.
 *
 * Passing %NULL or "" as path disables the icon database.
 *
 * Since: 1.8
 */
void webkit_favicon_database_set_path(WebKitFaviconDatabase* database, const gchar* path)
{
    g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));

    // Always try to close because the deprecated icondatabase is opened by default.
    webkitFaviconDatabaseClose(database);

    database->priv->importFinished = false;
    if (!path || !path[0]) {
        database->priv->path.set(0);
        iconDatabase().setEnabled(false);
        return;
    }

    iconDatabase().setClient(&database->priv->iconDatabaseClient);
    IconDatabase::delayDatabaseCleanup();
    iconDatabase().setEnabled(true);
    if (!iconDatabase().open(filenameToString(path), IconDatabase::defaultDatabaseFilename())) {
        IconDatabase::allowDatabaseCleanup();
        return;
    }

    database->priv->path.set(g_strdup(path));
}

/**
 * webkit_favicon_database_get_favicon_uri:
 * @database: a #WebKitFaviconDatabase
 * @page_uri: URI of the page containing the icon
 *
 * Obtains the URI for the favicon for the given page URI.
 * See also webkit_web_view_get_icon_uri().
 *
 * Returns: a newly allocated URI for the favicon, or %NULL
 *
 * Since: 1.8
 */
gchar* webkit_favicon_database_get_favicon_uri(WebKitFaviconDatabase* database, const gchar* pageURI)
{
    g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), 0);
    g_return_val_if_fail(pageURI, 0);
    ASSERT(isMainThread());

    String iconURI = iconDatabase().synchronousIconURLForPageURL(String::fromUTF8(pageURI));
    if (iconURI.isEmpty())
        return 0;

    return g_strdup(iconURI.utf8().data());
}

static GdkPixbuf* getIconPixbufSynchronously(WebKitFaviconDatabase* database, const String& pageURL, const IntSize& iconSize)
{
    ASSERT(isMainThread());

    // The exact size we pass is irrelevant to the iconDatabase code.
    // We must pass something greater than 0x0 to get a pixbuf.
    Image* icon = iconDatabase().synchronousIconForPageURL(pageURL, !iconSize.isZero() ? iconSize : IntSize(1, 1));
    if (!icon)
        return 0;

    GRefPtr<GdkPixbuf> pixbuf = adoptGRef(icon->getGdkPixbuf());
    if (!pixbuf)
        return 0;

    // A size of (0, 0) means the maximum available size.
    if (!iconSize.isZero() && (icon->width() != iconSize.width() || icon->height() != iconSize.height()))
        pixbuf = gdk_pixbuf_scale_simple(pixbuf.get(), iconSize.width(), iconSize.height(), GDK_INTERP_BILINEAR);
    return pixbuf.leakRef();
}

/**
 * webkit_favicon_database_try_get_favicon_pixbuf:
 * @database: a #WebKitFaviconDatabase
 * @page_uri: URI of the page containing the icon
 * @width: the desired width for the icon
 * @height: the desired height for the icon
 *
 * Obtains a #GdkPixbuf of the favicon for the given page URI, or
 * %NULL if there is no icon for the given page or it hasn't been
 * loaded from disk yet. Use webkit_favicon_database_get_favicon_uri()
 * if you need to distinguish these cases.  To make sure this method
 * will return a valid icon when the given URI has one, you should
 * connect to #WebKitFaviconDatabase::icon-loaded and use this function
 * in the callback.
 *
 * If @width and @height ar both 0 then this method will return the
 * maximum available size for the icon. Note that if you specify a
 * different size the icon will be scaled each time you call this
 * function.
 *
 * Returns: (transfer full): a new reference to a #GdkPixbuf, or %NULL
 * if the given URI doesn't have an icon or it hasn't been loaded yet.
 *
 * Since: 1.8
 */
GdkPixbuf* webkit_favicon_database_try_get_favicon_pixbuf(WebKitFaviconDatabase* database, const gchar* pageURI, guint width, guint height)
{
    g_return_val_if_fail(WEBKIT_IS_FAVICON_DATABASE(database), 0);
    g_return_val_if_fail(pageURI, 0);
    g_return_val_if_fail((width && height) || (!width && !height), 0);

    return getIconPixbufSynchronously(database, String::fromUTF8(pageURI), IntSize(width, height));
}

static PendingIconRequestVector* webkitFaviconDatabaseGetOrCreateRequests(WebKitFaviconDatabase* database, const String& pageURL)
{
    PendingIconRequestVector* icons = database->priv->pendingIconRequests.get(pageURL);
    if (!icons) {
        icons = new PendingIconRequestVector;
        database->priv->pendingIconRequests.set(pageURL, icons);
    }

    return icons;
}

static void webkitfavicondatabaseDeleteRequests(WebKitFaviconDatabase* database, PendingIconRequestVector* requests, const String& pageURL)
{
    database->priv->pendingIconRequests.remove(pageURL);
    delete requests;
}

static void getIconPixbufCancelled(void* userData)
{
    PendingIconRequest* request = static_cast<PendingIconRequest*>(userData);
    request->asyncResultCancel();

    const String& pageURL = request->pageURL();
    WebKitFaviconDatabase* database = webkit_get_favicon_database();
    PendingIconRequestVector* icons = database->priv->pendingIconRequests.get(pageURL);
    if (!icons)
        return;

    size_t itemIndex = icons->find(request);
    if (itemIndex != notFound)
        icons->remove(itemIndex);
    if (icons->isEmpty())
        webkitfavicondatabaseDeleteRequests(database, icons, pageURL);
}

static void webkitFaviconDatabaseGetIconPixbufCancelled(GCancellable* cancellable, PendingIconRequest* request)
{
    // Handle cancelled in a in idle since it might be called from any thread.
    callOnMainThread(getIconPixbufCancelled, request);
}

/**
 * webkit_favicon_database_get_favicon_pixbuf:
 * @database: a #WebKitFaviconDatabase
 * @page_uri: URI of the page containing the icon
 * @width: the desired width for the icon
 * @height: the desired height for the icon
 * @cancellable: (allow-none): A #GCancellable or %NULL.
 * @callback: (allow-none): A #GAsyncReadyCallback to call when the request is
 *            satisfied or %NULL if you don't care about the result.
 * @user_data: The data to pass to @callback.
 *
 * Asynchronously obtains a #GdkPixbuf of the favicon for the given
 * page URI. The advantage of this method over
 * webkit_favicon_database_try_get_favicon_pixbuf() is that it always returns the
 * cached icon if it's in the database asynchronously waiting for the
 * icon to be read from the database.
 *
 * This is an asynchronous method. When the operation is finished, callback will
 * be invoked. You can then call webkit_favicon_database_get_favicon_pixbuf_finish()
 * to get the result of the operation.
 * See also webkit_favicon_database_try_get_favicon_pixbuf().
 *
 * If @width and @height are both 0 then this method will return the
 * maximum available size for the icon. Note that if you specify a
 * different size the icon will be scaled each time you call this
 * function.
 *
 * Since: 1.8
 */
void webkit_favicon_database_get_favicon_pixbuf(WebKitFaviconDatabase* database, const gchar* pageURI, guint width, guint height, GCancellable* cancellable, GAsyncReadyCallback callback, gpointer userData)
{
    g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));
    g_return_if_fail(pageURI);
    g_return_if_fail((width && height) || (!width && !height));

    GRefPtr<GSimpleAsyncResult> result = adoptGRef(g_simple_async_result_new(G_OBJECT(database), callback, userData,
                                                                             reinterpret_cast<gpointer>(webkit_favicon_database_get_favicon_pixbuf)));

    // If we don't have an icon for the given URI return ASAP.
    if (database->priv->importFinished && iconDatabase().synchronousIconURLForPageURL(String::fromUTF8(pageURI)).isEmpty()) {
        g_simple_async_result_set_op_res_gpointer(result.get(), 0, 0);
        g_simple_async_result_complete_in_idle(result.get());
        return;
    }

    String pageURL = String::fromUTF8(pageURI);
    PendingIconRequest* request = new PendingIconRequest(pageURL, result.get(), cancellable, IntSize(width, height));

    // Register icon request before asking for the icon to avoid race conditions.
    PendingIconRequestVector* icons = webkitFaviconDatabaseGetOrCreateRequests(database, pageURL);
    ASSERT(icons);
    icons->append(adoptPtr(request));

    GdkPixbuf* pixbuf = getIconPixbufSynchronously(database, pageURL, IntSize(width, height));
    if (!pixbuf && !database->priv->importFinished) {
        // Initial import is ongoing, the icon data will be available later.
        return;
    }

    request->asyncResultCompleteInIdle(pixbuf);

    // Remove the request we have just created as it isn't pending
    // anymore because we already have the pixbuf.
    ASSERT(icons->last().get() == request);
    icons->removeLast();
    if (icons->isEmpty())
        webkitfavicondatabaseDeleteRequests(database, icons, pageURL);
}

/**
 * webkit_favicon_database_get_favicon_pixbuf_finish:
 * @database: a #WebKitFaviconDatabase
 * @result: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to webkit_favicon_database_get_favicon_pixbuf()
 * @error: (allow-none): Return location for error or %NULL.
 *
 * Finishes an operation started with webkit_favicon_database_get_favicon_pixbuf().
 *
 * Returns: (transfer full): a new reference to a #GdkPixbuf, or %NULL.
 *
 * Since: 1.8
 */
GdkPixbuf* webkit_favicon_database_get_favicon_pixbuf_finish(WebKitFaviconDatabase* database, GAsyncResult* result, GError** error)
{
    GSimpleAsyncResult* simpleResult = G_SIMPLE_ASYNC_RESULT(result);
    g_return_val_if_fail(g_simple_async_result_get_source_tag(simpleResult) == webkit_favicon_database_get_favicon_pixbuf, 0);

    if (g_simple_async_result_propagate_error(simpleResult, error))
        return 0;

    GCancellable* cancellable = static_cast<GCancellable*>(g_object_get_data(G_OBJECT(simpleResult), "cancellable"));
    if (cancellable && g_cancellable_is_cancelled(cancellable)) {
        g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Operation was cancelled"));
        return 0;
    }

    GdkPixbuf* icon = static_cast<GdkPixbuf*>(g_simple_async_result_get_op_res_gpointer(simpleResult));
    if (!icon)
        return 0;

    return static_cast<GdkPixbuf*>(icon);
}

static void webkitFaviconDatabaseProcessPendingIconsForURI(WebKitFaviconDatabase* database, const String& pageURL)
{
    PendingIconRequestVector* icons = database->priv->pendingIconRequests.get(pageURL);
    if (!icons)
        return;

    for (size_t i = 0; i < icons->size(); ++i) {
        PendingIconRequest* request = icons->at(i).get();
        if (request->asyncResult())
            request->asyncResultComplete(getIconPixbufSynchronously(database, pageURL, request->iconSize()));
    }
    webkitfavicondatabaseDeleteRequests(database, icons, pageURL);
}

static void webkitFaviconDatabaseImportFinished(WebKitFaviconDatabase* database)
{
    ASSERT(isMainThread());
    database->priv->importFinished = true;

    // Import is complete, process pending requests for pages that are not in the database,
    // since didImportIconDataForPageURL() will never be called for them.
    Vector<String> toDeleteURLs;
    PendingIconRequestMap::const_iterator end = database->priv->pendingIconRequests.end();
    for (PendingIconRequestMap::const_iterator iter = database->priv->pendingIconRequests.begin(); iter != end; ++iter) {
        String iconURL = iconDatabase().synchronousIconURLForPageURL(iter->first);
        if (!iconURL.isEmpty())
            continue;

        PendingIconRequestVector* icons = iter->second;
        for (size_t i = 0; i < icons->size(); ++i) {
            PendingIconRequest* request = icons->at(i).get();
            if (request->asyncResult())
                request->asyncResultComplete(0);
        }

        toDeleteURLs.append(iter->first);
    }

    for (size_t i = 0; i < toDeleteURLs.size(); ++i)
        webkitfavicondatabaseDeleteRequests(database, database->priv->pendingIconRequests.get(toDeleteURLs[i]), toDeleteURLs[i]);
}

/**
 * webkit_favicon_database_clear:
 * @database: a #WebKitFaviconDatabase
 *
 * Clears all icons from the database.
 *
 * Since: 1.8
 */
void webkit_favicon_database_clear(WebKitFaviconDatabase* database)
{
    g_return_if_fail(WEBKIT_IS_FAVICON_DATABASE(database));

    iconDatabase().removeAllIcons();
}
