/* input completion library */

/* TODO:
    -- test on non-US keyboard layouts (too much use of KeyCode)
    -- lazy data model (xmlhttprequest, or generic callbacks)
    -- drop-down menu?
    -- option to disable comma-separated mode (or explicitly ask for it)
*/

/*
  Copyright (c) 2005, Six Apart, Ltd.
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:

  * Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.

  * Redistributions in binary form must reproduce the above
  copyright notice, this list of conditions and the following disclaimer
  in the documentation and/or other materials provided with the
  distribution.

  * Neither the name of "Six Apart" nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/


/* ***************************************************************************

  Class: InputCompleteData

  About: An InputComplete object needs a data source to auto-complete
          from.  This is that model.  You can create one from an
          array, or create a lazy version that gets its data over the
          network, on demand.  You will probably not use this class'
          methods directly, as they're called by the InputComplete
          object.

  Constructor:

    var model = new InputCompleteData ([ "foo", "bar", "alpha" ]);

*************************************************************************** */

var InputCompleteData = new Class ( Object, {
    init: function () {
	if (arguments[0] instanceof Array) {
	    this.source = arguments[0];
	}
    },

    // method: given prefix, returns best suffix, or null if no answer
    bestFinish: function (pre) {
	if (! pre || pre.length == 0)
	    return null;

	if (! this.source)
	    return null;

	var i;
	for (i=this.source.length-1; i>=0; i--) {
	    var item = this.source[i];
	    if (item.substring(0, pre.length) == pre) {
		var suff =  item.substring(pre.length, item.length);
		return suff;
	    }
	}
	return null;
    },

    // method: given a piece of data, learn it, and prioritize it for future completions
    learn: function (word) {
	if (!word) return false;
	if (!this.source) return false;
	this.source[this.source.length++] = word;

        if (this.onModelChange)
 	   this.onModelChange();
    },

    getItems: function () {
	if (!this.source) return [];

	// return only unique items to caller
	var uniq = [];
	var seen = {};
	for (i=this.source.length-1; i>=0; i--) {
	    var item = this.source[i];
	    if (! seen[item]) {
		seen[item] = 1;
		uniq.length++;
		uniq[uniq.length - 1] = item;
	    }
	}

	return uniq;
    },

    dummy: 1
});

/* ***************************************************************************

  Class: InputComplete

  About: 

  Constructor:

*************************************************************************** */

var InputComplete = new Class( Object, {
    init: function () {
	var opts = arguments[0];
	var ele;
	var model;
	var debug;

	if (arguments.length == 1) {
	    ele = opts["target"];
	    model = opts["model"];
	    debug = opts["debug"];
	} else {
	    ele = arguments[0];
	    model = arguments[1];
	    debug = arguments[2];
	}

	this.ele   = ele;
	this.model = model;
	this.debug = debug;

	// no model?  don't setup object.
	if (! ele) {
	    this.disabled = true;
	    return;
	}

	// return false if auto-complete won't work anyway
	if (! (
	       ("selectionStart" in ele) ||
	       (document.selection && document.selection.createRange)
	       )
	    )
	{
	    this.disabled = true;
	    return;
	}

	// I don't trust closures to capture function arguments or "this"
	// after the old Safari bug.  so copy it into an explicit variable
	// on the function object's call chain, so I know it'll be captured
	var me = this;

	// register a bunch of event handler methods.  not sure how to register
	// methods, so this is a sort of ghetto hack, so each handler has acces to
	// the "this" object through the "me" variable, passed into it.
	DOM.addEventListener(ele, "focus",   function (e) { e||window.event; return InputComplete.onFocus   (me, e);});
	DOM.addEventListener(ele, "keydown", function (e) { e||window.event; return InputComplete.onKeyDown (me, e);});
	DOM.addEventListener(ele, "keyup",   function (e) { e||window.event; return InputComplete.onKeyUp   (me, e);});
	DOM.addEventListener(ele, "blur",    function (e) { e||window.event; return InputComplete.onBlur    (me, e);});
    },

    dbg: function (msg) {
	if (this.debug) {
	    this.debug(msg);
	}
    },

    // returns the word currently being typed, or null
    wordInProgress: function () {
	var sel = this.getSelectedRange();
	if (!sel) return null;
    
	var cidx = sel.selectionStart; // current indx
	var sidx = cidx;  // start of word index
	while (sidx > 0 && this.ele.value.charAt(sidx) != ',') {
	    sidx--;
	}
	var skipStartForward = function (chr) { return (chr == "," || chr == " "); }
    
	while (skipStartForward(this.ele.value.charAt(sidx))) {
	    sidx++;
	}
    
	return this.ele.value.substring(sidx, this.ele.value.length);
    },

    // appends some selected text after the care
    addSelectedText: function (chars) {
	var sel = this.getSelectedRange();
	this.ele.value = this.ele.value + chars;
	this.setSelectedRange(sel.selectionStart, this.ele.value.length);
    },

    moveCaretToEnd: function () {
	var len = this.ele.value.length;
	this.setSelectedRange(len, len);
    },

    getSelectedRange: function () {
	var ret = {};
	var ele = this.ele;

	if ("selectionStart" in ele) {
	    ret.selectionStart = ele.selectionStart;
	    ret.selectionEnd   = ele.selectionEnd;
	    return ret;
	}

	if (document.selection && document.selection.createRange) {
	    var range = document.selection.createRange();
	    ret.selectionStart = InputComplete.IEOffset(range, "StartToStart");
	    ret.selectionEnd   = InputComplete.IEOffset(range, "EndToEnd");
	    return ret;
	}
	
	return null;
    },

    setSelectedRange: function (sidx, eidx) {
	var ele = this.ele;

	// IE
	if (document.selection && document.selection.createRange) {
	    ele.focus();
	    var sel = document.selection.createRange ();
	    sel.moveStart('character', -ele.value.length);
	    sel.moveStart('character', sidx);
	    sel.moveEnd('character', eidx - sidx);
	    sel.select();
	    return true;
	}

	// mozilla
	if ("selectionStart" in ele) {
	    ele.selectionStart = sidx;
	    ele.selectionEnd   = eidx;
	    return true;
	}
	return false;
    },

    dummy: 1	
});

InputComplete.onKeyDown = function (me, e) {
    var code = e.keyCode || e.which;
   
    me.dbg("onKeyDown, code="+code+", shift="+e.shiftKey);

    // if comma, but not with a shift which would be "<".  (FIXME: what about other keyboards layouts?)
    if ((code == 188 || code == 44) && ! e.shiftKey) {
        me.moveCaretToEnd();
	return Event.stop(e);
    }

    return true;
};

InputComplete.onKeyUp = function (me, e) {
    var val = me.ele.value;

    var code = e.keyCode || e.which;
    me.dbg("keyUp = " + code);

    // ignore tab, backspace, left, right, and delete
    if (code == 9 || code == 8 || code == 37 || code == 39 || code == 46)
       return false;

    var sel = me.getSelectedRange();

    var ss = sel.selectionStart;
    var se = sel.selectionEnd;

    me.dbg("keyUp, got ss="+ss +  ", se="+se+", val.length="+val.length);

    // only auto-complete if we're at the end of the line
    if (se != val.length) return false;

    var chr = String.fromCharCode(code);

    me.dbg("keyUp, got chr="+chr);

    if (code == 188 || chr == ",") {
	me.dbg("hit comma! .. value = " + me.ele.value);

	me.ele.value = me.ele.value.replace(/[\s,]+$/, "") + ", ";
	me.moveCaretToEnd();

	return Event.stop(e);
    }


    var inProg = me.wordInProgress();
    if (!inProg) return true;

    var rest = me.model.bestFinish(inProg);

    if (rest && rest.length > 0) {
	me.addSelectedText(rest);
    }
};

InputComplete.onBlur = function (me, e) {
    var tg = e.target || e.srcElement;

    var list = tg.value;

    var noendjunk = list.replace(/[\s,]+$/, "");
    if (noendjunk != list) {
	tg.value = list = noendjunk;
    }

    var tags = list.split(",");
    for (var i =0; i<tags.length; i++) {
	var tag = tags[i].replace(/^\s+/,"").replace(/\s+$/,"");
	if (tag.length) {
	    me.model.learn(tag);
	}
    }
};

InputComplete.onFocus = function (me, e) {
};


InputComplete.IEOffset = function ( range, compareType ) {
    var range2 = range.duplicate();
    range2.collapse( true );
    var parent = range2.parentElement();
    var length = range2.text.length;
    range2.move("character", -parent.value.length);

    var delta = max( 1, finiteInt( length * 0.5 ) );
    range2.collapse( true );
    var offset = 0;
    var steps = 0;
        
    // bail after 10k iterations in case of borkage
    while( (test = range2.compareEndPoints( compareType, range )) != 0 ) {
	if( test < 0 ) {
	    range2.move( "character", delta );
	    offset += delta;
	} else {
	    range2.move( "character", -delta );
	    offset -= delta;
	}
	delta = max( 1, finiteInt( delta * 0.5 ) );
	steps++;
	if( steps > 1000 )
	    throw "unable to find textrange endpoint in " + steps + " steps";
    }

    return offset;
};
