/*
 * Seagull: A SQLite helper library
 * Copyright (C) Seagull Developers
 *
 * Seagull is the legal property of its developers, whose names are too
 * numerous to list here. Please refer to the AUTHORS file distributed with
 * this source distribution
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation; either version 2.1 of the License, or (at your
 * option) any later version.
 *
 * This library 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <sqlite3.h>

#include "seagullstatement.h"

#define SEAGULL_STATEMENT_GERROR_VARIANT_FORMAT "(sis)"

struct _SeagullStatement {
	GObject parent;

	SeagullSqlite3 *db;
	char *sql;

	SeagullSqlite3Stmt *statement;

	/* These are caches of column and parameter names to their indices. */
	GHashTable *columns;
	GHashTable *parameters;
};

enum {
	PROP_0,
	PROP_DB,
	PROP_SQL,
	PROP_STMT,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

G_DEFINE_QUARK(seagull-statement-error, seagull_statement_error)

/******************************************************************************
 * Helpers
 *****************************************************************************/
static inline const char *
seagull_statement_column_type_name(int column_type) {
	if(column_type == SQLITE_INTEGER) {
		return "integer";
	} else if(column_type == SQLITE_FLOAT) {
		return "double";
	} else if(column_type == SQLITE_TEXT) {
		return "text";
	} else if(column_type == SQLITE_BLOB) {
		return "blob";
	} else if(column_type == SQLITE_NULL) {
		return "null";
	}

	return "unknown";
}

static inline gboolean
seagull_statement_check_column_type(int expected, int actual, GError **error) {
	if(actual != expected && actual != SQLITE_NULL) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_WRONG_COLUMN_TYPE,
		            "expected type %s but column is of type %s",
		            seagull_statement_column_type_name(expected),
		            seagull_statement_column_type_name(actual));

		return FALSE;
	}

	return TRUE;
}

static void
seagull_statement_set_db(SeagullStatement *statement, SeagullSqlite3 *db) {
	statement->db = db;

	g_object_notify_by_pspec(G_OBJECT(statement), properties[PROP_DB]);
}

static void
seagull_statement_set_sql(SeagullStatement *statement, const char *sql) {
	if(g_set_str(&statement->sql, sql)) {
		g_object_notify_by_pspec(G_OBJECT(statement), properties[PROP_SQL]);
	}
}

static int
seagull_statement_find_column(SeagullStatement *statement, const char *name,
                              GError **error)
{
	gpointer value = NULL;
	gboolean found = FALSE;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	/* If the cache is empty, populate it. */
	if(g_hash_table_size(statement->columns) == 0) {
		for(int i = 0; i < sqlite3_column_count(statement->statement); i++) {
			const char *column_name = NULL;

			column_name = sqlite3_column_origin_name(statement->statement, i);
			if(column_name != NULL) {
				char *full_name = NULL;
				const char *database_name = NULL;
				const char *table_name = NULL;

				/* Grab the database and table names to add the additional
				 * aliases for columns. This lets stuff like
				 * `SELECT foo.a from foo` work as column_name is just `a`.
				 */
				database_name = sqlite3_column_database_name(statement->statement,
				                                             i);
				table_name = sqlite3_column_table_name(statement->statement, i);

				g_hash_table_insert(statement->columns, g_strdup(column_name),
				                    GINT_TO_POINTER(i));

				full_name = g_strdup_printf("%s.%s", table_name, column_name);
				g_hash_table_insert(statement->columns, full_name,
				                    GINT_TO_POINTER(i));

				full_name = g_strdup_printf("%s.%s.%s", database_name,
				                            table_name, column_name);
				g_hash_table_insert(statement->columns, full_name,
				                    GINT_TO_POINTER(i));
			}

			/* 'AS' columns are only available via sqlite3_column_name so we
			 * have to check them as well and only add them if they're
			 * different.
			 */
			column_name = sqlite3_column_name(statement->statement, i);
			if(column_name != NULL) {
				if(!g_hash_table_contains(statement->columns, column_name)) {
					g_hash_table_insert(statement->columns,
					                    g_strdup(column_name),
					                    GINT_TO_POINTER(i));
				}
			}
		}
	}

	/* Our cache has been built, so look for stuff in it. */
	found = g_hash_table_lookup_extended(statement->columns, name, NULL,
	                                     &value);
	if(found) {
		return GPOINTER_TO_INT(value);
	}

	g_set_error(error,
	            SEAGULL_STATEMENT_ERROR,
	            SEAGULL_STATEMENT_ERROR_BIND_PARAMETER_NOT_FOUND,
	            "failed to find a parameter named '%s'",
	            name);

	return -1;
}

static int
seagull_statement_find_parameter(SeagullStatement *statement, const char *name,
                                 GError **error)
{
	gpointer value = NULL;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);

	value = g_hash_table_lookup(statement->parameters, name);
	if(value != NULL) {
		return GPOINTER_TO_INT(value);
	}

	index = sqlite3_bind_parameter_index(statement->statement, name);
	if(index == 0) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_PARAMETER_NOT_FOUND,
		            "failed to find a parameter named '%s'",
		            name);

		return FALSE;
	}

	g_hash_table_insert(statement->parameters, g_strdup(name),
	                    GINT_TO_POINTER(value));

	return index;
}

/**
 * seagull_statement_parameter_to_property_name: (skip)
 * @parameter_name: the full parameter name including the named parameter char
 * @prefix: (nullable): and optional prefix
 *
 * Converts a SQL column name into the idiomatic GObject property name.
 *
 * This basically means converting `_`'s to `-`'s but could be more in the
 * future.
 *
 * Returns: (transfer full) (nullable): The property name
 *
 * Since: 0.4
 */
static char *
seagull_statement_parameter_to_property_name(const char *parameter_name,
                                             const char *prefix)
{
	char *property_name = NULL;
	const char *valid = "0123456789"
	                    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	                    "abcdefghijklmnopqrstuvwxyz";
	guint prefix_len = 0;

	if(parameter_name == NULL) {
		return NULL;
	}

	/* Increment past the named parameter prefix character of $, :, or @. */
	parameter_name += 1;

	if(prefix != NULL) {
		if(!g_str_has_prefix(parameter_name, prefix)) {
			return NULL;
		}

		prefix_len = strlen(prefix);
	}

	property_name = g_strdup(parameter_name + prefix_len);

	g_strcanon(property_name, valid, '-');

	return property_name;
}

/******************************************************************************
 * GInitable Implementation
 *****************************************************************************/
static gboolean
seagull_statement_initable_init(GInitable *initable,
                                G_GNUC_UNUSED GCancellable *cancellable,
                                GError **error)
{
	SeagullStatement *statement = SEAGULL_STATEMENT(initable);
	int rc = SQLITE_OK;
	const char *trailing = NULL;

	if(statement->db == NULL) {
		g_set_error_literal(error,
		                    SEAGULL_STATEMENT_ERROR,
		                    SEAGULL_STATEMENT_ERROR_NO_DB_PROVIDED,
		                    "no SQLite database was provided");

		return FALSE;
	}

	g_clear_pointer(&statement->statement, sqlite3_finalize);
	rc = sqlite3_prepare_v2(statement->db, statement->sql, -1,
	                        &statement->statement, &trailing);

	if(rc != SQLITE_OK) {
		if(rc == SQLITE_MISUSE) {
			g_set_error_literal(error,
			                    SEAGULL_STATEMENT_ERROR,
			                    SEAGULL_STATEMENT_ERROR_NO_SQL_PROVIDED,
			                    "no SQL statement was provided");
		} else {
			g_set_error(error,
			            SEAGULL_STATEMENT_ERROR,
			            SEAGULL_STATEMENT_ERROR_PREPARATION_FAILED,
			            "failed to prepare statement: %s",
			            sqlite3_errmsg(statement->db));
		}

		return FALSE;
	}

	if(trailing != NULL && trailing[0] != '\0') {
		g_set_error_literal(error,
		                    SEAGULL_STATEMENT_ERROR,
		                    SEAGULL_STATEMENT_ERROR_MORE_THAN_ONE_STATEMENT,
		                    "more than one SQL statement was provided.");

		return FALSE;
	}

	return TRUE;
}

static void
seagull_statement_initable_iface_init(GInitableIface *iface) {
	iface->init = seagull_statement_initable_init;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE_WITH_CODE(
	SeagullStatement,
	seagull_statement,
	G_TYPE_OBJECT,
	G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE,
	                      seagull_statement_initable_iface_init))

static void
seagull_statement_finalize(GObject *obj) {
	SeagullStatement *statement = SEAGULL_STATEMENT(obj);

	statement->db = NULL;
	g_clear_pointer(&statement->sql, g_free);

	g_clear_pointer(&statement->statement, sqlite3_finalize);

	g_clear_pointer(&statement->columns, g_hash_table_unref);
	g_clear_pointer(&statement->parameters, g_hash_table_unref);

	G_OBJECT_CLASS(seagull_statement_parent_class)->finalize(obj);
}

static void
seagull_statement_get_property(GObject *obj, guint param_id, GValue *value,
                               GParamSpec *pspec)
{
	SeagullStatement *statement = SEAGULL_STATEMENT(obj);

	switch(param_id) {
	case PROP_DB:
		g_value_set_pointer(value, seagull_statement_get_db(statement));
		break;
	case PROP_SQL:
		g_value_set_string(value, seagull_statement_get_sql(statement));
		break;
	case PROP_STMT:
		g_value_set_pointer(value, seagull_statement_get_stmt(statement));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
seagull_statement_set_property(GObject *obj, guint param_id,
                               const GValue *value, GParamSpec *pspec)
{
	SeagullStatement *statement = SEAGULL_STATEMENT(obj);

	switch(param_id) {
	case PROP_DB:
		seagull_statement_set_db(statement, g_value_get_pointer(value));
		break;
	case PROP_SQL:
		seagull_statement_set_sql(statement, g_value_get_string(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
seagull_statement_init(SeagullStatement *statement) {
	statement->columns = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
	                                           NULL);
	statement->parameters = g_hash_table_new_full(g_str_hash, g_str_equal,
	                                              g_free, NULL);
}

static void
seagull_statement_class_init(SeagullStatementClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = seagull_statement_finalize;
	obj_class->get_property = seagull_statement_get_property;
	obj_class->set_property = seagull_statement_set_property;

	/**
	 * SeagullStatement:db:
	 *
	 * The SQLite database connection.
	 *
	 * Since: 0.3
	 */
	properties[PROP_DB] = g_param_spec_pointer(
		"db", NULL, NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * SeagullStatement:sql:
	 *
	 * The raw SQL statement.
	 *
	 * Since: 0.3
	 */
	properties[PROP_SQL] = g_param_spec_string(
		"sql", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * SeagullStatement:stmt:
	 *
	 * The underlying sqlite3_stmt.
	 *
	 * Since: 0.3
	 */
	properties[PROP_STMT] = g_param_spec_pointer(
		"stmt", NULL, NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
gboolean
seagull_statement_bind_boolean(SeagullStatement *statement, const char *name,
                               gboolean value, GError **error)
{
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	return seagull_statement_bind_int(statement, name, (int)value, error);
}

gboolean
seagull_statement_bind_bytes(SeagullStatement *statement, const char *name,
                             GBytes *value, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	if(value == NULL) {
		rc = sqlite3_bind_null(statement->statement, index);
	} else {
		gconstpointer data = NULL;
		gsize data_size = 0;

		data = g_bytes_get_data(value, &data_size);

		/* We use SQLITE_TRANSIENT so that SQLite will make a copy of the data
		 * and manage its lifecycle. We can't use g_bytes_unref as the destroy
		 * notify because the data isn't the GBytes, but instead just the data
		 * contained in the GBytes.
		 */
		rc = sqlite3_bind_blob64(statement->statement, index, data, data_size,
		                         SQLITE_TRANSIENT);
	}

	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_date_time(SeagullStatement *statement,
                                 const char *name, GDateTime *value,
                                 GError **error)
{
	char *str = NULL;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	if(value != NULL) {
		str = g_date_time_format_iso8601(value);
	}

	return seagull_statement_bind_text(statement, name, str, -1, g_free,
	                                   error);
}

gboolean
seagull_statement_bind_double(SeagullStatement *statement, const char *name,
                              double value, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	rc = sqlite3_bind_double(statement->statement, index, value);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_enum(SeagullStatement *statement, const char *name,
                            int value, GError **error)
{
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	return seagull_statement_bind_int(statement, name, value, error);
}

gboolean
seagull_statement_bind_error(SeagullStatement *statement, const char *name,
                             GError *value, GError **error)
{
	GVariant *variant = NULL;
	GBytes *bytes = NULL;
	gboolean ret = FALSE;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	if(value == NULL) {
		return seagull_statement_bind_null(statement, name, error);
	}

	variant = g_variant_new(SEAGULL_STATEMENT_GERROR_VARIANT_FORMAT,
	                        g_quark_to_string(value->domain),
	                        value->code, value->message);
	bytes = g_variant_get_data_as_bytes(variant);
	g_clear_pointer(&variant, g_variant_unref);

	ret = seagull_statement_bind_bytes(statement, name, bytes, error);

	g_clear_pointer(&bytes, g_bytes_unref);

	return ret;
}


gboolean
seagull_statement_bind_flags(SeagullStatement *statement, const char *name,
                             guint value, GError **error)
{
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	return seagull_statement_bind_int(statement, name, (int)value, error);
}

gboolean
seagull_statement_bind_int(SeagullStatement *statement, const char *name,
                           int value, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	rc = sqlite3_bind_int(statement->statement, index, value);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_int64(SeagullStatement *statement, const char *name,
                             gint64 value, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	rc = sqlite3_bind_int64(statement->statement, index, value);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_null(SeagullStatement *statement, const char *name,
                            GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	rc = sqlite3_bind_null(statement->statement, index);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_object(SeagullStatement *statement, const char *prefix,
                              GObject *obj, GError **error)
{
	GObjectClass *obj_class = NULL;
	guint n_parameters = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);
	g_return_val_if_fail(G_IS_OBJECT(obj), FALSE);

	obj_class = G_OBJECT_GET_CLASS(obj);

	/* SQLite parameters start at one. */
	n_parameters = sqlite3_bind_parameter_count(statement->statement);
	for(guint i = 1; i <= n_parameters; i++) {
		GError *local_error = NULL;
		GParamSpec *pspec = NULL;
		GType type = G_TYPE_INVALID;
		GValue value = G_VALUE_INIT;
		char *property_name = NULL;
		const char *parameter_name = NULL;

		parameter_name = sqlite3_bind_parameter_name(statement->statement, i);
		property_name = seagull_statement_parameter_to_property_name(parameter_name, prefix);
		if(property_name == NULL) {
			continue;
		}

		pspec = g_object_class_find_property(obj_class, property_name);
		g_clear_pointer(&property_name, g_free);

		/* If we didn't find a pspec, we move on to the next one, as it's
		 * possible to bind multiple objects to the same query.
		 */
		if(pspec == NULL) {
			continue;
		}

		g_object_get_property(obj, g_param_spec_get_name(pspec), &value);

		type = G_VALUE_TYPE(&value);
		if(type == G_TYPE_BOOLEAN) {
			seagull_statement_bind_int(statement, parameter_name,
			                           g_value_get_boolean(&value),
			                           &local_error);
		} else if(type == G_TYPE_BYTES) {
			seagull_statement_bind_bytes(statement, parameter_name,
			                             g_value_get_boxed(&value),
			                             &local_error);
		} else if(type == G_TYPE_DATE_TIME) {
			seagull_statement_bind_date_time(statement, parameter_name,
			                                 g_value_get_boxed(&value),
			                                 &local_error);
		} else if(type == G_TYPE_DOUBLE) {
			seagull_statement_bind_double(statement, parameter_name,
			                              g_value_get_double(&value),
			                              &local_error);
		} else if(type == G_TYPE_ERROR) {
			seagull_statement_bind_error(statement, parameter_name,
			                             g_value_get_boxed(&value),
			                             &local_error);
		} else if(type == G_TYPE_INT) {
			seagull_statement_bind_int(statement, parameter_name,
			                           g_value_get_int(&value), &local_error);
		} else if(type == G_TYPE_INT64) {
			seagull_statement_bind_int64(statement, parameter_name,
			                             g_value_get_int64(&value),
			                             &local_error);
		} else if(type == G_TYPE_STRING) {
			seagull_statement_bind_text(statement, parameter_name,
			                            g_value_steal_string(&value),
			                            -1,
			                            g_free,
			                            &local_error);
		} else if(type == G_TYPE_TIME_ZONE) {
			seagull_statement_bind_time_zone(statement, parameter_name,
			                                 g_value_get_boxed(&value),
			                                 &local_error);
		} else if(type == G_TYPE_UINT) {
			seagull_statement_bind_uint(statement, parameter_name,
			                            g_value_get_uint(&value),
			                            &local_error);
		} else if(type == G_TYPE_UINT64) {
			seagull_statement_bind_uint64(statement, parameter_name,
			                              g_value_get_uint64(&value),
			                              &local_error);
		} else if(G_TYPE_IS_ENUM(type)) {
			seagull_statement_bind_enum(statement, parameter_name,
			                            g_value_get_enum(&value), &local_error);
		} else if(G_TYPE_IS_FLAGS(type)) {
			seagull_statement_bind_flags(statement, parameter_name,
			                             g_value_get_flags(&value),
			                             &local_error);
		} else {
			local_error = g_error_new(SEAGULL_STATEMENT_ERROR,
			                          SEAGULL_STATEMENT_ERROR_BIND_OBJECT_PROPERTY_UNKNOWN_TYPE,
			                          "found object property %s for parameter %s which has unknown type %s",
			                          g_param_spec_get_name(pspec),
			                          parameter_name,
			                          G_VALUE_TYPE_NAME(&value));
		}

		g_value_unset(&value);

		if(local_error != NULL) {
			g_propagate_error(error, local_error);
			return FALSE;
		}
	}

	return TRUE;
}

gboolean
seagull_statement_bind_text(SeagullStatement *statement, const char *name,
                            const char *value, int len,
                            GDestroyNotify destroy_notify, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;
	gpointer sqlite3_notify = SQLITE_STATIC;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	if(destroy_notify != NULL) {
		sqlite3_notify = destroy_notify;
	}

	rc = sqlite3_bind_text(statement->statement, index, value, len,
	                       sqlite3_notify);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_time_zone(SeagullStatement *statement, const char *name,
                                 GTimeZone *time_zone, GError **error)
{
	const char *str = NULL;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	if(time_zone != NULL) {
		str = g_time_zone_get_identifier(time_zone);
	}

	return seagull_statement_bind_text(statement, name, str, -1, NULL, error);
}

gboolean
seagull_statement_bind_uint(SeagullStatement *statement, const char *name,
                            guint value, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	rc = sqlite3_bind_int(statement->statement, index, value);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_bind_uint64(SeagullStatement *statement, const char *name,
                              guint64 value, GError **error)
{
	GError *local_error = NULL;
	int rc = 0;
	int index = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	index = seagull_statement_find_parameter(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return FALSE;
	}

	rc = sqlite3_bind_int64(statement->statement, index, value);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_BIND_FAILED,
		            "failed to bind parameter '%s': %s",
		            name,
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_clear_bindings(SeagullStatement *statement, GError **error) {
	int rc = SQLITE_OK;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	rc = sqlite3_clear_bindings(statement->statement);
	if(rc != SQLITE_OK) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_CLEAR_BINDINGS_FAILED,
		            "failed to clear bindings: %s",
		            sqlite3_errmsg(statement->db));

		return FALSE;
	}

	return TRUE;
}

gboolean
seagull_statement_column_boolean(SeagullStatement *statement, const char *name,
                                 GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);
	g_return_val_if_fail(name != NULL, FALSE);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return FALSE;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return FALSE;
	}

	return (gboolean)sqlite3_column_int(statement->statement, column);
}

GBytes *
seagull_statement_column_bytes(SeagullStatement *statement, const char *name,
                               GError **error)
{
	gconstpointer data = NULL;
	int column = -1;
	int column_type = 0;
	int data_size = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);
	g_return_val_if_fail(name != NULL, NULL);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return NULL;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_BLOB, column_type, error)) {
		return NULL;
	}

	data_size = sqlite3_column_bytes(statement->statement, column);
	if(data_size == 0) {
		return NULL;
	}

	data = sqlite3_column_blob(statement->statement, column);

	return g_bytes_new(data, data_size);
}

GDateTime *
seagull_statement_column_date_time(SeagullStatement *statement,
                                   const char *name, GError **error)
{
	GDateTime *dt = NULL;
	GTimeZone *tz = NULL;
	const char *iso8601 = NULL;
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);
	g_return_val_if_fail(name != NULL, NULL);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return NULL;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_TEXT, column_type, error)) {
		return NULL;
	}

	iso8601 = (const char *)sqlite3_column_text(statement->statement, column);
	if(iso8601 == NULL) {
		return NULL;
	}

	tz = g_time_zone_new_utc();
	dt = g_date_time_new_from_iso8601(iso8601, tz);
	g_time_zone_unref(tz);

	return dt;
}

double
seagull_statement_column_double(SeagullStatement *statement, const char *name,
                                GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_FLOAT, column_type, error)) {
		return 0;
	}

	return sqlite3_column_double(statement->statement, column);
}

int
seagull_statement_column_enum(SeagullStatement *statement, const char *name,
                              GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return 0;
	}

	return sqlite3_column_int(statement->statement, column);
}

GError *
seagull_statement_column_error(SeagullStatement *statement, const char *name,
                               GError **error)
{
	GBytes *bytes = NULL;
	GError *local_error = NULL;
	GError *value = NULL;
	GVariant *variant = NULL;
	int code;
	const char *domain = NULL;
	const char *message = NULL;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	bytes = seagull_statement_column_bytes(statement, name, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		return NULL;
	}

	if(bytes == NULL) {
		return NULL;
	}

	variant = g_variant_new_from_bytes(G_VARIANT_TYPE(SEAGULL_STATEMENT_GERROR_VARIANT_FORMAT),
	                                   bytes, TRUE);
	g_clear_pointer(&bytes, g_bytes_unref);

	/* Pull the values out of the variant. */
	g_variant_get_child(variant, 0, "&s", &domain);
	g_variant_get_child(variant, 1, "i", &code);
	g_variant_get_child(variant, 2, "&s", &message);

	/* Build the error. */
	value = g_error_new(g_quark_from_string(domain), code, message);

	g_variant_unref(variant);

	return value;
}

guint
seagull_statement_column_flags(SeagullStatement *statement, const char *name,
                               GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return 0;
	}

	return (guint)sqlite3_column_int(statement->statement, column);
}

int
seagull_statement_column_int(SeagullStatement *statement, const char *name,
                             GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return 0;
	}

	return sqlite3_column_int(statement->statement, column);
}

gint64
seagull_statement_column_int64(SeagullStatement *statement, const char *name,
                               GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return 0;
	}

	return sqlite3_column_int64(statement->statement, column);
}

gboolean
seagull_statement_column_object(SeagullStatement *statement,
                                const char *prefix,
                                GObject *obj,
                                GError **error)
{
	GObjectClass *obj_class = NULL;
	guint n_columns = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);
	g_return_val_if_fail(G_IS_OBJECT(obj), FALSE);

	obj_class = G_OBJECT_GET_CLASS(obj);

	/* SQLite columns start at zero. */
	n_columns = sqlite3_column_count(statement->statement);
	for(guint i = 0; i < n_columns; i++) {
		GError *local_error = NULL;
		GParamSpec *pspec = NULL;
		GValue value = G_VALUE_INIT;
		const char *column_name = NULL;

		/* sqlite3_column_name returns the named assigned with the AS keyword
		 * or the column name if no AS keyword was used. This is true at least
		 * as of version 3.46.1. We have a unit test to verify if this changes.
		 */
		column_name = sqlite3_column_name(statement->statement, i);
		if(column_name == NULL) {
			column_name = sqlite3_column_origin_name(statement->statement, i);
		}

		/* If we have a prefix, it must exist in the column name. */
		if(prefix != NULL) {
			if(!g_str_has_prefix(column_name, prefix)) {
				continue;
			} else {
				column_name += strlen(prefix);
			}
		}
		pspec = g_object_class_find_property(obj_class, column_name);

		/* If we didn't find a pspec, we move on to the next one, as it's
		 * possible to bind multiple objects to the same query.
		 */
		if(pspec == NULL) {
			continue;
		}

		if(pspec->value_type == G_TYPE_BOOLEAN) {
			g_value_init(&value, G_TYPE_BOOLEAN);
			g_value_set_boolean(&value,
			                    sqlite3_column_int(statement->statement, i));
		} else if(pspec->value_type == G_TYPE_BYTES) {
			GBytes *bytes = NULL;
			gsize data_size = 0;

			data_size = sqlite3_column_bytes(statement->statement, i);
			if(data_size > 0) {
				gconstpointer data = NULL;

				data = sqlite3_column_blob(statement->statement, i);
				bytes = g_bytes_new(data, data_size);
			}

			g_value_init(&value, G_TYPE_BYTES);
			g_value_take_boxed(&value, bytes);
		} else if(pspec->value_type == G_TYPE_DATE_TIME) {
			GDateTime *dt = NULL;
			const char *str = NULL;

			str = (const char *)sqlite3_column_text(statement->statement, i);
			if(str != NULL) {
				GTimeZone *tz = g_time_zone_new_utc();

				dt = g_date_time_new_from_iso8601(str, tz);
				g_time_zone_unref(tz);
			}
			g_value_init(&value, G_TYPE_DATE_TIME);
			g_value_take_boxed(&value, dt);
		} else if(pspec->value_type == G_TYPE_DOUBLE) {
			g_value_init(&value, G_TYPE_DOUBLE);
			g_value_set_double(&value,
			                   sqlite3_column_double(statement->statement, i));
		} else if(pspec->value_type == G_TYPE_ERROR) {
			GBytes *bytes = NULL;
			GError *error_value = NULL;
			gsize data_size = 0;

			data_size = sqlite3_column_bytes(statement->statement, i);
			if(data_size > 0) {
				gconstpointer data = NULL;

				data = sqlite3_column_blob(statement->statement, i);
				bytes = g_bytes_new(data, data_size);
			}

			if(bytes != NULL) {
				GVariant *variant = NULL;
				const char *domain = NULL;
				const char *message = NULL;
				int code = 0;

				variant = g_variant_new_from_bytes(G_VARIANT_TYPE(SEAGULL_STATEMENT_GERROR_VARIANT_FORMAT),
				                                   bytes, TRUE);

				g_clear_pointer(&bytes, g_bytes_unref);

				/* Pull the values out of the variant. */
				g_variant_get_child(variant, 0, "&s", &domain);
				g_variant_get_child(variant, 1, "i", &code);
				g_variant_get_child(variant, 2, "&s", &message);

				/* Build the error. */
				error_value = g_error_new(g_quark_from_string(domain), code,
				                          message);

				g_clear_pointer(&variant, g_variant_unref);
			}

			g_value_init(&value, G_TYPE_ERROR);
			g_value_take_boxed(&value, error_value);
		} else if(pspec->value_type == G_TYPE_INT) {
			g_value_init(&value, G_TYPE_INT);
			g_value_set_int(&value,
			                sqlite3_column_int(statement->statement, i));
		} else if(pspec->value_type == G_TYPE_INT64) {
			g_value_init(&value, G_TYPE_INT64);
			g_value_set_int64(&value,
			                  sqlite3_column_int64(statement->statement, i));
		} else if(pspec->value_type == G_TYPE_STRING) {
			g_value_init(&value, G_TYPE_STRING);
			g_value_set_string(&value,
			                   (const char *)sqlite3_column_text(statement->statement, i));
		} else if(pspec->value_type == G_TYPE_TIME_ZONE) {
			GTimeZone *tz = NULL;
			const char *str = NULL;

			str = (const char *)sqlite3_column_text(statement->statement, i);
			if(str != NULL) {
				tz = g_time_zone_new_identifier(str);
			}
			g_value_init(&value, G_TYPE_TIME_ZONE);
			g_value_take_boxed(&value, tz);
		} else if(pspec->value_type == G_TYPE_UINT) {
			g_value_init(&value, G_TYPE_UINT);
			g_value_set_uint(&value, sqlite3_column_int(statement->statement,
			                 i));
		} else if(pspec->value_type == G_TYPE_UINT64) {
			g_value_init(&value, G_TYPE_UINT64);
			g_value_set_uint64(&value,
			                   sqlite3_column_int64(statement->statement, i));
		} else if(G_TYPE_IS_ENUM(pspec->value_type)) {
			g_value_init(&value, pspec->value_type);
			g_value_set_enum(&value,
			                 sqlite3_column_int(statement->statement, i));
		} else if(G_TYPE_IS_FLAGS(pspec->value_type)) {
			g_value_init(&value, pspec->value_type);
			g_value_set_flags(&value,
			                  (guint)sqlite3_column_int(statement->statement, i));
		} else {
			local_error = g_error_new(SEAGULL_STATEMENT_ERROR,
			                          SEAGULL_STATEMENT_ERROR_COLUMN_OBJECT_PROPERTY_UNKNOWN_TYPE,
			                          "found object property %s for column %s which has unknown type %s",
			                          g_param_spec_get_name(pspec),
			                          column_name,
			                          G_PARAM_SPEC_TYPE_NAME(pspec));
		}

		if(G_VALUE_TYPE(&value) != G_TYPE_INVALID) {
			g_object_set_property(obj, g_param_spec_get_name(pspec), &value);
			g_value_unset(&value);
		}

		if(local_error != NULL) {
			g_propagate_error(error, local_error);
			return FALSE;
		}
	}

	return TRUE;
}

const char *
seagull_statement_column_text(SeagullStatement *statement, const char *name,
                              GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);
	g_return_val_if_fail(name != NULL, NULL);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return NULL;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_TEXT, column_type, error)) {
		return 0;
	}

	return (const char *)sqlite3_column_text(statement->statement, column);
}

GTimeZone *
seagull_statement_column_time_zone(SeagullStatement *statement,
                                   const char *name, GError **error)
{
	int column = -1;
	int column_type = 0;
	const char *identifier = NULL;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);
	g_return_val_if_fail(name != NULL, NULL);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return NULL;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_TEXT, column_type, error)) {
		return NULL;
	}

	identifier = (const char *)sqlite3_column_text(statement->statement,
	                                               column);

	/* Passing NULL to g_time_zone_new_identifier will use the local time,
	 * which is not what we want to do here as that loses the context that the
	 * timezone was not set.
	 */
	if(identifier == NULL) {
		return NULL;
	}

	return g_time_zone_new_identifier(identifier);
}

guint
seagull_statement_column_uint(SeagullStatement *statement, const char *name,
                              GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return 0;
	}

	return sqlite3_column_int(statement->statement, column);
}

guint64
seagull_statement_column_uint64(SeagullStatement *statement, const char *name,
                                GError **error)
{
	int column = -1;
	int column_type = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), 0);
	g_return_val_if_fail(name != NULL, 0);

	column = seagull_statement_find_column(statement, name, error);
	if(column == -1) {
		return 0;
	}

	column_type = sqlite3_column_type(statement->statement, column);
	if(!seagull_statement_check_column_type(SQLITE_INTEGER, column_type,
	                                        error))
	{
		return 0;
	}

	return sqlite3_column_int64(statement->statement, column);
}

SeagullSqlite3 *
seagull_statement_get_db(SeagullStatement *statement) {
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);

	return statement->db;
}

char *
seagull_statement_get_expanded_sql(SeagullStatement *statement) {
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);

	return sqlite3_expanded_sql(statement->statement);
}

const char *
seagull_statement_get_sql(SeagullStatement *statement) {
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);

	/* SQLite3 doesn't do any normalization or anything and just returns the
	 * original string, which we already have, so we just return that directly.
	 */
	return statement->sql;
}

SeagullSqlite3Stmt *
seagull_statement_get_stmt(SeagullStatement *statement) {
	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), NULL);

	return statement->statement;
}

SeagullStatement *
seagull_statement_new(SeagullSqlite3 *db, const char *sql, GError **error) {
	return g_initable_new(
		SEAGULL_TYPE_STATEMENT,
		NULL,
		error,
		"db", db,
		"sql", sql,
		NULL);
}

SeagullStatement *
seagull_statement_new_from_resource(SeagullSqlite3 *db, const char *path,
                                    GError **error)
{
	SeagullStatement *stmt = NULL;
	GBytes *data = NULL;
	GError *local_error = NULL;

	g_return_val_if_fail(path != NULL, NULL);

	/* Attempt to load the resource. */
	data = g_resources_lookup_data(path, G_RESOURCE_LOOKUP_FLAGS_NONE,
	                               &local_error);
	if(data == NULL || local_error != NULL) {
		if(local_error == NULL) {
			local_error = g_error_new(SEAGULL_STATEMENT_ERROR,
			                          SEAGULL_STATEMENT_ERROR_FAILED_TO_LOAD_RESOURCE,
			                          "failed to load resource %s",
			                          path);
		}

		g_propagate_error(error, local_error);

		g_clear_pointer(&data, g_bytes_unref);

		return NULL;
	}

	stmt = g_initable_new(
		SEAGULL_TYPE_STATEMENT,
		NULL,
		error,
		"db", db,
		"sql", (const char *)g_bytes_get_data(data, NULL),
		NULL);

	g_clear_pointer(&data, g_bytes_unref);

	return stmt;
}

gboolean
seagull_statement_reset(SeagullStatement *statement, GError **error) {
	int rc = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	rc = sqlite3_reset(statement->statement);
	if(rc == SQLITE_OK) {
		return TRUE;
	}

	if(rc != SQLITE_DONE) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_RESET_FAILED,
		            "failed to reset the statement : %s",
		            sqlite3_errmsg(statement->db));
	}

	return FALSE;
}

gboolean
seagull_statement_step(SeagullStatement *statement, GError **error) {
	int rc = 0;

	g_return_val_if_fail(SEAGULL_IS_STATEMENT(statement), FALSE);

	rc = sqlite3_step(statement->statement);
	if(rc == SQLITE_ROW) {
		return TRUE;
	}

	if(rc != SQLITE_DONE) {
		g_set_error(error,
		            SEAGULL_STATEMENT_ERROR,
		            SEAGULL_STATEMENT_ERROR_STEP_FAILED,
		            "failed to step the statement : %s",
		            sqlite3_errmsg(statement->db));
	}

	return FALSE;
}
