/* 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.
 */

private extern string? sqlite3_expanded_sql(Sqlite.Statement stmt);


public class Geary.Db.Statement : Geary.Db.Context {

    public string sql {
        get { return this.stmt.sql(); }
    }

    public Connection connection { get; private set; }

    internal Sqlite.Statement stmt;

    private Gee.HashMap<string, int>? column_map = null;

    /**
     * Fired when the Statement is executed the first time (after creation or after a reset).
     */
    public signal void executed();

    /**
     * Fired when the Statement is reset.
     */
    public signal void resetted();

    /**
     * Fired when the Statement's bindings are cleared.
     */
    public signal void bindings_cleared();

    private Gee.HashSet<Memory.Buffer> held_buffers = new Gee.HashSet<Memory.Buffer>();

    internal Statement(Connection connection, string sql) throws DatabaseError {
        this.connection = connection;
        throw_on_error("Statement.ctor", connection.db.prepare_v2(sql, -1, out stmt, null), sql);
    }

    /** Returns SQL for the statement with bound parameters expanded. */
    public string? get_expanded_sql() {
        // Replace all this with `Sqlite.Statement.expanded_sql` is
        // readily available. See:
        // https://gitlab.gnome.org/GNOME/vala/merge_requests/74
        string* sqlite = sqlite3_expanded_sql(this.stmt);
        string? sql = sqlite;
        Sqlite.Memory.free((void*) sqlite);
        return sql;
    }

    /**
     * Reset the Statement for reuse, optionally clearing all bindings as well.  If bindings are
     * not cleared, valued bound previously will be maintained.
     *
     * See http://www.sqlite.org/c3ref/reset.html and http://www.sqlite.org/c3ref/clear_bindings.html
     */
    public Statement reset(ResetScope reset_scope) throws DatabaseError {
        if (reset_scope == ResetScope.CLEAR_BINDINGS)
            throw_on_error("Statement.clear_bindings", stmt.clear_bindings());

        throw_on_error("Statement.reset", stmt.reset());

        // fire signals after Statement has been altered -- this prevents reentrancy while the
        // Statement is in a halfway state
        if (reset_scope == ResetScope.CLEAR_BINDINGS)
            bindings_cleared();

        resetted();

        return this;
    }

    /**
     * Returns the number of columns the Statement will return in a Result.
     */
    public int get_column_count() {
        return stmt.column_count();
    }

    /**
     * Returns the column name for column at the zero-based index.
     *
     * The name may be used with Result.int_for() (and other *_for() variants).
     */
    public unowned string? get_column_name(int index) {
        return stmt.column_name(index);
    }

    /**
     * Returns the zero-based column index matching the column name.  Column names are
     * case-insensitive.
     *
     * Returns -1 if column name is unknown.
     */
    public int get_column_index(string name) {
        // prepare column map only if names requested
        if (column_map == null) {
            column_map = new Gee.HashMap<string, int>(Geary.String.stri_hash, Geary.String.stri_equal);

            int cols = stmt.column_count();
            for (int ctr = 0; ctr < cols; ctr++) {
                string? column_name = stmt.column_name(ctr);
                if (!String.is_empty(column_name))
                    column_map.set(column_name, ctr);
            }
        }

        return column_map.has_key(name) ? column_map.get(name) : -1;
    }

    /**
     * Executes the Statement and returns a Result object.  The Result starts pointing at the first
     * row in the result set.  If empty, Result.finished will be true.
     */
    public Result exec(Cancellable? cancellable = null) throws Error {
        Result results = new Result(this, cancellable);

        executed();

        return results;
    }

    /**
     * Executes the Statement and returns the last inserted rowid.  If this Statement is not
     * an INSERT, it will return the rowid of the last prior INSERT.
     *
     * See Connection.last_insert_rowid.
     */
    public int64 exec_insert(Cancellable? cancellable = null) throws Error {
        new Result(this, cancellable);
        int64 rowid = connection.last_insert_rowid;

        // fire signal after safely retrieving the rowid
        executed();

        return rowid;
    }

    /**
     * Executes the Statement and returns the number of rows modified by the operation.  This
     * Statement should be an INSERT, UPDATE, or DELETE, otherwise this will return the number
     * of modified rows from the last INSERT, UPDATE, or DELETE.
     *
     * See Connection.last_modified_rows.
     */
    public int exec_get_modified(Cancellable? cancellable = null) throws Error {
        new Result(this, cancellable);
        int modified = connection.last_modified_rows;

        // fire signal after safely retrieving the count
        executed();

        return modified;
    }

    /**
     * index is zero-based.
     */
    public Statement bind_double(int index, double d) throws DatabaseError {
        throw_on_error("Statement.bind_double", stmt.bind_double(index + 1, d));

        return this;
    }

    /**
     * index is zero-based.
     */
    public Statement bind_int(int index, int i) throws DatabaseError {
        throw_on_error("Statement.bind_int", stmt.bind_int(index + 1, i));

        return this;
    }

    /**
     * index is zero-based.
     */
    public Statement bind_uint(int index, uint u) throws DatabaseError {
        return bind_int64(index, (int64) u);
    }

    /**
     * index is zero-based.
     */
    public Statement bind_long(int index, long l) throws DatabaseError {
        return bind_int64(index, (int64) l);
    }

    /**
     * index is zero-based.
     */
    public Statement bind_int64(int index, int64 i64) throws DatabaseError {
        throw_on_error("Statement.bind_int64", stmt.bind_int64(index + 1, i64));

        return this;
    }

    /**
     * Binds a bool to the column.  A bool is stored as an integer, false == 0, true == 1.  Note
     * that fetching a bool via Result is more lenient; see Result.bool_at() and Result.bool_from().
     *
     * index is zero-based.
     */
    public Statement bind_bool(int index, bool b) throws DatabaseError {
        return bind_int(index, b ? 1 : 0);
    }

    /**
     * index is zero-based.
     *
     * This will bind the value to the column as an int64 unless it's INVALID_ROWID, in which case
     * the column is bound as NULL.  WARNING: This does *not* work in WHERE clauses. For WHERE, you
     * must use "field IS NULL".
     */
    public Statement bind_rowid(int index, int64 rowid) throws DatabaseError {
        return (rowid != Db.INVALID_ROWID) ? bind_int64(index, rowid) : bind_null(index);
    }

    /**
     * index is zero-based.
     *
     * WARNING: This does *not* work in WHERE clauses. For WHERE, you must use "field IS NULL".
     */
    public Statement bind_null(int index) throws DatabaseError {
        throw_on_error("Statement.bind_null", stmt.bind_null(index + 1));

        return this;
    }

    /**
     * index is zero-based.
     */
    public Statement bind_string(int index, string? s) throws DatabaseError {
        throw_on_error("Statement.bind_string", stmt.bind_text(index + 1, s));

        return this;
    }

    /**
     * Binds the string representation of a {@link Memory.Buffer} to the replacement value
     * in the {@link Statement}.
     *
     * If buffer supports {@link Memory.UnownedStringBuffer}, the unowned string will be used
     * to avoid a memory copy.  However, this means the Statement will hold a reference to the
     * buffer until the Statement is destroyed.
     *
     * index is zero-based.
     */
    public Statement bind_string_buffer(int index, Memory.Buffer? buffer) throws DatabaseError {
        if (buffer == null)
            return bind_string(index, null);

        Memory.UnownedStringBuffer? unowned_buffer = buffer as Memory.UnownedStringBuffer;
        if (unowned_buffer == null) {
            throw_on_error("Statement.bind_string_buffer", stmt.bind_text(index + 1, buffer.to_string()));

            return this;
        }

        // hold on to buffer for lifetime of Statement, SQLite's callback isn't enough for us to
        // selectively unref each Buffer as it's done with it
        held_buffers.add(unowned_buffer);

        // note use of _bind_text, which is for static and other strings with their own memory
        // management
        stmt._bind_text(index + 1, unowned_buffer.to_unowned_string());

        return this;
    }

    public override Statement? get_statement() {
        return this;
    }
}

