/**
 *
 * A SecureSQL database transaction. See `Ext.space.securesql.Database.createTransaction`
 *
 * Queries added to a transaction will be executed as part of a single transaction.
 * Each query returns a promise with the data for the query. The `execute` statement
 * returns a promise that will resolve when the transaction is complete.
 *
 * If any of the queries generate an error then the transaction will be rolled back
 * and any data mutations will not be applied to the database.
 
 * The promise of the query that failed will reject. And the promise returned by
 * `execute` will also reject.
 *
 *      var tx = testDatabase.createTransaction();
 *
 *      tx.query("select * from test where id = ?",[2]).then(function(rows){
 *          //
 *      }).error(function(e){
 *          Ext.space.Logger.error("Error with query?",e);
 *      });
 *
 *      tx.query("select * from test where id = ?",[1]).then(function(rows){
 *          //
 *      }).error(function(e){
 *          Ext.space.Logger.error("Error with query?",e);
 *      });
 *
 *      tx.execute().then(function(){
 *          //
 *      }).error(function(e){
 *          Ext.space.Logger.error("Error with TX?",e);
 *      });
 */
Ext.define("Ext.space.securesql.Transaction", {
 
    /**
     * @private
     */
    database: null,
 
    /**
     * @private
     */
    queries: null,
 
    /**
     * @private
     */
    id: null,
 
    /**
     * @private
     */
    began: false,
 
    /**
     * @private
     */
    skipDatabaseLoaded: false,
 
    /**
     * @private
     */
    executePromise: null,
 
    constructor: function(database, options) {
        this.database = database;
        this.queries = [];
        // allow for moving ahead even if the database hasn't declared itself to 
        // be loaded (because in order to load it, we have to create a version table, 
        // so we need a transaction first in an unfortunate chicken and egg scenario) 
        if (options && options.immediate) {
            this.skipDatabaseLoaded = true;
        }
    },
 
    /**
     * Add a query this transaction.
     *
     * The returned promise will provide an array of records that match the query.
     * Each array value will be an object; the object properties will match field
     * names in the query. If the query uses column aliases then the alias will be
     * returned as the field name.
     *
     *      testDatabase.query("select id, name from test where id = ?",[1]).then(function(rows) {
     *          var row = rows[0];
     *          Ext.space.Logger.log("record ",row.id, row.name);
     *      }).error(function(e) {
     *          Ext.space.Logger.error("Error with query?",e);
     *      });
     *
     *      testDatabase.query("select count(*) as countOfTest from test").then(function(rows) {
     *          var row = rows[0];
     *          Ext.space.Logger.log("Count of rows in test ", row.countOfTest);
     *      }).error(function(e) {
     *          Ext.space.Logger.error("Error with query?",e);
     *      });
     *
     * @param {String} query The SQL statement
     * @param {Array} fields The fields of the statement.
     * @return {Ext.space.Promise} promise that will resolve when the query completes
     */
    query: function(query, fields){
        if (this.began) {
            throw new Error("Ext.space.securesql.Transaction#query: Cannot add queries to a transaction that has already begun.");
        }
        var me = this;
        return this.sql(query, fields).then(function(rs){
            return me.database.convertResults(rs);
        });
    },
 
    /**
     * Insert a single record into a table. Pass this function an array or object:
     * - If an array, then the order of elements in the array must match the order
     *   of the fields.
     * - If an object, then the field names will be extracted from the properties
     *
     * Array Example:
     *
     *      tx.insert('test', ["id", "name"], [1, "one"]).then(function(insertId) {
     *          //done with insert
     *          //insertId will be 1 or the auto increment value of the id of the table if the table has one.
     *      }).error(function(e) {
     *          Ext.space.Logger.error("Error with insert",e);
     *      });
     *
     *  would be equivalent to
     *
     *      INSERT INTO TABLE test (id, name) VALUES (1, 'one');
     *
     *  Object Example:
     *
     *      tx.insert('test', ["id", "name"], {id: 2, name:"two"}).then(function() {
     *          //done with insert
     *          //insertId will be 2 or the auto increment value of the id of the table if the table has one.
     *      }).error(function(e) {
     *          Ext.space.Logger.error("Error with insert",e);
     *      });
     *
     *  would be equivalent to
     *
     *      INSERT INTO TABLE test (id, name) VALUES (2, 'two');
     *
     * @param {String} table the name of the table the record will be inserted into
     * @param {Array} fields an array of field names to insert.
     * @param {Array|Object} values  either an array or object to insert.
     * @return  {Ext.space.Promise} promise that will resolve when the insert is complete.
     */
    insert: function(table, fields, values){
        if (this.began) {
            throw new Error("Ext.space.securesql.Transaction#insert: Cannot add queries to a transaction that has already begun.");
        }
        var query = this.database._buildInsertStatement(table, fields, values);
        return this.sql(query.statement, query.values).then(function(rs){
            return rs.insertId;
        });
    },
 
    /**
     * Execute the queries in the transaction.
     *
     * @return  {Ext.space.Promise} Promise that resolves if all the queries are successful
     *                        or rejects if any query fails.
     */
    execute: function() {
        if (!this.executePromise) {
            this.executePromise = new Ext.space.Promise();
            if (this.skipDatabaseLoaded) {
                this.run();
            } else {
                this.database.queueTransaction(this, this.executePromise);
            }
        }
        return this.executePromise;
    },
 
 
 
    // 
    // private native bridge interface methods follow 
    // 
 
    /**
     * Add a raw SQL statement to this transaction.
     *
     * This differs from `query()` in that it will not do any processing on the
     * results returned by the query.
     *
     * @private
     * @param {String} sql The SQL statement to execute. This is required.
     * @param {Array} args The arguments array to bind each "?" placeholder in the Sql statement. This is optional.
     * @return {Ext.space.Promise} The promise that is resolved when the SQL statement has finished executing.
     */
    sql: function(sql, args) {
        if (this.began) {
            throw new Error("Ext.space.securesql.Transaction#sql: Cannot add SQL queries to a transaction that has already begun.");
        }
        var queryPromise = new Ext.space.Promise();
        this.queries.push({statement: sql, values: args, promise: queryPromise});
        return queryPromise;
    },
 
    /**
     * Create the transaction in the native client.
     *
     * @param {Object} config Transaction options
     * @param {Boolean} config.readOnly `true` if this is a read-only transaction
     * @return {Ext.space.Promise} Promise that resolves when the transaction is created.
     */
    create: function(config) {
        config = config || {};
        var promise = new Ext.space.Promise(),
            me = this;
 
        Ext.space.Communicator.send({
            command: "Sqlite#createTransaction",
            databaseId: me.database.id,
            readOnly: config.readOnly,
            queue: true,
            callbacks: {
                success: function(id) {
                    me.id = id;
                    promise.fulfill();
                },
                failure: function(e) {
                    promise.reject(e);
                }
            }
        });
 
        return promise;
    },
 
    /**
     * Begin executing the transaction.
     *
     * @private
     * @return {Ext.space.Promise} Promise that resolves when the transaction is ready to begin executing SQL queries.
     */
    begin: function() {
        var me = this,
            promise = new Ext.space.Promise();
 
        if (!me.began) {
            me.began = true;
            Ext.space.Communicator.send({
                command: "Sqlite#beginTransaction",
                transactionId: me.id,
                callbacks: {
                    success: function() {
                        promise.fulfill();
                    },
                    failure: function(e) {
                        promise.reject(e);
                        me.rollback(e);
                    }
                }
            });
        } else {
            promise.reject("Ext.space.securesql.Transaction#begin: Cannot begin a transaction that has already been started");
        }
 
        return promise;
    },
 
    /**
     * Execute the SQL statements in this transaction in sequence.
     *
     * @private
     */
    executeStatements: function() {
        var me = this;
 
        function next(query) {
            if (!query) {
                me.commit();
                return;
            }
 
            Ext.space.Communicator.send({
                command: "Sqlite#executeStatement",
                transactionId: me.id,
                sqlStatement: query.statement,
                arguments: JSON.stringify(query.values),
                callbacks: {
                    success: function(rs) {
                        // Protect against a DB deadlock in case promise handler throws an exception. 
                        try {
                            query.promise.fulfill(rs);
                            next(me.queries.shift());
                        } catch(e) {
                            try {
                                query.promise.reject(e);
                            } catch(ex) {
                                // no-op 
                            }
                            me.rollback(e);
                        }
                    },
                    failure: function(e) {
                        // Protect against a DB deadlock in case promise handler throws an exception. 
                        try {
                            query.promise.reject(e);
                        } catch(ex) {
                            // no-op 
                        }
                        me.rollback(e);
                    }
                }
            });
        }
 
        next(me.queries.shift());
    },
 
    /**
     * Commit the transaction.
     *
     * @private
     */
    commit: function() {
        var me = this;
        Ext.space.Communicator.send({
            command: "Sqlite#commitTransaction",
            transactionId: me.id,
            callbacks: {
                success: function() {
                    me.executePromise.fulfill();
                },
                failure: function() {
                    me.rollback();
                }
            }
        });
    },
 
    /**
     * Roll back the transaction
     *
     * @private
     */
    rollback: function(e) {
        Ext.space.Communicator.send({
            command: "Sqlite#rollbackTransaction",
            transactionId: this.id
        });
 
        this.executePromise.reject(e);
    },
 
    /**
     * Batch executes all queued Sql statements inside a transaction, handling errors and commit/rollback automatically.
     *
     * @private
     * @return {Ext.space.Promise}
     * The promise that is resolved when the transaction has been committed or rolled back.
     */
    run: function() {
        var start,
            create = this.create.bind(this),
            begin = this.begin.bind(this),
            executeStatements = this.executeStatements.bind(this);
 
        if (this.skipDatabaseLoaded) {
            start = create();
        } else {
            start = this.database.loaded.then(function() { return create(); });
        }
        return start.then(begin).then(executeStatements);
    }
});