class Statement;
class Backup;
class Database : public node::ObjectWrap {
public:

	// Proper error handling logic for when an sqlite3 operation fails.
	void ThrowDatabaseError() {
		if (was_js_error) was_js_error = false;
		else ThrowSqliteError(db_handle);
	}
	static void ThrowSqliteError(sqlite3* db_handle) {
		assert(db_handle != NULL);
		ThrowSqliteError(sqlite3_errmsg(db_handle), sqlite3_extended_errcode(db_handle));
	}
	static void ThrowSqliteError(const char* message, int code) {
		assert(message != NULL);
		assert((code & 0xff) != SQLITE_OK);
		assert((code & 0xff) != SQLITE_ROW);
		assert((code & 0xff) != SQLITE_DONE);
		EasyIsolate;
		v8::Local<v8::Value> args[2] = { StringFromUtf8(isolate, message, -1), CS::Code(isolate, code) };
		isolate->ThrowException(v8::Local<v8::Function>::New(isolate, SqliteError)->NewInstance(OnlyContext, 2, args).ToLocalChecked());
	}

	// Allows Statements to log their executed SQL.
	bool Log(v8::Isolate* isolate, v8::Local<v8::Value> data) {
		if (!has_logger) return false;
		assert(was_js_error == false);
		return was_js_error = v8::Local<v8::Function>::Cast(v8::Local<v8::Value>::New(isolate, logger))
			->Call(OnlyContext, v8::Undefined(isolate), 1, &data)
			.IsEmpty();
	}
	bool Log(v8::Isolate* isolate, sqlite3_stmt* handle) {
		if (!has_logger) return false;
		char* expanded = sqlite3_expanded_sql(handle);
		bool failed = Log(isolate, StringFromUtf8(isolate, expanded ? expanded : sqlite3_sql(handle), -1));
		if (expanded) sqlite3_free(expanded);
		return failed;
	}

	// Allow Statements to manage themselves when created and garbage collected.
	inline void AddStatement(Statement* stmt) { stmts.insert(stmts.end(), stmt); }
	inline void RemoveStatement(Statement* stmt) { stmts.erase(stmt); }

	// Allow Backups to manage themselves when created and garbage collected.
	inline void AddBackup(Backup* backup) { backups.insert(backups.end(), backup); }
	inline void RemoveBackup(Backup* backup) { backups.erase(backup); }

	// A view for Statements to see and modify Database state.
	// The order of these fields must exactly match their actual order.
	struct State {
		const bool open;
		bool busy;
		const bool pragma_mode;
		const bool safe_ints;
		bool was_js_error;
		unsigned short iterators;
	};

	inline State* GetState() {
		return reinterpret_cast<State*>(&open);
	}
	inline sqlite3* GetHandle() {
		return db_handle;
	}

	~Database() {
		if (open) dbs.erase(this);
		CloseHandles();
	}

private:

	class CompareDatabase { public:
		bool operator() (Database const * const a, Database const * const b) const {
			return a < b;
		}
	};

	class CompareStatement { public:
		bool operator() (Statement const * const a, Statement const * const b) const {
			return Statement::Compare(a, b);
		}
	};

	class CompareBackup { public:
		bool operator() (Backup const * const a, Backup const * const b) const {
			return Backup::Compare(a, b);
		}
	};

	explicit Database(sqlite3* _db_handle, v8::Isolate* isolate, v8::Local<v8::Value> _logger) : node::ObjectWrap(),
		db_handle(_db_handle),
		open(true),
		busy(false),
		pragma_mode(false),
		safe_ints(false),
		was_js_error(false),
		has_logger(_logger->IsFunction()),
		iterators(0),
		logger(isolate, _logger),
		stmts(),
		backups() {
		assert(_db_handle != NULL);
		dbs.insert(this);
	}

	REGISTER(Init) {
		v8::Local<v8::FunctionTemplate> t = v8::FunctionTemplate::New(isolate, JS_new);
		t->InstanceTemplate()->SetInternalFieldCount(1);
		t->SetClassName(StringFromUtf8(isolate, "Database", -1));

		NODE_SET_PROTOTYPE_METHOD(t, "prepare", JS_prepare);
		NODE_SET_PROTOTYPE_METHOD(t, "exec", JS_exec);
		NODE_SET_PROTOTYPE_METHOD(t, "pragma", JS_pragma);
		NODE_SET_PROTOTYPE_METHOD(t, "checkpoint", JS_checkpoint);
		NODE_SET_PROTOTYPE_METHOD(t, "backup", JS_backup);
		NODE_SET_PROTOTYPE_METHOD(t, "function", JS_function);
		NODE_SET_PROTOTYPE_METHOD(t, "aggregate", JS_aggregate);
		NODE_SET_PROTOTYPE_METHOD(t, "loadExtension", JS_loadExtension);
		NODE_SET_PROTOTYPE_METHOD(t, "close", JS_close);
		NODE_SET_PROTOTYPE_METHOD(t, "defaultSafeIntegers", JS_defaultSafeIntegers);
		NODE_SET_PROTOTYPE_GETTER(t, "open", JS_open);
		NODE_SET_PROTOTYPE_GETTER(t, "inTransaction", JS_inTransaction);

		UseContext;
		exports->Set(ctx, StringFromUtf8(isolate, "Database", -1), t->GetFunction(ctx).ToLocalChecked()).FromJust();
		SqliteError.Reset(isolate, v8::Local<v8::Function>::Cast(Require(module, "../lib/sqlite-error")));
		node::AtExit(Database::AtExit);
	}

	NODE_METHOD(JS_new) {
		assert(info.IsConstructCall());
		REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> filename);
		REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> filenameGiven);
		REQUIRE_ARGUMENT_BOOLEAN(third, bool in_memory);
		REQUIRE_ARGUMENT_BOOLEAN(fourth, bool readonly);
		REQUIRE_ARGUMENT_BOOLEAN(fifth, bool must_exist);
		REQUIRE_ARGUMENT_INT32(sixth, int timeout);
		REQUIRE_ARGUMENT_ANY(seventh, v8::Local<v8::Value> logger);

		UseIsolate;
		sqlite3* db_handle;
		v8::String::Utf8Value utf8(EXTRACT_STRING(isolate, filename));
		int mask = readonly ? SQLITE_OPEN_READONLY
			: must_exist ? SQLITE_OPEN_READWRITE
			: (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);

		if (sqlite3_open_v2(*utf8, &db_handle, mask, NULL) != SQLITE_OK) {
			ThrowSqliteError(db_handle);
			int status = sqlite3_close(db_handle);
			assert(status == SQLITE_OK); ((void)status);
			return;
		}

		assert(sqlite3_db_mutex(db_handle) == NULL);
		sqlite3_extended_result_codes(db_handle, 1);
		sqlite3_busy_timeout(db_handle, timeout);
		sqlite3_limit(db_handle, SQLITE_LIMIT_LENGTH, MAX_BUFFER_SIZE < MAX_STRING_SIZE ? MAX_BUFFER_SIZE : MAX_STRING_SIZE);
		sqlite3_limit(db_handle, SQLITE_LIMIT_SQL_LENGTH, MAX_STRING_SIZE);
		int status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
		assert(status == SQLITE_OK);
		status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
		assert(status == SQLITE_OK);

		UseContext;
		Database* db = new Database(db_handle, isolate, logger);
		db->Wrap(info.This());
		SetFrozen(isolate, ctx, info.This(), CS::memory, in_memory ? v8::True(isolate) : v8::False(isolate));
		SetFrozen(isolate, ctx, info.This(), CS::readonly, readonly ? v8::True(isolate) : v8::False(isolate));
		SetFrozen(isolate, ctx, info.This(), CS::name, filenameGiven);

		info.GetReturnValue().Set(info.This());
	}

	NODE_METHOD(JS_prepare) {
		REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> source);
		v8::MaybeLocal<v8::Object> maybe_statement = Statement::New(OnlyIsolate, info.This(), source);
		if (!maybe_statement.IsEmpty()) info.GetReturnValue().Set(maybe_statement.ToLocalChecked());
	}

	NODE_METHOD(JS_exec) {
		Database* db = Unwrap<Database>(info.This());
		REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> source);
		REQUIRE_DATABASE_OPEN(db);
		REQUIRE_DATABASE_NOT_BUSY(db);
		REQUIRE_DATABASE_NO_ITERATORS(db);
		db->busy = true;
		UseIsolate;
		if (db->Log(isolate, source)) {
			db->busy = false;
			db->ThrowDatabaseError();
			return;
		}
		v8::String::Utf8Value sql(EXTRACT_STRING(isolate, source));
		int status = sqlite3_exec(db->db_handle, *sql, NULL, NULL, NULL);
		db->busy = false;
		if (status == SQLITE_OK) info.GetReturnValue().Set(info.This());
		else db->ThrowDatabaseError();
	}

	NODE_METHOD(JS_pragma) {
		REQUIRE_ARGUMENT_BOOLEAN(first, Unwrap<Database>(info.This())->pragma_mode);
	}

	NODE_METHOD(JS_checkpoint) {
		Database* db = Unwrap<Database>(info.This());
		REQUIRE_DATABASE_OPEN(db);
		REQUIRE_DATABASE_NOT_BUSY(db);
		REQUIRE_DATABASE_NO_ITERATORS(db);
		sqlite3* db_handle = db->db_handle;

		if (info.Length()) {
			REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> onlyDatabase);
			v8::String::Utf8Value only_database(EXTRACT_STRING(OnlyIsolate, onlyDatabase));
			if (only_database.length() == 0) {
				return ThrowTypeError("Invalid database name (empty string)");
			}
			if (sqlite3_wal_checkpoint_v2(db_handle, *only_database, SQLITE_CHECKPOINT_RESTART, NULL, NULL) != SQLITE_OK) {
				return db->ThrowDatabaseError();
			}
			return info.GetReturnValue().Set(info.This());
		}

		sqlite3_stmt* stmt;
		if (sqlite3_prepare_v2(db_handle, "PRAGMA database_list", -1, &stmt, NULL) != SQLITE_OK) {
			return db->ThrowDatabaseError();
		}
		std::vector<std::string> database_names;
		while (sqlite3_step(stmt) == SQLITE_ROW) {
			database_names.emplace_back(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1)), sqlite3_column_bytes(stmt, 1));
		}
		if (sqlite3_finalize(stmt) != SQLITE_OK) {
			return db->ThrowDatabaseError();
		}

		bool threw_error = false;
		for (std::string const &name : database_names) {
			if (sqlite3_wal_checkpoint_v2(db_handle, name.c_str(), SQLITE_CHECKPOINT_RESTART, NULL, NULL) != SQLITE_OK) {
				if (!threw_error) {
					db->ThrowDatabaseError();
					threw_error = true;
				}
			}
		}
		if (!threw_error) info.GetReturnValue().Set(info.This());
	}

	NODE_METHOD(JS_backup) {
		REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> attachedName);
		REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> destFile);
		REQUIRE_ARGUMENT_BOOLEAN(third, bool unlink);
		UseIsolate;
		v8::MaybeLocal<v8::Object> maybe_backup = Backup::New(isolate, info.This(), attachedName, destFile, v8::Boolean::New(isolate, unlink));
		if (!maybe_backup.IsEmpty()) info.GetReturnValue().Set(maybe_backup.ToLocalChecked());
	}

	NODE_METHOD(JS_function) {
		Database* db = Unwrap<Database>(info.This());
		REQUIRE_ARGUMENT_FUNCTION(first, v8::Local<v8::Function> fn);
		REQUIRE_ARGUMENT_STRING(second, v8::Local<v8::String> nameString);
		REQUIRE_ARGUMENT_INT32(third, int argc);
		REQUIRE_ARGUMENT_INT32(fourth, int safe_ints);
		REQUIRE_ARGUMENT_BOOLEAN(fifth, bool deterministic);
		REQUIRE_DATABASE_OPEN(db);
		REQUIRE_DATABASE_NOT_BUSY(db);
		REQUIRE_DATABASE_NO_ITERATORS(db);

		UseIsolate;
		v8::String::Utf8Value name(EXTRACT_STRING(isolate, nameString));
		int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
		safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);

		if (sqlite3_create_function_v2(db->db_handle, *name, argc, mask, new CustomFunction(isolate, db, fn, *name, safe_ints), CustomFunction::xFunc, NULL, NULL, CustomFunction::xDestroy) != SQLITE_OK) {
			return db->ThrowDatabaseError();
		}
		info.GetReturnValue().Set(info.This());
	}

	NODE_METHOD(JS_aggregate) {
		Database* db = Unwrap<Database>(info.This());
		REQUIRE_ARGUMENT_ANY(first, v8::Local<v8::Value> start);
		REQUIRE_ARGUMENT_FUNCTION(second, v8::Local<v8::Function> step);
		REQUIRE_ARGUMENT_ANY(third, v8::Local<v8::Value> inverse);
		REQUIRE_ARGUMENT_ANY(fourth, v8::Local<v8::Value> result);
		REQUIRE_ARGUMENT_STRING(fifth, v8::Local<v8::String> nameString);
		REQUIRE_ARGUMENT_INT32(sixth, int argc);
		REQUIRE_ARGUMENT_INT32(seventh, int safe_ints);
		REQUIRE_ARGUMENT_BOOLEAN(eighth, bool deterministic);
		REQUIRE_DATABASE_OPEN(db);
		REQUIRE_DATABASE_NOT_BUSY(db);
		REQUIRE_DATABASE_NO_ITERATORS(db);

		UseIsolate;
		v8::String::Utf8Value name(EXTRACT_STRING(isolate, nameString));
		auto xInverse = inverse->IsFunction() ? CustomAggregate::xInverse : NULL;
		auto xValue = xInverse ? CustomAggregate::xValue : NULL;
		int mask = deterministic ? SQLITE_UTF8 | SQLITE_DETERMINISTIC : SQLITE_UTF8;
		safe_ints = safe_ints < 2 ? safe_ints : static_cast<int>(db->safe_ints);

		if (sqlite3_create_window_function(db->db_handle, *name, argc, mask, new CustomAggregate(isolate, db, start, step, inverse, result, *name, safe_ints), CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse, CustomAggregate::xDestroy) != SQLITE_OK) {
			return db->ThrowDatabaseError();
		}
		info.GetReturnValue().Set(info.This());
	}

	NODE_METHOD(JS_loadExtension) {
		Database* db = Unwrap<Database>(info.This());
		REQUIRE_ARGUMENT_STRING(first, v8::Local<v8::String> filenameString);
		REQUIRE_DATABASE_OPEN(db);
		REQUIRE_DATABASE_NOT_BUSY(db);
		REQUIRE_DATABASE_NO_ITERATORS(db);
		v8::String::Utf8Value filename(EXTRACT_STRING(OnlyIsolate, filenameString));
		char* error;
		int status = sqlite3_load_extension(db->db_handle, *filename, NULL, &error);
		if (status == SQLITE_OK) info.GetReturnValue().Set(info.This());
		else ThrowSqliteError(error, status);
		sqlite3_free(error);
	}

	NODE_METHOD(JS_close) {
		Database* db = Unwrap<Database>(info.This());
		if (db->open) {
			REQUIRE_DATABASE_NOT_BUSY(db);
			REQUIRE_DATABASE_NO_ITERATORS(db);
			dbs.erase(db);
			db->CloseHandles();
		}
		info.GetReturnValue().Set(info.This());
	}

	NODE_METHOD(JS_defaultSafeIntegers) {
		Database* db = Unwrap<Database>(info.This());
		if (info.Length() == 0) db->safe_ints = true;
		else { REQUIRE_ARGUMENT_BOOLEAN(first, db->safe_ints); }
		info.GetReturnValue().Set(info.This());
	}

	NODE_GETTER(JS_open) {
		info.GetReturnValue().Set(Unwrap<Database>(info.This())->open);
	}

	NODE_GETTER(JS_inTransaction) {
		Database* db = Unwrap<Database>(info.This());
		info.GetReturnValue().Set(db->open && !static_cast<bool>(sqlite3_get_autocommit(db->db_handle)));
	}

	void CloseHandles() {
		if (open) {
			open = false;
			for (Statement* stmt : stmts) stmt->CloseHandles();
			for (Backup* backup : backups) backup->CloseHandles();
			stmts.clear();
			backups.clear();
			int status = sqlite3_close(db_handle);
			assert(status == SQLITE_OK); ((void)status);
		}
	}

	static void AtExit(void* _) {
		for (Database* db : dbs) db->CloseHandles();
		dbs.clear();
	}

	static std::set<Database*, Database::CompareDatabase> dbs;
	static v8::Persistent<v8::Function> SqliteError;
	static const int MAX_BUFFER_SIZE = node::Buffer::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(node::Buffer::kMaxLength);
	static const int MAX_STRING_SIZE = v8::String::kMaxLength > INT_MAX ? INT_MAX : static_cast<int>(v8::String::kMaxLength);

	sqlite3* const db_handle;
	bool open;
	bool busy;
	bool pragma_mode;
	bool safe_ints;
	bool was_js_error;
	const bool has_logger;
	unsigned short iterators;
	const CopyablePersistent<v8::Value> logger;
	std::set<Statement*, Database::CompareStatement> stmts;
	std::set<Backup*, Database::CompareBackup> backups;
};
