/*
 * Copyright 2010, The Android Open Source Project
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * 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 THE COPYRIGHT HOLDERS ``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 THE COPYRIGHT OWNER OR
 * 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 "WebCache.h"

#include "JNIUtility.h"
#include "WebCoreJni.h"
#include "WebRequestContext.h"
#include "WebUrlLoaderClient.h"
#include "net/http/http_network_session.h"
#include <wtf/text/CString.h>

using namespace WTF;
using namespace disk_cache;
using namespace net;
using namespace std;

namespace android {

static WTF::Mutex instanceMutex;

static string storageDirectory()
{
    static const char* const kDirectory = "/webviewCacheChromium";

    JNIEnv* env = JSC::Bindings::getJNIEnv();
    jclass bridgeClass = env->FindClass("android/webkit/JniUtil");
    jmethodID method = env->GetStaticMethodID(bridgeClass, "getCacheDirectory", "()Ljava/lang/String;");
    string storageDirectory = jstringToStdString(env, static_cast<jstring>(env->CallStaticObjectMethod(bridgeClass, method)));
    env->DeleteLocalRef(bridgeClass);

    // Return empty string if storageDirectory is an empty string
    if (storageDirectory.empty())
        return storageDirectory;

    storageDirectory.append(kDirectory);
    return storageDirectory;
}

static scoped_refptr<WebCache>* instance(bool isPrivateBrowsing)
{
    static scoped_refptr<WebCache> regularInstance;
    static scoped_refptr<WebCache> privateInstance;
    return isPrivateBrowsing ? &privateInstance : &regularInstance;
}

WebCache* WebCache::get(bool isPrivateBrowsing)
{
    MutexLocker lock(instanceMutex);
    scoped_refptr<WebCache>* instancePtr = instance(isPrivateBrowsing);
    if (!instancePtr->get())
        *instancePtr = new WebCache(isPrivateBrowsing);
    return instancePtr->get();
}

void WebCache::cleanup(bool isPrivateBrowsing)
{
    MutexLocker lock(instanceMutex);
    scoped_refptr<WebCache>* instancePtr = instance(isPrivateBrowsing);
    *instancePtr = 0;
}

WebCache::WebCache(bool isPrivateBrowsing)
    : m_doomAllEntriesCallback(this, &WebCache::doomAllEntries)
    , m_onClearDoneCallback(this, &WebCache::onClearDone)
    , m_isClearInProgress(false)
    , m_openEntryCallback(this, &WebCache::openEntry)
    , m_onGetEntryDoneCallback(this, &WebCache::onGetEntryDone)
    , m_isGetEntryInProgress(false)
    , m_cacheBackend(0)
{
    base::Thread* ioThread = WebUrlLoaderClient::ioThread();
    scoped_refptr<base::MessageLoopProxy> cacheMessageLoopProxy = ioThread->message_loop_proxy();

    static const int kMaximumCacheSizeBytes = 20 * 1024 * 1024;
    m_hostResolver = net::CreateSystemHostResolver(net::HostResolver::kDefaultParallelism, 0, 0);

    m_proxyConfigService = new ProxyConfigServiceAndroid();
    net::HttpCache::BackendFactory* backendFactory;
    if (isPrivateBrowsing)
        backendFactory = net::HttpCache::DefaultBackend::InMemory(kMaximumCacheSizeBytes / 2);
    else {
        string storage(storageDirectory());
        if (storage.empty()) // Can't get a storage directory from the OS
            backendFactory = net::HttpCache::DefaultBackend::InMemory(kMaximumCacheSizeBytes / 2);
        else {
            FilePath directoryPath(storage.c_str());
            backendFactory = new net::HttpCache::DefaultBackend(net::DISK_CACHE, directoryPath, kMaximumCacheSizeBytes, cacheMessageLoopProxy);
        }
    }

    m_cache = new net::HttpCache(m_hostResolver.get(),
                                 new CertVerifier(),
                                 0, // dnsrr_resolver
                                 0, // dns_cert_checker
                                 net::ProxyService::CreateWithoutProxyResolver(m_proxyConfigService, 0 /* net_log */),
                                 net::SSLConfigService::CreateSystemSSLConfigService(),
                                 net::HttpAuthHandlerFactory::CreateDefault(m_hostResolver.get()),
                                 0, // network_delegate
                                 0, // net_log
                                 backendFactory);
}

void WebCache::clear()
{
    base::Thread* thread = WebUrlLoaderClient::ioThread();
    if (thread)
        thread->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &WebCache::clearImpl));
}

void WebCache::certTrustChanged()
{
    base::Thread* thread = WebUrlLoaderClient::ioThread();
    if (thread)
        thread->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &WebCache::certTrustChangedImpl));
}

void WebCache::certTrustChangedImpl()
{
    net::HttpNetworkSession* session = m_cache->GetSession();
    if (session)
        session->cert_verifier()->ClearCache();
    m_cache->CloseAllConnections();
}

void WebCache::closeIdleConnections()
{
    base::Thread* thread = WebUrlLoaderClient::ioThread();
    if (thread)
        thread->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &WebCache::closeIdleImpl));
}

void WebCache::closeIdleImpl()
{
    m_cache->CloseIdleConnections();
}

void WebCache::clearImpl()
{
    if (m_isClearInProgress)
        return;
    m_isClearInProgress = true;

    if (!m_cacheBackend) {
        int code = m_cache->GetBackend(&m_cacheBackend, &m_doomAllEntriesCallback);
        // Code ERR_IO_PENDING indicates that the operation is still in progress and
        // the supplied callback will be invoked when it completes.
        if (code == ERR_IO_PENDING)
            return;
        else if (code != OK) {
            onClearDone(0 /*unused*/);
            return;
        }
    }
    doomAllEntries(0 /*unused*/);
}

void WebCache::doomAllEntries(int)
{
    if (!m_cacheBackend) {
        onClearDone(0 /*unused*/);
        return;
    }

    // Code ERR_IO_PENDING indicates that the operation is still in progress and
    // the supplied callback will be invoked when it completes.
    if (m_cacheBackend->DoomAllEntries(&m_onClearDoneCallback) == ERR_IO_PENDING)
        return;
    onClearDone(0 /*unused*/);
}

void WebCache::onClearDone(int)
{
    m_isClearInProgress = false;
}

scoped_refptr<CacheResult> WebCache::getCacheResult(String url)
{
    // This is called on the UI thread.
    MutexLocker lock(m_getEntryMutex);
    if (m_isGetEntryInProgress)
        return 0; // TODO: OK? Or can we queue 'em up?

    // The Chromium methods are asynchronous, but we need this method to be
    // synchronous. Do the work on the Chromium thread but block this thread
    // here waiting for the work to complete.
    base::Thread* thread = WebUrlLoaderClient::ioThread();
    if (!thread)
        return 0;

    m_entry = 0;
    m_isGetEntryInProgress = true;
    m_entryUrl = url.threadsafeCopy();
    thread->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &WebCache::getEntryImpl));

    while (m_isGetEntryInProgress)
        m_getEntryCondition.wait(m_getEntryMutex);

    if (!m_entry)
        return 0;

    return new CacheResult(m_entry, url);
}

void WebCache::getEntryImpl()
{
    if (!m_cacheBackend) {
        int code = m_cache->GetBackend(&m_cacheBackend, &m_openEntryCallback);
        if (code == ERR_IO_PENDING)
            return;
        else if (code != OK) {
            onGetEntryDone(0 /*unused*/);
            return;
        }
    }
    openEntry(0 /*unused*/);
}

void WebCache::openEntry(int)
{
    if (!m_cacheBackend) {
        onGetEntryDone(0 /*unused*/);
        return;
    }

    if (m_cacheBackend->OpenEntry(string(m_entryUrl.utf8().data()), &m_entry, &m_onGetEntryDoneCallback) == ERR_IO_PENDING)
        return;
    onGetEntryDone(0 /*unused*/);
}

void WebCache::onGetEntryDone(int)
{
    // Unblock the UI thread in getEntry();
    MutexLocker lock(m_getEntryMutex);
    m_isGetEntryInProgress = false;
    m_getEntryCondition.signal();
}

} // namespace android