/**
 * An autocompleter implementation that provides a more advanced UI
 * (completed elements are stored in separate DIV elements).
 *
 * Events handled by this class:
 *   - AutoComplete:focus
 *   - AutoComplete:handlers
 *   - AutoComplete:reset
 *   - AutoComplete:update
 *
 * Events triggered by this class:
 *   - AutoComplete:resize
 *
 * @author     Michael Slusarz <slusarz@horde.org>
 * @author     Michael J Rubinsky <mrubinsk@horde.org>
 * @copyright  2008-2015 Horde LLC
 * @license    GPL-2 (http://www.horde.org/licenses/gpl)
 */
var IMP_PrettyAutocompleter = Class.create({

    // box,
    // dimg,
    // elt,
    // input,

    itemid: 0,
    lastinput: '',

    initialize: function(elt, params)
    {
        var ac, active, p_clone;

        this.p = Object.extend({
            // Outer div/fake input box and CSS class
            // box (created below)
            boxClass: 'hordeACBox',
            boxClassFocus: '',
            // <ul> CSS class
            listClass: 'hordeACList',
            listClassItem: 'hordeACListItem',
            // input (created below)
            // CSS class for real input field
            growingInputClass: 'hordeACTrigger impACTrigger',
            removeClass: 'hordeACItemRemove',
            // Allow for a function that filters the display value
            // This function should *always* return escaped HTML
            displayFilter: function(t) { return t.escapeHTML(); },
            filterCallback: this.filterChoices.bind(this),
            maxItemSize: 50,
            onAdd: Prototype.K,
            onRemove: Prototype.K,
            processValueCallback: this.processValueCallback.bind(this),
            requireSelection: false
        }, params || {});

        // The original input element is transformed into the hidden input
        // field that hold the text values.
        this.elt = $(elt);

        this.box = new Element('DIV', { className: this.p.boxClass });

        // The input element and the <li> wrapper
        this.input = new Element('INPUT', {
            autocomplete: 'off',
            className: this.p.growingInputClass
        });

        // Build the outer box
        this.box.insert(
            // The list - where the chosen items are placed as <li> nodes
            new Element('UL', { className: this.p.listClass }).insert(
                new Element('LI').insert(this.input)
            )
        );

        // Replace the single input element with the new structure and
        // move the old element into the structure while making sure it's
        // hidden.
        active = (document.activeElement && (document.activeElement == this.elt));
        this.box.insert(this.elt.replace(this.box).hide());
        if (active) {
            this.focus();
        }

        // Look for clicks on the box to simulate clicking in an input box
        this.box.observe('click', this.clickHandler.bindAsEventListener(this));

        // Double-clicks cause an edit on existing entries.
        this.box.observe('dblclick', this.dblclickHandler.bindAsEventListener(this));

        this.input.observe('blur', this.blur.bind(this));
        this.input.observe('keydown', this.keydownHandler.bindAsEventListener(this));

        new PeriodicalExecuter(this.inputWatcher.bind(this), 0.25);

        p_clone = $H(this.p).toObject();
        p_clone.onSelect = this.updateElement.bind(this);
        p_clone.paramName = this.elt.readAttribute('name');
        p_clone.tokens = [];

        ac = new Ajax.Autocompleter(this.input, this.p.uri, p_clone);
        ac.getToken = function() {
            return $F(this.input);
        }.bind(this);

        this.reset();

        document.observe('AutoComplete:focus', function(e) {
            if (e.memo == this.elt) {
                this.focus();
                e.stop();
            }
        }.bindAsEventListener(this));
        document.observe('AutoComplete:handlers', function(e) {
            e.memo[this.elt.identify()] = this;
        }.bind(this));
        document.observe('AutoComplete:reset', this.reset.bind(this));
        document.observe('AutoComplete:update', this.processInput.bind(this));
    },

    focus: function()
    {
        this.input.focus();
        this.box.addClassName(this.p.boxClassFocus);
    },

    blur: function()
    {
        this.box.removeClassName(this.p.boxClassFocus);
    },

    reset: function()
    {
        this.currentEntries().invoke('remove');
        this.updateInput('');
        this.addNewItem(this.processValue($F(this.elt)));
    },

    processInput: function()
    {
        this.addNewItem($F(this.input));
        this.updateInput('');
    },

    processValue: function(val)
    {
        if (this.p.requireSelection) {
            return val;
        }

        return this.p.processValueCallback(this, val.replace(/^\s+/, ''));
    },

    processValueCallback: function(ob, val)
    {
        var chr, pos = 0;

        chr = val.charAt(pos);
        while (chr !== "") {
            if (ob.p.tokens.indexOf(chr) === -1) {
                ++pos;
            } else {
                if (!pos) {
                    val = val.substr(1);
                } else {
                    ob.addNewItem(val.substr(0, pos));
                    val = val.substr(pos + 2);
                    pos = 0;
                }
            }

            chr = val.charAt(pos);
        }

        return val.replace(/^\s+/, '');
    },

    // Used as the updateElement callback.
    updateElement: function(item)
    {
        if (this.addNewItem(item)) {
            this.updateInput('');
        }
    },

    /**
     * Adds a new element to the UI, ignoring duplicates.
     *
     * @return boolean True on success, false on failure/duplicate.
     */
    addNewItem: function(value)
    {
        var displayValue;

        // Don't add if it's already present.
        if (value.empty() || !this.filterChoices([ value ]).size()) {
            return false;
        }

        displayValue = this.p.displayFilter(value.truncate(this.p.maxItemSize));

        this.input.up('LI').insert({
            before: new Element('LI', {
                        className: this.p.listClassItem,
                        title: value
                    })
                    .insert(displayValue)
                    .insert(this.deleteImg().clone(true).show())
                    .store('raw', value)
                    .store('itemid', ++this.itemid)
        });

        // Add to hidden input field.
        this.updateHiddenInput();

        this.p.onAdd(value);

        return true;
    },

    filterChoices: function(c)
    {
        var cv = this.currentValues();

        return c.select(function(v) {
            return !cv.include(v);
        });
    },

    currentEntries: function()
    {
        return this.input.up('UL').select('LI.' + this.p.listClassItem);
    },

    currentValues: function()
    {
        return this.currentEntries().invoke('retrieve', 'raw');
    },

    updateInput: function(input)
    {
        if (Object.isElement(input)) {
            input = input.remove().retrieve('raw');
            this.updateHiddenInput();
        }

        this.input.setValue(input);
        this.resize();
    },

    updateHiddenInput: function()
    {
        this.elt.setValue(this.currentValues().join(', '));
    },

    resize: function()
    {
        this.input.setStyle({
            width: Math.max(80, $F(this.input).length * 9) + 'px'
        });
        this.input.fire('AutoComplete:resize');
    },

    toObject: function(elt)
    {
        var ob = {};

        this.currentEntries().each(function(c) {
            ob[c.retrieve('itemid')] = elt ? c : c.retrieve('raw');
        });

        return ob;
    },

    deleteImg: function()
    {
        if (!this.dimg) {
            this.dimg = new Element('IMG', {
                className: this.p.removeClass,
                src: this.p.deleteIcon
            }).hide();
            this.box.insert(this.dimg);
        }

        return this.dimg;
    },

    /* Event handlers. */

    clickHandler: function(e)
    {
        var elt = e.element();

        if (elt.hasClassName(this.p.removeClass)) {
            elt.up('LI').remove();
            this.updateHiddenInput();
        }

        this.focus();
    },

    dblclickHandler: function(e)
    {
        var elt = e.findElement('LI');

        if (elt && elt.hasClassName(this.p.listClassItem)) {
            this.addNewItem($F(this.input));
            this.updateInput(elt);
        } else {
            this.focus();
        }
    },

    keydownHandler: function(e)
    {
        var tmp;

        switch (e.which || e.keyCode || e.charCode) {
        case Event.KEY_DELETE:
        case Event.KEY_BACKSPACE:
            if (!$F(this.input).length &&
                (tmp = this.currentEntries().last())) {
                this.updateInput(tmp);
                e.stop();
            }
            break;
        }
    },

    inputWatcher: function()
    {
        var input = $F(this.input), processed;

        if (input != this.lastinput) {
            processed = this.processValue(input);
            if (processed != input) {
                this.input.setValue(processed);
            }
            this.lastinput = $F(this.input);
            this.resize();
            // Pre-load the delete image now.
            this.deleteImg();
        }
    }

});
