/*
 * Copyright (C) 2006-2007	Andre Beckedorf
 * 					 		<evilJazz _AT_ katastrophos _DOT_ net>
 *
 * 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 <qlayout.h>
#include <qdatastream.h>
#include <qintcache.h>
#include <qregexp.h>
#include <qdatetime.h>
#include <qdir.h>

#include "debug.h"
#include "mediadatabase.h"
#include "media.h"
#include "qprogressdialog.h"

#define CACHE_INITSIZE_PRIME 571

const QString dbSafeString(const QString &in, const bool escapePercentage)
{
    QString retval = in;
	retval.replace(QRegExp("\""), "\\\"");
	retval.replace(QRegExp("\\"), "\\\\");
	if (escapePercentage)
		retval.replace(QRegExp("%"), "\\%");

    return retval;
}

const QString dbFormatDate(const QDateTime &in)
{
	return QString().sprintf("%04d%02d%02d%02d%02d%02d",
		in.date().year(),
		in.date().month(),
		in.date().day(),
		in.time().hour(),
		in.time().minute(),
		in.time().second());
}

/*
static uint elfHash(const char * name)
{
    const uchar *k;
    uint h = 0;
    uint g;

    if (name)
    {
    	k = (const uchar*)name;
    	while (*k)	
    	{
    		h = (h << 4) + *k++;
    		if ((g = (h & 0xf0000000)) != 0)
    			h ^= g >> 24;
    		h &= ~g;
    	}
    }

    if (!h)
    	h = 1;

    return h;
}

static uint safeHashString(const QString &str)
{
	uint hash = 0;
	int len = str.length();
	for (int a = 0; a < len; a++)
	{
		hash = str[a].unicode() + (hash * 17);
	}
	return hash;
}

static uint fastHashString(const QString &str)
{
	uint hash = 0;
	if (!str.isEmpty())
	{
		const QChar* curr = str.unicode();
		const QChar* end = curr + str.length();
		//QChar c;
		for (; curr < end ;)
		{
			//c = *curr;
			//hash = c.unicode() + (hash * 17);
			hash = curr->unicode() + (hash * 17);
			++curr;
		}
	}

	return hash;
}
*/

uint fastHashString(const QString &str)
{
	uint hash = 0;

	if (!str.isEmpty())
	{
		const QChar *curr = str.unicode();
		const QChar *end = curr + str.length();
		while (curr < end)
			hash = curr++->unicode() + (hash * 17);
	}

	if (!hash)
    	hash = 1;

	return hash;
}

uint elfHashString(const QString &str)
{
    uint hash = 0;

    if (!str.isEmpty())
    {
        uint temp;
        
		const QChar *curr = str.unicode();
		const QChar *end = curr + str.length();

		while (curr < end)	
    	{
    		hash = (hash << 4) + curr++->unicode();
    		if ((temp = (hash & 0xF0000000)) != 0)
    			hash ^= temp >> 24;
    		hash &= ~temp;
    	}
    }

    if (!hash)
    	hash = 1;

    return hash;
}

void sqlite3_hash_function(sqlite3_context *context, int, sqlite3_value **params)
{
	switch (sqlite3_value_type(params[0]))
	{
		case SQLITE_TEXT:
		{
			QString text = QString::fromUtf8((const char *)sqlite3_value_text(params[0]));
			uint hash = elfHashString(text);			
			DPRINTF("hash(%s): %d", (const char*)(text.utf8()), hash);
			sqlite3_result_int64(context, hash);
			break;
		}
		default:
		{
			sqlite3_result_null(context);
			break;
		}
	}
}

void sqlite3_dir_exists_function(sqlite3_context *context, int, sqlite3_value **params)
{
	QString dirname = QString::fromUtf8((const char *)sqlite3_value_text(params[0]));
	
	switch (sqlite3_value_type(params[0]))
	{
		case SQLITE_TEXT:
		{
			int result = (QDir(dirname).exists() ? 1 : 0);
			DPRINTF("direxists(%s): %d", (const char*)(dirname.utf8()), result);
			sqlite3_result_int(context, result);
			break;
		}
		default:
		{
			sqlite3_result_int(context, 0);
			DPRINTF("direxists(%s): null", (const char*)(dirname.utf8()));
			break;
		}
	}
}

void sqlite3_file_exists_function(sqlite3_context *context, int, sqlite3_value **params)
{
	QString filename = QString::fromUtf8((const char *)sqlite3_value_text(params[0]));
	
	switch (sqlite3_value_type(params[0]))
	{
		case SQLITE_TEXT:
		{
			int result = (QFile(filename).exists() ? 1 : 0);
			DPRINTF("fileexists(%s): %d", (const char*)(filename.utf8()), result);
			sqlite3_result_int(context, result);
			break;
		}
		default:
		{
			sqlite3_result_int(context, 0);
			DPRINTF("fileexists(%s): null", (const char*)(filename.utf8()));
			break;
		}
	}
}

MediaDatabase::MediaDatabase(const QString &mediaDBFilename)
	: QObject(),
	  dbFilename_(mediaDBFilename),
	  cache_(CACHE_INITSIZE_PRIME, true), 
	  mediaids_(CACHE_INITSIZE_PRIME),
	  transactionOpen_(false),
	  updateCount_(0),
	  db_(NULL)	  
{
	cache_.setAutoDelete(true);
	mediaids_.setAutoDelete(false);
	
	cacheMediaVideo_ = new MediaVideoCache(41, 41);
	cacheMediaVideo_->setAutoDelete(true);

	cacheMediaAudio_ = new MediaAudioCache(41, 41);
	cacheMediaAudio_->setAutoDelete(true);
	
	cacheMediaMetadataExt_ = new MediaMetadataExtCache(41, 41);
	cacheMediaMetadataExt_->setAutoDelete(true);
	
	cacheMediaMetadata_ = new MediaMetadataCache(101, 101);
	cacheMediaMetadata_->setAutoDelete(true);
	
	cacheMediaFileInfo_ = new MediaFileInfoCache(101, 101);
	cacheMediaFileInfo_->setAutoDelete(true);
}

MediaDatabase::~MediaDatabase()
{
	if (isDBOpen())
	{
		closeDB();
	}
	
	delete cacheMediaVideo_;
	delete cacheMediaAudio_;
	delete cacheMediaMetadataExt_;
	delete cacheMediaMetadata_;
	delete cacheMediaFileInfo_;
}

bool MediaDatabase::openDB(const QString & db)
{
	bool ok = false;
	int  err;
	
	if (isDBOpen())
		closeDB();
	
	//try to verify the SQLite version 3 file header
	QFile dbfile(db);
	if (dbfile.open(IO_ReadOnly))
	{
		QString contents = QString("");
		dbfile.readLine(contents, 16);
		dbfile.close();
		if (!contents.startsWith("SQLite format 3"))
		{
			DPRINTF("File is not a SQLite 3 database");
			return false;
        }
	}
	else
	{
        DPRINTF("File could not be read");
        return false;
	}
	
	//lastErrorMessage = QString("no error");
	
	err = sqlite3_open(db.utf8(), &db_);
	if (err)
	{
		//lastErrorMessage = QString::fromUtf8(sqlite3_errmsg(db_));
		DHOP(dbDebug());
		sqlite3_close(db_);
		db_ = 0;
		return false;
	}

	sqlite3_create_function(db_, "direxists", 1, SQLITE_UTF8, NULL, &sqlite3_dir_exists_function, NULL, NULL);
	sqlite3_create_function(db_, "fileexists", 1, SQLITE_UTF8, NULL, &sqlite3_file_exists_function, NULL, NULL);
	//sqlite3_create_function(db_, "hash", 1, SQLITE_UTF8, NULL, &sqlite3_hash_function, NULL, NULL);
	
	prepareStatements();	
	
	return ok;
}

bool MediaDatabase::createDB(const QString & db)
{
	bool ok = false;
    
	if (isDBOpen())
		closeDB();
	
	//lastErrorMessage = QString("no error");
	
    if (sqlite3_open(db.utf8(), &db_) != SQLITE_OK )
	{
        //lastErrorMessage = QString::fromUtf8(sqlite3_errmsg(db_));
    	DHOP(dbDebug());
        sqlite3_close(db_);
        db_ = 0;
        return false;
	}
	
	return ok;
}

void MediaDatabase::prepareStatements()
{
	if (isDBOpen())
	{
		DPRINTF("Preparing statements...");
	
		sqlite3_prepare(db_, "SELECT media_id FROM media WHERE media_id = ?1;", -1, &stmtSelectMediaIDByMediaID, 0);	
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT media_id FROM media WHERE hash = ?1;", -1, &stmtSelectMediaIDByHash, 0);	
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT media_id FROM media, media_location WHERE media_location.location = ?1 AND media.filename = ?2 AND media.location_id = media_location.location_id;", -1, &stmtSelectMediaIDByFilename, 0);
		DHOP(dbDebug());

		sqlite3_prepare(db_, "SELECT location || filename FROM media, media_location WHERE media_id = ?1 AND media.location_id = media_location.location_id;", -1, &stmtSelectFilenameByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT playtime, track, title, album, artist, genre, year, compilation FROM media WHERE media_id = ?1;", -1, &stmtSelectMediaMetadataByMediaID, 0);
		DHOP(dbDebug());

		sqlite3_prepare(db_, "SELECT hash, location || filename, type, filesize, modified_date FROM media, media_location WHERE media_id = ?1 AND media.location_id = media_location.location_id;", -1, &stmtSelectMediaFileInfoByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT * FROM media_audio WHERE media_id = ?1;", -1, &stmtSelectMediaAudioByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT * FROM media_video WHERE media_id = ?1;", -1, &stmtSelectMediaVideoByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT * FROM media_metadata_ext WHERE media_id = ?1;", -1, &stmtSelectMediaMetadataExtByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "DELETE FROM media WHERE media_id = ?1;", -1, &stmtDeleteMediaByMediaID, 0);
		DHOP(dbDebug());

		sqlite3_prepare(db_, "DELETE FROM media_metadata_ext WHERE media_id = ?1;", -1, &stmtDeleteMediaMetadataExtByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "INSERT OR REPLACE INTO media VALUES(?1, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);", -1, &stmtInsertMedia, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "UPDATE media SET playtime = ?2, track = ?3, title = ?4, album = ?5, artist = ?6, genre = ?7, year = ?8, compilation = ?9 WHERE media_id = ?1;", -1, &stmtInsertMediaMetaData, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "UPDATE media SET hash = ?2, location_id = ?3, filename = ?4, type = ?5, filesize = ?6, modified_date = ?7 WHERE media_id = ?1;", -1, &stmtInsertMediaFileInfo, 0);
		DHOP(dbDebug());

		sqlite3_prepare(db_, "INSERT OR REPLACE INTO media_audio VALUES(?1, ?2, ?3, ?4, ?5, ?6);", -1, &stmtInsertMediaAudio, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "INSERT OR REPLACE INTO media_video VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7);", -1, &stmtInsertMediaVideo, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "INSERT OR REPLACE INTO media_metadata_ext VALUES(?1, ?2, ?3);", -1, &stmtInsertMediaMetadataExt, 0);
		DHOP(dbDebug());
		
		// statements for loadMediaHeuristically:
		sqlite3_prepare(db_, "SELECT media_id, media.location_id, location FROM media, media_location WHERE filename = ?1 AND filesize = ?2 AND modified_date = ?3 AND media.location_id = media_location.location_id;", -1, &stmtSelectMediaIDLocationIDByFilenameFilesizeLastModified, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT media_id, filename FROM media WHERE location_id = ?1;", -1, &stmtSelectMediaIDByLocationID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "UPDATE media SET hash = ?2 WHERE media_id = ?1;", -1, &stmtUpdateMediaHashByMediaID, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "UPDATE media_location SET location = ?2 WHERE location_id = ?1;", -1, &stmtUpdateMediaLocationByLocationID, 0);
		DHOP(dbDebug());

		sqlite3_prepare(db_, "INSERT OR REPLACE INTO media_location VALUES(?1, ?2);", -1, &stmtInsertMediaLocation, 0);
		DHOP(dbDebug());
		
		sqlite3_prepare(db_, "SELECT location_id FROM media_location WHERE location = ?1;", -1, &stmtSelectLocationIDByLocation, 0);
		DHOP(dbDebug());
		
		emit preparingStatements(db_);
	}
}

void MediaDatabase::finalizeStatements()
{
	if (isDBOpen())
	{
		DPRINTF("Finalizing statements...");
		
		emit finalizingStatements(db_);

		sqlite3_finalize(stmtSelectMediaIDByMediaID); stmtSelectMediaIDByMediaID = NULL;
		sqlite3_finalize(stmtSelectMediaIDByHash); stmtSelectMediaIDByHash = NULL;
		sqlite3_finalize(stmtSelectMediaIDByFilename); stmtSelectMediaIDByFilename = NULL;
		sqlite3_finalize(stmtSelectFilenameByMediaID); stmtSelectFilenameByMediaID = NULL;		
		sqlite3_finalize(stmtSelectMediaMetadataByMediaID); stmtSelectMediaMetadataByMediaID = NULL;
		sqlite3_finalize(stmtSelectMediaFileInfoByMediaID); stmtSelectMediaFileInfoByMediaID = NULL;
		
		sqlite3_finalize(stmtSelectMediaAudioByMediaID); stmtSelectMediaAudioByMediaID = NULL;
		sqlite3_finalize(stmtSelectMediaVideoByMediaID); stmtSelectMediaVideoByMediaID = NULL;
		sqlite3_finalize(stmtSelectMediaMetadataExtByMediaID); stmtSelectMediaMetadataExtByMediaID = NULL;
		
		sqlite3_finalize(stmtDeleteMediaByMediaID); stmtDeleteMediaByMediaID = NULL;
		sqlite3_finalize(stmtDeleteMediaMetadataExtByMediaID); stmtDeleteMediaMetadataExtByMediaID = NULL;
		
		sqlite3_finalize(stmtInsertMedia); stmtInsertMedia = NULL;
		sqlite3_finalize(stmtInsertMediaMetaData); stmtInsertMediaMetaData = NULL;
		sqlite3_finalize(stmtInsertMediaFileInfo); stmtInsertMediaFileInfo = NULL;
		sqlite3_finalize(stmtInsertMediaAudio); stmtInsertMediaAudio = NULL;
		sqlite3_finalize(stmtInsertMediaVideo); stmtInsertMediaVideo = NULL;
		sqlite3_finalize(stmtInsertMediaMetadataExt); stmtInsertMediaMetadataExt = NULL;
		
		// statements for loadMediaHeuristically:
		sqlite3_finalize(stmtSelectMediaIDLocationIDByFilenameFilesizeLastModified); stmtSelectMediaIDLocationIDByFilenameFilesizeLastModified = NULL;
		sqlite3_finalize(stmtSelectMediaIDByLocationID); stmtSelectMediaIDByLocationID = NULL;		
		sqlite3_finalize(stmtUpdateMediaHashByMediaID); stmtUpdateMediaHashByMediaID = NULL;
		sqlite3_finalize(stmtUpdateMediaLocationByLocationID); stmtUpdateMediaLocationByLocationID = NULL;
		
		sqlite3_finalize(stmtInsertMediaLocation); stmtInsertMediaLocation = NULL;
		sqlite3_finalize(stmtSelectLocationIDByLocation); stmtSelectLocationIDByLocation = NULL; 
		
		DHOP(dbDebug());
	}
}

void MediaDatabase::ensurePreparedStatementsAreValid()
{
	DENTERMETHOD("MediaDatabase::ensurePreparedStatementsAreValid()");
	if (stmtSelectMediaIDByFilename && sqlite3_expired(stmtSelectMediaIDByFilename))
	{
		finalizeStatements();
		prepareStatements();
	}
	DOP(else)
		DPRINTF("Prepared statements are still valid.");
	
	DEXITMETHOD("MediaDatabase::ensurePreparedStatementsAreValid()");
}

void MediaDatabase::saveDB()
{
    if (isDBOpen())
    {
		if (transactionOpen_)
			endUpdate();
	}
}

void MediaDatabase::closeDB()
{
    if (isDBOpen())
    {
		saveDB();

		finalizeStatements();
		
		DPRINTF("Closing database...");
		sqlite3_close(db_);
		DHOP(dbDebug());
    }
	db_ = 0;
}

void MediaDatabase::compactDB()
{
	bool transactionWasOpen_ = transactionOpen_;
	
	if (transactionOpen_)
		endUpdate();
		
	finalizeStatements();

	DPRINTF("Compacting DB...");
	
	sqlite3_stmt *vm;
	
	sqlite3_prepare(db_, "DELETE FROM media WHERE media_id IN (SELECT media_id FROM media LEFT JOIN media_location ON media.location_id = media_location.location_id WHERE FILEEXISTS(media_location.location || media.filename) = 0);", -1, &vm, 0);
	sqlite3_step(vm);
	sqlite3_finalize(vm);
	
	sqlite3_prepare(db_, "DELETE FROM media_location WHERE location_id IN (SELECT location_id FROM media_location WHERE DIREXISTS(location) = 0);", -1, &vm, 0);
	sqlite3_step(vm);
	sqlite3_finalize(vm);

	DPRINTF("Vacuuming DB...");
	sqlite3_busy_timeout(db_, 2000);
	while (sqlite3_exec(db_, "vacuum;", NULL, NULL, NULL) == SQLITE_BUSY)
	{
		DHOP(dbDebug());
	}

	DPRINTF("Reindexing DB...");
	sqlite3_busy_timeout(db_, 2000);
	while (sqlite3_exec(db_, "reindex;", NULL, NULL, NULL) == SQLITE_BUSY)
	{
		DHOP(dbDebug());
	}
	
	DPRINTF("Analyzing DB...");
	sqlite3_busy_timeout(db_, 2000);
	while (sqlite3_exec(db_, "analyze;", NULL, NULL, NULL) == SQLITE_BUSY)
	{
		DHOP(dbDebug());
	}
	
	prepareStatements();
	
	if (transactionWasOpen_)
		beginUpdate();
	
	emit databaseCompacted();
}

void MediaDatabase::dbDebug(const QString &msg)
{ 
#ifndef NO_DEBUG
	if (!msg.isEmpty())
		DPRINTF("[SQLite - %s] errcode %d : errmsg %s", (const char *)msg.utf8(), sqlite3_errcode(db_), sqlite3_errmsg(db_));
	else
		DPRINTF("[SQLite] errcode %d : errmsg %s", sqlite3_errcode(db_), sqlite3_errmsg(db_));
#endif
};

/*
 * try to get media by mediaID
 * NOTE: this may fail and might return NULL when no media is found in the database.
 */
Media *MediaDatabase::locateMedia(unsigned long mediaID)
{
	DHENTERMETHOD("MediaDatabase::media(%d)", (int)mediaID);
	
	// check whether we already have the requested media in our cache...
	Media *media = mediaids_[mediaID];
	
	if (!media)
	{
		DHPRINTF("media ID not found in mediaID cache...");
		
		// check whether we can get the media by media ID
		media = new Media(this);
		
		// loadFromDB will fail if it can't find anything suitable...
		if (!media->loadByID(mediaID))
		{
			DHPRINTF("not found in DB: %d", (int)mediaID);
			delete media;
			media = NULL;
			
			// not found, so this is going to be more expensive
			// try to get filename for media id
			QString filename = getFilenameByMediaID(mediaID);
			
			// if that works, use our more expensive location-based media method below...
			if (filename != "")
				media = this->media(filename);
		}
		else
		{
			DHPRINTF("found in DB: %d => %s", (int)mediaID, (const char *)(media->location().toString().utf8()));
			cache_.insert(media->location(), media);
			mediaids_.insert(mediaID, media);
			hasChanged_ = true;
		}
	}
	
	DHEXITMETHOD("MediaDatabase::media(%d)", (int)mediaID);
	return media;
}

/*
 * get media by location
 * if it can't find anything in the database, it will scan the file and add it to the DB. 
 * Will create a Media instance in any case.
 */
Media *MediaDatabase::media(const MediaLocation& location)
{
	DHENTERMETHOD("MediaDatabase::media(%s)", (const char *)(location.toString().utf8()));
	bool isModified = true;
	int mediaid = 0;
	
	// check whether we already have the requested media in our cache...
	Media *media = cache_[location];

	if (!media)
	{
		// check whether we can get the media by its filename/location
		media = new Media(this);
		
		// loadByLocation will fail if it can't find anything suitable...
		if (!media->loadByHash(elfHashString(location.toString())))
		{
			if (!media->loadByLocation(location))
			{
				// if nothing was found, try with a little magic...
				if (!loadMediaHeuristically(location, media))
				{
					// if still nothing was found, fallback and force rescan of file...				
					DHPRINTF("not found in DB: %s", (const char *)(location.toString().utf8()));
					delete media;
					media = NULL;
				}
			}
		}
		
		if (media)
		{
			DHPRINTF("found in DB: %s", (const char *)(location.toString().utf8()));
			cache_.insert(location, media);
			mediaids_.insert(media->mediaid_, media);
			hasChanged_ = true;
		}
	}

	if (media)
	{
		if (location.isLocalFile())
		{
			if (media->lastModified() == QFileInfo(location.toString()).lastModified())
				isModified = false;
		}
		else
			isModified = false; // by default we take non-local files for unmodified.				

		if (isModified)
		{
			mediaid = media->mediaid_; // save old media id because the file was modified...
			
			DHPRINTF(
				"modified %s : %s != %s", 
				(const char *)(location.toString().utf8()),
				(const char *)(media->lastModified().toString().utf8()),
				(const char *)(QFileInfo(location.toString()).lastModified().toString().utf8())
			);
				
			cache_.remove(location);
			mediaids_.remove(mediaid);
			media = NULL;

			// delete old main entry and all depending sub-entries (via trigger)...
			deleteMediaByID(mediaid);
		}
		else
		{
			DHEXITMETHOD("MediaDatabase::media(%s)", (const char *)(location.toString().utf8()));
			return media;
		}
	}

	// and if we can't find any match, well then we need to scan
	// the file and add it to the DB...
	media = new Media(this);
	media->initializeFromLocation(location);

	// if the file was modified, we need to set the previous
	// media ID and save it.
	if (isModified && mediaid)
	{
		// set the previous media ID to reuse it.
		media->mediaid_ = mediaid;
	}
	
	// now save to database...
	media->saveToDB();

	cache_.insert(location, media);
	mediaids_.insert(media->mediaid_, media);
	
	hasChanged_ = true;

	DHEXITMETHOD("MediaDatabase::media(%s)", (const char *)(location.toString().utf8()));
	return media;
}

bool MediaDatabase::loadMediaHeuristically(const MediaLocation& location, Media *media)
{
	DENTERMETHOD("MediaDatabase::loadMediaHeuristically");
	int returnval = false;
	
	if (location.isLocalFile())
	{
		QString filename = location.fileName();
		QString path = location.dirPath();
		if (path.right(1) != "/") path += "/"; // IMPORTANT!!
		
		QFileInfo fi(location.toString());
			
		sqlite3_stmt *vm = stmtSelectMediaIDLocationIDByFilenameFilesizeLastModified;
		// SELECT media_id, media.location_id, location FROM media, media_location WHERE filename = ?1 AND filesize = ?2 AND modified_date = ?3 AND media.location_id = media_location.location_id;
		
		sqlite3_bind_text(vm, 1, filename.utf8(), -1, SQLITE_TRANSIENT);
		sqlite3_bind_int(vm, 2, fi.size());
		sqlite3_bind_text(vm, 3, dbFormatDate(fi.lastModified()).utf8(), -1, SQLITE_TRANSIENT);
		
		int errno = sqlite3_step(vm);
		DPRINTF("errno: %d", errno);
		
		// do we have a hit here?
		if (errno == SQLITE_ROW)
		{
			//uint mediaID = sqlite3_column_int(vm, 0);
			//media->loadByID(mediaID);
			media->mediaid_ = sqlite3_column_int(vm, 0);
			
			DPRINTF("media->mediaid_ = %d", media->mediaid_);
			
			uint locationID = sqlite3_column_int(vm, 1);
			QString oldDirPath = QString::fromUtf8((const char *)sqlite3_column_text(vm, 2));
			
			// MANAGEMENT CODE
			// The following code will take care of the separate media_location table
			// Additionally to the current item, it will update other similar items on a speculative basis
			// proceeding from the assumption that those files probably will be loaded after this item.
			// That way we have corrected all items in this pass and don't need to start over again each time
			// we try to load those other files.
			// In the end this approach may pay off.

			sqlite3_stmt *vm2;

			// Get count of entries by that location_id...
			sqlite3_prepare(db_, "SELECT COUNT(media_id) FROM media WHERE location_id = ?1;", -1, &vm2, 0);
			sqlite3_bind_int(vm2, 1, locationID);
			errno = sqlite3_step(vm2);
			DPRINTF("count of entries by that location_id -> errno: %d", errno);
			int countEntries = sqlite3_column_int(vm2, 0);
			DPRINTF("countEntries = %d", countEntries);
			sqlite3_finalize(vm2);
			
			// Get count of entries by that location_id where files no longer exist...
			// Mark missing files by setting hash to NULL
			errno = sqlite3_create_function(db_, "fileexists", 1, SQLITE_UTF8, NULL, &sqlite3_file_exists_function, NULL, NULL);
			DPRINTF("create function fileexists -> errno: %d", errno);
			dbDebug();
			
			sqlite3_prepare(
				db_, 
				"UPDATE media SET hash = NULL WHERE media_id IN (SELECT media_id FROM media LEFT JOIN media_location ON media_location.location_id = media.location_id WHERE media.location_id = ?1 AND FILEEXISTS(media_location.location || media.filename) = 0);",
				-1, &vm2, 0
			);
			sqlite3_bind_int(vm2, 1, locationID);
			errno = sqlite3_step(vm2);
			DPRINTF("mark NULL -> errno: %d", errno);
			dbDebug();
			sqlite3_finalize(vm2);
				
			// Count marked missing files
			sqlite3_prepare(db_, "SELECT COUNT(media_id) FROM media WHERE location_id = ?1 AND hash IS NULL;", -1, &vm2, 0);
			sqlite3_bind_int(vm2, 1, locationID);
			errno = sqlite3_step(vm2);
			DPRINTF("Count marked missing files -> errno: %d", errno);
			int countEntriesMissingFiles = sqlite3_column_int(vm2, 0);
			DPRINTF("countEntriesMissingFiles = %d", countEntriesMissingFiles);
			sqlite3_finalize(vm2);
			
			
			
			// Check if entry by that location dir already exists...
			// if yes, update items with the location id...
			sqlite3_prepare(db_, "SELECT location_id FROM media_location WHERE location = ?1", -1, &vm2, 0);
			sqlite3_bind_text(vm2, 1, path.utf8(), -1, SQLITE_TRANSIENT);
			int existingLocationID = 0;
			errno = sqlite3_step(vm2);
			DPRINTF("select location id by location -> errno: %d", errno);
			if (errno == SQLITE_ROW)
				existingLocationID = sqlite3_column_int(vm2, 0);
			sqlite3_finalize(vm2);

			if (existingLocationID)
			{
				DPRINTF("Existing location with ID %d", existingLocationID);
				
				sqlite3_prepare(db_, "UPDATE media SET location_id = ?1 WHERE location_id = ?2 AND hash IS NULL", -1, &vm2, 0);
				sqlite3_bind_int(vm2, 1, existingLocationID);
				sqlite3_bind_int(vm2, 2, locationID);
				errno = sqlite3_step(vm2);
				DPRINTF("UPDATE media SET location_id = ?1 WHERE location_id = ?2 AND hash IS NULL -> errno: %d", errno);
				sqlite3_finalize(vm2);
				
				// Did me make the old location an orphan? If so, remove it... 
				sqlite3_prepare(db_, "SELECT COUNT(media_id) FROM media WHERE location_id = ?1;", -1, &vm2, 0);
				sqlite3_bind_int(vm2, 1, locationID);
				int countAssociatedMediaEntries = -1;
				errno = sqlite3_step(vm2);
				DPRINTF("SELECT COUNT(media_id) FROM media WHERE location_id = ?1; -> errno: %d", errno);
				if (errno == SQLITE_ROW)
					countAssociatedMediaEntries = sqlite3_column_int(vm2, 0);
				
				DPRINTF("countAssociatedMediaEntries = %d", countAssociatedMediaEntries);
				
				sqlite3_finalize(vm2);
				
				if (countAssociatedMediaEntries == 0)
				{
					sqlite3_prepare(db_, "DELETE FROM media_location WHERE location_id = ?1;", -1, &vm2, 0);
					sqlite3_bind_int(vm2, 1, locationID);
					errno = sqlite3_step(vm2);
					DPRINTF("DELETE FROM media_location WHERE location_id = ?1; -> errno: %d", errno);
					sqlite3_finalize(vm2);
				}
				
				locationID = existingLocationID;
			}
			else
			{
				// if all entries in the DB are missing, we just update the location and thus reuse its location ID...
				if (countEntriesMissingFiles == countEntries)
				{
					DPRINTF("All entries are missing, updating location entry to point at new location...");
					
					// Rename location entry by location_id to new location				
					sqlite3_prepare(db_, "UPDATE media_location SET location = ?1 WHERE location_id = ?2;", -1, &vm2, 0);
					DPRINTF("path = %s", (const char*)(path.utf8()));
					sqlite3_bind_text(vm2, 1, path.utf8(), -1, SQLITE_TRANSIENT);
					sqlite3_bind_int(vm2, 2, locationID);
					errno = sqlite3_step(vm2);
					DPRINTF("UPDATE media_location SET location = ?1 WHERE location_id = ?2; -> errno: %d", errno);
					sqlite3_finalize(vm2);
				}
				else
				{
					DPRINTF("Some entries are missing, adding new location entry and updating media entries...");
					
					// If we couldn't reuse the location ID, we need to create a new location entry and...
					sqlite3_prepare(db_, "INSERT INTO media_location VALUES (NULL, ?1);", -1, &vm2, 0);
					sqlite3_bind_text(vm2, 1, path.utf8(), -1, SQLITE_TRANSIENT);
					errno = sqlite3_step(vm2);
					DPRINTF("INSERT INTO media_location VALUES (NULL, ?1); -> errno: %d", errno);
					sqlite3_finalize(vm2);
					
					// ...  change the location id of the non existing files to the new location ID
					int newLocationID = sqlite3_last_insert_rowid(db_);
					
					sqlite3_prepare(db_, "UPDATE media SET location_id = ?1 WHERE location_id = ?2 AND hash IS NULL;", -1, &vm2, 0);
					sqlite3_bind_int(vm2, 1, newLocationID);
					sqlite3_bind_int(vm2, 2, locationID);
					errno = sqlite3_step(vm2);
					DPRINTF("UPDATE media SET location_id = ?1 WHERE location_id = ?2 AND hash IS NULL; -> errno: %d", errno);
					sqlite3_finalize(vm2);
					
					locationID = newLocationID;
				}
			}
			
			DPRINTF("locationID = %d", locationID);

			// Update hash values on previously marked files
			sqlite3_prepare(db_, "SELECT media_id, filename FROM media WHERE location_id = ?1 AND hash IS NULL;", -1, &vm2, 0);
			sqlite3_bind_int(vm2, 1, locationID);

			while (sqlite3_step(vm2) == SQLITE_ROW)
			{
				uint entryMediaID = sqlite3_column_int(vm2, 0);
				QString entryFilename = QString::fromUtf8((const char *)sqlite3_column_text(vm2, 1));
				
				MediaLocation newEntryLocation(path + entryFilename);
				DHPRINTF("newEntryLocation: %s", (const char*)(newEntryLocation.toString().utf8()));

				sqlite3_stmt *vm3 = stmtUpdateMediaHashByMediaID;
				// UPDATE media SET hash = ?2 WHERE media_id = ?1;
				sqlite3_bind_int(vm3, 1, entryMediaID);
				sqlite3_bind_int(vm3, 2, elfHashString(newEntryLocation.toString()));
				DHPRINTF("updating hash...");
				errno = sqlite3_step(vm3);
				DHPRINTF("errno: %d", errno);
				sqlite3_reset(vm3);
			}
			
			sqlite3_finalize(vm2);
			
			returnval = true;
		}
		
		sqlite3_reset(vm);
	}
	
	DEXITMETHOD("MediaDatabase::loadMediaHeuristically");
	return returnval;
}

void MediaDatabase::save(QWidget*)
{
	compactDB();
	saveDB();
	DPRINTF("DB saved.");
	hasChanged_ = false;
}

void MediaDatabase::beginUpdate()
{
	DENTERMETHOD("MediaDatabase::beginUpdate()");

	++updateCount_;
	if (!transactionOpen_)
	{
		finalizeStatements();
		
		DPRINTF("Beginning DB transaction...");
		sqlite3_exec(db_, "BEGIN;", NULL, NULL, NULL);
		DHOP(dbDebug());
		transactionOpen_ = true;
		
		prepareStatements();
	}
	
	DEXITMETHOD	("MediaDatabase::beginUpdate()");
}

void MediaDatabase::endUpdate()
{
	DENTERMETHOD("MediaDatabase::endUpdate()");

	--updateCount_;
	if (transactionOpen_ && updateCount_ == 0)
	{
		finalizeStatements();
		
		DPRINTF("Committing DB transaction...");

		sqlite3_busy_timeout(db_, 2000);
		while (sqlite3_exec(db_, "COMMIT;", NULL, NULL, NULL) == SQLITE_BUSY)
		{
			DHOP(dbDebug());
		}
		
		//sqlite3_exec(db_, "COMMIT TRANSACTION RESTOREPOINT;", NULL, NULL, NULL);
		//dbDebug();
		
		transactionOpen_ = false;
		
		//prepareStatements();

		// TEST:
		DPRINTF("Beginning DB transaction...");
		sqlite3_exec(db_, "BEGIN;", NULL, NULL, NULL);
		DHOP(dbDebug());
		transactionOpen_ = true;

		prepareStatements();
	}
	
	DEXITMETHOD("MediaDatabase::endUpdate()");	
}


QString MediaDatabase::getDBSchema()
{
	QString sql = ""
		"CREATE TABLE media (\n"
		"	media_id INTEGER PRIMARY KEY,\n"
		"	hash INTEGER,\n"
		"	location_id INTEGER,\n"
		"	filename TEXT,\n"
		"	type INTEGER,\n"
		"	filesize INTEGER,\n"
		"	modified_date TIMESTAMP,\n"
		"	playtime INTEGER,\n"
		"	track INTEGER,\n"
		"	title TEXT,\n"
		"	album TEXT,\n"
		"	artist TEXT,\n"
		"	genre TEXT,\n"
		"	year INTEGER,\n"
		"	compilation INTEGER\n"
		");\n"
		"\n"
		"CREATE INDEX idx_media_hash\n"
		"	ON media(hash);\n"
		"\n"
		"CREATE INDEX idx_media_location_id\n"
		"	ON media(location_id);\n"
		"\n"
		"CREATE INDEX idx_media_filename\n"
		"	ON media(filename);\n"
		"\n"
		"CREATE INDEX idx_media_album\n" 
		"	ON media(album);\n"
		"\n"	
		"CREATE INDEX idx_media_artist\n" 
		"	ON media(artist);\n"
		"\n"	
		"CREATE INDEX idx_media_title\n" 
		"	ON media(title);\n"
		"\n"
		"CREATE INDEX idx_media_genre\n"
		"	ON media(genre);\n"
		"\n"
		"CREATE TABLE media_location (\n"
		"	location_id INTEGER PRIMARY KEY,\n"
		"	location TEXT\n"
		");\n"
		"\n"
		"CREATE INDEX idx_media_location_location\n"
		"	ON media_location(location);\n"
		"\n"
		"CREATE TABLE playlist (\n"
		"	idx INTEGER,\n"
		"	playlist_id INTEGER PRIMARY KEY,\n"
		"	media_id INTEGER\n"
		");\n"
		"\n"
		"CREATE INDEX idx_playlist_media_id\n"
		"	ON playlist(media_id);\n"
		"\n"
		"CREATE TABLE onthego_playlist (\n"
		"	idx INTEGER,\n"
		"	playlist_id INTEGER PRIMARY KEY,\n"
		"	media_id INTEGER\n"
		");\n"
		"\n"
		"CREATE INDEX idx_onthego_playlist_media_id\n"
		"	ON onthego_playlist(media_id);\n"
		"\n"
		"CREATE TABLE media_metadata_ext (\n"
		"	media_id INTEGER,\n"
		"	key TEXT,\n"
		"	value TEXT\n"
		");\n"
		"\n"
		"CREATE INDEX idx_media_metadata_ext_media_id\n"
		"	on media_metadata_ext(media_id ASC);\n"
		"\n"
		"CREATE INDEX idx_media_metadata_ext_key\n"
		"	on media_metadata_ext(key ASC);\n"
		"\n"
		"CREATE TABLE media_audio (\n"
		"	media_id INTEGER PRIMARY KEY,\n"
		"	acodec TEXT,\n"
		"	aformat INTEGER,\n"
		"	abitrate INTEGER,\n"
		"	asamplerate INTEGER,\n"
		"	achannels INTEGER\n"
		");\n"
		"\n"
		"CREATE TABLE media_video (\n"
		"	media_id INTEGER PRIMARY KEY,\n"
		"	vcodec TEXT,\n"
		"	vbitrate INTEGER,\n"
		"	vwidth INTEGER,\n"
		"	vheight INTEGER,\n"
		"	vfps INTEGER,\n"
		"	vaspect INTEGER\n"
		");\n"
		"\n"
		"CREATE TRIGGER delete_media DELETE ON media\n"
		"BEGIN\n"
		"	DELETE FROM playlist WHERE media_id = OLD.media_id;\n"
		"	DELETE FROM onthego_playlist WHERE media_id = OLD.media_id;\n"
		"	DELETE FROM media_metadata_ext WHERE media_id = OLD.media_id;\n"
		"	DELETE FROM media_audio WHERE media_id = OLD.media_id;\n"
		"	DELETE FROM media_video WHERE media_id = OLD.media_id;\n"
		"END;\n";
	
	return sql;
}

void MediaDatabase::load(QWidget*)
{
	if (QFile::exists(dbFilename_))
	{
		DPRINTF("Opening database...");
		openDB(dbFilename_);
	}
	else
	{
		DPRINTF("Creating database...");
		createDB(dbFilename_);

		DPRINTF("Creating database schema...");
		const QString s = getDBSchema();
		sqlite3_exec(db_, s.utf8(), NULL, NULL, NULL);
		DOP(dbDebug());

		prepareStatements();

		DPRINTF("Saving database...");
		saveDB();
	}
	
	hasChanged_ = false;
}

/*
int MediaDatabase::getMediaIDbyFilename(const QString &filename)
{
	sqlite3_stmt *vm;
	vm = stmtSelectMediaIDByFilename;
	sqlite3_bind_text(vm, 1, filename.utf8(), -1, SQLITE_TRANSIENT);
	if (sqlite3_step(vm) == SQLITE_OK)
	{
		int retval = sqlite3_column_int(vm, 0);
		sqlite3_reset(vm);
		return retval;
	}
	else
	{
		DHOP(dbDebug());
		sqlite3_reset(vm);
		return 0;
	}
}
*/

QString MediaDatabase::getFilenameByMediaID(const int mediaID)
{
	sqlite3_stmt *vm;
	vm = stmtSelectFilenameByMediaID;
	sqlite3_bind_int(vm, 1, mediaID);
	if (sqlite3_step(vm) == SQLITE_OK)
	{
		QString retval = QString::fromUtf8((const char *)sqlite3_column_text(vm, 0));
		sqlite3_reset(vm);
		return retval;
	}
	else
	{
		DHOP(dbDebug());
		sqlite3_reset(vm);
		return "";
	}
}

bool MediaDatabase::deleteMediaByID(const int mediaID)
{
	DHPRINTF("deleting media by ID %d", mediaID);
	
	sqlite3_stmt *vm;
	vm = stmtDeleteMediaByMediaID;
	sqlite3_bind_int(vm, 1, mediaID);
	if (sqlite3_step(vm) == SQLITE_OK)
	{
		sqlite3_reset(vm);
		return true;
	}
	else
	{
		DOP(dbDebug());
		sqlite3_reset(vm);		
		return false;
	}
}