//Copyright 2007 Roman Ardern-Corris
//Licenced to 3legs.com


// Version 3
// Rewrite using prototyping and multiple master dataset stuff

extend(ligoDataset, ligoCommon);

function ligoDataset(paramsObj) {
    //Constructor

    //all parameters are passed in JSON notation

    //Attributes
    //Default values
    this.uniqueName;                //A unique name for a dataset, does not really do anything, but is passed back notifying the server of a row change as the master row
    this.masterDataset;
    this.remoteHost                 //              ^
    this.remoteProtocol = "http";   //              |
    this.remotePort;                //              |
    this.remotePath;                //Use these attributes to set the URI, remote path is the method for getting data
    this.currentRow;                //This holds the data on the current row
    this.currentRowNum;             //This holds the row number of the current row
    this.columnNames;               //Array of column names, as returned from the datasource
    this.columnLabels;              //Holds column labels in an array, returned from the datasource
    this.data;                      //Actual data is held here, array of arrays
    this.nameIndex = {};            //a hash that contains the list of column names as key and the column number as the value
    this.remoteWrite;               //Name of the remote method URI for writing data
    this.pendingNew = new Array();  //these pending* attributes hold an array of row numbers that are pending an action
    this.pendingUpdate = new Array();
    this.pendingDelete = new Array();
    this.pendingNewRaw = new Array();   //the *Raw attributes hold the raw data passed to Save() before being filtered
    this.pendingUpdateRaw = new Array();
    this.oldRowsUpdates = new Array();  //Holds a hash of rows passed when an update happens, also for deletes
    this.delayWrite = false;        //Should writing occurs as soon as Save() is called or should be deal with them when _do_write() is explicitly called
    this.remoteUpdate;              //these ...
    this.remoteNew;                 // attributes store the ...
    this.remoteDelete;              //  name of specific remote methods for writing
    this.footer;                    //row of data to be displayed as the footer
    this.formats;                   //format description (documented in Azure::TableData)
    this.sendRawData = false;       //whether or not to send the raw data passed to Save(), in addition to filtered stuff
    this.force_callback = false;    //used for multi-master dataset stuff (see fireCallback() below)

    this.sort_column_idx = null;    //the column index that the data is sorted by
    this.sort_asc = false;          //Direction that the above index is sorted by. Ascending if true, else descending
    
    this.dependants = new Array();  //An array of objects that are using this dataset, we will use this to send notification events to these objects

    this._data_processed = false;
    this._data_processing = false;
    this._data_writing = false;

    this.lastQueryString = '';

    ligoDataset.superclass.call(this, paramsObj);

    // If given a masterDataset register this object with it
    if (this.masterDataset) {
        this.masterDataset.addDependant(this);
    }

    // Find a working JSON string function and point to it for later:
    if (typeof JSON == 'object' && typeof JSON.stringify == 'function') {
        // Prototype, JSON.org lib, and Firefox 3.5 (i.e. fast native function)
        this._jsonify = JSON.stringify;
    }
    else if (typeof Object.toJSON == 'function') {
        // Not sure where I saw this one, but we don't support anything but Prototype anyway
        this._jsonify = Object.toJSON;
    }
    else {
        this._jsonify = function() { throw "I don't have a _jsonify function!"; }
    }
}

//#####################################################################################################################

ligoDataset.prototype.hello = function () {
    alert("Dataset: Hello");
}

//#####################################################################################################################

ligoDataset.prototype.field_by_row_and_name = function (row_num, field_name) {
    // XXX: sometimes field_name is undefined, figure out why
    return (
        field_name
            ? this.data[row_num][this.nameIndex[field_name]]
            : '@@@@@'
    );
}

//#####################################################################################################################

ligoDataset.prototype.addDependant = function (dependant_obj) {
    // Register a given object as depending on this dataset so it receives callback signals
    this.dependants.push(dependant_obj);

}

//#####################################################################################################################

ligoDataset.prototype._get_query_url = function (remote_method) {

    // FIXME: Are these needed at all?
    /*
    var port_string = ':' + this.remotePort;
    if ( ! this.remotePort ) { port_string = "" } 
    var out_string = this.remoteProtocol + '://' + this.remoteHost + port_string + '/' + remote_method;
    */

    var out_string = '/' + remote_method;

    return out_string;
}

//#####################################################################################################################

ligoDataset.prototype.refetch = function () {
    //This function uses lastQueryString to re-fetch the last call made to this dataset

    //console.log("Calling getData(): " + this.lastQueryString);

    this.getData(this.lastQueryString);

}

//#####################################################################################################################

ligoDataset.prototype.getData = function (query_string, ajax_request_params) {

    //console.log("Calling getData() query_string : " + ajax_request_params);
    this.currentRowNum = null;
    this.currentRow = null;

    this.lastQueryString = query_string;

    this._data_processing = true;
    
    var myAjax = new Ajax.Request(
        this._get_query_url(this.remotePath),
        {   method: 'post',
            parameters: query_string,
            onComplete: this._process_dataset_results.bind(this),
            onLoading: this._dataset_loading.bind(this),
            onFailure: this._dataset_load_failure.bind(this)
        }   // the bind() method is from prototype.js and ensures that this in the callback refers to the this here.
            // It does this by using a closure
    );

}

//#####################################################################################################################

ligoDataset.prototype.Save = function (record, isNew) {
    //record is a hash
    //isNew is a bool
    var row_changed;

    if ( isNew ) {
        //Add a new row to the end of the dataset
        row_changed = this._insertRow(record);
        //Flag as new row
        // pendingNew/Update only contain the columns in this dataset,
        // pending*Raw contains the actual form values which might contain extra stuff (external ligoSelects etc.)
        this.pendingNew.push(row_changed);
        this.pendingNewRaw.push(record);
    }
    else {
        //write record to the data at currentRowNum
        row_changed = this._updateRow(record);
        //Flag as updated row
        this.pendingUpdate.push(row_changed);
        this.pendingUpdateRaw.push(record);
    }


    if ( ! this.delayWrite) {
        this._do_write();
    }


    //handle refreshing dependants stuff
    this.fireCallback("DS_newData");

    //refresh this dataset
    //this.refresh();

    //move to new row
    this.setCurrentRow(row_changed);

}

//#####################################################################################################################

ligoDataset.prototype.Delete = function () {

    //Delete the current row

    this._deleteRow();

    var current_row = this.currentRowNum;

    this.pendingDelete.push(current_row);

    if ( ! this.delayWrite) {
        this._do_write();
    }


    //handle refreshing dependants stuff
    this.fireCallback("DS_newData");

    //move to new row
    var new_currentrownum = current_row - 1;
    if (new_currentrownum < 1) {
        new_currentrownum = 0;
    }
    this.setCurrentRow(new_currentrownum);

}

//#####################################################################################################################

ligoDataset.prototype._deleteRow = function () {

    this.oldRowsUpdates.push(this.currentRow);

    this.data.splice(this.currentRowNum, 1);

}

//#####################################################################################################################

ligoDataset.prototype._insertRow = function (record) {
    //add this row to the end of this.data
    this.data.push(this._row_hash_to_array(record));

    return this.data.length-1;
}

//#####################################################################################################################

ligoDataset.prototype._updateRow = function (record) {
    //Save the current row
    this.oldRowsUpdates.push(this.currentRow);

    //merge record with currentRow
    for (var i=0; i < this.columnNames.length; i++) {
        if ( ! record[this.columnNames[i]] ) {
            record[this.columnNames[i]] = this.currentRow[i];
        }
    }

    this.data[this.currentRowNum] = this._row_hash_to_array(record);

    return this.currentRowNum;
}

//#####################################################################################################################

ligoDataset.prototype._do_write = function () {
    // Do any pending writes
    this._perform_remote_write('new');
    this._perform_remote_write('update');
    this._perform_remote_write('delete');


    // If write is ok (XXX: where does it get checked?) then clear the value from the appropriate pending array
    this._clear_pending('new');
    this._clear_pending('update');
    this._clear_pending('delete');

    //update the dataset after a write (the remote app could change the data that we provide)

    this.getData(this.lastQueryString);

}

//#####################################################################################################################

ligoDataset.prototype._clear_pending = function (type) {
    //Clears the appropriate pending write array
    switch(type) {
        case 'new':
            this.pendingNew = new Array();
            this.pendingNewRaw = new Array();
            break;
        case 'update':
            this.pendingUpdate = new Array();
            this.pendingUpdateRaw = new Array();
            this.oldRowsUpdates = new Array();
            break;
        case 'delete':
            this.pendingDelete = new Array();
            this.oldRowsUpdates = new Array();
            break;
    }

}

//#####################################################################################################################

ligoDataset.prototype._perform_remote_write = function (writeType) {
    //Implement default write method (the actual new, update, delete needs to be passed in)
    //use either a specified remoteXXX method or the default remoteWrite method
    var count;
    var pendingVarName;
    var remoteMethod;
    var remote_return;
    var parameters = {action: writeType};
    
    switch(writeType) {
        case 'new':
            pendingVarName = 'pendingNew';
            remoteMethod = this.remoteNew;
            break;
        case 'update':
            pendingVarName = 'pendingUpdate';
            remoteMethod = this.remoteUpdate;
            break;
        case 'delete':
            pendingVarName = 'pendingDelete';
            remoteMethod = this.remoteDelete;
            break;
    }

    //loop around pending type
    for (var count = 0; count < this[pendingVarName].length; count++) {
        //console.log({writeType: writeType, pendingVarName: pendingVarName, remoteMethod: remoteMethod});

        switch(writeType) {
            case 'new':
                parameters.row = this._row_to_json(this.data[this[pendingVarName][count]]);
                if (this.masterDataset) {
                    parameters.master_row = this.masterDataset.getVerboseCurrentRow();
                }
                if (this.sendRawData && this[pendingVarName+"Raw"][count]) {
                    parameters.raw_data = this._jsonify(this[pendingVarName+"Raw"][count]);
                }
                break;

            case 'update':
                parameters.row = this._row_to_json(this.data[this[pendingVarName][count]]);
                parameters.old_row = this._row_to_json(this.oldRowsUpdates[count]);
                if (this.sendRawData && this[pendingVarName+"Raw"][count]) {
                    parameters.raw_data = this._jsonify(this[pendingVarName+"Raw"][count]);
                }
                break;

            case 'delete':
                parameters.old_row = this._row_to_json(this.oldRowsUpdates[count]);
                break;
        }

        var return_status = new Ajax.Request( this._get_query_url(remoteMethod),
                                                { asynchronous: false, parameters: parameters }
                                            ).transport.responseText;
        //build an object that tells us the status of the writes and gives us any validation errors

    }

    return remote_return;
}


//#####################################################################################################################

ligoDataset.prototype._process_dataset_results = function (originalRequest) {
    //Evaluate the returned JSON data    
    var returnedData = eval("(" + originalRequest.responseText + ")");
                
    this.columnNames = returnedData.fields;
    this.columnLabels = returnedData.labels;
    this.only_disp_cols_with_labels = returnedData.only_disp_cols_with_labels;
    this.data = returnedData.data;
    this.footer = returnedData.footer;
    this.formats = returnedData.formats;

    this._setNameIndex();

    this._data_processing = false;
    this._data_processed = true;

    this.sort_column_idx = null;
    this.sort_asc = false;

    //Fire an event here, that lets other objects that care know that our data is ready to be used
    this.fireCallback("DS_dataLoaded");

}

//#####################################################################################################################

ligoDataset.prototype._dataset_loading = function (originalRequest) {

    this.fireCallback('DS_dataLoading');

}

//#####################################################################################################################

ligoDataset.prototype._dataset_load_failure = function (originalRequest) {

    this.fireCallback('DS_dataLoadError');

}

//#####################################################################################################################

ligoDataset.prototype._setNameIndex = function () {

    for (var i=0; i < this.columnNames.length; i++) {
        this.nameIndex[this.columnNames[i]] = i;
    }

}

//#####################################################################################################################

ligoDataset.prototype.forceNextCallback = function () {
    // This sets a flag which says that for the next callback call everything and don't check that there's a master-dep link
    this.force_callback = true;

}

//#####################################################################################################################

ligoDataset.prototype.fireCallback = function (callbackName) {
    // Sends off callback function calls to all dependant objects
    var remote_method_name = "on" + callbackName;

    // Set to true for large amounts of callback debug information in firebug log
    var debug = false;

    debug && console.info(this.uniqueName + '.fireCallback()', callbackName);

    for (var i=0; i<this.dependants.length; i++) {
        var dependant = this.dependants[i];

        // Unless it's forced to, check that each dep "belongs" to this dataset before doing callbacks on them
        // This is to prevent things where the dep also has this as a dependency which would cause infinite recursion
        if ( ! this.force_callback ) {
            // Figure out what the name of the dep's master dataset is, if it has one
            var dep_is_ds = dependant instanceof ligoDataset;
            var dep_ds_name = (dep_is_ds && !!dependant.masterDataset && dependant.masterDataset.uniqueName)
                              || (!!dependant.Dataset && dependant.Dataset.uniqueName)
                              || null;

            // only used for debug printing
            if ( debug ) {
                var dep_func_name = ((dep_is_ds && dependant.uniqueName)||dependant.instanceName)
                                    + '.' + remote_method_name + '()';
                console.log('  calling ' + dep_func_name);
            }

            // If this isn't the dep's master dataset ignore it
            if ( dep_ds_name != this.uniqueName ) {
                debug && console.log('  ...skipping because this is an indirect dependency - dataset='+dep_ds_name);
                continue;
            }

        }

        if ( debug ) {
            console.log(dependant[remote_method_name]);
        }

        if ( typeof dependant[remote_method_name] == 'function' ) { // don't try to run undefined stuff
            try {
                dependant[remote_method_name](this);
            }
            catch (error) {
                //handle silently
                debug && console.error('***fireCallback: Error "' + error + '" while calling ' + dep_func_name);
            }
        }
    }

    // reset flag
    this.force_callback = false;

}

//#####################################################################################################################

ligoDataset.prototype.setCurrentRow = function (row_num) {

    this.currentRowNum = row_num;
    this.currentRow = this.data[row_num];

    //reset the pending stuff
    //changing rows loses all pending writes
    //Actually we will not do this for the moment, this is probably safe

    //alert('row changed to: ' + row_num);

    this.fireCallback("DS_currentRowChange");

}

//#####################################################################################################################

ligoDataset.prototype.getVerboseCurrentRow = function () {
    //returns a json string that reprents the current row and field names
    return this._row_to_json(this.currentRow);
}

//#####################################################################################################################

ligoDataset.prototype.getVerboseRowObject = function (row_struct) {
    // Encode a row object as JSON. The row_struct must be from the dataset, because this also reads columnNames.
    
    // Initialise the object
    var js_obj = { name: this.uniqueName, data: {} };
    var cols = this.columnNames.length;

    for (var i=0; i < cols; i++) {
        var colname = this.columnNames[i];
        js_obj.data[colname] = row_struct[i];
    }

    return js_obj;
}

//#####################################################################################################################

ligoDataset.prototype.getVerboseCurrentRowObject = function () {
    // returns an object for the current row, or empty if no current row
    if ( this.currentRow ) {
        return this.getVerboseRowObject(this.currentRow);
    }
    else {
        return { name: this.uniqueName, data: {} };
    }
}

//#####################################################################################################################

ligoDataset.prototype._row_hash_to_array = function (record) {
    // Returns an array of {name:value} pairs for all fields in this.columnNames
    var out_array = new Array();

    for (var i=0; i < this.columnNames.length; i++) {
        out_array.push( record[this.columnNames[i]] );
    }

    return out_array;
}

//#####################################################################################################################

ligoDataset.prototype._row_to_json = function (row_struct) {
    return this._jsonify( this.getVerboseRowObject(row_struct) );
}

//#####################################################################################################################

ligoDataset.prototype.aggregate_column = function (column_number, routine) {
    
    var sum = 0;
    var count = 0;
    var value = 0;  //the value that we will return
    
    for (var i=0; i<this.data.length; i++) {
        var cell_value = this.data[i][column_number];
        
        //make sure that we are dealing with a number
        cell_value = parseFloat(cell_value);
               
        if (isNaN(cell_value)) {
            cell_value = 0;
        }
        
        sum += cell_value;
        count++;
    }
    
    if (routine == 'sum') { return(sum) }
    if (routine == 'count') { return(count) }
    if (routine == 'mean' ) { return(sum/count) }

    return '*';
}

//#####################################################################################################################

ligoDataset.prototype.onDS_currentRowChange = function (source) {
    // Row Change callback
    //this occurs when the master dataset has changed its row
    //This is not when we change current row (that can be done in setCurrentRow() )

    //We basically want to repopulate this dataset with data as would be returned from our URI
    //but we want to pass into that remote function the current row of the master dataset as a JSON object
    this.refresh(source);

}

//#####################################################################################################################

ligoDataset.prototype.Sort = function (column, sort_asc) {
    
    this.sort_asc = sort_asc;
        
    this.sort_by_column_idx(column);

    //tell other objects that we have changed
    //FIXME: this should fire a different event, so that objects can decide how to handle the change
    this.fireCallback("DS_dataLoaded");
    
}

//#####################################################################################################################

ligoDataset.prototype.sort_by_column_idx = function (column_idx) {
    
    this.sort_column_idx = column_idx;
    
    this.data.sort(this._sort_by_col(column_idx));
        
    if (! this.sort_asc) {
        this.data.reverse();
    }

}

//#####################################################################################################################

ligoDataset.prototype._sort_by_col = function (col_idx) {
    //return a sort callback function based on the col_idx
    return function (a, b) {
        // The 0s are so we can compare nulls to numbers correctly
        var x = a[col_idx] || '0';
        var y = b[col_idx] || '0';

        // XXX: This should check the data format set in ligoDSVisualiser somehow
        // If they both look like numbers, typecast them so they're compared as numbers
        var re = /^-?\d+(\.\d+)?$/;
        if ( x.match && y.match && x.match(re) && y.match(re) ) {
            x = +x;
            y = +y;
        }

        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
    };
}

//#####################################################################################################################

ligoDataset.prototype.refresh = function (source) {
    // Re-downloads the entire dataset from the server
    var masterCurrentRowJSON;

    if ( source && source.uniqueName ) {
        masterCurrentRowJSON = source.getVerboseCurrentRow();
    }
    else {
        masterCurrentRowJSON = this.masterDataset.getVerboseCurrentRow();
    }

    this.getData('masterRow=' + encodeURIComponent(masterCurrentRowJSON));

}

//#####################################################################################################################

ligoDataset.prototype.refreshUsingMasterRow = function (masterRow, dont_append_masterrow) {
    //This routine refreshes the dataset but uses a verboseCurrentRow string to pass to the getData() method
    
    if (! dont_append_masterrow) {
        this.getData('masterRow=' + encodeURIComponent(masterRow));
    }
    else {
        this.getData(masterRow);    
    }
    
}

//#####################################################################################################################

ligoDataset.prototype.row_count = function() {
    return this.data.length;
}
