/*
 * 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 Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2,1 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 "WebKitSoupCookieJarSqlite.h"

#include <WebCore/SQLiteDatabase.h>
#include <WebCore/SQLiteStatement.h>
#include <WebCore/SQLiteTransaction.h>
#include <libsoup/soup.h>
#include <wtf/CurrentTime.h>
#include <wtf/MathExtras.h>

using namespace WebCore;

struct _WebKitSoupCookieJarSqlitePrivate {
    String databasePath;
    SQLiteDatabase database;
    bool isLoading;
};

G_DEFINE_TYPE(WebKitSoupCookieJarSqlite, webkit_soup_cookie_jar_sqlite, SOUP_TYPE_COOKIE_JAR)

enum {
    ColumnID,
    ColumnName,
    ColumnValue,
    ColumnHost,
    ColumnPath,
    ColumnExpiry,
    ColumnLastAccess,
    ColumnSecure,
    ColumnHTTPOnly
};

static bool webkitSoupCookieJarSqliteOpenDatabase(WebKitSoupCookieJarSqlite* sqliteJar)
{
    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
    if (priv->database.isOpen())
        return true;

    ASSERT(!priv->databasePath.isEmpty());
    if (!priv->database.open(priv->databasePath)) {
        g_warning("Can't open database %s", priv->databasePath.utf8().data());
        return false;
    }

    priv->database.setSynchronous(SQLiteDatabase::SyncOff);
    priv->database.executeCommand("PRAGMA secure_delete = 1;");

    return true;
}

static bool webkitSoupCookieJarSqliteCreateTable(WebKitSoupCookieJarSqlite* sqliteJar)
{
    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
    if (priv->database.tableExists("moz_cookies"))
        return true;

    if (!priv->database.executeCommand("CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT, expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER)")) {
        g_warning("Failed to create table moz_cookies: (%i) - %s", priv->database.lastError(), priv->database.lastErrorMsg());
        priv->database.close();

        return false;
    }

    return true;
}

static void webkitSoupCookieJarSqliteLoad(WebKitSoupCookieJarSqlite* sqliteJar)
{
    if (!webkitSoupCookieJarSqliteOpenDatabase(sqliteJar))
        return;
    if (!webkitSoupCookieJarSqliteCreateTable(sqliteJar))
        return;

    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
    priv->isLoading = true;
    SQLiteStatement query(priv->database, "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly FROM moz_cookies;");
    if (query.prepare() != SQLITE_OK) {
        g_warning("Failed to prepare all cookies query");
        priv->isLoading = false;
        return;
    }

    SoupCookieJar* jar = SOUP_COOKIE_JAR(sqliteJar);
    time_t now = floorf(currentTime());
    int result;
    while ((result = query.step()) == SQLITE_ROW) {
        int expireTime = query.getColumnInt(ColumnExpiry);
        if (now >= expireTime)
            continue;

        SoupCookie* cookie = soup_cookie_new(query.getColumnText(ColumnName).utf8().data(), query.getColumnText(ColumnValue).utf8().data(),
                                             query.getColumnText(ColumnHost).utf8().data(), query.getColumnText(ColumnPath).utf8().data(),
                                             expireTime - now <= G_MAXINT ? expireTime - now : G_MAXINT);
        if (query.getColumnInt(ColumnSecure))
            soup_cookie_set_secure(cookie, TRUE);
        if (query.getColumnInt(ColumnHTTPOnly))
            soup_cookie_set_http_only(cookie, TRUE);

        soup_cookie_jar_add_cookie(jar, cookie);
    }

    if (result != SQLITE_DONE)
        g_warning("Error reading cookies from database");
    priv->isLoading = false;
}

static bool webkitSoupCookieJarSqliteInsertCookie(WebKitSoupCookieJarSqlite* sqliteJar, SoupCookie* cookie)
{
    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
    SQLiteStatement query(priv->database, "INSERT INTO moz_cookies VALUES(NULL, ?, ?, ?, ?, ?, NULL, ?, ?);");
    if (query.prepare() != SQLITE_OK) {
        g_warning("Failed to prepare insert cookies query");
        return false;
    }

    query.bindText(1, String::fromUTF8(cookie->name));
    query.bindText(2, String::fromUTF8(cookie->value));
    query.bindText(3, String::fromUTF8(cookie->domain));
    query.bindText(4, String::fromUTF8(cookie->path));
    query.bindInt(5, static_cast<int64_t>(soup_date_to_time_t(cookie->expires)));
    query.bindInt(6, cookie->secure);
    query.bindInt(7, cookie->http_only);
    if (query.step() != SQLITE_DONE) {
        g_warning("Error adding cookie (name=%s, domain=%s) to database", cookie->name, cookie->name);
        return false;
    }

    return true;
}

static bool webkitSoupCookieJarSqliteDeleteCookie(WebKitSoupCookieJarSqlite* sqliteJar, SoupCookie* cookie)
{
    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
    SQLiteStatement query(priv->database, "DELETE FROM moz_cookies WHERE name = (?) AND host = (?);");
    if (query.prepare() != SQLITE_OK) {
        g_warning("Failed to prepare delete cookies query");
        return false;
    }

    query.bindText(1, String::fromUTF8(cookie->name));
    query.bindText(2, String::fromUTF8(cookie->domain));
    if (query.step() != SQLITE_DONE) {
        g_warning("Error deleting cookie (name=%s, domain=%s) from database", cookie->name, cookie->name);
        return false;
    }

    return true;
}

static void webkitSoupCookieJarSqliteChanged(SoupCookieJar* jar, SoupCookie* oldCookie, SoupCookie* newCookie)
{
    WebKitSoupCookieJarSqlite* sqliteJar = WEBKIT_SOUP_COOKIE_JAR_SQLITE(jar);
    if (sqliteJar->priv->isLoading)
        return;
    if (!webkitSoupCookieJarSqliteOpenDatabase(sqliteJar))
        return;
    if (!oldCookie && (!newCookie || !newCookie->expires))
        return;
    if (!webkitSoupCookieJarSqliteCreateTable(sqliteJar))
        return;

    SQLiteTransaction updateTransaction(sqliteJar->priv->database);
    updateTransaction.begin();

    if (oldCookie && !webkitSoupCookieJarSqliteDeleteCookie(sqliteJar, oldCookie))
        return;

    if (newCookie && newCookie->expires && !webkitSoupCookieJarSqliteInsertCookie(sqliteJar, newCookie))
        return;

    updateTransaction.commit();
}

static void webkitSoupCookieJarSqliteFinalize(GObject* object)
{
    WEBKIT_SOUP_COOKIE_JAR_SQLITE(object)->priv->~WebKitSoupCookieJarSqlitePrivate();
    G_OBJECT_CLASS(webkit_soup_cookie_jar_sqlite_parent_class)->finalize(object);
}

static void webkit_soup_cookie_jar_sqlite_init(WebKitSoupCookieJarSqlite* sqliteJar)
{
    WebKitSoupCookieJarSqlitePrivate* priv = G_TYPE_INSTANCE_GET_PRIVATE(sqliteJar, WEBKIT_TYPE_SOUP_COOKIE_JAR_SQLITE, WebKitSoupCookieJarSqlitePrivate);
    sqliteJar->priv = priv;
    new (priv) WebKitSoupCookieJarSqlitePrivate();
}

static void webkit_soup_cookie_jar_sqlite_class_init(WebKitSoupCookieJarSqliteClass* sqliteJarClass)
{
    SoupCookieJarClass* cookieJarClass = SOUP_COOKIE_JAR_CLASS(sqliteJarClass);
    cookieJarClass->changed = webkitSoupCookieJarSqliteChanged;

    GObjectClass* gObjectClass = G_OBJECT_CLASS(sqliteJarClass);
    gObjectClass->finalize = webkitSoupCookieJarSqliteFinalize;

    g_type_class_add_private(sqliteJarClass, sizeof(WebKitSoupCookieJarSqlitePrivate));
}

SoupCookieJar* webkitSoupCookieJarSqliteNew(const String& databasePath)
{
    ASSERT(!databasePath.isEmpty());
    WebKitSoupCookieJarSqlite* sqliteJar = WEBKIT_SOUP_COOKIE_JAR_SQLITE(g_object_new(WEBKIT_TYPE_SOUP_COOKIE_JAR_SQLITE, NULL));
    sqliteJar->priv->databasePath = databasePath;
    webkitSoupCookieJarSqliteLoad(sqliteJar);
    return SOUP_COOKIE_JAR(sqliteJar);
}
