/**

@fileoverview

This class encapsulates our corpus display behavior so that we can
simply instantiate on the given page, handing it the base URL for all
operations.

@author Michael Alan Dorman <mdorman@ironicdesign.com>

*/

/**

Declare the new class, using the style established by prototype.js

@constructor

*/

var Corpus = Class.create();

/**

Do the real work of creating a new Corpus display instance.

<p>

This is called automagically by the standard constructor code that
prototype.js provides.

<p>

This extracts some content from the page to be used as a template,
attaches bits of behavior to several items, then kicks off an async
request to load the data.

*/

Corpus.prototype.initialize = function (advanced) {
    console.log ("Entering initialize()");

    console.log ("Setting our defaults for filtering, sorting and paging");
    this.filterstring = null;
    this.sortcolumn = 'created';
    this.direction = 'descending';
    this.page = 0;
    this.pagelength = 300;
    this.advanced = advanced;

    console.log ("Figure out the correct sort function");
    this.chooseSort();

    console.log ("Instantiating our template and clearing out the source");
    this.template = new Template ($('corpus_body').innerHTML.sub ('%7Bid%7D', '{id}'));
    $('corpus_body').update('');

    console.log ("Setting behaviors on our header bits");
    $$('input.iterate').each (this.attach.bind(this, 'click', 'callbackMultiple'));
    $$('input.toggle').each (this.attach.bind(this, 'click', 'callbackToggle'));
    $$('th.sort').each (this.attach.bind(this, 'click', 'callbackSort'));
    $$('form.pageForm input[type="button"]').each (this.attach.bind(this, 'click', 'callbackRelativePage'));
    $$('form.pageForm input[type="submit"]').each (this.attach.bind(this, 'click', 'callbackPage'));
    $$('form#filterForm input#count').each (this.attach.bind(this, 'click', 'fetchCount'));
    $$('form#filterForm input#fetch').each (this.attach.bind(this, 'click', 'fetchData'));

    this.attach('submit', 'callbackFilter', 'filterForm');

    console.log ("Retrieve the count");
    new Ajax.Request ("count", {method: 'post',
                                onFailure: function () {
                                    $('loading').hide ();
                                    alert ("Failed to retrieve list");
                                },
                                onSuccess: this.loadCount.bind (this)});

    console.log ("Leaving initialize()");
};

/**

Attach a method to an element's event.

<p>

This is just a small wrapper around bindAsEventListener that to avoid
a certain amount of excess typing.  It helps us make sure that 'this'
is bound to the instantiated object instead of some other item.

@param {String} event The name of the event to observe
@param {String} handler The name of the handler routine to run
@param {Element,String} el The element to which it should be bound

*/
Corpus.prototype.attach = function (event, handler, el) {
    $(el).observe (event, this[handler].bindAsEventListener (this));
};

/**

Fetch the count

<p>

This generates a request to retrieve corpus counts from the server.

*/

Corpus.prototype.fetchCount = function (e) {
    console.log ("Entering fetchCount()");

    $('loading').show ();

    var el = Event.element (e);
    new Ajax.Request ("count", {method: 'post',
                                onFailure: function () {
                                    $('loading').hide ();
                                    alert ("Failed to retrieve list");
                                },
                                onSuccess: this.loadCount.bind (this),
                                parameters: el.form.serialize(false)});

    console.log ("Leaving fetchCount()");
};

/**

Fetch the dataset.

<p>

This generates a request to retrieve corpus records from the server.

*/

Corpus.prototype.fetchData = function (e) {
    console.log ("Entering fetchData()");

    $('loading').show ();

    var el = Event.element (e);
    new Ajax.Request ("list", {method: 'post',
                               onFailure: function () {
                                   $('loading').hide ();
                                   alert ("Failed to retrieve list");
                               },
                               onSuccess: this.loadData.bind (this),
                               parameters: el.form.serialize(false)});

    console.log ("Leaving fetchData()");
};

/**

Handle receipt of the count

<p>

This gets the data from the response and updates the span contents

@param {XmlHttpRequest} transport The transport from which we can get our count

*/

Corpus.prototype.loadCount = function (transport) {
    console.log ("Entering loadCount()");

    try {
        this.corpus_count = new Number (transport.responseText.evalJSON (false));
    } catch (e) {
        $('loading').hide ();
        alert ("Error in data, please email support@antespam.com");
    }

    $('loading').hide ();

    $('corpus_count').update(this.corpus_count);
    if ($('corpus_total').innerHTML == "0") {
        $('corpus_total').update(this.corpus_count);
    }


    console.log ("Leaving loadCount()");
};

/**

Handle receipt of the dataset.

<p>

This pulls the JSON we received into a hash.  It also copies it into
the hash of selected records, then hand off to render the html.

@param {XmlHttpRequest} transport The transport from which we can get our JSON

*/

Corpus.prototype.loadData = function (transport) {
    console.log ("Entering loadData()");

    console.log ("Evaling JSON");

    console.time ("JSON");

    try {
        this.messages = new Hash (transport.responseText.evalJSON (true));
    } catch (e) {
        $('loading').hide ();
        alert ("Error in data, please email support@antespam.com");
    }

    console.timeEnd ("JSON");

    console.log ("Sanitizing fields for HTML");
    this.messages.each (function (r) {
        if (r.value.sender)
            r.value.sender = r.value.sender.escapeHTML();
        if (r.value.subject)
            r.value.subject = r.value.subject.escapeHTML();
        if (r.value.recipient)
            r.value.recipient = r.value.recipient.escapeHTML();
    });

    $('loading').hide ();

    this.renderHTML();

    console.log ("Leaving loadData()");
};

/**

Render the data into HTML.

<p>

Throw up a progress box, then sort the records as requested and chop
it into page-sized chunks.  Grab the page we want; if there's
something there, slap the HTML up.

*/

Corpus.prototype.renderHTML = function () {
    console.log ("Entering render()");

    this.countAdjust ();

    $$('div.formblock').each (function (e) {
        e.show ()
    });
    $('corpus').show ();

    $('sorting').show ();

    console.time ("Sort");

    console.log ("Sorting and splitting data into pages");

    var start = this.page * this.pagelength;

    console.log ("First record is " + start);

    var displayable = this.messages.values().sort(this.sortfunction).slice (start, start + this.pagelength);

    console.log ("We have our displayables");

    console.timeEnd ("Sort");

    // Iterate over our list to produce html of the table
    if (displayable) {

        console.time ("Display");

        // Start with an empty array
        var html = new Array();

        // Iterate over the list
        displayable.each (function (r) {
            html.push (this.template.evaluate (r));
        }.bind(this));

        // Update the HTML for the table
        $('corpus_body').update (html.join (''));

        // Make sure buttons and links are set to work properly
        $('corpus_body').getElementsBySelector ('input[type="image"]').each (this.attach.bind(this, 'click', 'callbackSingle'));
        $('corpus_body').getElementsBySelector ('input[type="button"]').each (this.attach.bind(this, 'click', 'callbackSingle'));

        console.timeEnd ("Display");
    }

    // Admit that we're done
    $('sorting').hide ();

    console.log ("Disabling buttons based on page number");

    $$('div.formblock input[type="button"]').each (function (el) {
        if ((this.page == 0 && (el.name == "first" || el.name == "previous")) || (this.page == this.totalpages - 1 && (el.name == "next" || el.name == "last"))) {
            el.disabled = true;
        } else {
            el.disabled = false;
        }
    }.bind(this));

    console.log ("Leaving render()");
};

/**

*/

Corpus.prototype.countAdjust = function () {
    console.log ("Entering countAdjust()");

    this.totalpages = (this.messages.size() / this.pagelength | 0) + 1;

    console.log ("totalpages is " + this.totalpages);

    this.page = Math.min (this.page, this.totalpages - 1);

    console.log ("current page is " + this.page);

    $$('div.formblock span.count').invoke ('update', this.totalpages);
    $('corpus_count').update(this.messages.size());

    $$('div.formblock input[name="current"]').each (function (el) {
        el.value = this.page + 1;
    }.bind(this));

    console.log ("Leaving countAdjust()");
};

/**

Select the sort function for the system.

<p>

Grovels through the column and direction bits to come up with a good
comparison function.

*/

Corpus.prototype.chooseSort = function () {
    console.log ("Entering chooseSort()");

    if (this.direction == "ascending") {

        console.log ("Sort is ascending");
        switch (this.sortcolumn) {
        case 'domain':
            this.sortfunction = function (a, b) {
                return a['domain'] < b['domain'] ? -1 : a['domain'] === b['domain'] ? a['sender'] < b['sender'] ? -1 : a['sender'] === b['sender'] ? 0 : 1 : 1;
            }.bind (this);
            break;
        case 'size':
            this.sortfunction = function (a, b) {
                return a['size'] - b['size'];
            }.bind (this);
            break;
        default:
            this.sortfunction = function (a, b) {
                return a[this.sortcolumn] < b[this.sortcolumn] ? -1 : a[this.sortcolumn] === b[this.sortcolumn] ? 0 : 1;
            }.bind (this);
            break;
        }
    } else {

        console.log ("Sort is descending");
        switch (this.sortcolumn) {
        case 'domain':
            this.sortfunction = function (a, b) {
                return b['domain'] < a['domain'] ? -1 : b['domain'] === a['domain'] ? b['sender'] < a['sender'] ? -1 : b['sender'] === a['sender'] ? 0 : 1 : 1;
            }.bind (this);
            break;
        case 'size':
            this.sortfunction = function (a, b) {
                return b['size'] - a['size'];
            }.bind (this);
            break;
        default:
            this.sortfunction = function (a, b) {
                return b[this.sortcolumn] < a[this.sortcolumn] ? -1 : b[this.sortcolumn] === a[this.sortcolumn] ? 0 : 1;
            }.bind (this);
            break;
        }
    }
    console.log ("Leaving chooseSort()");
}

/**

Intended to be a parameter to Enumerable.collect, it finds checkbox
elements that are checked.

@param {Element} el The element to check for checkedness
@see Enumerable.collect

*/

Corpus.prototype.checked = function (el) {
    if (el.checked) {
        Form.disable (el.form);
        return el.value;
    }
};


/**

Common code for making disposition request

@param {String} action The action to take on the message(s)
@param {Array] ids The list of ids upon which to act

*/

Corpus.prototype.dispose = function (action, last, ids) {
    console.log ("Entering dispose()");

    console.log ("Making request");
    new Ajax.Request (action,
                      {evalScripts : true,
                       onFailure : function () {
                           alert ("Couldn't remove entry");
                       },
                       onSuccess : function () {
                           ids.each (function (id) {
                               $(id).remove();
                               if ($(id + "_body"))
                                   $(id + "_body").remove();
                               this.messages.remove (id);
                               $('corpus_total').update(new Number ($('corpus_total').innerHTML) - 1);
                           }.bind(this));
                           this.countAdjust ();
                           console.log ("Checking to see if we need to refill the page");
                           if (last) this.renderHTML();
                       }.bind(this),
                       parameters : {id: ids}});

    console.log ("Leaving dispose()");
};

/**

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackMultiple = function (e) {
    var el = Event.element (e);
    var action = el.readAttribute ('name');

    if ( window.confirm('Are you sure you want to ' + action.charAt(0).toUpperCase() + action.slice(1) + ' all of the selected emails?') ) {
        var count = $$('tbody#corpus_body tr').size();
        var end_count = count - $$('tbody input[type="checkbox"]').collect (this.checked).size();

        $$('tbody input[type="checkbox"]').collect (this.checked).compact().eachSlice (10, function (ids) {
            console.log (end_count + "/" + count);
            console.log (ids);
            count -= 10;
            var last = (this.totalpages > 1 && count <= end_count && end_count < (this.pagelength / 2)) ? true : false;
            console.log ("Last is " + last);
            this.dispose (action, last, ids);
        }.bind(this));
        $$('input.toggle').each (this.uncheck);
    }
    console.log ("Leaving callbackMultiple");
};

/**

Handle a button from a single message

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackSingle = function (e) {
    console.log ("Entering callbackSingle()");
    var el = Event.element (e);
    var id = el.up ('tr').id;
    var action = el.readAttribute ('name');

    switch (action) {
    case 'display':
        this.display (id);
        break;
    case 'passlist':
        this.passlist (id);
        break;
    default:
        var last = (this.totalpages > 1 && $$('tbody#corpus_body tr').size() - 1 < (this.pagelength / 2)) ? true : false;
        console.log ("Last is " + last);
        this.dispose (action, last, [id]);
        break;
    }

    console.log ("Leaving callbackSingle()");
};

/**

Display a single message in-line

@param {String} id ID of message to act on

*/

Corpus.prototype.display = function (id) {
    console.log ("Entering display()");

    var body_id = id + "_body";

    console.log ("Checking for existing copy of the message");
    if ($(body_id)) {
        $(body_id).toggle();
        $(id).down ("input[name='display']").value="+";
        $(id).down ("input[name='display']").src="/static/images/show.png";
        $(id).down ("input[name='display']").title="Show this message";
    } else {
        console.log ("Retrieving copy of the message");
        var link = "message/" + id;
        new Ajax.Updater (id, link, {method: 'get',
                                     insertion: Insertion.After});
        $(id).down ("input[name='display']").value="-";
        $(id).down ("input[name='display']").src="/static/images/hide.png";
        $(id).down ("input[name='display']").title="Hide this message";
    }

    console.log ("Leaving display()");
};

/**

Quick-add a passlist entry

@param {String} id ID of message to act on

*/

Corpus.prototype.passlist = function (id) {
    console.log ("Entering pass()");

    var promptText = this.advanced ? "Edit the sender, or enter all or part of the email subject, then click OK." : "Edit the sender, if desired, then click OK.";

    var sender = window.prompt (promptText, this.messages[id].sender);

    console.log ("sender is %s", sender);

    if (sender) {
        new Ajax.Request ("../list/pass/add",
                          {method: 'post',
                           onFailure: function () {
                               alert ("Failed to retrieve list");
                           },
                           onSuccess: function () {
                               $(id).down ("input[name='passlist']").remove();
                           },
                           parameters: {"add": sender}});
    }

    console.log ("Leaving pass()");
};

/**

Callback for clicking on a header of a sortable column.

<p>

Figures out what the new sort function is

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackSort = function (e) {
    console.log ("Entering callbackSort()");

    // Get our element from the event
    var el = Event.element (e);

    // Set the new sort column and direction
    var classNames = el.classNames().toString().split(/\s+/);
    classNames.each (function (i) {
        if (i == "created" || i == "sender" || i == "domain" || i == "recipient" || i == "subject" || i == "size") {
            newsort = i;
        }
    }.bind(this));

    console.log ("New sort is " + newsort);

    newdirection = el.hasClassName('ascending') ? "descending" : "ascending";

    console.log ("New direction is " + newdirection);

    console.log ("Removing asc/desc classes from " + this.sortcolumn);

    // Remove classes from existing sort column
    $$('th.' + this.sortcolumn).each (function (r) {r.removeClassName ("ascending");});
    $$('th.' + this.sortcolumn).each (function (r) {r.removeClassName ("descending");});

    console.log ("Recording new sort info");

    // Set the new sort column and direction
    this.sortcolumn = newsort;
    this.direction = newdirection;

    // Actually set the sort function
    this.chooseSort();

    // Remove classes from existing sort column
    $$('th.' + this.sortcolumn).each (function (r) {r.addClassName (this.direction)}.bind(this));

    // Blast the HTML out
    this.renderHTML ();
    console.log ("Leaving callbackSort()");
};

/**

Toggle the state of _all_ the checkboxes

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackToggle = function (e) {
    var el = Event.element (e);
    var c = el.checked;
    $$('table input[type="checkbox"]').each (function (i) {
        i.checked = c;
    });
};

/**

Move a relative amount based on the pressed button

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackRelativePage = function (e) {
    console.log ("Entering callbackRelativePage()");

    $('loading').show ();

    var el = Event.element (e);
    var action = el.readAttribute ('name');

    var newpage;

    switch (action) {
    case 'first':
        newpage = 0;
        break;
    case 'previous':
        newpage = Math.max (this.page - 1, 0);
        break;
    case 'next':
        newpage = Math.min (this.page + 1, this.totalpages - 1);
        break;
    case 'last':
        newpage = this.totalpages - 1;
        break;
    }

    console.log ("new page is " + newpage);

    if (newpage != this.page) {
        console.log ("Re-rendering with new page()");
        this.page = newpage;
        this.renderHTML();
    }

    $('loading').hide ();

    console.log ("Exiting callbackRelativePage()");
};

/**

Move to an absolute page

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackPage = function (e) {
    console.log ("Entering callbackPage()");

    console.log ("Stopping event");
    Event.stop (e);

    $('loading').show ();

    var el = Event.element (e);

    var form = el.tagName.toLowerCase() == "form" ? el : el.form;

    console.log ("form %o", form);

    el = Element.down (form, 'input[type="text"]');

    var newpage = Math.min (Math.max (el.value - 1, 0), this.totalpages - 1);

    console.log ("new page is " + newpage);

    if (newpage != this.page) {
        console.log ("Re-rendering with new page()");
        this.page = newpage;
        this.renderHTML();
    }

    this.countAdjust();

    $('loading').hide ();

    console.log ("Exiting callbackPage()");
};

/**

Filter the data on the specified string

@param {Event} e Event that triggered the callback

*/

Corpus.prototype.callbackFilter = function (e) {
    console.log ("Entering callbackFilter()");

    console.log ("Stopping event");
    Event.stop (e);

    var el = Event.element (e);

    var form = el.tagName.toLowerCase() == "form" ? el : el.form;

    console.log ("form %o", form);

    subject = Element.down (form, 'input[name="subject"]');

    this.subjectfilter = subject.value.toLowerCase();

    console.log ("new subject filter is " + this.subjectfilter);

    this.page = 0;

    this.loadData();
    this.renderHTML();

    $$('div.formblock input[name="subject"]').each (function (el) {
        el.value = this.subjectfilter;
    }.bind(this));

    console.log ("Exiting callbackPage()");
};

/**

Intended to be run across a collection of elements to uncheck using
Enumerable.each(), to uncheck them.

@param {Element} el The element we should uncheck
@see #callbackMultiple

*/

Corpus.prototype.uncheck = function (el) {
    el.checked = false;
};
