/* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
 * license.  See http://svn.openlayers.org/trunk/openlayers/license.txt for the
 * full text of the license. */

/**
 * @requires Gears/gears_init.js
 * @requires OpenLayers/Protocol/SQL.js
 * @requires OpenLayers/Format/JSON.js
 * @requires OpenLayers/Format/WKT.js
 */

/**
 * Class: OpenLayers.Protocol.SQL.Gears
 * This Protocol stores feature in the browser via the Gears Database module 
 * <http://code.google.com/apis/gears/api_database.html>.
 *
 * The main advantage is that all the read, create, update and delete operations 
 * can be done offline.
 *
 * Inherits from:
 *  - <OpenLayers.Protocol.SQL>
 */
OpenLayers.Protocol.SQL.Gears = OpenLayers.Class(OpenLayers.Protocol.SQL, {

    /**
     * Property: FID_PREFIX
     * {String}
     */
    FID_PREFIX: '__gears_fid__',

    /**
     * Property: NULL_GEOMETRY
     * {String}
     */
    NULL_GEOMETRY: '__gears_null_geometry__',

    /**
     * Property: NULL_FEATURE_STATE
     * {String}
     */
    NULL_FEATURE_STATE: '__gears_null_feature_state__',

    /**
     * Property: jsonParser
     * {<OpenLayers.Format.JSON>}
     */
    jsonParser: null,

    /**
     * Property: wktParser
     * {<OpenLayers.Format.WKT>}
     */
    wktParser: null,

    /**
     * Property: fidRegExp
     * {RegExp} Regular expression to know whether a feature was
     *      created in offline mode.
     */
    fidRegExp: null,

    /**
     * Property: saveFeatureState
     * {Boolean} Whether to save the feature state (<OpenLayers.State>)
     *      into the database, defaults to true.
     */    
    saveFeatureState: true,

    /**
     * Property: typeOfFid
     * {String} The type of the feature identifier, either "number" or
     *      "string", defaults to "string".
     */
    typeOfFid: "string",

    /**
     * Property: db
     * {GearsDatabase}
     */
    db: null,

    /**
     * Constructor: OpenLayers.Protocol.SQL.Gears
     */
    initialize: function(options) {
        if (!this.supported()) {
            return;
        }
        OpenLayers.Protocol.SQL.prototype.initialize.apply(this, [options]);
        this.jsonParser = new OpenLayers.Format.JSON();
        this.wktParser = new OpenLayers.Format.WKT();

        this.fidRegExp = new RegExp('^' + this.FID_PREFIX);
        this.initializeDatabase();

        
    },

    /**
     * Method: initializeDatabase
     */
    initializeDatabase: function() {
        this.db = google.gears.factory.create('beta.database');
        this.db.open(this.databaseName);
        this.db.execute(
            "CREATE TABLE IF NOT EXISTS " + this.tableName +
            " (fid TEXT UNIQUE, geometry TEXT, properties TEXT," +
            "  state TEXT)");
   },

    /**
     * APIMethod: destroy
     * Clean up the protocol.
     */
    destroy: function() {
        this.db.close();
        this.db = null;

        this.jsonParser = null;
        this.wktParser = null;

        OpenLayers.Protocol.SQL.prototype.destroy.apply(this);
    },

    /**
     * APIMethod: supported
     * Determine whether a browser supports Gears
     *
     * Returns:
     * {Boolean} The browser supports Gears
     */
    supported: function() {
        return !!(window.google && google.gears);
    },

    /**
     * APIMethod: read
     * Read all features from the database and return a
     * <OpenLayers.Protocol.Response> instance. If the options parameter
     * contains a callback attribute, the function is called with the response
     * as a parameter.
     *
     * Parameters:
     * options - {Object} Optional object for configuring the request; it
     *      can have the {Boolean} property "noFeatureStateReset" which
     *      specifies if the state of features read from the Gears
     *      database must be reset to null, if "noFeatureStateReset"
     *      is undefined or false then each feature's state is reset
     *      to null, if "noFeatureStateReset" is true the feature state
     *      is preserved.
     *
     * Returns:
     * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
     *      object.
     */
    read: function(options) {
        OpenLayers.Protocol.prototype.read.apply(this, arguments);
        options = OpenLayers.Util.applyDefaults(options, this.options);

        var feature, features = [];
        var rs = this.db.execute("SELECT * FROM " + this.tableName);
        while (rs.isValidRow()) {
            feature = this.unfreezeFeature(rs);
            if (this.evaluateFilter(feature, options.filter)) {
                if (!options.noFeatureStateReset) {
                    feature.state = null;
                }
                features.push(feature);
            }
            rs.next();
        }
        rs.close();

        var resp = new OpenLayers.Protocol.Response({
            code: OpenLayers.Protocol.Response.SUCCESS,
            requestType: "read",
            features: features
        });

        if (options && options.callback) {
            options.callback.call(options.scope, resp);
        }

        return resp;
    },

    /**
     * Method: unfreezeFeature
     *
     * Parameters:
     * row - {ResultSet}
     *
     * Returns:
     * {<OpenLayers.Feature.Vector>}
     */
    unfreezeFeature: function(row) {
        var feature;
        var wkt = row.fieldByName('geometry');
        if (wkt == this.NULL_GEOMETRY) {
            feature = new OpenLayers.Feature.Vector();
        } else {
            feature = this.wktParser.read(wkt);
        }

        feature.attributes = this.jsonParser.read(
            row.fieldByName('properties'));

        feature.fid = this.extractFidFromField(row.fieldByName('fid'));

        var state = row.fieldByName('state');
        if (state == this.NULL_FEATURE_STATE) {
            state = null;
        }
        feature.state = state;

        return feature;
    },

    /**
     * Method: extractFidFromField
     *
     * Parameters:
     * field - {String}
     *
     * Returns
     * {String} or {Number} The fid.
     */
    extractFidFromField: function(field) {
        if (!field.match(this.fidRegExp) && this.typeOfFid == "number") {
            field = parseFloat(field);
        }
        return field;
    },

    /**
     * APIMethod: create
     * Create new features into the database.
     *
     * Parameters:
     * features - {Array({<OpenLayers.Feature.Vector>})} or
     *            {<OpenLayers.Feature.Vector>} The features to create in
     *            the database.
     * options - {Object} Optional object for configuring the request.
     *
     * Returns:
     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
     *          object.
     */
    create: function(features, options) {
        options = OpenLayers.Util.applyDefaults(options, this.options);

        var resp = this.createOrUpdate(features);
        resp.requestType = "create";

        if (options && options.callback) {
            options.callback.call(options.scope, resp);
        }

        return resp;
    },

    /**
     * APIMethod: update
     * Construct a request updating modified feature.
     *
     * Parameters:
     * features - {Array({<OpenLayers.Feature.Vector>})} or
     *            {<OpenLayers.Feature.Vector>} The features to update in
     *            the database.
     * options - {Object} Optional object for configuring the request.
     *
     * Returns:
     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
     *          object.
     */
    update: function(features, options) {
        options = OpenLayers.Util.applyDefaults(options, this.options);

        var resp = this.createOrUpdate(features);
        resp.requestType = "update";

        if (options && options.callback) {
            options.callback.call(options.scope, resp);
        }

        return resp;
    },

    /**
     * Method: createOrUpdate
     * Construct a request for updating or creating features in the
     * database.
     *
     * Parameters:
     * features - {Array({<OpenLayers.Feature.Vector>})} or
     *      {<OpenLayers.Feature.Vector>} The feature to create or update
     *      in the database.
     *
     * Returns:
     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
     *          object.
     */
    createOrUpdate: function(features) {
        if (!(features instanceof Array)) {
            features = [features];
        }

        var i, len = features.length, feature;
        var insertedFeatures = new Array(len);
 
        for (i = 0; i < len; i++) {
            feature = features[i];
            var params = this.freezeFeature(feature);
            this.db.execute(
                "REPLACE INTO " + this.tableName + 
                " (fid, geometry, properties, state)" + 
                " VALUES (?, ?, ?, ?)",
                params);

            var clone = feature.clone();
            clone.fid = this.extractFidFromField(params[0]);
            insertedFeatures[i] = clone;
        }

        return new OpenLayers.Protocol.Response({
            code: OpenLayers.Protocol.Response.SUCCESS,
            features: insertedFeatures,
            reqFeatures: features
        });
    },

    /**
     * Method: freezeFeature
     *
     * Parameters:
     * feature - {<OpenLayers.Feature.Vector>}
     * state - {String} The feature state to store in the database.
     *
     * Returns:
     * {Array}
     */
    freezeFeature: function(feature) {
        // 2 notes:
        // - fid might not be a string
        // - getFeatureStateForFreeze needs the feature fid to it's stored
        //   in the feature here
        feature.fid = feature.fid != null ?
            "" + feature.fid : OpenLayers.Util.createUniqueID(this.FID_PREFIX);

        var geometry = feature.geometry != null ?
            feature.geometry.toString() : this.NULL_GEOMETRY;

        var properties = this.jsonParser.write(feature.attributes);

        var state = this.getFeatureStateForFreeze(feature);

        return [feature.fid, geometry, properties, state];
    },

    /**
     * Method: getFeatureStateForFreeze
     * Get the state of the feature to store into the database.
     *
     * Parameters:
     * feature - {<OpenLayers.Feature.Vector>} The feature.
     *
     * Returns
     * {String} The state
     */
    getFeatureStateForFreeze: function(feature) {
        var state;
        if (!this.saveFeatureState) {
            state = this.NULL_FEATURE_STATE;
        } else if (this.createdOffline(feature)) {
            // if the feature was created in offline mode, its
            // state must remain INSERT
            state = OpenLayers.State.INSERT;
        } else {
            state = feature.state;
        }
        return state;
    },

    /**
     * APIMethod: delete
     * Delete features from the database.
     *
     * Parameters:
     * features - {Array({<OpenLayers.Feature.Vector>})} or
     *            {<OpenLayers.Feature.Vector>}
     * options - {Object} Optional object for configuring the request.
     *       This object is modified and should not be reused.
     *
     * Returns:
     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
     *          object.
     */
    "delete": function(features, options) {
        if (!(features instanceof Array)) {
            features = [features];
        }

        options = OpenLayers.Util.applyDefaults(options, this.options);

        var i, len, feature;
        for (i = 0, len = features.length; i < len; i++) {
            feature = features[i];

            // if saveFeatureState is set to true and if the feature wasn't created
            // in offline mode we don't delete it in the database but just update 
            // it state column
            if (this.saveFeatureState && !this.createdOffline(feature)) {
                var toDelete = feature.clone();
                toDelete.fid = feature.fid;
                if (toDelete.geometry) {
                    toDelete.geometry.destroy();
                    toDelete.geometry = null;
                }
                toDelete.state = feature.state;
                this.createOrUpdate(toDelete);
            } else {
                this.db.execute(
                    "DELETE FROM " + this.tableName +
                    " WHERE fid = ?", [feature.fid]);
            }
        }

        var resp = new OpenLayers.Protocol.Response({
            code: OpenLayers.Protocol.Response.SUCCESS,
            requestType: "delete",
            reqFeatures: features
        });

        if (options && options.callback) {
            options.callback.call(options.scope, resp);
        }

        return resp;
    },

    /**
     * Method: createdOffline
     * Returns true if the feature had a feature id when it was created in
     *      the Gears database, false otherwise; this is determined by
     *      checking the form of the feature's fid value.
     *
     * Parameters:
     * feature - {<OpenLayers.Feature.Vector>}
     *
     * Returns:
     * {Boolean}
     */
    createdOffline: function(feature) {
        return (typeof feature.fid == "string" &&
                !!(feature.fid.match(this.fidRegExp)));
    },

    /**
     * APIMethod: commit
     * Go over the features and for each take action
     * based on the feature state. Possible actions are create,
     * update and delete.
     *
     * Parameters:
     * features - {Array({<OpenLayers.Feature.Vector>})}
     * options - {Object} Object whose possible keys are "create", "update",
     *      "delete", "callback" and "scope", the values referenced by the
     *      first three are objects as passed to the "create", "update", and
     *      "delete" methods, the value referenced by the "callback" key is
     *      a function which is called when the commit operation is complete
     *      using the scope referenced by the "scope" key.
     *
     * Returns:
     * {Array({<OpenLayers.Protocol.Response>})} An array of
     *       <OpenLayers.Protocol.Response> objects, one per request made
     *       to the database.
     */
    commit: function(features, options) {
        var opt, resp = [], nRequests = 0, nResponses = 0;

        function callback(resp) {
            if (++nResponses < nRequests) {
                resp.last = false;
            }
            this.callUserCallback(options, resp);
        }

        var feature, toCreate = [], toUpdate = [], toDelete = [];
        for (var i = features.length - 1; i >= 0; i--) {
            feature = features[i];
            switch (feature.state) {
            case OpenLayers.State.INSERT:
                toCreate.push(feature);
                break;
            case OpenLayers.State.UPDATE:
                toUpdate.push(feature);
                break;
            case OpenLayers.State.DELETE:
                toDelete.push(feature);
                break;
            }
        }
        if (toCreate.length > 0) {
            nRequests++;
            opt = OpenLayers.Util.applyDefaults(
                {"callback": callback, "scope": this},
                options.create
            );
            resp.push(this.create(toCreate, opt));
        }
        if (toUpdate.length > 0) {
            nRequests++;
            opt = OpenLayers.Util.applyDefaults(
                {"callback": callback, "scope": this},
                options.update
            );
            resp.push(this.update(toUpdate, opt));
        }
        if (toDelete.length > 0) {
            nRequests++;
            opt = OpenLayers.Util.applyDefaults(
                {"callback": callback, "scope": this},
                options["delete"]
            );
            resp.push(this["delete"](toDelete, opt));
        }

        return resp;
    },

    /**
     * Method: clear
     * Removes all rows of the table.
     */
    clear: function() {
        this.db.execute("DELETE FROM " + this.tableName);
    },

    /**
     * Method: callUserCallback
     * This method is called from within commit each time a request is made
     * to the database, it is responsible for calling the user-supplied
     * callbacks.
     *
     * Parameters:
     * options - {Object} The map of options passed to the commit call.
     * resp - {<OpenLayers.Protocol.Response>}
     */
    callUserCallback: function(options, resp) {
        var opt = options[resp.requestType];
        if (opt && opt.callback) {
            opt.callback.call(opt.scope, resp);
        }
        if (resp.last && options.callback) {
            options.callback.call(options.scope);
        }
    },

    CLASS_NAME: "OpenLayers.Protocol.SQL.Gears"
});
