// http://msdn.microsoft.com/en-us/library/hh524453 <- xml comment docu
// http://weblogs.asp.net/bleroy/archive/2007/04/23/the-format-for-javascript-doc-comments.aspx <- examples with some not working code

(function () {
    "use strict";

    var Queryable, Filterable, Sortable, Typed, Query,
        NS = WinJS.Namespace.define("Telerik.Data");

    Query = WinJS.Class.define(function Query_ctor(tableName, supressTableError) { // ctor
        this._fragments = [];
        this._orderByAdded = false;
        supressTableError = supressTableError || false;
        this._supressTableError = supressTableError;
        this._fragments.push({String: 'select * from ' + tableName + ' as x', Parameter: null});
    }, {
        addWhere: function (propertyName, operator, value) {
            this._fragments.push({
                String: ' where x.' + propertyName + ' ' + operator + NS.Database._stringifyParameterPlaceHolder(value),
                Parameter: NS.Database._stringifyParameterValue(value)
            });
        },
        addAnd: function (propertyName, operator, value) {
            this._fragments.push({
                String: ' and x.' + propertyName + ' ' + operator + NS.Database._stringifyParameterPlaceHolder(value),
                Parameter: NS.Database._stringifyParameterValue(value)
            });
        },
        addOr: function (propertyName, operator, value) {
            this._fragments.push({
                String: ' or x.' + propertyName + ' ' + operator + NS.Database._stringifyParameterPlaceHolder(value),
                Parameter: NS.Database._stringifyParameterValue(value)
            });
        },
        addSort: function (propertyName, direction) {
            if (this._orderByAdded ===  false) {
                this._fragments.push({
                    String: ' order by x.' + propertyName + ' ' + direction,
                    Parameter: null
                });
                this._orderByAdded = true;
            } else {
                this._fragments.push({
                    String: ' ,x.' + propertyName + ' ' + direction,
                    Parameter: null
                });
            }
        },
        execute: function (db) {
            var data, sqlString = '', parameter = [];
            while (this._fragments.length > 0) {
                data = this._fragments.pop();
                sqlString = data.String + sqlString;
                if (data.Parameter !== null) {
                    parameter.push(data.Parameter);
                }
            }
            parameter.reverse();
            return db._query(sqlString, parameter, this._supressTableError);
        }
    });

    // todo: where with brackets, joins? groupby?
    Queryable = WinJS.Class.define(function Queryable_ctor(db, tableName) { // ctor
        /// <field name="_query" type="Query"/>
        this._query = new Query(tableName, false);
        /// <field name="_db" type="NS.Database"/>
        this._db = db;
    }, {
        filter: function (propertyName, operator, value) {
            /// <returns type='Filterable' />
            this._query.addWhere(propertyName, operator, value);
            return new Filterable(this._db, this._query);
        },
        sort: function (propertyName, direction) {
            /// <returns type='Sortable' />
            direction = direction || '';
            this._query.addSort(propertyName, direction);
            return new Sortable(this._db, this._query);
        },
        execute: function () {
            /// <returns type='WinJS.Promise' />
            return this._query.execute(this._db);
        }
    });

    Filterable = WinJS.Class.define(function Filterable_ctor(db, query, supressTableError) { // ctor
        this._query = query;
        this._db = db;
        this._supressTableError = supressTableError || false;
    }, {
        and: function (propertyName, operator, value) {
            /// <returns type='Filterable' />
            this._query.addAnd(propertyName, operator, value);
            return this;
        },
        or: function (propertyName, operator, value) {
            /// <returns type='Filterable' />
            this._query.addOr(propertyName, operator, value);
            return this;
        },
        sort: function (propertyName, direction) {
            /// <returns type='Sortable' />
            direction = direction || '';
            this._query.addSort(propertyName, direction);
            return new Sortable(this._db, this._query, this._supressTableError);
        },
        execute: function () {
            /// <returns type='WinJS.Promise' />
            return this._query.execute(this._db);
        }
    });

    Sortable = WinJS.Class.define(function Sortable_ctor(db, query, supressTableError) { // ctor
        this._query = query;
        this._db = db;
        this._supressTableError = supressTableError || false;
    }, {
        sort: function (propertyName, direction) {
            /// <returns type='Sortable' />
            direction = direction || '';
            this._query.addSort(propertyName, direction);
            return this;
        },
        execute: function () {
            /// <returns type='WinJS.Promise' />
            return this._query.execute(this._db);
        }
    });

    Typed = WinJS.Class.define(function Typed_ctor(db, typeName) { // ctor
        this._typeName = typeName;
        this._db = db;
        this._query = new Query(typeName, true);
    }, {
        insert: function (data) {
            return this._db.insert(this._typeName, data);
        },
        update: function (data) {
            return this._db.update(this._typeName, data);
        },
        remove: function (data) {
            return this._db.remove(this._typeName, data);
        },
        sync: function () {
            /// <returns type='WinJS.Promise' />
            return this._db.sync();
        },
        filter: function (propertyName, operator, value) {
            /// <returns type='Filterable' />
            this._query.addWhere(propertyName, operator, value);
            return new Filterable(this._db, this._query);
        },
        sort: function (propertyName, direction) {
            /// <returns type='Sortable' />
            direction = direction || '';
            this._query.addSort(propertyName, direction);
            return new Sortable(this._db, this._query);
        },
        execute: function () {
            /// <returns type='WinJS.Promise' />
            return this._query.execute(this._db);
        },
        close: function () {
            return this._db.close();
        }
    });

    NS.Database = WinJS.Class.define(function Database_ctor(dbName, location) {
        var loc = TelerikData.Storage.DatabaseLocation.local;
        if (location === 'roaming') {
            loc = TelerikData.Storage.DatabaseLocation.roaming;
        }
        else if (location === 'temporary') {
            loc = TelerikData.Storage.DatabaseLocation.temporary;
        }
        this._innerDB = TelerikData.Storage.Database.getOrCreateDatabase(dbName, loc);
        this._innerDB.autoSchemaUpdates = true;
        this._serializeObject = true;
        this._toInsert = [];
        this._toUpdate = [];
        this._toDelete = [];
    }, {
        _createToStoreState: function(storeName, data, operation){
            var mc = new TelerikData.Storage.MetadataClass(storeName), // todo: cache
                state = new TelerikData.Storage.PersistentState(mc),
                mm,
                s,
                val,
                name,
                type,
                prop,
                ret,
                ignore;

            state.state = operation;

            if (this._serializeObject === true) {
                mm = mc.appendProperty('String', NS.Database._serializedColumnName, true, false, false);
                s = new TelerikData.Storage.PersistentValue(mm, true, JSON.stringify(data));
                state.values.append(s);
            }

            for (prop in data) {
                if (data.hasOwnProperty(prop)) { // only my props
                    val = data[prop];
                    name = prop;
                    type = NS.Database._classof(val);
                    ignore = false;
                    switch (type) {
                        case 'String':
                        case 'Number':
                        case 'Boolean':
                            break;
                        case 'Date':
                            type = 'DateTime';
                            val = val.toJSON();
                            break;
                        case 'Null':
                            break;
                        default:
                            ignore = true;
                    }
                    if (ignore === false) {
                        if (name === 'id') {
                            mm = mc.appendProperty(type, name, true, true, false);
                        } else {
                            mm = mc.appendProperty(type, name, true, false, false);
                        }
                        s = new TelerikData.Storage.PersistentValue(mm, true, val);
                        state.values.append(s);
                    }
                }
            }
            return state;
        },
        _query: function (sql, parameter, supressTableError) {
            return NS.Database._executeGet(this._innerDB, sql, parameter).then(function (result) {
                if (result.hasError) {
                    if (supressTableError && result.result.indexOf('no such table:') > 0) {
                        return WinJS.Promise.as([]);
                    }
                    return WinJS.Promise.wrapError({ message: result.result });
                }
                return WinJS.Promise.as(result.result);
            });
        },
        queryScalar: function (sqlStatement, parameter) {
            parameter = parameter || [];
            /// <returns type='WinJS.Promise' />
            return NS.Database._executeGet(this._innerDB, sqlStatement, parameter).then(function (result) {
                var first,
                    prop,
                    val;
                if (result.hasError) {
                    return WinJS.Promise.wrapError({ message: result.result });
                }
                first = result.result[0];
                for (prop in first) {
                    if (first.hasOwnProperty(prop)) {
                        val = first[prop];
                        break;
                    }
                }
                return WinJS.Promise.as(val);
            });
        },
        query: function (sqlStatement, parameter) {
            parameter = parameter || [];
            /// <returns type='WinJS.Promise' />
            return this._query(sqlStatement, parameter, false);
        },
        insert: function (storeName, data) {
            /// <summary>Adds data to the actual store queue. To execute the real insert, sync() has to be called.</summary>
            /// <param name='storeName' type='String'>The name of the bag this data should be stored in. It can be used like a TableName.</param>
            /// <param name='data' type='Object'>The data that should be stored.</param>
            this._toInsert.push({ type: storeName, value: data });
        },
        update: function (storeName, data) {
            this._toUpdate.push({ type: storeName, value: data });
        },
        remove: function (storeName, data) {
            this._toDelete.push({ type: storeName, value: data });
        },
        get: function (storeName) {
            /// <returns type='Queryable' />
            return new Queryable(this, storeName);
        },
        close: function () {
            this._innerDB.close();
        },
        use: function (storeName) {
            /// <summary>Returns a typed database instance that deal with one explicitly named type of data.</summary>
            /// <param name='storeName' type='String'>The name of the bag the returned instance deals with. It can be used like a TableName.</param>
            /// <returns type='Typed'>The typed database instance that handles one specific type of data.</returns>
            var lastType = new Typed(this, storeName);
            this[storeName] = lastType;
            return lastType;
        },
        setSerializeObject: function (value) {
            value = value || false;
            this._serializeObject = value;
        },
        getSerializeObject: function () { return this._serializeObject; },
        sync: function () {
            /// <returns type='WinJS.Promise' />
            var i,
                state,
                data,
                result,
                states = [];
            while(this._toInsert.length > 0){
                data = this._toInsert.pop();
                state = this._createToStoreState(data.type, data.value, TelerikData.Storage.DataState.inserted);
                states.push(state);
            }
            while (this._toUpdate.length > 0) {
                data = this._toUpdate.pop();
                state = this._createToStoreState(data.type, data.value, TelerikData.Storage.DataState.modified);
                states.push(state);
            }
            while (this._toDelete.length > 0) {
                data = this._toDelete.pop();
                state = this._createToStoreState(data.type, data.value, TelerikData.Storage.DataState.deleted);
                states.push(state);
            }
            if (states.length > 0) {
                return this._innerDB.persistChangesAsync(states).then(function (result) {
                    if (result.hasError === true) { // todo error handling
                        return WinJS.Promise.wrapError({ message: result.message + " [" + result.storageError + "]" });
                    }
                });
            }
            return WinJS.Promise.as();
        }
    }, {
        _serializedColumnName: '$JSON$',
        _classof: function (o) {
            if (o === null) { return "Null"; }
            if (o === undefined) { return "Undefined"; }
            return Object.prototype.toString.call(o).slice(8, -1);
        },
        _stringifyValue: function (value) {
            var type = NS.Database._classof(value);
            if (type === 'Date') {
                return "datetime('" + value.toJSON() + "')";
            }
            return value.toString();
        },
        _stringifyParameterPlaceHolder: function (value) {
            var type = NS.Database._classof(value);
            if (type === 'Date') {
                return "datetime(?)";
            }
            return "?";
        },
        _stringifyParameterValue: function (value) {
            var type = NS.Database._classof(value);
            if (type === 'Date') {
                return value.toJSON();
            }
            return value;
        },
        _executeGet: function (innerDB, sql, parameter) {
            return innerDB.readDataAsync(sql, parameter, null).then(function (result) {
                var results,
                    i,
                    data,
                    obj,
                    j,
                    val,
                    name,
                    prop,
                    type,
                    date;
                if (result.hasError === false) {
                    results = [];
                    // produce data from values and return as array
                    for (i = 0; i < result.result.length; i = i + 1) {
                        data = result.result[i];
                        obj = {};
                        for (j = 0; j < data.values.length; j = j + 1) {
                            val = data.values[j].value;
                            name = data.values[j].metadataMember.propertyName;
                            if (name === NS.Database._serializedColumnName) {
                                obj = JSON.parse(val);
                                break;
                            }
                            obj[name] = val;
                        }

                        for (prop in obj) { // try to convert strings that look like dates into dates.
                            if (obj.hasOwnProperty(prop)) {
                                val = obj[prop];
                                type = NS.Database._classof(val);
                                if (type === 'String' && val.length === 24 && val[23] === 'Z') {
                                    date = new Date(val);
                                    if (isNaN(date.valueOf()) === false) {
                                        obj[prop] = date;
                                    }
                                }
                            }
                        }
                        results.push(obj);
                    }
                    return { hasError: false, result: results };
                }
                return { hasError: true, result: result.message + " [" + result.storageError + "]" };
            });
        },
        open: function (databaseName, databaseLocation) {
            /// <summary>Returns a Database instance. Entry point ot the Telerik persistence API.</summary>
            /// <param name='databaseName' type='String'>The name of the database. The name is used to create the database file. Equal names open the same database.</param>
            /// <param name='databaseLocation' type='String'>Location where the database should be stored. Allowed values are 'local', 'roaming' or 'temporary'. If nothing is specified, 'local' is assumed.</param>
            /// <returns type='NS.Database' />
            databaseLocation = databaseLocation || 'local';
            return new NS.Database(databaseName, databaseLocation);
        }
    });
}());