var PraxeonObject = {
 create : function() {
    function F(){}
    F.prototype = this;
    var o = new F();
    o.initialize.apply(o, arguments);
    return o;
  },
 
 initialize : function() {
    //placeholder
    //sub-objects override this
  },
 
 //Extend an object with another's properties and methods
 extend : function(target, definition) {
    for(var i in definition) {
      var val = definition[i];
      target[i] = val;
    }
    return target;
  },
 
 /* Use this to create a new Praxeon object, the new object's prototype will be this object
    The object returned can be 'instantiated' with .create() */
 construct : function(definition) {
    var o = this.create();
    return this.extend(o, definition);
  }
};

var PraxeonWidget = PraxeonObject.construct({
    //shared table for callbacks
    callbacks : {
	'callback' : function() {
	    //the default, does nothing
	}
    },
      
    /* Accessor functions so widgets that inherit from here can get to the callback table */
    set_callback : function(callback_name, fn) {
	PraxeonWidget.callbacks[callback_name] = fn;
    },
    
    get_callback : function(callback_name) {
	if(callback_name) {
	    return PraxeonWidget.callbacks[callback_name];
	}
    },
    
    run_callback : function() {
	var callback_name = arguments[0];
	if(callback_name) {
	    var args = [];
	    for(var i=1; i< arguments.length; i++) {
		args.push(arguments[i]);
	    }
	    var fn = PraxeonWidget.get_callback(callback_name);
	    if(fn) {
		fn.apply(null, args);
	    }
	}
    },
    

    /* Useful methods for sub-widgets */

    generate_unique_callback_string : function() {
	    var str = 'callback_';
	    var choices = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
	    for(var i=0; i<11;i ++) {
		str = str + choices[Math.round(Math.random() * (choices.length - 1))]
	    }
	    if(!PraxeonWidget.get_callback(str)) {
		return str;
	    }
	    else {
		return this.generate_unique_callback_string();
	    }
     },

     is_element : function(el_or_id) {
           return el_or_id.constructor != String; //this could be better...
     },

     element_for_element_or_id : function(el_or_id) {
	   if(this.is_element(el_or_id)) {
	       return el_or_id
	   }
	   else {
	       return document.getElementById(el_or_id);
	   }    
     },

     insert_script_tag : function(url) {
          var script_el = document.createElement('script');
	  script_el.setAttribute('src', url);
	  document.body.appendChild(script_el);
	  return script_el;
     }

});


var Suggestor = PraxeonWidget.construct({
	suggestions_class_name : 'suggestions',
	inactive_period : 0.5,
	max_options : 8,
    
	input_element_id : null,
	display_element_id : null,
	
	input_element_cache : null,
	display_element_cache : null,
	
	delayed_checker : null,
     
	initialize : function(input_element_or_id, display_element_or_id) {
	    if(this.is_element(input_element_or_id)) {
		this.input_element_cache = input_element_or_id;
	    }
	    else {
		this.input_element_id = input_element_or_id;
	    }
	    if(this.is_element(display_element_or_id)) {
		this.display_element_cache = display_element_or_id;
	    }
	    else {
		this.display_element_id = display_element_or_id;
	    }
	},

	//cleans up and behaviours than might interfere
	//can also be used if you're not sure whether an element might already have a suggestor attached
	prepare : function(input_element_or_id) {
	    var el;
	    if(this.is_element(input_element_or_id)) {
		el = input_element_or_id;
	    }
	    else {
		el = document.getElementById(input_element_or_id);
	    }
	    el.onkeypress = null;
	},

	start : function() {
	    var suggestor = this;
	    this.input_element().onkeypress = function(event) { 
		suggestor.handle_text_change(event); 
	    };
	    return this;
	},
	
	stop : function() {
	    this.input_element().onkeypress = null;
	    if(this.deleted_checker) this.delayed_checker.stop();
	    return this;
	},
	
	input_element : function() {
	    if(!this.input_element_cache) {
		this.input_element_cache = document.getElementById(this.input_element_id);      
	    }
	    return this.input_element_cache;
	},
	
	display_element : function() {
	    if(!this.display_element_cache) {
		this.display_element_cache = document.getElementById(this.display_element_id);      
	    }
	    return this.display_element_cache;
	},
	
	handle_text_change : function(event) {
	    //user has started typing again, so stop the delayed_checker, if there's one running
	    if(this.delayed_checker) this.delayed_checker.stop();
	    
	    var code;
	    if (!event) var event = window.event;
	    if (event.keyCode) code = event.keyCode;
	    else if (event.which) code = event.which;
	    
	    //get suggestions on space, return, sentence end (period, exclamation, question)
	    var key_space    = 32;
	    var key_bang     = 33;   //!
	    var key_period   = 46;   //.
	    var key_question = 63;   //?
	    
	    if(code == key_space || code == key_bang || code == key_period || code == key_question || code == Event.KEY_RETURN) {
		this.get_suggestions();
	    }
	    else {
		//start waiting -- get suggestions if no activity for inactive_period seconds
		var suggestor = this;
		this.delayed_checker = ScheduledTask.create(function() { 
			suggestor.get_suggestions();
		    }, this.inactive_period);
	    }
	},
	
	display_suggestions : function (suggestions) {
	    this.display_element().innerHTML = '';
	    for(var i=0; i<suggestions.length; i++) {
		if(this.input_element().value.match(new RegExp("\\b" +  suggestions[i].original + "\\b"))) { //check to see whether the error is still there
		    this.display_element().appendChild(this.element_for_suggestion(suggestions[i]));
		}
	    }
	},    
	
	element_for_suggestion : function(s) {
	    var suggestor = this;
	    var el = JSML.transform(['p', {'class' : suggestor.suggestions_class_name}, s.original, ': '].concat(
                   s.options.slice(0, this.max_options).map(function(o) { 
			   return ['a', {href: '#', onclick: function(){ suggestor.replace_word(s.original, o); suggestor.remove_suggestion(this); return false; }}, o];
		       }).interleave(' ')).concat([' (', ['a', {href: '#', onclick: function(){suggestor.remove_suggestion(this); return false;}}, 'x'], ')']));
	    return el;
	},
	
	get_suggestions : function() {
	    this.get_suggestions_for_text(this.input_element().value);
	},
	
	get_suggestions_for_text : function(text) {
	    var suggestor = this;
	    var callback_string = this.generate_unique_callback_string();
	    
	    var insert;
	    
	    var widget = this;
	    widget.set_callback(callback_string, function(suggestions) {
		    suggestor.display_suggestions(suggestions);
		    //clean up after yourself!
		    widget.set_callback(callback_string, null);
		    if(insert && insert.parentNode) {
			insert.parentNode.removeChild(insert);
		    }
		});
	    
	    insert = this.insert_script_tag('http://www.curbside.md/widget_results/suggestor.js?text=' + encodeURIComponent(text) + '&callback=' + callback_string + '&key=');
	},
	
	replace_word : function(str, replacement) {
	    this.input_element().value = this.input_element().value.replace(new RegExp("\\b" + str + "\\b"), replacement); // not global replacement -- could be same misspelling of different words
	    this.input_element().focus();  ///@@@ does this work?
	},
	
	remove_suggestion : function(el) {
	    //el is the anchor -- remove the first parent element with a class=suggestions
	    var p = el.parentNode
	    if(p) {
		var cn = p.className || p.getAttribute('class');
		if(cn && (cn == this.suggestions_class_name)) {
		    p.parentNode.removeChild(p);
		}
		else {
		    this.remove_suggestion(p);
		}
	    }
	}
    });

//calls the given function after the given period (of seconds) has elapsed, unless stop() was called first
var ScheduledTask = PraxeonObject.construct({
	task : null,
	proceed : true,
	initialize : function(fn, period) {
	    this.task = fn;
	    var o = this;
	    setTimeout(function(){o.do_it();}, period * 1000);
	},
	do_it : function() {
	    if(this.proceed) {
		this.task();
	    }
	},
	stop : function() {
	    this.proceed = false;
	}
});

//Array helper functions
Array.prototype.map = function(fn) {
    var newarr = [];
    for(var i=0; i<this.length; i++) {
	newarr[i] = fn(this[i]);
    }
    return newarr;
};
Array.prototype.interleave = function(x) {
    var newarr = [];
    for(var i=0; i<this.length; i++) {
	newarr[newarr.length] = this[i];
	if(i != this.length - 1) {
	    newarr[newarr.length] = x;
	}
    }
    return newarr;
};

var JSML = {
 transform : function(tree) {
    var name = tree.shift();
    var el = document.createElement(name);
    
    for(var i in tree) {
      var node = tree[i];
      if(node.constructor == String) {
	el.appendChild(document.createTextNode(node));
      }
      else if(node.constructor == Array) {
	el.appendChild( JSML.transform(node) );
      }
      else {
	for(var key in node) {
	  var value = node[key];
	  if(value.constructor == Function) {
	    el[key] = value;
	  }
	  else if(value.constructor == String) {
	    el.setAttribute(key, value);
	  }
	  else {
	    var combined_val = '';
	    for(var prop in value) {
	      combined_val += prop + ': ' + value[prop] + '; ';
	    }
	    el.setAttribute(key, combined_val);
	  }
	}
      }
    }
    return el;
  }
};
