/*
 * Copyright (C) 2008 Fabien Chereau
 *
 * 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; either version 2
 * of the License, or (at your option) any later version.
 *
 * 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.
 */

#include "MultiLevelJsonBase.hpp"
#include "StelJsonParser.hpp"
#include "StelApp.hpp"
#include "StelFileMgr.hpp"
#include "StelProjector.hpp"
#include "StelCore.hpp"
#include "kfilterdev.h"
#include "StelUtils.hpp"

#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QHttp>
#include <QUrl>
#include <QBuffer>
#include <QThread>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <stdexcept>

// #include <QNetworkDiskCache>

// Init statics
QNetworkAccessManager* MultiLevelJsonBase::networkAccessManager = NULL;

QNetworkAccessManager& MultiLevelJsonBase::getNetworkAccessManager()
{
	if (networkAccessManager==NULL)
	{
		networkAccessManager = new QNetworkAccessManager(&StelApp::getInstance());
// Cache on JSON files doesn't work, and I don't know why.
// There may be a problem in Qt for text objects caching (compression is activated in cache)
// 		QNetworkDiskCache* cache = new QNetworkDiskCache(networkAccessManager);
// 		QString cachePath = StelApp::getInstance().getCacheDir();
// 		cache->setCacheDirectory(cachePath+"/JSONCache");
// 		networkAccessManager->setCache(cache);
		connect(networkAccessManager, SIGNAL(finished(QNetworkReply*)), &StelApp::getInstance(), SLOT(reportFileDownloadFinished(QNetworkReply*)));
	}
	return *networkAccessManager;
}

/*************************************************************************
  Class used to load a JSON file in a thread
 *************************************************************************/
class JsonLoadThread : public QThread
{
	public:
		JsonLoadThread(MultiLevelJsonBase* atile, QByteArray content, bool aqZcompressed=false, bool agzCompressed=false) : QThread((QObject*)atile),
			tile(atile), data(content), qZcompressed(aqZcompressed), gzCompressed(agzCompressed){;}
		virtual void run();
	private:
		MultiLevelJsonBase* tile;
		QByteArray data;
		const bool qZcompressed;
		const bool gzCompressed;
};

void JsonLoadThread::run()
{
	try
	{
		QBuffer buf(&data);
		buf.open(QIODevice::ReadOnly);
		tile->temporaryResultMap = MultiLevelJsonBase::loadFromJSON(buf, qZcompressed, gzCompressed);
	}
	catch (std::runtime_error e)
	{
		qWarning() << "WARNING : Can't parse loaded JSON description: " << e.what();
		tile->errorOccured = true;
	}
}

MultiLevelJsonBase::MultiLevelJsonBase(MultiLevelJsonBase* parent) : StelSkyLayer(parent)
{
	errorOccured = false;
	httpReply = NULL;
	downloading = false;
	loadThread = NULL;
	loadingState = false;
	lastPercent = 0;
	// Avoid tiles to be deleted just after constructed
	timeWhenDeletionScheduled = -1.;
	deletionDelay = 2.;

	if (parent!=NULL)
	{
		deletionDelay = parent->deletionDelay;
	}
}

void MultiLevelJsonBase::initFromUrl(const QString& url)
{
	const MultiLevelJsonBase* parent = qobject_cast<MultiLevelJsonBase*>(QObject::parent());
	contructorUrl = url;
	if (!url.startsWith("http://") && (parent==NULL || !parent->getBaseUrl().startsWith("http://")))
	{
		// Assume a local file
		QString fileName;
		try
		{
			fileName = StelFileMgr::findFile(url);
		}
		catch (std::runtime_error e)
		{
			try
			{
				if (parent==NULL)
					throw std::runtime_error("NULL parent");
				fileName = StelFileMgr::findFile(parent->getBaseUrl()+url);
			}
			catch (std::runtime_error e)
			{
				qWarning() << "WARNING : Can't find JSON description: " << url << ": " << e.what();
				errorOccured = true;
				return;
			}
		}
		QFileInfo finf(fileName);
		baseUrl = finf.absolutePath()+'/';
		QFile f(fileName);
		f.open(QIODevice::ReadOnly);
		const bool compressed = fileName.endsWith(".qZ");
		const bool gzCompressed = fileName.endsWith(".gz");
		try
		{
			loadFromQVariantMap(loadFromJSON(f, compressed, gzCompressed));
		}
		catch (std::runtime_error e)
		{
			qWarning() << "WARNING : Can't parse JSON description: " << fileName << ": " << e.what();
			errorOccured = true;
			f.close();
			return;
		}
		f.close();
	}
	else
	{
		// Use a very short deletion delay to ensure that tile which are outside screen are discared before they are even downloaded
		// This is useful to reduce bandwidth when the user moves rapidely
		deletionDelay = 0.001;
		QUrl qurl;
		if (url.startsWith("http://"))
		{
			qurl.setUrl(url);
		}
		else
		{
			Q_ASSERT(parent->getBaseUrl().startsWith("http://"));
			qurl.setUrl(parent->getBaseUrl()+url);
		}
		Q_ASSERT(httpReply==NULL);
		QNetworkRequest req(qurl);
		req.setRawHeader("User-Agent", StelUtils::getApplicationName().toAscii());
		httpReply = getNetworkAccessManager().get(req);
		//qDebug() << "Started downloading " << httpReply->request().url().path();
		Q_ASSERT(httpReply->error()==QNetworkReply::NoError);
		//qDebug() << httpReply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
		connect(httpReply, SIGNAL(finished()), this, SLOT(downloadFinished()));
		//connect(httpReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
		//connect(httpReply, SIGNAL(destroyed()), this, SLOT(replyDestroyed()));
		downloading = true;
		QString turl = qurl.toString();
		baseUrl = turl.left(turl.lastIndexOf('/')+1);
	}
}

// Constructor from a map used for JSON files with more than 1 level
void MultiLevelJsonBase::initFromQVariantMap(const QVariantMap& map)
{
	const MultiLevelJsonBase* parent = qobject_cast<MultiLevelJsonBase*>(QObject::parent());
	if (parent!=NULL)
	{
		baseUrl = parent->getBaseUrl();
		contructorUrl = parent->contructorUrl + "/?";
	}
	try
	{
		loadFromQVariantMap(map);
	}
	catch (std::runtime_error e)
	{
		qWarning() << "WARNING: invalid variant map: " << e.what();
		errorOccured = true;
		return;
	}
	downloading = false;
}

// Destructor
MultiLevelJsonBase::~MultiLevelJsonBase()
{
	if (httpReply)
	{
		//qDebug() << "Abort: " << httpReply->request().url().path();
		//httpReply->abort();

		// TODO: This line should not be commented, but I have to keep it because of a Qt bug.
		// It should be fixed with Qt 4.5.1
		// It causes a nasty memory leak, but prevents an even more nasty
		//httpReply->deleteLater();
		httpReply = NULL;
	}
	if (loadThread && loadThread->isRunning())
	{
		//qDebug() << "--> Abort thread " << contructorUrl;
		disconnect(loadThread, SIGNAL(finished()), this, SLOT(jsonLoadFinished()));
		// The thread is currently running, it needs to be properly stopped
		if (loadThread->wait(1)==false)
		{
			loadThread->terminate();
			//loadThread->wait(2000);
		}
	}
	foreach (MultiLevelJsonBase* tile, subTiles)
	{
		tile->deleteLater();
	}
	subTiles.clear();
}


void MultiLevelJsonBase::scheduleChildsDeletion()
{
	foreach (MultiLevelJsonBase* tile, subTiles)
	{
		if (tile->timeWhenDeletionScheduled<0)
			tile->timeWhenDeletionScheduled = StelApp::getInstance().getTotalRunTime();
	}
}

// If a deletion was scheduled, cancel it.
void MultiLevelJsonBase::cancelDeletion()
{
	timeWhenDeletionScheduled=-1.;
	foreach (MultiLevelJsonBase* tile, subTiles)
	{
		tile->cancelDeletion();
	}
}

// Load the tile information from a JSON file
QVariantMap MultiLevelJsonBase::loadFromJSON(QIODevice& input, bool qZcompressed, bool gzCompressed)
{
	StelJsonParser parser;
	QVariantMap map;
	if (qZcompressed && input.size()>0)
	{
		QByteArray ar = qUncompress(input.readAll());
		input.close();
		QBuffer buf(&ar);
		buf.open(QIODevice::ReadOnly);
		map = parser.parse(&buf).toMap();
		buf.close();
	}
	else if (gzCompressed)
	{
		QIODevice* d = KFilterDev::device(&input, "application/x-gzip", false);
		d->open(QIODevice::ReadOnly);
		map = parser.parse(d).toMap();
		d->close();
		delete d;
	}
	else
	{
		map = parser.parse(&input).toMap();
	}

	if (map.isEmpty())
		throw std::runtime_error("empty JSON file, cannot load");
	return map;
}


// Called when the download for the JSON file terminated
void MultiLevelJsonBase::downloadFinished()
{
	//qDebug() << "Finished downloading " << httpReply->request().url().path();
	Q_ASSERT(downloading);
	if (httpReply->error()!=QNetworkReply::NoError)
	{
		if (httpReply->error()!=QNetworkReply::OperationCanceledError)
			qWarning() << "WARNING : Problem while downloading JSON description for " << httpReply->request().url().path() << ": "<< httpReply->errorString();
		errorOccured = true;
		httpReply->deleteLater();
		httpReply=NULL;
		downloading=false;
		timeWhenDeletionScheduled = StelApp::getInstance().getTotalRunTime();
		return;
	}

	QByteArray content = httpReply->readAll();
	if (content.isEmpty())
	{
		qWarning() << "WARNING : empty JSON description for " << httpReply->request().url().path();
		errorOccured = true;
		httpReply->deleteLater();
		httpReply=NULL;
		downloading=false;
		return;
	}

	const bool qZcompressed = httpReply->request().url().path().endsWith(".qZ");
	const bool gzCompressed = httpReply->request().url().path().endsWith(".gz");
	httpReply->deleteLater();
	httpReply=NULL;

	Q_ASSERT(loadThread==NULL);
	loadThread = new JsonLoadThread(this, content, qZcompressed, gzCompressed);
	connect(loadThread, SIGNAL(finished()), this, SLOT(jsonLoadFinished()));
	loadThread->start(QThread::LowestPriority);
}

// Called when the element is fully loaded from the JSON file
void MultiLevelJsonBase::jsonLoadFinished()
{
	loadThread->wait();
	delete loadThread;
	loadThread = NULL;
	downloading = false;
	if (errorOccured)
		return;
	try
	{
		loadFromQVariantMap(temporaryResultMap);
	}
	catch (std::runtime_error e)
	{
		qWarning() << "WARNING: invalid variant map: " << e.what();
		errorOccured = true;
		return;
	}
}


// Delete all the subtiles which were not displayed since more than lastDrawTrigger seconds
void MultiLevelJsonBase::deleteUnusedSubTiles()
{
	if (subTiles.isEmpty())
		return;
	const double now = StelApp::getInstance().getTotalRunTime();
	bool deleteAll = true;
	foreach (MultiLevelJsonBase* tile, subTiles)
	{
		if (tile->timeWhenDeletionScheduled<0 || (now-tile->timeWhenDeletionScheduled)<deletionDelay)
		{
			deleteAll = false;
			break;
		}
	}
	if (deleteAll==true)
	{
		//qDebug() << "Delete all tiles for " << this << ": " << contructorUrl;
		foreach (MultiLevelJsonBase* tile, subTiles)
			tile->deleteLater();
		subTiles.clear();
	}
	else
	{
		// Nothing to delete at this level, propagate
		foreach (MultiLevelJsonBase* tile, subTiles)
			tile->deleteUnusedSubTiles();
	}
}

void MultiLevelJsonBase::updatePercent(int tot, int toBeLoaded)
{
	if (tot+toBeLoaded==0)
	{
		if (loadingState==true)
		{
			loadingState=false;
			emit(loadingStateChanged(false));
		}
		return;
	}

	int p = (int)(100.f*tot/(tot+toBeLoaded));
	if (p>100)
		p=100;
	if (p<0)
		p=0;
	if (p==100 || p==0)
	{
		if (loadingState==true)
		{
			loadingState=false;
			emit(loadingStateChanged(false));
		}
		return;
	}
	else
	{
		if (loadingState==false)
		{
			loadingState=true;
			emit(loadingStateChanged(true));
		}
	}
	if (p==lastPercent)
		return;
	lastPercent=p;
	emit(percentLoadedChanged(p));
}
