/*
 * Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

/**
 * A Connection represents a connection to an open database.
 *
 * Because SQLite uses a synchronous interface, all calls are
 * blocking.  Db.Database offers asynchronous queries by pooling
 * connections and invoking queries from background threads.
 *
 * Connections are associated with a Database.  Use
 * Database.open_connection() to create one.
 *
 * A Connection will close when its last reference is dropped.
 */

public class Geary.Db.Connection : Geary.Db.Context {
    /**
     * Default value is for *no* timeout, that is, the Sqlite will not retry BUSY results.
     */
    public const int DEFAULT_BUSY_TIMEOUT_MSEC = 0;

    /**
     * This value gives a generous amount of time for SQLite to finish a big write operation and
     * relinquish the lock to other waiting transactions.
     */
    public const int RECOMMENDED_BUSY_TIMEOUT_MSEC = 60 * 1000;

    private const string PRAGMA_FOREIGN_KEYS = "foreign_keys";
    private const string PRAGMA_RECURSIVE_TRIGGERS = "recursive_triggers";
    private const string PRAGMA_USER_VERSION = "user_version";
    private const string PRAGMA_SCHEMA_VERSION = "schema_version";
    private const string PRAGMA_SECURE_DELETE = "secure_delete";
    private const string PRAGMA_SYNCHRONOUS = "synchronous";
    private const string PRAGMA_FREELIST_COUNT = "freelist_count";
    private const string PRAGMA_PAGE_COUNT = "page_count";
    private const string PRAGMA_PAGE_SIZE = "page_size";

    // this is used for logging purposes only; connection numbers mean nothing to SQLite
    private static int next_cx_number = 0;

    /**
     * See [[http://www.sqlite.org/c3ref/last_insert_rowid.html]]
     */
    public int64 last_insert_rowid { get {
        return db.last_insert_rowid();
    } }

    /**
     * See [[http://www.sqlite.org/c3ref/changes.html]]
     */
    public int last_modified_rows { get {
        return db.changes();
    } }

    /**
     * See [[http://www.sqlite.org/c3ref/total_changes.html]]
     */
    public int total_modified_rows { get {
        return db.total_changes();
    } }

    public weak Database database { get; private set; }

    internal Sqlite.Database db;

    private int cx_number;
    private int busy_timeout_msec = DEFAULT_BUSY_TIMEOUT_MSEC;

    internal Connection(Database database, int sqlite_flags, Cancellable? cancellable) throws Error {
        this.database = database;

        lock (next_cx_number) {
            cx_number = next_cx_number++;
        }

        check_cancelled("Connection.ctor", cancellable);

        try {
            throw_on_error(
                "Connection.ctor",
                Sqlite.Database.open_v2(database.path, out db, sqlite_flags, null)
            );
        } catch (DatabaseError derr) {
            // don't throw BUSY error for open unless no db object was returned, as it's possible for
            // open_v2() to return an error *and* a valid Database object, see:
            // http://www.sqlite.org/c3ref/open.html
            if (!(derr is DatabaseError.BUSY) || (db == null))
                throw derr;
        }
    }

    /**
     * Execute a plain text SQL statement.  More than one SQL statement may be in the string.  See
     * [[http://www.sqlite.org/lang.html]] for more information on SQLite's SQL syntax.
     *
     * There is no way to retrieve a result iterator from this call.
     *
     * This may be called from a TransactionMethod called within
     * {@link exec_transaction} or {@link exec_transaction_async}.
     *
     * See [[http://www.sqlite.org/c3ref/exec.html]]
     */
    public void exec(string sql, Cancellable? cancellable = null) throws Error {
        check_cancelled("Connection.exec", cancellable);

        throw_on_error("Connection.exec", db.exec(sql), sql);

        // Don't use Context.log(), which is designed for logging Results and Statements
        Logging.debug(Logging.Flag.SQL, "exec:\n\t%s", sql);
    }

    /**
     * Loads a text file of SQL commands into memory and executes them at once with exec().
     *
     * There is no way to retrieve a result iterator from this call.
     *
     * This may be called from a TransactionMethod called within
     * {@link exec_transaction} or {@link exec_transaction_async}.
     */
    public void exec_file(File file, Cancellable? cancellable = null) throws Error {
        check_cancelled("Connection.exec_file", cancellable);

        string sql;
        FileUtils.get_contents(file.get_path(), out sql);

        exec(sql, cancellable);
    }

    /**
     * Executes a plain text SQL statement and returns a Result object directly.
     * This call creates an intermediate Statement object which may be fetched from Result.statement.
     */
    public Result query(string sql, Cancellable? cancellable = null) throws Error {
        return (new Statement(this, sql)).exec(cancellable);
    }

    /**
     * Prepares a Statement which may have values bound to it and executed.  See
     * [[http://www.sqlite.org/c3ref/prepare.html]]
     */
    public Statement prepare(string sql) throws DatabaseError {
        return new Statement(this, sql);
    }

    /**
     * See set_busy_timeout_msec().
     */
    public int get_busy_timeout_msec() {
        return busy_timeout_msec;
    }

    /**
     * Sets busy timeout time in milliseconds.
     *
     * Zero or a negative value indicates that all operations that
     * SQLite returns BUSY will be retried until they complete with
     * success or error.  Otherwise, after said amount of time has
     * transpired, DatabaseError.BUSY will be thrown.
     *
     * This is imperative for {@link exec_transaction} {@link
     * exec_transaction_async}, because those calls will throw a
     * DatabaseError.BUSY call immediately if another transaction has
     * acquired the reserved or exclusive locks.
     */
    public void set_busy_timeout_msec(int busy_timeout_msec) throws Error {
        if (this.busy_timeout_msec == busy_timeout_msec)
            return;

        throw_on_error("Database.set_busy_timeout", db.busy_timeout(busy_timeout_msec));
        this.busy_timeout_msec = busy_timeout_msec;
    }

    /**
     * Returns the result of a PRAGMA as a boolean.  See [[http://www.sqlite.org/pragma.html]]
     *
     * Note that if the PRAGMA does not return a boolean, the results are undefined.  A boolean
     * in SQLite, however, includes 1 and 0, so an integer may be mistaken as a boolean.
     */
    public bool get_pragma_bool(string name) throws Error {
        string response = query("PRAGMA %s".printf(name)).nonnull_string_at(0);
        switch (response.down()) {
            case "1":
            case "yes":
            case "true":
            case "on":
                return true;

            case "0":
            case "no":
            case "false":
            case "off":
                return false;

            default:
                debug("Db.Connection.get_pragma_bool: unknown PRAGMA boolean response \"%s\"",
                    response);

                return false;
        }
    }

    /**
     * Sets a boolean PRAGMA value to either "true" or "false".
     */
    public void set_pragma_bool(string name, bool b) throws Error {
        exec("PRAGMA %s=%s".printf(name, b ? "true" : "false"));
    }

    /**
     * Returns the result of a PRAGMA as an integer.  See [[http://www.sqlite.org/pragma.html]]
     *
     * Note that if the PRAGMA does not return an integer, the results are undefined.  Since a
     * boolean in SQLite includes 1 and 0, it's possible for those values to be converted to an
     * integer.
     */
    public int get_pragma_int(string name) throws Error {
        return query("PRAGMA %s".printf(name)).int_at(0);
    }

    /**
     * Sets an integer PRAGMA value.
     */
    public void set_pragma_int(string name, int d) throws Error {
        exec("PRAGMA %s=%d".printf(name, d));
    }

    /**
     * Returns the result of a PRAGMA as a 64-bit integer. See [[http://www.sqlite.org/pragma.html]]
     *
     * Note that if the PRAGMA does not return an integer, the results are undefined.  Since a
     * boolean in SQLite includes 1 and 0, it's possible for those values to be converted to an
     * integer.
     */
    public int64 get_pragma_int64(string name) throws Error {
        return query("PRAGMA %s".printf(name)).int64_at(0);
    }

    /**
     * Sets a 64-bit integer PRAGMA value.
     */
    public void set_pragma_int64(string name, int64 ld) throws Error {
        exec("PRAGMA %s=%s".printf(name, ld.to_string()));
    }

    /**
     * Returns the result of a PRAGMA as a string.  See [[http://www.sqlite.org/pragma.html]]
     */
    public string get_pragma_string(string name) throws Error {
        return query("PRAGMA %s".printf(name)).nonnull_string_at(0);
    }

    /**
     * Sets a string PRAGMA value.
     */
    public void set_pragma_string(string name, string str) throws Error {
        exec("PRAGMA %s=%s".printf(name, str));
    }

    /**
     * Returns the user_version number maintained by SQLite.
     *
     * A new database has a user version number of zero.
     *
     * @see set_user_version_number
     */
    public int get_user_version_number() throws Error {
        return get_pragma_int(PRAGMA_USER_VERSION);
    }

    /**
     * Sets the user version number, which is a private number maintained by the user.
     * VersionedDatabase uses this to maintain the version number of the database.
     *
     * See [[http://www.sqlite.org/pragma.html#pragma_schema_version]]
     */
    public void set_user_version_number(int version) throws Error {
        set_pragma_int(PRAGMA_USER_VERSION, version);
    }

    /**
     * Gets the schema version number, which is maintained by SQLite. See
     * [[http://www.sqlite.org/pragma.html#pragma_schema_version]]
     *
     * Since this number is maintained by SQLite, Geary.Db doesn't offer a way to set it.
     */
    public int get_schema_version_number() throws Error {
        return get_pragma_int(PRAGMA_SCHEMA_VERSION);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_foreign_keys]]
     */
    public void set_foreign_keys(bool enabled) throws Error {
        set_pragma_bool(PRAGMA_FOREIGN_KEYS, enabled);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_foreign_keys]]
     */
    public bool get_foreign_keys() throws Error {
        return get_pragma_bool(PRAGMA_FOREIGN_KEYS);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_recursive_triggers]]
     */
    public void set_recursive_triggers(bool enabled) throws Error {
        set_pragma_bool(PRAGMA_RECURSIVE_TRIGGERS, enabled);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_recursive_triggers]]
     */
    public bool get_recursive_triggers() throws Error {
        return get_pragma_bool(PRAGMA_RECURSIVE_TRIGGERS);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_secure_delete]]
     */
    public void set_secure_delete(bool enabled) throws Error {
        set_pragma_bool(PRAGMA_SECURE_DELETE, enabled);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_secure_delete]]
     */
    public bool get_secure_delete() throws Error {
        return get_pragma_bool(PRAGMA_SECURE_DELETE);
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_synchronous]]
     */
    public void set_synchronous(SynchronousMode mode) throws Error {
        set_pragma_string(PRAGMA_SYNCHRONOUS, mode.sql());
    }

    /**
     * See [[http://www.sqlite.org/pragma.html#pragma_synchronous]]
     */
    public SynchronousMode get_synchronous() throws Error {
        return SynchronousMode.parse(get_pragma_string(PRAGMA_SYNCHRONOUS));
    }

    /**
     * See [[https://www.sqlite.org/pragma.html#pragma_freelist_count]]
     */
    public int64 get_free_page_count() throws Error {
        return get_pragma_int64(PRAGMA_FREELIST_COUNT);
    }

    /**
     * See [[https://www.sqlite.org/pragma.html#pragma_page_count]]
     */
    public int64 get_total_page_count() throws Error {
        return get_pragma_int64(PRAGMA_PAGE_COUNT);
    }

    /**
     * See [[https://www.sqlite.org/pragma.html#pragma_page_size]]
     */
    public int get_page_size() throws Error {
        return get_pragma_int(PRAGMA_PAGE_SIZE);
    }

    /**
     * Executes one or more queries inside an SQLite transaction.  This call will initiate a
     * transaction according to the TransactionType specified (although this is merely an
     * optimization -- no matter the transaction type, SQLite guarantees the subsequent operations
     * to be atomic).  The commands executed inside the TransactionMethod against the
     * supplied Db.Connection will be in the context of the transaction.  If the TransactionMethod
     * returns TransactionOutcome.COMMIT, the transaction will be committed to the database,
     * otherwise it will be rolled back and the database left unchanged.
     *
     * It's inadvisable to call exec_transaction() inside exec_transaction().  SQLite has a notion
     * of savepoints that allow for nested transactions; they are not currently supported.
     *
     * See [[http://www.sqlite.org/lang_transaction.html]]
     */
    public TransactionOutcome exec_transaction(TransactionType type, TransactionMethod cb,
        Cancellable? cancellable = null) throws Error {
        // initiate the transaction
        try {
            exec(type.sql(), cancellable);
        } catch (Error err) {
            if (!(err is IOError.CANCELLED))
                debug("Connection.exec_transaction: unable to %s: %s", type.sql(), err.message);

            throw err;
        }

        // If transaction throws an Error, must rollback, always
        TransactionOutcome outcome = TransactionOutcome.ROLLBACK;
        Error? caught_err = null;
        try {
            // perform the transaction
            outcome = cb(this, cancellable);
        } catch (Error err) {
            if (!(err is IOError.CANCELLED))
                debug("Connection.exec_transaction: transaction threw error: %s", err.message);

            caught_err = err;
        }

        // commit/rollback ... don't use Cancellable for TransactionOutcome because it's SQL *must*
        // execute in order to unlock the database
        try {
            exec(outcome.sql());
        } catch (Error err) {
            debug("Connection.exec_transaction: Unable to %s transaction: %s", outcome.to_string(),
                err.message);
        }

        if (caught_err != null)
            throw caught_err;

        return outcome;
    }

    /**
     * Starts a new asynchronous transaction for this connection.
     *
     * Asynchronous transactions are handled via background
     * threads. The background thread calls {@link exec_transaction};
     * see that method for more information about coding a
     * transaction. The only caveat is that the {@link
     * TransactionMethod} passed to it must be thread-safe.
     *
     * Throws {@link DatabaseError.OPEN_REQUIRED} if not open.
     */
    public async TransactionOutcome exec_transaction_async(TransactionType type,
                                                           TransactionMethod cb,
                                                           Cancellable? cancellable)
        throws Error {
        // create job to execute in background thread
        TransactionAsyncJob job = new TransactionAsyncJob(
            this, type, cb, cancellable
        );

        this.database.add_async_job(job);
        return yield job.wait_for_completion_async();
    }

    public override Connection? get_connection() {
        return this;
    }

    public string to_string() {
        return "[%d] %s".printf(cx_number, database.path);
    }
}
