/*
    Getme Ltd reserve all intellectual property rights of this source code.
    
    EXCEPT AS OTHERWISE EXPRESSLY PROVIDED IN A WRITTEN AGREEMENT BETWEEN YOU 
    AND GETME LTD, THIS SOURCE CODE IS PROVIDED "AS IS" AND WITHOUT ANY 
    WARRANTIES OF ANY KIND, INCLUDING WARRANTIES OF MERCHANTABILITY OR FITNESS 
    FOR A PARTICULAR PURPOSE.

    IN NO EVENT SHALL GETME LTD BE LIABLE FOR ANY DIRECT, INDIRECT, 
    INCIDENTIAL, SPECIAL OR CONSEQUENTIAL DAMAGES OR DAMAGES FROM BUSINESS 
    INTERRUPTION OR LOSS OF PROFITS, REVENUE, DATA OR USE, INCURRED BY YOU OR 
    ANY THIRD PARTY, WHETHER IN CONTRACT OR TORT, ARISING FROM YOUR ACCESS TO, 
    OR USE OF, THIS SOURCE CODE EVEN IF GETME LTD HAS BEEN ADVISED OF THE 
    POSSIBILITY OF SUCH DAMAGES.
    
    Copyright (c)2009 Getme Ltd.
 */

/**
 * @fileoverview    Support for pagination of information.
 * @version         0.0.1
 * @author          Anthony Blackshaw ant@getme.co.uk
 */

// Dependency check(s)
if (window.jQuery===undefined) {
    throw Error('JQuery JavaScript framework (http://jquery.com/) is required.');
}

if (window.jQuery.fn.ajaxForm===undefined) {
    throw Error('JQuery JavaScript form plugin (http://malsup.com/jquery/form) is required.');
}

if(!Array.indexOf) {
    /**
     * Ensure indexOf method is supported (cheers for this IE)
     * @ignore
     */    
    Array.prototype.indexOf = function(obj) {
        for(var i=0; i<this.length; i++) {
            if(this[i]==obj) {
                return i;
            }
        }
        return -1;
    }
}


/**
 * The FORM namespace.
 * The Form library uses FORM to namespace it's classes and functions.
 * @final
 * @type Namespace
 */  
var PAGINATION = {};

/**
 * Create a new Page. 
 * @class A class for pagination of information.
 * @param {String} url The URL used when requesting paginated information.
 * @param {Array} column_list A list of Columns used to display and manage 
 * the information.
 * @param {Array} row_list A list of Rows (snippets of information) that hold
 * the data for the page.
 * @param {Number} number The page number.
 * @param {Number} row_count The total Row count.
 * @param {Number} page_count The total Page count.
 * @constructor
 */ 
PAGINATION.Page = function(url, filter_list, column_list, row_list, number,
        row_count, page_count, id) {
    
    if (!url) {
        return;
    }
    
    /**
     * The URL used to request paginated information from.
     * @type String
     */
    this.url = url;
    
    /**
     * A list of filters for the page.
     * @type Array
     */
    this.filter_list = filter_list || new Array();
    
    /**
     * A list of columns used to display and manage paginated information.
     * @type Array
     */
    this.column_list = column_list;
    
    /**
     * A list of rows (snippets of information for the page).
     * @type Array
     */
    this.row_list = row_list;
    
    /**
     * The page number.
     * @type Number
     */
    this.number = number;
    
    /**
     * The total Row count.
     * @type Number
     */
    this.row_count = row_count;    

    /**
     * The total Page count.
     * @type Number
     */
    this.page_count = page_count;  

    /**
     * The next page number.
     * @type Number
     */
    this.next_page_number = Math.min(number+1,this.page_count);  

    /**
     * The previous page number.
     * @type Number
     */
    this.previous_page_number = Math.max(number-1,1);  

    /**
     * The element ID of the page in the HTML document.
     * @type String
     */
    this.id = id || '__page';
    
    /**
     * A function called when the page is changed, receives the new page.
     * @type Function
     */ 
     this.page_change = function(paging) {};
     
     /**
      * A function called when a row is selected, recieves the event that
      * triggered the call.
      * @type Function
      */
     this.row_select = function(ev) {};

    /**
     * An object containing filter and order data.
     * @array 
     */
    this._data = {'page': this.number};
    for (var i=0; i<this.filter_list.length; i++) {
        this._data[this.filter_list[i].name] = this.filter_list[i].value;
    }

    // Update Filters, Rows & Columns with a reference to the parent page    
    for (var i=0; i<this.filter_list.length; i++) {
        this.filter_list[i].page = this;
    }
    for (var i=0; i<this.column_list.length; i++) {
        this.column_list[i].page = this;
    }
    for (var i=0; i<this.row_list.length; i++) {
        this.row_list[i].page = this;
    }
}

/**
 * Return the filter and order data for the page.
 * @return Return an object containing filter and order settings.
 * @type Boolean
 */
PAGINATION.Page.prototype.get_data = function(other_data) {
    // Build a copy (leave the original intact)
    var data = {};
    for (var name in this._data) {
        data[name] = this._data[name];
    }
    // Update the values with those in the provided other data
    for (var name in other_data) {
        data[name] = other_data[name];
    }
    return data;
}

/**
 * Return the number of visible Filters for the page.
 * @return Return the number of filters on the page that are not hidden.
 * @type Boolean
 */
PAGINATION.Page.prototype.get_visible_filter_length = function() {
    var filter_count = 0;
    for (var i=0; i<this.filter_list.length; i++) {
        if (this.filter_list[i].visible) {
            filter_count++;
        }
    }
    return filter_count;
}

/**
 * Return true if there is a next page.
 * @return Whether there is a next page.
 * @type Boolean
 */
PAGINATION.Page.prototype.has_next_page = function() {
    return this.number < this.page_count;
}

/**
 * Return true if there is a previous page.
 * @return Whether there is a previous page.
 * @type Boolean
 */
PAGINATION.Page.prototype.has_previous_page = function() {
    return this.number > 1;
}

/**
 * Return true if there is a next or previous page.
 * @return Whether there is a next or previous page.
 * @type Boolean
 */
PAGINATION.Page.prototype.has_other_pages = function() {
    return this.has_next_page() || this.has_previous_page();
}

/**
 * Render the filter controls for the Page.
 * @param {Element} node The node to render the Page filters within.
 * @private
 */
PAGINATION.Page.prototype._filter = function(node) {
    
    // Create the filter control form
    $(node).append('<form></form>');
    var form_node = $(node).find('form:last');
    $(form_node).attr('action', this.url);
    $(form_node).attr('method', 'POST');
    $(form_node).attr('enctype', 'application/x-www-form-urlencoded');
    $(form_node).addClass('pagination-filter-control');
    
    // Add a fieldset into which all the filters are rendered
    $(form_node).append('<fieldset></fieldset>');
    var fieldset_node = $(form_node).find('fieldset:last');
    $(fieldset_node).append('<legend>Filter</legend>');
    
    // Render the filters
    for (var i=0; i<this.filter_list.length; i++) {
        this.filter_list[i].render(fieldset_node);
    }
    
    // Render an apply button
    $(fieldset_node).append('<input type="submit" name="apply" value="Apply" />');
    
    // The forms submission is captured so that the filter settings can be 
    // applied to other settings such as page number and order clauses. The 
    // resulting data is then sent to 'page_change'.
    var _page = this;
    $(form_node).ajaxForm({
        dataType: 'html',
        beforeSubmit: function(data) {
            // Compile filter data
            var paging = {};
            for (var i=0; i<_page.filter_list.length; i++) {
                var filter = _page.filter_list[i];
                paging[filter.name] = $('#' + filter.id).fieldValue();
                if (paging[filter.name].length == 1) {
                    paging[filter.name] = paging[filter.name][0];
                }
                if (paging[filter.name] == filter.label) {
                    paging[filter.name] = '';
                }
            }
            // Apply the filter settings
            _page.page_change(_page.get_data(paging));
            // Don't submit the form
            return false;
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            throw Error(textStatus);
        }
    });        
}

/**
 * Render the navigation controls for the Page.
 * @param {Element} node The node to render the Page navigation within.
 * @private
 */
PAGINATION.Page.prototype._nav = function(node) {
    $(node).append('<div class="pagination-nav"></div>');
    var nav_node = $(node).find('.pagination-nav:last');
    // Page Count
    $(nav_node).append('<div class="pagination-page-tally">Page '+this.number+' of '+this.page_count+'</div>');    
    if (this.has_other_pages()) {
        $(nav_node).append('<ul class="pagination-page-controls"></ul>');
        var control_node = $(nav_node).find('.pagination-page-controls:last');
        if (this.has_previous_page()) {
            // Previous
            $(control_node).append('<li class="puesdo-link previous-page">Previous</li>');
            $(control_node).find('.previous-page').bind('click',this,function(ev) {
                ev.data.page_change(ev.data.get_data({'page':ev.data.previous_page_number}));
            });
        }
        if (this.has_next_page()) {
            // Prev
            $(control_node).append('<li class="puesdo-link next-page">Next</li>');
            $(control_node).find('.next-page').bind('click',this,function(ev) {
                ev.data.page_change(ev.data.get_data({'page':ev.data.next_page_number}));
            });
        }
    }
}

/**
 * Render the Page.
 * @param {Element} node The node to render the Page within.
 */
PAGINATION.Page.prototype.render = function(node) {
    $(node).append('<div class="pagination-page"></div>');
    var page_node = $(node).find('.pagination-page:last');
        
    if (this._id) {
        page_node.attr('id',this.id);
    }
    
    if (this.get_visible_filter_length()>0) {
        this._filter(page_node);
    }
    
    if(this.row_count==0) {
        $(page_node).append('<span class="pagination-no-rows">No records found</span>');
        if (this.get_visible_filter_length()>0) {
            $(page_node).find('.pagination-no-rows').addClass('pagination-has-filter');
        }
    } else {
        // Render the page navigation
        this._nav(page_node);
        // Render the Columns & Rows
        $(page_node).append('<table><thead><tr></tr></thead><tbody></tbody></table>');
        var header_node = $(page_node).find('thead tr:last');
        for (var i=0; i<this.column_list.length; i++) {
            this.column_list[i].render(header_node);
        }
        var body_node = $(node).find('tbody:last');
        for (var i=0; i<this.row_list.length; i++) {
            this.row_list[i].render(body_node,this.column_list,i%2,this.row_select);
        }
        this._nav(page_node);
    }
}


/**
 * Create a new Gallery. 
 * @class A class for pagination of image information.
 * @param {String} url The URL used when requesting paginated information.
 * @param {Array} column_list A list of Columns used to display and manage 
 * the information.
 * @param {Array} row_list A list of ImageRows (snippets of information) that 
 * hold the data for the page.
 * @param {Number} number The page number.
 * @param {Number} row_count The total Row count.
 * @param {Number} page_count The total Page count.
 * @constructor
 */ 
PAGINATION.Gallery = function(url, filter_list, column_list, row_list, number,
        row_count, page_count, id) {
    PAGINATION.Page.call(this, url, filter_list, column_list, row_list, number,
        row_count, page_count, id);
}

// Inheritance
PAGINATION.Gallery.prototype = new PAGINATION.Page;

/**
 * Render the Gallery.
 * @param {Element} node The node to render the Gallery within.
 */
PAGINATION.Gallery.prototype.render = function(node) {
    $(node).append('<div class="pagination-page"></div>');
    var page_node = $(node).find('.pagination-page:last');
        
    if (this._id) {
        page_node.attr('id',this.id);
    }
    
    if (this.get_visible_filter_length()>0) {
        this._filter(page_node);
    }
    
    if(this.row_count==0) {
        $(page_node).append('<span class="pagination-no-rows">No records found</span>');
        if (this.get_visible_filter_length()>0) {
            $(page_node).find('.pagination-no-rows').addClass('pagination-has-filter');
        }
    } else {
        // Render the page navigation
        this._nav(page_node);
        // Render the Rows
        $(page_node).append('<ul class="pagination-gallery"></ul>');
        var body_node = $(node).find('ul:last');
        for (var i=0; i<this.row_list.length; i++) {
            this.row_list[i].render(body_node,this.column_list,i%2,this.row_select);
        }
        this._nav(page_node);
    }
}


/**
 * Create a new Filter.
 * @class A base Pagination Filter class.
 * @constructor
 */ 
PAGINATION.Filter = function(name, label, value) {

    if (!name) {
        return;
    }
    
    /**
     * The Filter's name.
     * @type String
     */
    this.name = name;
    
    /**
     * The Filter's label.
     * @type String
     */
    this.label = label || '';
    /**
     * The Filter's value.
     * @type String
     */
    this.value = value || '';

    /**
     * The Filter's ID.
     * @type String
     */
     this.id = 'pagination_filter_' + this.name;
     
    /**
     * Is the filter visible.
     * @type Boolean
     */
    this.visible = true;
    
    /**
     * The page associated with this Filter.
     * @type PAGINATION.Page
     */
    this.page = null;
}

/**
 * Render a Filter.
 * @param {Element} node The node to render the Filter within.
 */
PAGINATION.Filter.prototype.render = function(node) {}


/**
 * Create a new HiddenFilter.
 * @class A Hidden Filter class.
 * @constructor
 */ 
PAGINATION.HiddenFilter = function(name, label, value) {
    PAGINATION.Filter.call(this, name, label, value);

    /**
     * Is the filter visible.
     * @type Boolean
     */
    this.visible = false;
}

// Inheritance
PAGINATION.HiddenFilter.prototype = new PAGINATION.Filter;

/**
 * Render a HiddenFilter.
 * @param {Element} node The node to render the Filter within.
 */
PAGINATION.HiddenFilter.prototype.render = function(node) {
    $(node).append('<input id="' + this.id + '" type="hidden" name="' + this.name + '" class="pagination-filter-text" />');
    $('#'+this.id).val(this.value);
}


/**
 * Create a new BooleanFilter.
 * @class A Text Filter class.
 * @constructor
 */ 
PAGINATION.BooleanFilter = function(name, label, value) {
    PAGINATION.Filter.call(this, name, label, value);

}

// Inheritance
PAGINATION.BooleanFilter.prototype = new PAGINATION.Filter;

/**
 * Render a BooleanFilter.
 * @param {Element} node The node to render the Filter within.
 */
PAGINATION.BooleanFilter.prototype.render = function(node) {
    
    // Render the filter
    $(node).append('<label for="' + this.id + '" class="pagination-filter-visible-label">' + this.label + '</label>');
    $(node).append('<input id="' + this.id + '" type="checkbox" name="' + this.name + '" class="pagination-filter-boolean" />');
    
    var filter_node = $('#'+this.id);
    
    // Set the value
    $(filter_node).attr('title', this.label);
    $(filter_node).val('1');
    if (this.value) { 
        $(filter_node).attr('checked', true);
    }
}


/**
 * Create a new TextFilter.
 * @class A Text Filter class.
 * @constructor
 */ 
PAGINATION.TextFilter = function(name, label, value) {
    PAGINATION.Filter.call(this, name, label, value);

}

// Inheritance
PAGINATION.TextFilter.prototype = new PAGINATION.Filter;

/**
 * Render a TextFilter.
 * @param {Element} node The node to render the Filter within.
 */
PAGINATION.TextFilter.prototype.render = function(node) {
    
    // Render the filter
    $(node).append('<label for="' + this.id + '">Search</label>');
    $(node).append('<input id="' + this.id + '" type="text" name="' + this.name + '" class="pagination-filter-text" />');
    
    var filter_node = $('#'+this.id);
    
    // Set the value
    $(filter_node).attr('title', this.label);
    $(filter_node).val(this.value||this.label);
    
    if (!this.value) {
        $(filter_node).addClass('pagination-filter-hint');
    }
    
    // Use the filters label as a hint for the filter
    $(filter_node).bind('blur', this, function(ev) {
        if ($.trim($(filter_node).val()) == '') {
            $(filter_node).val(ev.data.label);
            $(filter_node).addClass('pagination-filter-hint');
        }
    });
    $(filter_node).bind('focus', this, function(ev) {
        if ($.trim($(filter_node).val()) == ev.data.label) {
            $(filter_node).val('');
            $(filter_node).removeClass('pagination-filter-hint');
        }
    });    
}

/**
 * Create a new SelectFilter.
 * @class A Select Filter class.
 * @constructor
 */ 
PAGINATION.SelectFilter = function(name, label, option_list, value) {
    PAGINATION.Filter.call(this, name, label, value);
    
    if (!name) {
        return;
    }
    
    /**
     * A list of options for the filter.
     */
    this.option_list = option_list;
}

// Inheritance
PAGINATION.SelectFilter.prototype = new PAGINATION.Filter;

/**
 * Render a SelectFilter.
 * @param {Element} node The node to render the Filter within.
 */
PAGINATION.SelectFilter.prototype.render = function(node) {
    
    // Render the filter
    $(node).append('<label for="' + this.id + '">Search</label>');
    $(node).append('<select id="' + this.id + '" name="' + this.name + '" class="pagination-filter-select" />');
    
    var filter = this;
    var filter_node = $('#'+this.id);
    $(filter_node).attr('title', this.label);
    $(filter_node).append('<option value="">' + this.label + '</option>')
    $(filter_node).find('option:last').addClass('pagination-filter-hint');
    
    // Render the options
    for (var i=0; i<this.option_list.length; i++) {
        var option = this.option_list[i];
        $(filter_node).append('<option value="' + option.value + '">' + option.label + '</option>');
        $(filter_node).find('option:last').addClass('pagination-filter-option');
        $(filter_node).bind('change', this, function(ev) {
            if ($(this).val()) {
                $(filter_node).removeClass('pagination-filter-hint');
            } else {
                $(filter_node).addClass('pagination-filter-hint');
            }
        });
    }
    
    $(filter_node).val(this.value);
    if (!this.value) {
        $(filter_node).addClass('pagination-filter-hint');
    }
}

/**
 * Create a new FilterOption. 
 * @class A base web Form Filter Option class.
 * @param {String} value The value of the FilterOption.
 * @param {String} Label A label for the FilterOption.
 * @constructor
 */ 
PAGINATION.FilterOption = function(value, label) {
    
    /**
     * The Filter option's value.
     * @type String
     */
    this.value = value;
    
    /**
     * The Filter option's label.
     * @type String
     */
    this.label = label;
}

/**
 * Create a new Column. 
 * @class A class for managing the display and ordering of information.
 * @param {String} name The name of the Column (used to extract data from 
 * each Row).
 * @param {String} label A label that will be displayed for the Column.
 * @param {String} alignment The alignment of data within the Column.
 * @constructor
 */ 
PAGINATION.Column = function(name, label, content_type, alignment, orderable,
    order_direction) {

    /**
     * The name of the Column.
     * @type String
     */
    this.name = name;
    
    /**
     * A label for the Column.
     * @type String
     */
    this.label = label;
    
    /**
     * The content type of the data within the Column.
     * @type String
     */
    this.content_type = content_type || 'text';    
    
    /**
     * The alignment of data within the Column.
     * @type String
     */
    this.alignment = alignment || 'left';
    
    /**
     * A flag to indicate if the Column can be used to order the paginated 
     * data.
     * @type Boolean
     */
    this.orderable = orderable;
    
    /**
     * A flag indicating the direction of the Column's order by. Can be '',
     * 'asc' or 'desc'.
     * @type String
     */
    this.order_direction = order_direction || '';

    /**
     * The page associated with this Column.
     * @type PAGINATION.Page
     */
    this.page = null;
}

/**
 * Render a Column.
 * @param {Element} node The node to render the Column within.
 */
PAGINATION.Column.prototype.render = function(node, column_select) {
    $(node).append('<th>'+this.label+'</th>');
    var column_node = $(node).find('th:last');
    if (this.orderable) {
        switch (this.order_direction.toLowerCase()) {
            case 'asc':
                $(column_node).addClass('pagination-order-asc');
                break;
            case 'desc':
                $(column_node).addClass('pagination-order-desc');
                break;
            default:
                $(column_node).addClass('pagination-no-order');
        }            
        $(column_node).bind('click', this, function(ev){
            var paging = {'page': 1}
            switch (ev.data.order_direction.toLowerCase()) {
                case 'asc':
                    paging['order-by'] = '-' + ev.data.name;
                    break;
                case 'desc':
                    paging['order-by'] = ev.data.name;
                    break;
                default:
                    paging['order-by'] = ev.data.name;
            }
            // Apply the filter settings
            ev.data.page.page_change(ev.data.page.get_data(paging));
        });
    }
}


/**
 * Create a new Row. 
 * @class A class for displaying a single snippet of information.
 * @param {Object} data A map of data for the row.
 * @constructor
 */ 
PAGINATION.Row = function(data) {
    
    if (!data) {
        return;
    }
    
    /**
     * The data for the row as a map.
     * @type Object
     */
    this.data = data;
    
    /**
     * The page associated with this Row.
     * @type PAGINATION.Page
     */
    this.page = null;
}

/**
 * Render a Row.
 * @param {Element} node The node to render the Row within.
 * @param {Array} column_list The list of Columns in the Row.
 * @param {Boolean} stripe If true then the row has the a stripe class added to
 * it.
 * @param {Function} row_select A function that is triggered when the row is 
 * selected.
 */
PAGINATION.Row.prototype.render = function(node,column_list,stripe,row_select) {
    $(node).append('<tr></tr>');
    var row_node = $(node).find('tr:last');
    for (var i=0; i<column_list.length; i++) {
        var column = column_list[i];
        var value = this.data[column.name] || 'Not specified';
        $(row_node).append('<td></td>');
        var division_node = $(row_node).find('td:last');
        if (this.content_type='html') {
            $(division_node).html(value)
        } else {
            $(division_node).text(value);
        }
        $(division_node).css('text-align',column.alignment);
        if (stripe) {
            $(division_node).addClass('pagination-stripe');
        }
        $(division_node).bind('click',this,row_select);
    }
}

/**
 * Create a new ImageRow. 
 * @class A class for displaying a single snippet of information.
 * @param {String} title A title to display for the image.
 * @param {String} image_url A URL to the image.
 * @param {String} alt_text Alternative text for the image.
 * @param {Object} data A map of data for the row.
 * @constructor
 */ 
PAGINATION.GalleryRow = function(title, url, alt_text, data) {
    PAGINATION.Row.call(this, data);
    
    /**
     * The title of the image.
     * @type String
     */
    this.title = title;
    
    /**
     * The URL of the thumbnail to display.
     * @type String
     */
    this.url = url;
    
    /**
     * The alternative text for the image.
     * @type String
     */
    this.alt_text = alt_text;
}

// Inheritance
PAGINATION.GalleryRow.prototype = new PAGINATION.Row;

/**
 * Render an Image Row.
 * @param {Element} node The node to render the Image Row within.
 * @param {Array} column_list The list of Columns in the Row.
 * @param {Boolean} stripe If true then the row has the a stripe class added to
 * it.
 * @param {Function} row_select A function that is triggered when the row is 
 * selected.
 */
PAGINATION.GalleryRow.prototype.render = function(node, column_list, stripe, row_select) {
    $(node).append('<li></li>');
    var row_node = $(node).find('li:last');
    $(row_node).append('<div class="pagination-thumbnail"><img src="' + this.url + '" alt="' + this.alt_text + '"/></div>');
    var thumbnail = $(row_node).find('div:last');
    $(thumbnail).attr('title', this.alt_text);
    $(thumbnail).css('background-image', 'url(' + this.url + ')');
    $(row_node).append('<div class="pagination-title"></div>');
    var title = $(row_node).find('div:last');
    title.text(this.title);
    $(row_node).bind('click', this, row_select);
}
