//          Copyright Brian Schott (Sir Alaran) 2012.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

module cache;

import etc.c.sqlite3;
import std.c.stdlib;
import std.datetime;
import std.file;
import std.uuid;
import std.array;
import std.string;
import std.conv;
import std.d.lexer;

import location;
import parser;
import types;

private sqlite3* database;

version (Posix)
{
	private immutable char* DB_PATH = "~/.dscanner/cache.db";
}
else version (Windows)
{
	pragma(msg, "Caching not supported on Windows yet");
	immutable string DB_PATH = "";
}

private enum Queries : string
{
	getUpdateTime = "select mtime from files where filepath = ?",
	insertContainer = "insert into containers values ()",
	deleteContainer = "delete from containers where fileId = ?",
	deleteSymbol = "delete from symbols where containerId = ?",
	deleteFile = "delete from files where path = ?",
	getPublicImports = "select importedId from publicImports where importerId = ?",
	getModuleId = "select id from files where path = ?",
	getContainersByModule = "select id from containers where fileId = ?"
}

private sqlite3* getDatabase()
{
	if (database !is null)
		return database;
	int status = sqlite3_open(DB_PATH, &database);
	if (status != SQLITE_OK)
	{
		throw new Exception("Could not open %s: %s".format(DB_PATH,
			sqlite3_errmsg(database)));
	}
	return database;
}

void closeDatabase()
{
	if (database !is null)
	{
		sqlite3_close(database);
		database = null;
	}
}

private long getCachedModTime(sqlite3* db, sqlite3_stmt* statement, string filePath)
{
	bindText(statement, 1, filePath);
	if (sqlite3_step(statement) != SQLITE_ROW)
		throw new Exception("%s".format(sqlite3_errmsg(db)));
	return sqlite3_column_int64(statement, 1);
}

/**
 * Updates the sqlite database with current autocomplete information for the
 * given modules.
 */
void updateCache(string dirs[], string moduleNames[])
{
	string[] filePaths;
	foreach (moduleName; moduleNames)
	{
		string path = findAbsPath(dirs, moduleName);
		if (path is null)
			continue;
		filePaths ~= path;
	}

	sqlite3* db = getDatabase();
	sqlite3_stmt* statement;
	scope(exit) { if (statement) sqlite3_finalize(statement); }
	char* pzTail;
	scope(exit) { if (pzTail) free(pzTail); }
	sqlite3_prepare_v2(db, Queries.getUpdateTime.toStringz(),
		cast(int) Queries.getUpdateTime.length + 1, &statement, &pzTail);

	foreach (string filePath; filePaths)
	{
		immutable long mtime = getCachedModTime(db, statement, filePath);
		SysTime timeLastModified = timeLastModified(filePath);
		// if the times match, we don't need to update the cache.
		if (timeLastModified.stdTime == mtime)
			continue;

//		// re-parse the module
//		Module m = parseModule(byToken(readText(filePath)).array());
//
//		updateCache(m);

		sqlite3_reset(statement);
	}
}

private void updateCache(const Module m)
in
{
	assert(m !is null);
}
body
{
}

private string[] getImportedModules(string modulePath, sqlite3_stmt* statement = null)
{
	auto app = appender!(string[])();
	sqlite3* db = getDatabase();
	bool statementAllocated = false;
	scope(exit) { if (statementAllocated && statement !is null) sqlite3_finalize(statement); }
	if (statement is null)
	{
		statementAllocated = true;
		char* pzTail;
		scope(exit) { if (pzTail) free(pzTail); }
		sqlite3_prepare_v2(db, Queries.getPublicImports.toStringz(),
			cast(int) Queries.getPublicImports.length + 1, &statement, &pzTail);
	}

	string moduleId = getModuleIdFromPath(modulePath);
	bindText(statement, 1, moduleId);
	while (sqlite3_step(statement) == SQLITE_ROW)
	{
		app.put(to!string(sqlite3_column_text(statement, 1)));
	}
	sqlite3_reset(statement);
	foreach (string imported; app.data)
	{
		string[] r = getImportedModules(imported, statement);
	}
	return app.data;
}

private string getModuleIdFromPath(string filePath)
{
	sqlite3* db = getDatabase();
	sqlite3_stmt* statement;
	char* pzTail;
	scope(exit) if (pzTail) free(pzTail);
	sqlite3_prepare_v2(db, Queries.getModuleId.toStringz(),
		cast(int) Queries.getModuleId.length + 1, &statement,
		&pzTail);
	bindText(statement, 1, filePath);
	if (sqlite3_step(statement) != SQLITE_ROW)
		return null;
	return to!string(sqlite3_column_text(statement, 1));
}

/**
 * Returns: the container IDs of the containers that have
 * been imported
 */
public string[] getContainersImported(string modulePath)
{
	immutable string moduleId = getModuleIdFromPath(modulePath);
	sqlite3* db = getDatabase();
	sqlite3_stmt* statement;
	char* pzTail;
	scope(exit) if (pzTail) free(pzTail);
	string[] moduleIds = getImportedModules(modulePath);
	string[] containerIds;
	foreach (string id; moduleIds)
	{
		containerIds ~= getContainersByModule(id);
	}
	return containerIds;
}

private string[] getContainersByModule(string moduleId)
{
	sqlite3* db = getDatabase();
	sqlite3_stmt* statement;
	scope(exit) if (statement !is null) sqlite3_finalize(statement);
	char* pzTail;
	prepareStatement(db, statement, Queries.getContainersByModule);
	bindText(statement, 1, moduleId);
	string[] rVal;
	while (sqlite3_step(statement) == SQLITE_ROW)
	{
		rVal ~= to!string(sqlite3_column_text(statement, 1));
	}
	return rVal;
}

private void prepareStatement(sqlite3* db, sqlite3_stmt* statement, string query)
{
	char* pzTail;
	scope(exit) if (pzTail) free(pzTail);
	sqlite3_prepare_v2(db, query.toStringz(), cast(int) query.length + 1,
		&statement, &pzTail);
}

private void bindText(sqlite3_stmt* statement, int argPos, string text)
{
	sqlite3_bind_text(statement, argPos, text.toStringz(),
		cast(int) text.length + 1, SQLITE_TRANSIENT);
}