/*
 * Ironic Design Field Filter
 * Copyright 2011, FanMail.com, L.L.C., All Rights Reserved.
 * http://fanmail.com/
*/
/* global console, dojo, jQuery */
(function () {
    var c = console;
    var d = dojo;

    d.provide ("IDFieldFilter");

    d.require ("dojo.DeferredList");
    d.require ("dojo.NodeList-data");
    d.require ("dojo.NodeList-fx");
    d.require ("dojo.NodeList-manipulate");
    d.require ("dojo.NodeList-traverse");

    // Watches a field for events
    var IDFieldWatcher = d.declare (null, {

        // Given a name and a dojo.NodeList, initialize the
        // object, marking it as not selected and attaching event
        // handlers to it.
        constructor: function (name, field) {
            c.log ("Constructing IDFieldWatcher for", name);
            this._field = field;
            this._name = name;
            this._selected = 0;
            this._value = this.getValue ();
            this.setEventHandlers ();
        },

        // Return the label for the field
        getField: function () {
            return this._field;
        },

        // Return the label for the field
        getLabel: function () {
            if (this._label === undefined) {
                this._label = this.getField ().prev ("label").text ().replace (/:\s\*$/, "");
            }
            return this._label;
        },

        // Return the name of the field
        getName: function () {
            return this._name;
        },

        getSelectValues: function () {
            return this.getField ().children ("option").attr ("value");
        },

        // Return the fields state of selection
        getSelected: function () {
            return this._selected;
        },

        // Get the value of the field
        getValue: function () {
            var result;
            c.log ("Retrieving value");
            var value = this.getField ().val ();
            if (value != null && value != "") {
                c.log ("Field had value", value);
                result = value;
            }
            return result;
        },

        isManaged: 0,

        // Event attachment point
        onChange: function (changed) {
            c.log ("Retrieving new value");
            var newValue = this.getValue ();
            if (newValue !== this._value) {
                c.log ("New value", newValue, "and old value", this._value, "do not match");
                this._value = newValue;
                c.log ("Publishing change notification");
                d.publish ("IDFieldChange", [this]);
                var field = this.getField ();
                c.log ("Trigging jquery for", field[0]);
                jQuery(field[0]).trigger ('change');
            }
        },

        // Set event handler(s) for the DOM node, and attach the object to it
        setEventHandlers: function () {
            var field = this.getField ();

            field.data ('IDField', this);

            var handler = d.hitch (this, function () {
                this.onChange (this);
            });

            // field is a NodeList, dereference to get the DOM node
            var node = field[0];

            switch (node.tagName.toLowerCase()) {
            case 'select':
                field.connect ("onchange", handler);
                field.connect ("onkeyup", handler);
                break;
            case 'input':
                // getNodeProp interpolates DOM defaults
                if (d.getNodeProp (node, 'type') == "text") {
                    field.connect ("onblur", handler);
                } else {
                    //c.error ("Don't know the type of ", field);
                }
                break;
            default:
                //c.error ("Don't know the type of ", field);
                break;
            }
        },

        // If the field is selected, set its selection time, or clear it.
        setSelected: function (selected) {
            if (selected === true) {
                this._selected = (new Date).getTime ();
                c.log ("Setting modtime for field", this.getName (), "to", this._selected);
                c.log ("Getting current value from", this.getName ());
                this._value = this.getValue ();
                c.log ("Value is", this._value);
            } else if (selected === false) {
                this._selected = 0;
                c.log ("Clearing values from superseded field", this.getName ());
                // On the off chance this is a field with only one
                // possible value, we set it to something we know will
                // show up as empty
                this.getField ().innerHTML ('<option value="">- Select -</option>');
                this._value = undefined;
            } else {
                //c.error ("Must give me a boolean value", selected);
            }
        },

        // Set the value of the field, and generate a change event
        setValue: function (newValue) {
            c.log ("Setting value for field", this.getName (), "to", newValue, "among options", this.getSelectValues());
            var field = this.getField ();
            field.val (newValue);
            var check = field.val ();
            if (check === newValue) {
                c.log ("New value of", this.getName(), "is", this.getValue ());
                this.onChange (this);
                this._value = newValue;
            } else {
                //c.error ("Couldn't set", this.getName (), "to", newValue);
            }
        }
    });

    // A field we're actively managing
    var IDFieldManager = d.declare (IDFieldWatcher, {
        constructor: function (name, field) {
            c.log ("Constructing IDFieldManager for", name);
        },

        // Get the containing <div> tag
        getDiv: function () {
            if (this._div === undefined) {
                this._div = this.getField ().closest ("div.webform-component");
            }
            return this._div;
        },

        // Get the containing <fieldset> tag if there is one
        getFieldSet: function () {
            var fieldSet;
            if (this._fieldSet === undefined) {
                fieldSet = this.getField ().closest ("fieldset");
                if (fieldSet.length >= 1) {
                    this._fieldSet = fieldSet;
                } else {
                    this._fieldset = false;
                }
            }
            return this._fieldSet;
        },

        // Is the field disabled
        isInactive: function () {
            return this.getField ().attr ("disabled")[0] === true;
        },

        // Is the div hidden
        isHidden: function () {
            return this.getDiv ().style ("display")[0] === "none";
        },

        isManaged: 1,

        // Hide the div containing the field
        setHidden: function () {
            this.setInactive ();
            this.getDiv ().style ("display", "none");
        },

        // Set the field to be inactive
        setInactive: function (required) {
            this.getField ().innerHTML ('<option value="">- Select -</option>').attr ("disabled", true);
        },

        // Given the set of options for the field, put them in place
        // and make sure it's enabled and displayed
        setOptions: function (options) {
            var field = this.getField ();
            var output = d.map (options, function (option) {
                return '<option value="'+ option[0] +'">'+ option[1] +'</option>';
            }).join ('');
            c.log ("Output is", output);
            field.innerHTML (output).attr ("disabled", false);
            if (options.length !== 1) {
                field.val ("");
            } else {
                var newValue = this.getValue ();
            }
            this.getDiv ().style ("display", "block");
        }
    });

    // Handle sorting arrayrefs on their second item
    var option_sort = function (a, b) {
        if (a[1] < b[1])
            return -1;
        else if (a[1] > b[1])
            return 1;
        else
            return 0;
    };

    var findField = function (name) {
        var query, result;
        query = 'input[name$="[' + name + ']"], select[name$="[' + name + ']"]';
        c.log ("Looking for", query);
        if (d.isIE < 8) {
            var nodes = jQuery (query);
            if (nodes.size () === 1) {
                result = new d.NodeList ();
                result.push (nodes.get(0));
            } else if (nodes.size () > 1) {
                //c.error (name, "found more than one field", nodes);
            } else {
                //c.error (name, "not found");
            }
        } else {
            var field = d.query (query);
            if (field.length === 1) {
                result = field;
            } else if (field.length > 1) {
                //c.error (name, "found more than one field", field);
            } else {
                //c.error (name, "not found");
            }
        }
        return result;
    };

    // a group of fields that we're watching and/or manipulating
    d.declare ('IDFieldFilter', null, {
        constructor: function (required, managed, baseurl, defaults, data) {
            c.log ("Constructing IDFieldFilter");
            var deferreds = [], getDefaults, url;

            // If we get an URL we request the defaults
            if (typeof defaults === "string") {
                url = baseurl + "/" + defaults;
                c.log ("Loading defaults from", url);
                deferreds.push (d.xhrGet ({handleAs: "json",
                                           url: url}));
            } else {
                // If
                getDefaults = new d.Deferred;
                if (typeof defaults === "object") {
                    getDefaults.resolve (defaults);
                } else {
                    if (defaults !== undefined) {
                        //c.error ("Don't know how to deal with defaults of", defaults);
                    }
                    getDefaults.resolve ({});
                }
                deferreds.push (getDefaults);
            }

            var i = data.length;
            while (i--) {
                url = baseurl + "/" + data[i];
                c.log ("Loading data from", url);
                deferreds.push (d.xhrGet ({handleAs: "json",
                                           url: url}));
            }

            new d.DeferredList (deferreds).then (d.hitch (this, "setData"));
            this.setWatchers (required, managed);
            this.constructed = new d.Deferred ();
            d.subscribe ("IDFieldChange", this, "onChange");
        },

        // Return the list of fields watched/managed
        getFields: function () {
            return this._fields;
        },

        /*
         * Get the object representing the field that caused the event.
         * Look for it in the list of selected fields.  If it's found, set
         * its modification time to now (which will returnthe old
         * modification time if there was one) then un-set all fields
         * whose modification times were *after* it.  Then run our filter.
         */
        onChange: function (changed) {
            var oldselect = changed.getSelected () || (new Date).getTime ();
            c.log ("Handling change event for", changed.getName (), "with old select time", oldselect, "and value", changed.getValue ());
            var fields = this.getFields();
            var i = fields.length, field;
            while (i--) {
                field = fields[i];
                if (field === changed) {
                    c.log ("Updating modtime for selected field", field.getName ());
                    field.setSelected (true);
                } else {
                    var fieldTime = field.getSelected ();
                    if (fieldTime && fieldTime >= oldselect && field.isManaged) {
                        field.setSelected (false);
                    }
                }
            }

            var managed = [], selected = [];
            var sorted = fields.sort (function (j, k) {
                if (j.getSelected() < k.getSelected())
                    return -1;
                else if (j.getSelected() > k.getSelected())
                    return 1;
                else
                    return 0;
            });

            var max = sorted.length;
            i = 0;
            while (i < max) {
                if (sorted[i].getSelected ()) {
                    selected.push (sorted[i].getName());
                    if (sorted[i].isManaged) {
                        managed.push (sorted[i].getName());
                    }
                }
                i++;
            }

            c.log ("Selected fields", selected, "Managed fields", managed);

            this._selected.val (managed.join (","));

            this.setFields ();
        },

        onConstructed: function () {
            return this.constructed;
        },

        // Sieve for acceptable records given the current form state
        setFields: function () {
            var field, fieldList = this.getFields(), fieldName, fieldValue, fieldValues = {}, i, key, managed, name, newfields = {}, required, result, row, selected;

            managed = false;
            required = true;

            i = fieldList.length;
            while (i--) {
                field = fieldList[i];
                fieldName = field.getName ();
                fieldValues[fieldName] = {'field': field, 'value': undefined};
                fieldValue = field.getValue ();
                if (field.isManaged) {
                    if (fieldValue !== undefined) {
                        fieldValues[fieldName].value = fieldValue;
                        managed = true;
                    }
                } else {
                    if (fieldValue !== undefined) {
                        fieldValues[fieldName].value = fieldValue;
                    } else {
                        required = false;
                        break;
                    }
                }
            }
            c.log ("Current values are", fieldValues);

            if (required === true) {
                i = this._data.length;
                row: while (i--) {
                    row = this._data[i];
                    result = {};
                    for (fieldName in row) {
                        var qualifier = row[fieldName];
                        fieldValue = fieldValues[fieldName] ? fieldValues[fieldName].value : undefined;
                        if (fieldValue === undefined) {
                            if (d.isArray (qualifier)) {
                                // Nothing to do
                            } else if (d.isObject (qualifier) && qualifier != null) {
                                result[fieldName] = qualifier;
                                if (qualifier.value === undefined || qualifier.text === undefined) {
                                    //c.error ("Broken qualifier", qualifier);
                                }
                            } else {
                                //c.error (fieldName, qualifier, "is confusing");
                                continue row;
                            }
                        } else {
                            if (d.isArray (qualifier)) {
                                if (d.indexOf (qualifier, fieldValue) == -1) {
                                    continue row;
                                }
                            } else if (d.isObject (qualifier)) {
                                if (qualifier != null && qualifier.value != fieldValue) {
                                    continue row;
                                }
                            } else {
                                //c.error (fieldName, qualifier, "is confusing");
                                continue row;
                            }
                        }
                    }

                    for (fieldName in result) {
                        d.setObject (fieldName + '.' + result[fieldName].value, result[fieldName].text, newfields);
                    }
                }

                c.log ("Done iterating, field options are", newfields);

                i = fieldList.length;
                while (i--) {
                    field = fieldList[i];
                    if (field.getSelected()) {
                        continue;
                    }
                    fieldName = field.getName ();
                    if (newfields[fieldName]) {
                        c.log ("Deciding on content for", fieldName);
                        var options = [];
                        for (key in newfields[fieldName]) {
                            options.push ([key, newfields[fieldName][key]]);
                        }
                        options.sort (option_sort);
                        if (options.length !== 1) {
                            options.unshift (["", "- Select -"]);
                        }
                        field.setOptions (options);
                        if (options.length == 1) {
                            c.log ("Only one option, setting", field.getName(), "to", options[0][0]);
                            field.setSelected (true);
                            field.setValue (options[0][0]);
                        }
                        var fieldSet = field.getFieldSet ();
                        if (fieldSet) {
                            fieldSet.query ('p.activatemessage').orphan ();
                        } else {
                            field.getField ().siblings ('p.activatemessage').empty ();
                        }
                    } else if (field.isManaged) {
                        c.log (fieldName, "had no appropriate value, hiding");
                        field.setHidden ();
                    }

                }

                // No managed fields are filled in
                if (managed === false) {
                    c.log ("Handling defaults");
                    i = 0; var max = this._defaults.length;
                    // Set each of our fields to the default in turn
                    while (i < max) {
                        var selection = this._defaults[i++];
                        c.log ("Default", selection.field, "is", selection.value);
                        fieldName = selection.field;
                        if (fieldValues[fieldName]) {
                            fieldValues[fieldName].field.setValue (selection.value);
                        }
                    }
                }

            } else {
                c.log ("Requirements aren't fulfilled, not filtering yet");
                i = fieldList.length;
                while (i--) {
                    field = fieldList[i];
                    if (!field.isManaged) {
                        continue;
                    }
                    var text = '<p class="activatemessage">Please complete ' + this._missing + '</p>';
                    if ((fieldSet = field.getFieldSet())) {
                        if (!fieldSet.query ('p.activatemessage').length) {
                            fieldSet.query ('legend').after (text);
                        }
                        field.setHidden ();
                    } else {
                        field.getField ().after (text);
                        field.setInactive ();
                    }
                }
            }

            if (this.constructed) {
                c.log ("Calling back notifier", this.constructed);
                this.constructed.callback (true);
                delete this.constructed;
            }
        },

        // Handle the incoming data, and then fire off the manager
        setData: function (results) {
            // Get defaults data first
            var defaults = results.shift();
            if (defaults[0] == true) {
                this._defaults = defaults[1];
            } else {
                //c.error ("Couldn't load defaults successfully");
            }

            this._data = [];

            var a, j, i = results.length;
            while (i--) {
                if (results[i][0] == true) {
                    a = results[i][1];
                    j = a.length;
                    while (j--) {
                        this._data.push (a[j]);
                    }
                }
            }
            c.log ("Done loading data");
            this.setFields();
        },

        setWatchers: function (required, managed) {
            var f, field, fields = [], i, labels = [], name, query;
            i = required.length;
            while (i--) {
                name = required[i];
                field = findField (name);
                if (field !== undefined) {
                    f = new IDFieldWatcher (name, field);
                    fields.push (f);
                    labels.push (f.getLabel ());
                    if (f.getValue() !== undefined) {
                        c.log ("Found existing value, marking selected");
                        f.setSelected (true);
                    }
                }
            }
            this._missing = labels.join (", ");
            i = managed.length;
            while (i--) {
                name = managed[i];
                field = findField (name);
                if (field !== undefined) {
                    f = new IDFieldManager (name, field);
                    fields.push (f);
                }
            }
            this._fields = fields;
            this._selected = findField ('_idfieldfilter_selected');
        }
    });
})();

