/****************************************************************************
** Dooble - The Secure Internet Web Browser
**
** Copyright (c) 2012 Alexis Megas,
** Gunther van Dooble, and the Dooble Team.
** All rights reserved.
**
** License: GPL2 only:
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; version 2 of the License only.
**
** This program 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 General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
** or see here: http://www.gnu.org/licenses/gpl.html
**
** For the WebKit library, please see: http://webkit.org.
**
** THE CODE IS PROVIDED BY THE AUTHORS ''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 AUTHOR 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 APPLICATION, EVEN IF ADVISED
** OF THE POSSIBILITY OF SUCH DAMAGE.
**
** Please report all praise, requests, bugs, and problems to the project
** team and administrators: http://sf.net/projects/dooble.
**
** You can find us listed at our project page. New team members are welcome.
** The name of the authors should not be used to endorse or promote products
** derived from Dooble without specific prior written permission.
** If you use this code for other projects, please let us know.
**
** Web sites:
**   http://sf.net/projects/dooble
**   http://dooble.sf.net
****************************************************************************/

#include <QtCore>
#include <QBuffer>
#include <QSqlQuery>
#include <QSqlDatabase>

#include "dooble.h"
#include "dnetworkcache.h"

dnetworkcache::dnetworkcache(void)
{
  m_timer = 0;
  m_cache.setMaxCost(0);
}

dnetworkcache::~dnetworkcache()
{
  if(!dmisc::passphraseWasAuthenticated())
    clear();
  else
    {
      m_future.waitForFinished();
      m_populateFuture.waitForFinished();
    }
}

void dnetworkcache::populate(void)
{
  m_mutex.lock();
  m_cache.clear();
  m_mutex.unlock();

  if(!dmisc::passphraseWasAuthenticated())
    return;

  if(dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
			      false).toBool())
    if(m_populateFuture.isFinished())
      m_populateFuture = QtConcurrent::run
	(this, &dnetworkcache::readCacheFromDisk);

  if(!m_timer)
    {
      int interval =
	1000 * dooble::s_settings.
	value("settingsWindow/commitCacheToDiskInterval", 30.0).toDouble();

      m_timer = new QTimer(this);
      m_timer->setInterval(interval);
      connect(m_timer,
	      SIGNAL(timeout(void)),
	      this,
	      SLOT(slotTimeout(void)));

      if(dooble::s_settings.value("settingsWindow/diskCacheEnabled",
				  false).toBool())
	m_timer->start();
    }
}

void dnetworkcache::createCacheDatabase(void)
{
  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cache");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() +
		       "cache.db");

    if(db.open())
      {
	QSqlQuery query(db);

	query.exec("CREATE TABLE IF NOT EXISTS cache ("
		   "data BLOB DEFAULT NULL, "
		   "meta_data BLOB NOT NULL)");
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cache");
}

qint64 dnetworkcache::cacheSize(void) const
{
  /*
  ** What should we return here? The total cache size or
  ** the size of one of the caches?
  */

  return diskCacheSize() + networkCacheSize();
}

qint64 dnetworkcache::diskCacheSize(void) const
{
  /*
  ** I suppose that this may be replaced by a query of
  ** the database's file size.
  */

  static qint64 size = 0;

  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cache");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() +
		       "cache.db");

    if(db.open())
      {
	int pageCount = 0;
	int pageSize = 0;
	QSqlQuery query(db);

	if(query.exec("PRAGMA page_count"))
	  if(query.next())
	    {
	      pageCount = query.value(0).toInt();

	      if(query.exec("PRAGMA page_size"))
		if(query.next())
		  {
		    pageSize = query.value(0).toInt();
		    size = pageCount * pageSize;
		  }
	    }
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cache");
  return size;
}

qint64 dnetworkcache::networkCacheSize(void) const
{
  return m_cache.totalCost();
}

QIODevice *dnetworkcache::data(const QUrl &url)
{
  if(!dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
			       true).toBool())
    return 0;
  else if(url.isEmpty() || !url.isValid())
    return 0;
  else if(dooble::s_cacheExceptionsWindow->allowed(url.host()))
    return 0;

  QMutexLocker locker(&m_mutex);

  if(!m_cache.contains(url.toString(QUrl::StripTrailingSlash)))
    return 0;

  QPair<QByteArray, QNetworkCacheMetaData> *pair =
    m_cache.object(url.toString(QUrl::StripTrailingSlash));

  if(!pair)
    return 0;
  else if(pair->first.isEmpty())
    return 0;

  QBuffer *buffer = new QBuffer();

  buffer->setData(pair->first);
  buffer->open(QIODevice::ReadOnly);
  return buffer;
}

void dnetworkcache::insert(QIODevice *device)
{
  if(!device)
    return;
  else if(!dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
				    true).toBool())
    {
      device->deleteLater();
      return;
    }

  QBuffer *buffer = qobject_cast<QBuffer *> (device);

  if(!buffer)
    {
      device->deleteLater();
      return;
    }

  QUrl url(buffer->property("dooble-url").toUrl());
  QMutexLocker locker(&m_mutex);

  if(url.isEmpty() || !url.isValid())
    {
      buffer->deleteLater();
      return;
    }
  else if(dooble::s_cacheExceptionsWindow->allowed(url.host()))
    {
      buffer->deleteLater();
      return;
    }
  else if(!m_cache.contains(url.toString(QUrl::StripTrailingSlash)))
    {
      buffer->deleteLater();
      return;
    }

  QPair<QByteArray, QNetworkCacheMetaData> *pair =
    m_cache.object(url.toString(QUrl::StripTrailingSlash));

  if(!pair)
    {
      buffer->deleteLater();
      return;
    }

  pair->first = buffer->data();
  buffer->deleteLater();
}

QNetworkCacheMetaData dnetworkcache::metaData(const QUrl &url)
{
  QNetworkCacheMetaData metaData;

  if(!dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
			       true).toBool())
    return metaData;
  else if(url.isEmpty() || !url.isValid())
    return metaData;
  else if(dooble::s_cacheExceptionsWindow->allowed(url.host()))
    return metaData;

  QMutexLocker locker(&m_mutex);

  if(!m_cache.contains(url.toString(QUrl::StripTrailingSlash)))
    return metaData;
  else
    {
      QPair<QByteArray, QNetworkCacheMetaData> *pair =
	m_cache.object(url.toString(QUrl::StripTrailingSlash));

      if(pair)
	metaData = pair->second;
    }

  return metaData;
}

QIODevice *dnetworkcache::prepare(const QNetworkCacheMetaData &metaData)
{
  if(!dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
			       true).toBool())
    return 0;
  else if(!metaData.isValid() || metaData.url().isEmpty() ||
	  !metaData.url().isValid())
    return 0;
  else if(dooble::s_cacheExceptionsWindow->allowed(metaData.url().host()))
    return 0;

  int cacheSize = 1048576 * dooble::s_settings.value
    ("settingsWindow/webMemoryCacheSize", 50).toInt();

  m_mutex.lock();
  m_cache.setMaxCost(cacheSize);
  m_mutex.unlock();

  qint64 size = 0;

  foreach(QNetworkCacheMetaData::RawHeader header, metaData.rawHeaders())
    if(header.first.toLower() == "content-length")
      {
	size = header.second.toInt();

	/*
	** Be careful not to cache large objects.
	*/

	if(size > (cacheSize * 3) / 4)
	  return 0;
      }
    else if(header.first.toLower() == "content-type" &&
	    header.second.toLower().contains("application"))
      return 0;
    else if(header.first.toLower() == "content-type" &&
	    header.second.toLower().contains("javascript"))
      return 0;

  QBuffer *buffer = new QBuffer(this);

  buffer->setProperty("dooble-url", metaData.url());

  QPair<QByteArray, QNetworkCacheMetaData> *pair =
    new QPair<QByteArray, QNetworkCacheMetaData> (QByteArray(), metaData);

  m_mutex.lock();
  m_cache.insert
    (metaData.url().toString(QUrl::StripTrailingSlash), pair,
     qMax(size, qint64(1)));
  m_mutex.unlock();
  buffer->open(QIODevice::ReadWrite);
  return buffer;
}

bool dnetworkcache::remove(const QUrl &url)
{
  if(!dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
			       true).toBool())
    return false;
  else if(url.isEmpty() || !url.isValid())
    return false;

  QMutexLocker locker(&m_mutex);

  return m_cache.remove(url.toString(QUrl::StripTrailingSlash));
}

void dnetworkcache::updateMetaData(const QNetworkCacheMetaData &metaData)
{
  if(!dooble::s_settings.value("settingsWindow/memoryCacheEnabled",
			       true).toBool())
    return;

  QUrl url(metaData.url());

  if(url.isEmpty() || !url.isValid())
    return;
  else if(dooble::s_cacheExceptionsWindow->allowed(url.host()))
    return;

  QMutexLocker locker(&m_mutex);

  if(!m_cache.contains(url.toString(QUrl::StripTrailingSlash)))
    return;
  else
    {
      QPair<QByteArray, QNetworkCacheMetaData> *pair =
	m_cache.object(url.toString(QUrl::StripTrailingSlash));

      if(pair)
	pair->second = metaData;
    }
}

void dnetworkcache::clear(void)
{
  clearDiskCache();
  clearMemoryCache();
}

void dnetworkcache::clearDiskCache(void)
{
  if(dmisc::passphraseWasAuthenticated())
    {
      {
	QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cache");

	db.setDatabaseName(dooble::s_homePath +
			   QDir::separator() + "cache.db");

	if(db.open())
	  {
	    QSqlQuery query(db);

	    query.exec("PRAGMA synchronous = OFF");
	    query.exec("DELETE FROM cache");
	    query.exec("VACUUM");
	  }

	db.close();
      }

      QSqlDatabase::removeDatabase("cache");
    }
}

void dnetworkcache::clearMemoryCache(void)
{
  m_mutex.lock();
  m_cache.clear();
  m_cache.setMaxCost(0);
  m_mutex.unlock();
}

void dnetworkcache::slotTimeout(void)
{
  if(!m_future.isFinished() || !m_populateFuture.isFinished())
    return;

  m_mutex.lock();

  QStringList keys(m_cache.keys());

  m_mutex.unlock();

  QHash<QString, QPair<QByteArray, QNetworkCacheMetaData> > hash;

  while(!keys.isEmpty())
    {
      QString key(keys.takeFirst());

      m_mutex.lock();

      QPair<QByteArray, QNetworkCacheMetaData> *pair = m_cache.object(key);

      m_mutex.unlock();

      if(pair)
	{
	  QPair<QByteArray, QNetworkCacheMetaData> p(pair->first,
						     pair->second);

	  hash[key] = p;
	}
    }

  createCacheDatabase();
  m_future = QtConcurrent::run(this, &dnetworkcache::writeCacheToDisk, hash);
}

void dnetworkcache::readCacheFromDisk(void)
{
  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cache_read");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() +
		       "cache.db");

    if(db.open())
      {
	int cacheSize = 1048576 * dooble::s_settings.value
	  ("settingsWindow/webMemoryCacheSize", 50).toInt();

	m_mutex.lock();
	m_cache.setMaxCost(cacheSize);
	m_mutex.unlock();

	int ct = 0;
	int size = 0;
	QDateTime now(QDateTime::currentDateTime());
	QSqlQuery query(db);

	if(query.exec("SELECT COUNT(*) FROM cache"))
	  if(query.next())
	    size = query.value(0).toInt();

	if(query.exec("SELECT data, meta_data FROM cache"))
	  while(query.next())
	    {
	      QByteArray bytes1
		(dmisc::decodedString
		 (QByteArray::fromBase64
		  (qUncompress(query.value(0).toByteArray()))));
	      QByteArray bytes2
		(dmisc::decodedString
		 (QByteArray::fromBase64
		  (qUncompress(query.value(1).toByteArray()))));
	      QDataStream in(&bytes2, QIODevice::ReadOnly);
	      QNetworkCacheMetaData metaData;

	      in >> metaData;

	      if(!metaData.isValid() || metaData.url().isEmpty() ||
		 !metaData.url().isValid() ||
		 metaData.expirationDate().
		 toLocalTime() <= now.toLocalTime() ||
		 dooble::s_cacheExceptionsWindow->
		 allowed(metaData.url().host()))
		{
		  QSqlQuery deleteQuery(db);

		  deleteQuery.exec("PRAGMA synchronous = OFF");
		  deleteQuery.prepare("DELETE FROM cache WHERE "
				      "meta_data = ?");
		  deleteQuery.bindValue(0, query.value(1));
		  deleteQuery.exec();
		  ct += 1;
		  emit percentRead(100 * ct / size);
		  continue;
		}

	      QPair<QByteArray, QNetworkCacheMetaData> *pair = new
		QPair<QByteArray, QNetworkCacheMetaData>
		(bytes1, metaData);

	      m_mutex.lock();
	      m_cache.insert
		(metaData.url().toString(QUrl::StripTrailingSlash), pair,
		 bytes1.size() + bytes2.size());
	      m_mutex.unlock();
	      ct += 1;
	      emit percentRead(100 * ct / size);
	    }

	emit percentRead(100);
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cache_read");
}

void dnetworkcache::writeCacheToDisk
(const QHash<QString, QPair<QByteArray, QNetworkCacheMetaData> > &hash)
{
  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cache_write");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() +
		       "cache.db");

    if(db.open())
      {
	QSqlQuery query(db);

	query.exec("PRAGMA synchronous = OFF");
	query.exec("DELETE FROM cache");
	query.exec("VACUUM");
	query.prepare
	  ("INSERT OR REPLACE INTO cache "
	   "(data, meta_data) "
	   "VALUES (?, ?)");

	int ct = 0;
	int cacheSize = 1048576 * dooble::s_settings.value
	  ("settingsWindow/webDiskCacheSize", 50).toInt();
	qint64 approximateSize = 0;
	QHashIterator<QString, QPair<QByteArray, QNetworkCacheMetaData> >
	  it(hash);

	while(it.hasNext())
	  {
	    it.next();

	    if(it.value().first.isEmpty() ||
	       !it.value().second.isValid() ||
	       !it.value().second.saveToDisk() ||
	       it.value().second.url().isEmpty() ||
	       !it.value().second.url().isValid() ||
	       dooble::s_cacheExceptionsWindow->
	       allowed(it.value().second.url().host()))
	      {
		ct += 1;
		emit percentWritten(100 * ct / hash.size());
		continue;
	      }

	    QByteArray bytes1(it.value().first);

	    bytes1 = qCompress(dmisc::encodedString(bytes1, true).toBase64());

	    query.bindValue(0, bytes1);

	    QByteArray bytes2;
	    QDataStream out(&bytes2, QIODevice::WriteOnly);

	    out << it.value().second;
	    bytes2 = qCompress(dmisc::encodedString(bytes2, true).toBase64());
	    query.bindValue(1, bytes2);
	    query.exec();
	    approximateSize += bytes1.size() + bytes2.size();
	    ct += 1;
	    emit percentWritten(100 * ct / hash.size());

	    if(approximateSize >= cacheSize)
	      break;
	  }

	emit percentWritten(100);
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cache_write");
}

void dnetworkcache::slotDiskCacheEnabled(const bool state)
{
  if(m_timer)
    {
      if(state)
	{
	  int interval =
	    1000 * dooble::s_settings.
	    value("settingsWindow/commitCacheToDiskInterval", 30.0).toDouble();

	  if(interval != m_timer->interval())
	    m_timer->start(interval);
	  else if(!m_timer->isActive())
	    m_timer->start();
	}
      else
	m_timer->stop();
    }
}

void dnetworkcache::reencode(QProgressBar *progress)
{
  if(!dmisc::passphraseWasAuthenticated())
    return;

  if(progress)
    {
      m_mutex.lock();
      progress->setMaximum(m_cache.size());
      m_mutex.unlock();
      progress->setVisible(true);
      progress->update();
    }

  m_timer->stop();
  m_future.waitForFinished();
  m_populateFuture.waitForFinished();
  createCacheDatabase();

  {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "cache");

    db.setDatabaseName(dooble::s_homePath + QDir::separator() +
		       "cache.db");

    if(db.open())
      {
	QSqlQuery query(db);

	query.exec("PRAGMA synchronous = OFF");
	query.exec("DELETE FROM cache");
	query.exec("VACUUM");
	query.prepare
	  ("INSERT OR REPLACE INTO cache "
	   "(data, meta_data) "
	   "VALUES (?, ?)");

	int i = 1;
	int cacheSize = 1048576 * dooble::s_settings.value
	  ("settingsWindow/webDiskCacheSize", 50).toInt();
	qint64 approximateSize = 0;
	QStringList keys;

	m_mutex.lock();
	keys = m_cache.keys();
	m_mutex.unlock();

	while(!keys.isEmpty())
	  {
	    QString key(keys.takeFirst());

	    m_mutex.lock();

	    QPair<QByteArray, QNetworkCacheMetaData> *pair =
	      m_cache.object(key);

	    m_mutex.unlock();

	    if(pair)
	      {
		if(pair->first.isEmpty() ||
		   !pair->second.isValid() ||
		   !pair->second.saveToDisk() ||
		   pair->second.url().isEmpty() ||
		   !pair->second.url().isValid() ||
		   dooble::s_cacheExceptionsWindow->
		   allowed(pair->second.url().host()))
		  {
		    if(progress)
		      progress->setValue(i);

		    i += 1;
		    continue;
		  }

		QByteArray bytes1(pair->first);

		bytes1 = qCompress
		  (dmisc::encodedString(bytes1, true).toBase64());

		query.bindValue(0, bytes1);

		QByteArray bytes2;
		QDataStream out(&bytes2, QIODevice::WriteOnly);

		out << pair->second;
		bytes2 = qCompress
		  (dmisc::encodedString(bytes2, true).toBase64());
		query.bindValue(1, bytes2);
		query.exec();
		approximateSize += bytes1.size() + bytes2.size();
	      }

	    if(progress)
	      progress->setValue(i);

	    i += 1;

	    if(approximateSize >= cacheSize)
	      break;
	  }
      }

    db.close();
  }

  QSqlDatabase::removeDatabase("cache");

  if(progress)
    progress->setVisible(false);
}
