(function($) {

    /// JSON internal data format
    ///
    /// {
    ///     type: ''        //name of the data
    ///     keyfield: ''    //field being used as the key
    ///     timestamp:      //when the data was requested
    ///     count:          //total count of data (regardless of how much was requested)
    ///     sortfield:      //default sort field
    ///     sortorder:      //default sort order
    ///     data: {}        //actual data list
    /// }

    $.fn.vDatasource = function(options) {

        if (this.length > 1) {
            alert("vDatasource can only be applied to a single DOM element");
            return false;
        }

        //an element can only have one instance of a datasource
        return this.each(function() {
            var element = $(this);

            if (options.name == '') {
                alert("vRenderList needs a unique name");
                return;
            }

            var lookfor = "vDatasource-" + options.name;

            // Return early if this element already has a plugin instance
            if (element.data(lookfor)) {
                vLog('vDatasource', 'already instantiated!');
                return;
            }
            // pass options to plugin constructor
            var vrs = new vDatasource(this, options);

            // Store plugin object in this element's data
            element.data(lookfor, vrs);

            $(document).trigger('vnx-reg-vdatasource', vrs);

            /*
            $(this).bind('vds-select', function(event, selectparameters) {
            vLog('vdatasource', "event:select");
            vrs.selectparameters(selectparameters);
            });*/

        });
    };


    var vDatasource = function(element, options) {
        element = $(element);
        var obj = this;
        var internaldata = null;
        var internaldatacache = {};     //stores data locally
        var panellist = [];
        var config = {
            name: '',
            keyfield: 'id',
            dataurl: '/ajax/data.aspx',
            selectonnullparameters: false,
            passallparameters: false,
            selectmethod: 'Select',
            deletemethod: 'Delete',
            insertmethod: 'Insert',
            updatemethod: 'Update',
            insertparameters: {},
            updateparameters: {},
            selectparameters: {},
            deleteparameters: {},
            selectparameterstring: "",
            defaultsortfield: 'KeyID',
            defaultsortorder: 'ASC',
            data: null						//allows for direct data
        };
        $.extend(config, options); //extend them by any passed options
        var Us = this;

        //first time around
        config.selectparameterstring = JSON.stringify({ 'plist': config.selectparameters });

        this.applyto = function() {
            return config.name;
        }

        //we get to initialise all the elements with this particular tooltip class
        this.initialisedatasource = function() {
            if (config.data != null) {
                vLog('vDatasource', 'data is passed direct, parsing JSON');
                internaldata = this.jsonparse(config.data);
                internaldatacache[config.selectparameters] = internaldata;
            }
            vLog('vDatasource', 'Initialised ' + Us.applyto());
        }

        //register a nexus so it can be notified up updates
        this.registerpanel = function(thispanel) {
            //is it regisistred already
            if ($.inArray(thispanel, panellist)==-1) {
                panellist.push(thispanel);
                vLog('vDatasource', 'Registered a Panel : ' + thispanel.applyto() + " with " + Us.applyto());
            } else {
                vLog('vDatasource', 'Panel already registered : ' + thispanel.applyto() + " with " + Us.applyto());
            }
        }

        //change the select parameters
        this.selectparameters = function(parameters) {
            if (config.selectparameters != parameters) {
                vLog('vDatasource', 'Changed select parameters for ' + config.name);
                //change them (but doesn't actually perform a select yet
                config.selectparameters = parameters;
                config.selectparameterstring = JSON.stringify({ 'plist': config.selectparameters });
                //now refresh any connected nexus which in turn will bring the new data in
                jQuery.each(panellist, function() {
                    var pname = this.applyto();
                    //need to send the panel DOM object
                    var $panel = $('[data-vpanel=' + pname + ']');
                    this.doRefresh($panel, null);
                });
            }
        };

        //change the select parameters
        this.selectparametersarenull = function() {
            var isnull = true;
            $.each(config.selectparameters, function(i, val) {
                if (val != null && val != '') {
                    isnull = false;
                }
            });
            return isnull;
        };

        //take the new data and sync it with the existing data set
        this.syncdata = function(newdata) {
            var existingdata = this.getdata();
            var madechanges = false;
            //check the timestamps
            if (newdata.timestamp > existingdata.timestamp) {
                var keysdone = [];
                //loop through our dataset array
                for (var i = 0; i < existingdata.data.length; i++) {
                    var thiskey = existingdata.data[i][config.keyfield];
                    //does this match any of our updates?
                    for (var j = 0; j < newdata.data.length; j++) {
                        if (newdata.data[j][config.keyfield] == thiskey) {
                            existingdata.data[i] = newdata.data[j];
                            madechanges = true;
                            //delete the line from newdata
                            keysdone.push(thiskey);
                        }
                    }
                    if (keysdone.length == newdata.data.length) break;
                }
                //are there records left over? i.e. ones to insert?
                if (keysdone.length < newdata.data.length) {
                    for (var j = 0; j < newdata.data.length; j++) {
                        var lookforkey = newdata.data[j][config.keyfield];
                        if (keysdone.length == 0 || keysdone.indexOf(lookforkey) == -1) {
                            madechanges = true;
                            existingdata.data.push(newdata.data[j]);
                        }
                    }
                    //need to resort the data
                    this.sortdata(existingdata.currentsortfield, existingdata.currentsortdirection, true);
                }
            }
            return madechanges;
        };

        this.processdata = function(json) {
            internaldatacache[config.selectparameterstring] = json.d;  //cache it
            vLog('vDatasource', 'Caching data : ' + config.name + ' key ' + config.selectparameterstring + " json " + JSON.stringify(json.d) + " vnxtime " + json.d.vnxtime);
            //add some new parameters
            internaldatacache[config.selectparameterstring].currentsortfield = json.d.sortfield;
            internaldatacache[config.selectparameterstring].currentsortorder = json.d.sortorder;
            //now we may need to sort if the default orders don't match our default
            this.sortdata(config.defaultsortfield, config.defaultsortorder, false);
        };

        //we prepare the data, which means loading it in if it's not available
        //once it's loading in we continue with calling the function passed
        this.getasyncdata = function(sucessfunction) {

            if (internaldatacache[config.selectparameterstring] != null) {
                vLog('vDatasource', 'getasyncdata : is cached : ' + config.name);
                return true;    //we return true to say that we do have the data ready to go
            }
            //do we process if select parameters are null?
            if (!config.selectonnullparameters) {
                if (this.selectparametersarenull()) {
                    vLog('vDatasource', 'getasyncdata : selectparameters are null, no request');
                    //we return true in the sense that we have data ready even if it is null
                    //i.e. the calling function can go and request the null data immediately
                    return true;
                }
            }
            //show a wait on the nexus
            jQuery.each(panellist, function() {
                //we pass the select parameters in case the wait method needs them
                this.doWait(this, config.selectparameters);
            });

            //need to start loading the data
            vLog('vDatasource', 'getasyncdata Requesting remote data ASYNC : ' + config.name);
            var us = this;
            var ourname = config.selectparameterstring;

            $.vServerRequest({
                async: true,
                url: config.dataurl + "/" + config.selectmethod,
                cache: false,
                dataType: 'json',
                dataFilter: function(data) {
                    if (typeof (JSON) !== 'undefined' && typeof (JSON.parse) === 'function') {
                        var pj = JSON.parse(data);
                        return pj;
                    } else {
                        return eval('(' + data + ')');
                    }
                },
                success: function(json) {
                    vLog('vDatasource', 'PrepData Got ASYNC Data : ' + config.name + ' name ' + ourname);
                    us.processdata(json);
                    //with it done then call the function passed
                    sucessfunction();
                },
                failure: function() { alert("failed to get data"); },
                type: "POST",
                data: config.selectparameterstring,
                contentType: "application/json; charset=utf-8"
            });
            return false; //return false to say data isn't ready yet but the sucessfunction call back will complete the task
        };



        // Public method - can be called from client code
        this.getdata = function() {


            //do we have the data cached? in 99.99% of cases we should
            //because the prepdata should have run
            if (internaldatacache[config.selectparameterstring] != null) {
                vLog('vDatasource', 'Data is cached : ' + config.name + ' key ' + config.selectparameterstring);
                return internaldatacache[config.selectparameterstring];
            }
            if (!config.selectonnullparameters) {
                if (this.selectparametersarenull()) {
                    vLog('vDatasource', 'getdata : selectparameters are null, no request');
                    return null;
                }
            }

            vLog('vDatasource', 'FAILED TO get data for : ' + config.name + ' key ' + config.selectparameterstring);
            //but if we don't have it cached then we can request is synchronously
            //create the parameters to send
            /*
            //alert("have to get data synchronously...");
            us = this;
            //if we get here and there's no data we need to stop and see whether we can fetch it
            vLog('vDatasource', 'Requesting remote data SYNC : ' + config.name);
            alert("Requestinf data SYNC instead");
            $.vServerRequest({
            async: false,
            url: config.dataurl + "/" + config.selectmethod,
            cache: false,
            contentType: "application/json; charset=utf-8",
            dataType: 'json',
            success: function(json) {
            us.processdata(json);
            },
            type: 'POST',
            data: config.selectparameterstring
            });
            //of course by now either we have the data or there's a problem
            return internaldatacache[config.selectparameterstring];*/
            return internaldatacache[config.selectparameterstring];
            alert("no data yet for " + config.name + " ought to do a getasyncdata ");
        };

        this.getdatarange = function(from, to) {
            vLog('vDatasource', 'get datarange (' + from + ' to ' + to + ') for : ' + config.name);
            var thisdata = this.getdata();
            if (thisdata == null) return null;

            //return only the section requested
            return thisdata.data.slice(from, (to - from));
        };

        this.getdatabykey = function(keyvalue) {

            //this could be sped up using an index
            var thisdata = this.getdata();
            if (thisdata == null) return null;

            for (var i = 0; i < thisdata.data.length; i++) {
                if (thisdata.data[i][config.keyfield] == keyvalue) return thisdata.data[i];
            }
            return null;
        };

        this.getindexbykey = function(keyvalue) {
            var thisdata = this.getdata();
            if (thisdata == null) return null;
            for (var i = 0; i < thisdata.data.length; i++) {
                if (thisdata.data[i][config.keyfield] == keyvalue) return i;
            }
            return -1;
        };

        //returns a piece of data according to a position which can be number or a phrase
        this.getdatabyposition = function(position, start, pagesize, currentselectionkey) {
            var thisdata = this.getdata();
            var dataitem = null;
            if (thisdata == null) return null;
            var dpindex = parseInt(position);
            if (dpindex >= 0) {
                dataitem = dataset.data[dpindex];
            } else {
                switch (position) {
                    case 'start': dataitem = thisdata.data[0];
                        break;
                    case 'end': dataitem = thisdata.data[thisdata.data.length - 1];
                        break;
                    case 'currentselection': dataitem = Us.getdatabykey(currentselectionkey);
                        break;
                    case 'pagestart': dataitem = thisdata.data[start];
                        break;
                    case 'pageend':
                        var end = start + config.pageSize;
                        if (end >= thisdata.data.length) end = thisdata.data.length - 1;
                        dataitem = thisdata.data[end];
                        break;
                }
            }
            return dataitem;
        };

        //uses the above method to return just the keyfield
        this.getkeybyposition = function(position, start, pagesize, currentselectionkey) {
            var dataitem = Us.getdatabyposition(position, start, pagesize, currentselectionkey);
            if (dataitem == null) return null;
            return dataitem[config.keyfield];
        }


        this.updateitem = function(keyvalue, updatedata, afterupdate) {

            if (config.dataurl != "") {

                //if we have default parameters then use those as a template
                var newdata = {};
                $.each(config.updateparameters, function(name, val) {
                    if (updatedata[name] != undefined) {
                        newdata[name] = updatedata[name]
                    }
                    else {
                        newdata[name] = val;
                    }
                });
                if (config.passallparameters) {
                    $.each(updatedata, function(name, val) {
                        if (newdata[name] == undefined) {
                            newdata[name] = updatedata[name]
                        }
                    });
                }
                newdata["KeyID"] = keyvalue;
                var sdata = JSON.stringify({ 'plist': newdata });
                var us = this;

                $.vServerRequest({
                    url: config.dataurl + "/" + config.updatemethod,
                    cache: false,
                    contentType: "application/json; charset=utf-8",
                    dataType: 'json',
                    success: function(json) {
                        if (json.d) {
                            if (json.d.success && json.d.count > 0) {
                                us.syncdata(json.d);
                            }
                            afterupdate(json.d.success, json.d, updatedata);
                        } else {
                            afterupdate(false, { "message": "No valid data returned from server" }, deletedata);
                        }
                    },
                    failure: function() { alert("failed to get update data"); },
                    type: 'POST',
                    data: sdata
                });

                return true;

            }

        };

        this.insertitem = function(insertdata, afterinsert, placeholder) {
            //do we have a data url?
            if (config.dataurl != "") {

                var newdata = {};
                $.each(config.insertparameters, function(name, val) {
                    if (insertdata[name] != undefined) {
                        newdata[name] = insertdata[name]
                    }
                    else {
                        newdata[name] = val;
                    }
                });
                if (config.passallparameters) {
                    $.each(insertdata, function(name, val) {
                        if (newdata[name] == undefined) {
                            newdata[name] = insertdata[name]
                        }
                    });
                }
                var sdata = JSON.stringify({ 'plist': newdata });
                var us = this;

                $.vServerRequest({
                    url: config.dataurl + "/" + config.insertmethod,
                    cache: false,
                    contentType: "application/json; charset=utf-8",
                    dataType: 'json',
                    success: function(json) {
                        if (json.d) {
                            if (json.d.success && json.d.count > 0) {
                                us.syncdata(json.d);
                            }
                            afterinsert(json.d.success, json.d, insertdata, placeholder);
                        } else {
                            afterinsert(false, { "message": "No valid data returned from server" }, insertdata, placeholder);
                        }
                    },
                    type: 'POST',
                    data: sdata
                });
                return true;
            }
        };


        //take the keyvalue of the record, any parameters collected for deletion and
        //a call back once the delete is complete
        this.deleteitem = function(keyvalue, deletedata, afterdelete) {

            var newdata = {};
            $.each(config.deleteparameters, function(name, val) {
                if (deletedata[name] != undefined) {
                    newdata[name] = deletedata[name]
                }
                else {
                    newdata[name] = val;
                }
            });
            if (config.passallparameters) {
                $.each(deletedata, function(name, val) {
                    if (newdata[name] == undefined) {
                        newdata[name] = deletedata[name]
                    }
                });
            }
            newdata["KeyID"] = keyvalue;
            var sdata = JSON.stringify({ 'plist': newdata });
            var us = this;

            var thisdata = this.getdata();
            var index = this.getindexbykey(keyvalue);
            var dataitem = this.getdatabykey(keyvalue);

            //update the remote datastore
            if (config.dataurl != "") {
                $.vServerRequest({
                    url: config.dataurl + "/" + config.deletemethod,
                    cache: false,
                    contentType: "application/json; charset=utf-8",
                    dataType: 'json',
                    success: function(json) {
                        if (json.d) {
                            //sync the data
                            if (!us.syncdata(json.d)) {
                                //if we didn't make changes to the data then lets delete the local data item
                                //delete the record locally, get the position of the item
                                if (index > -1) {
                                    //remove the single element (splice it) from the array
                                    extracteddata = thisdata.data.splice(index, 1);
                                    thisdata.count--;
                                }
                            };
                            afterdelete(json.d.success, json.d, deletedata);
                        } else {
                            afterdelete(false, { "message": "No valid data returned from server" }, deletedata);
                        }
                    },
                    type: 'POST',
                    data: sdata
                });
                return true;
            }
            else {
                onsuccess(thisdata, keyvalue);
                return true;
            }



        };


        this.sortdata = function(sortfield, sortorder, forcesort) {

            var thisdata = internaldatacache[config.selectparameterstring];
            var field = thisdata.sortfield;
            if (thisdata == null) return null;

            if (thisdata.currentsortfield != sortfield || forcesort) {
                vLog('vDatasource', 're-sorting... ' + sortfield);
                if (sortorder == 'ASC') {
                    thisdata.data.sort(function(a, b) {
                        var x = a[field];
                        var y = b[field];
                        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
                    })
                }
                else {
                    thisdata.data.sort(function(a, b) {
                        var x = b[field];
                        var y = a[field];
                        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
                    })
                }
                thisdata.currentsortfield = sortfield;
                thisdata.currentsortorder = sortorder;
            }
            else if (thisdata.currentsortorder != sortorder) {
                //just reverse the current data
                thisdata.data.reverse();
                thisdata.currentsortorder = sortorder;
            }

        };
        this.showinfo = function() {
            element.html('vDatasource:' + config.name + "<br/>");
            if (internaldata != null) {
                element.append('count:' + internaldata.count + "<br/>");
                element.append('sort:' + internaldata.sort + " : " + internaldata.sortdirection + "<br/>");
            } else {
                element.append('url' + config.dataurl);
            }
        };
        this.getkeyfield = function() { return config.keyfield; };
        //all string data that comes in is parsed with this function. It tries to use the browsers built in JSON
        //parsing and then gets a extra function that translates dates properly because there's no standard JSON
        //way for doing this. Yes, WTF? In time the jQuery library will probably be updated so we can use that.
        this.jsonparse = function(stringdata) {
            return JSON.parse(stringdata, function(key, value) {
                //we are looking for a string that starts dt: which means it's a datetime. This is our way
                //of doing it. Seems like the fastest way to tell, better than RegExp
                if (value && typeof value === 'string' && value.length > 3 && value.substr(0, 3) == 'dt:') {
                    //console.log("Date:" + value);
                    var nv = parseInt(value.substr(3, value.length - 3));
                    value = new Date(nv);
                }
                //console.log(key + ":" + value)
                return value;
            });
            console.log("jsonparse done");
        }

        // Private method - can only be called from within this object
        var privateMethod = function() {
            console.log('private method called!');
        };



        var mergesets = function(keyvalue, newset) {
            //find the base data
            var index = getindexbykey(keyvalue);

            //we only change what is already there in the base set
            for (nm in internaldata.data[index]) {
                var sv = newset[nm]; //is there something there in the newset?
                //field name match
                if (sv != undefined) {
                    internaldata.data[index][nm] = newset[nm];
                }
            }
            return internaldata.data[index];
        };


        Us.initialisedatasource()
    };


})(jQuery);


