/// <reference path="websql.d.ts" />

interface DbState {
    database: Database;
    version: DOMString;
    transaction: (callback: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback) => void;
    readTransaction: (callback: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback) => void;
    changeVersion: (oldVersion: DOMString, newVersion: DOMString, callback?: SQLTransactionCallback, errorCallback?: SQLTransactionErrorCallback, successCallback?: SQLVoidCallback) => void
}

interface SqlQuery {
    sql: DOMString;
    args?: ObjectArray;
}

interface Results {
    data: any[];
    sqlResultSet: SQLResultSet;
}


(function () {
    var dbName: string = "websql-tests";

    // run the test
    createDatabase(dbName);


    function log(...msg: any[]) {
        if (console && console.log) {
            console.log.apply(console, msg);
        }
    }


    function createDatabase(name: string) {
        var tableOne = {
            name: "tableOne",
            columns: ["i_id", "s_name", "d_last"],
        };
        var tableTwo = {
            name: "tableTwo",
            columns: ["i_thing", "s_description", "d_decimal"],
        };

        openDatabase(dbName, "1", dbName, 1024 * 1024, function (dbState) {
            createTables(dbState, [
                { name: tableOne.name, columns: tableOne.columns },
                { name: tableTwo.name, columns: tableTwo.columns },
            ], function (res) {
                writeRecords(dbState, [
                    { name: tableOne.name, data: createMockRecord(tableOne.columns) },
                    { name: tableOne.name, data: createMockRecord(tableOne.columns) },
                    { name: tableTwo.name, data: createMockRecord(tableTwo.columns) },
                    { name: tableTwo.name, data: createMockRecord(tableTwo.columns) },
                    { name: tableTwo.name, data: createMockRecord(tableTwo.columns) },
                ], function (ress) {
                    log(ress);
                    readRecords(dbState, [tableOne.name, tableTwo.name], function (resss) {
                        log(resss);
                    });
                });
            });
        });
    }


    function initDb(db: Database): DbState {
        return {
            database: db,
            version: db.version,
            transaction: db.transaction.bind(db),
            readTransaction: db.readTransaction.bind(db),
            changeVersion: db.changeVersion.bind(db)
        };
    }


    function openDatabase(name: string, version: string, displayName: string, estimatedSize: number, callback: (dbState: DbState) => void) {
        estimatedSize = estimatedSize || (5 * 1024 * 1024);

        try {
            if (!window.openDatabase) {
                log("WebSQL not implemented");
            } else {
                // seems to synchronously open WebSQL, even though window.openDatabase is async
                var db: Database = window.openDatabase(name, version, displayName, estimatedSize);
                var dbState = initDb(db);
                if (db) {
                    callback(dbState);
                } else {
                    log("Failed to open database");
                }
            }
        } catch (ex) {
            log("Failed to open database '" + name + "': " + (JSON && JSON.stringify ? JSON.stringify(ex) : ex));
        }
    }


    function createTables(dbState: DbState, tableDefs: { name: string; columns: string[]; }[], callback: (rec: SQLResultSet[][]) => void) {
        var res: SQLResultSet[][] = [];
        var resI = 0, count = tableDefs.length;
        for (var i = 0; i < count; i++) {
            createTable(dbState, tableDefs[i].name, tableDefs[i].columns, function (rec: SQLResultSet[]) {
                res.push(rec);
                resI++;
                if (resI == count) {
                    callback(res);
                }
            });
        }
    }


    function createTable(dbState: DbState, name: string, columns: string[], callback: (rec: SQLResultSet[]) => void) {
        var sqls: SqlQuery[] = [];
        var colDefs = columns.map((col) => {
            var colType: string = null;
            var type = col.charAt(0);
            switch (type) {
                case 'i':
                case 'l':
                    colType = "INTEGER";
                    break;
                case 'f':
                case 'd':
                    colType = "REAL";
                    break;
                case 'b':
                case 'z':
                    colType = "INTEGER";
                    break;
                case 's':
                    colType = "TEXT";
                    break;
                default:
                    colType = "TEXT";
            }
            return col + " " + colType;
        });
        sqls.push({ sql: "CREATE TABLE IF NOT EXISTS " + name + " (" + colDefs.join(",") + ")", args: [] });
        execSqlStatements(dbState.transaction, sqls, callback);
    }


    /** column name prefixes
     * 'i', 'l' - integer
     * 'f', 'd' - float
     * 'b', 'z' - boolean
     * 's' - string
     * other - null
     */
    function createMockRecord(columns: string[]): any {
        var rec: { [id: string]: any } = {};
        for (var i = 0, size = columns.length; i < size; i++) {
            var col = columns[i];
            var type = col.charAt(0);
            switch (type) {
                case 'i':
                case 'l':
                    rec[col] = parseInt((Math.random() * 1000000).toString());
                    break;
                case 'f':
                case 'd':
                    rec[col] = (Math.random() - 0.5) * 2000000;
                    break;
                case 'b':
                case 'z':
                    rec[col] = Math.random() > 0.5;
                    break;
                case 's':
                    rec[col] = String.fromCharCode.apply(String, new Array(Math.round(Math.random() * 21)).join(".").split("").map((k) => 65 + Math.round(Math.random() * 25)));
                    break;
                default:
                    rec[col] = null;
            }
        }
        return rec;
    }


    function writeRecords(dbState: DbState, tableDefs: { name: string; data: any; }[], callback: (rec: SQLResultSet[][]) => void) {
        var res: SQLResultSet[][] = [];
        var resI = 0, count = tableDefs.length;
        for (var i = 0; i < count; i++) {
            writeRecord(dbState, tableDefs[i].name, tableDefs[i].data, function (rec: SQLResultSet[]) {
                res.push(rec);
                resI++;
                if (resI == count) {
                    callback(res);
                }
            });
        }
    }


    function writeRecord(dbState: DbState, tableName: string, record: any, callback: (rec: SQLResultSet[]) => void) {
        var props = Object.keys(record);
        var propCount = props.length;
        var args: ObjectArray = [];
        var sqlStatement = {
            sql: "INSERT INTO " + tableName + " values(" + (new Array(propCount + 1).join("?").split("").join(",")) + ")",
            args: args
        };
        for (var i = 0; i < propCount; i++) {
            var prop = props[i];
            args.push(record[prop]);
        }
        
        execSqlStatements(dbState.transaction, [sqlStatement], callback);
    }


    function readRecords(dbState: DbState, tableNames: string[], callback: (rec: Results[]) => void) {
        var res: Results[] = [];
        var resI = 0, count = tableNames.length;
        for (var i = 0; i < count; i++) {
            readRecord(dbState, tableNames[i], function (rec: Results) {
                res.push(rec);
                resI++;
                if (resI == count) {
                    callback(res);
                }
            });
        }
    }


    function readRecord(dbState: DbState, tableName: string, callback: (rec: Results) => void) {
        var args: ObjectArray = [];
        var sqlStatement = {
            sql: "SELECT * FROM " + tableName,
            args: args
        };

        execSqlStatements(dbState.transaction, [sqlStatement], function (rec: SQLResultSet[]) {
            var resObjs: Results[] = resultSetToResults(rec);
            callback(resObjs[0]);
        });
    }


    function resultSetToResults(results: SQLResultSet[]) {
        var resObjs: Results[] = [];
        for (var i = 0, count = results.length; i < count; i++) {
            var resAry: any[] = [];
            var sqlRsI = results[i];
            var resObj = {
                data: resAry,
                sqlResultSet: sqlRsI
            };
            resObjs.push(resObj);
            var rows = sqlRsI.rows;
            var rI = 0;
            for (var rI = 0, rCount = sqlRsI.rows.length; rI < rCount; rI++) {
                var r = rows.item(rI);
                resAry.push(r);
            }
        }
        return resObjs;
    }


    function compareRecords(rec1: any, rec2: any): boolean {
        var props = Object.keys(rec1);
        for (var i = 0, size = props.length; i < size; i++) {
            var prop = props[i];
            if (rec1[prop] !== rec2[prop]) {
                return false;
            }
        }
        return true;
    }


    function execSqlStatements<T, U>(xactMethod: (callback: SQLTransactionCallback) => void, sqlStatement: SqlQuery[],
            callback: (rs: SQLResultSet[]) => void) {
        var results: SQLResultSet[] = [];
        var sqlCount = sqlStatement.length;
        var resI = 0;

        function execCommand(xact: SQLTransaction, sql: DOMString, args: ObjectArray) {
            xact.executeSql(sql, args || [], function (xact: SQLTransaction, rs: SQLResultSet): void {
                results.push(rs);
                resI++;
                if (resI === sqlCount) {
                    callback(results);
                }
            }, function (transaction, err) {
                    log(err, transaction);
                return false;
            });
        }

        xactMethod(function (xact: SQLTransaction): void {
            for (var i = 0; i < sqlCount; i++) {
                execCommand(xact, sqlStatement[i].sql, sqlStatement[i].args);
            }
        });
    }

} ());
