
var Tokenizer = Class.create(
{

  initialize : function(token_holder, searcher) {
    var md = token_holder.getMetaData();

    // store the locations of everything
    this.searcher = searcher;
    this.token_holder = token_holder;
    this.token_field = token_holder.down('.tokenizer_field');
    this.store_name = md.store_name;
    this.empty_notice = '';
    if (md.empty_notice)
      this.empty_notice = md.empty_notice;

    // create a div to hold the choices
    this.choice_holder = new Element('div', { className : 'tokenizer_options' });
    this.token_field.insert({ after:this.choice_holder });
    this.choice_holder.hide();
    this.choices = new Array();
    this.tokens = new Array();

    // there may already be tokens in the box that need to be wired up
    this.token_holder.select('.token').each(function(token){
      // create the sort of token we know about
      this.tokens.push(new Token(this, this.store_name, token.getMetaData().id, token.innerHTML));
      // and kill the dummy one
      token.remove();
    }.bind(this));
    
    this.blur();
    this.set_box_width();
    //this.token_field.setStyle({width:'97%'});
    
    // track which requests have been sent and which is currently showing
    this.current_req = -1;
    this.req = 0;
    // don't bother running the last search again
    this.last_search = '';
    
    // attach the observer for when people type in the box
    Event.observe(this.token_field, 'keydown', this.keydown.bind(this));
    Event.observe(this.token_field, 'keyup', this.keyup.bind(this));
    Event.observe(this.token_field, 'focus', this.focus.bind(this));
    Event.observe(this.token_field, 'blur', this.blur.bind(this));
    // if they click in the holder we need to focus on the input box
    Event.observe(this.token_holder, 'click', function(){ this.token_field.focus(); }.bind(this));
  },
  
  
  /* deal with changing the box */
  
  keydown : function(e) {
    // check if it's one of the control keys first
    switch (e.keyCode) {
      case Event.KEY_DOWN:
        Event.stop(e);
        return this.handle_down();

      case Event.KEY_UP:
        Event.stop(e);
        return this.handle_up();

      case Event.KEY_TAB:
        // todo: this should probably only stop if there's a choice list on the go
        Event.stop(e);
        return this.handle_select();

      case Event.KEY_RETURN:
        Event.stop(e);
        return this.handle_select();

      case Event.KEY_BACKSPACE:
        if (this.token_field.value == '') {
          Event.stop(e);
          return this.handle_delete();
        }
    }
  },
    
  keyup : function(e) {
    // console.dir(e);
    // check if it's one of the control keys first (bail out - the keydown should have caught it already)
    switch (e.keyCode) {
      case Event.KEY_DOWN: return false;
      case Event.KEY_UP: return false;
      case Event.KEY_TAB: return false;
      case Event.KEY_RETURN: return false;
    }
    
    // make sure the box is big enough for the content
    this.set_box_width();
    
    // failing that we may need to run a new search
    var q = this.token_field.value;
    if (q.length > 0 && q != this.last_search)
      this.run_search(q);
    else
      this.clear_results();
  },
  handle_up : function() {
    if (this.choices.length > 0)
      this.highlight(this.highlighted-1);
  },
  handle_down : function() {
    if (this.choices.length > 0)
      this.highlight(this.highlighted+1);
  },
  handle_select : function() {
    if (this.choices.length > 0)
      this.item_selected(this.choices[this.highlighted]);
  },
  handle_delete : function() {
    // is there any thing to delete?
    if (this.tokens.length > 0)
      this.tokens.pop().remove();
  },  
  kill_with_id : function(id) {
    // get rid of any that have the given id
    this.tokens = this.tokens.findAll(function(t){ return (t.id != id); });
  },
  
  focus : function() {
    if (this.token_field.value == this.empty_notice)
      this.token_field.value = '';
  },
  blur : function() {
    if (this.token_field.value == '' && this.tokens.length == 0)
      this.token_field.value = this.empty_notice;
  },
  
  item_selected : function(choice) {
    if (choice) {
      // check to make sure it's not already in the list
      if (this.tokens.find(function(elm) { return (elm.id == choice.id); }))
        return false;
      
      // create the token before the current element
      this.tokens.push(new Token(this, this.store_name, choice.id, choice.name));
    
      // clear everything out so that they can enter another token
      this.clear_results();
      this.token_field.value = '';
      this.token_field.focus();
      this.current_req = -1;
      this.req = 0;
      this.last_search = '';
      this.choice_holder.hide();
    }
  },


  /* deal with running the search */
  
  run_search : function(q) {
    // running a new request
    this.req++;
    this.last_search = q;

    // run the external search (it will callback to search_finished with a list of options)
    this.searcher(this, q, this.req);
  },

  search_finished : function(new_tokens, req, q) {
    // pull out the results of the search
    if (req > this.current_req) {
      this.current_req = req;
      if (new_tokens.size() > 0)
        this.display_results(q, new_tokens);
      else
        this.clear_results();
    }
  },
  
  
  /* deal with displaying the results */
  
  display_results : function(q, search_tokens) {
    // build the new list of results
    this.choices = new Array();
    var ul = new Element('ul');

    search_tokens.each(function(row){
      // new option
      var tc = new TokenizerChoice(this, row.id, row.name, q);
      this.choices.push(tc);

      // add it to the list
      ul.insert(tc.li);
    }.bind(this));

    // insert the list into the page
    this.choice_holder.show();
    this.choice_holder.update(ul);
    // shuffle the choice list to make sure it's under the text box
    this.choice_holder.setStyle({ left:this.token_field.cumulativeOffset()[0].toString()+'px', top:(this.token_field.cumulativeOffset()[1]+this.token_field.getHeight()).toString()+'px' });
    
    // highlight the first option to start with
    this.highlight(0);
  },
  
  set_box_width : function(){
    this.token_field.setStyle({width:(this.token_field.value.length+1).toString()+'em'});
  },
  
  highlight : function(which) {
    // if something is highlighted at the moment, unhighlight it
    if (this.highlighted != null)
      this.choices[this.highlighted].li.removeClassName('selected');

    // set the bounds
    if (which > this.choices.length-1)
      which = 0;
    if (which < 0)
      which = this.choices.length-1;

    // highlight the new one
    this.highlighted = which;
    this.choices[this.highlighted].li.addClassName('selected');
  },
  
  clear_results : function() {
    // kill all the existing choices
    this.choice_holder.update();
    this.choices = new Array();
    this.highlighted = null;
    this.choice_holder.hide();
  },
  
  clear_all : function() {
    // get rid of the choices list
    this.clear_results();

    // kill all of the tokens
    this.tokens = new Array();
    this.token_holder.select('.token').each(function(token){ token.remove(); });
  }
  
});


// represents a single choice in the dropdown list
var TokenizerChoice = Class.create(
{
  
  initialize : function(tokenizer, id, name, q) {
    this.name = name;
    this.id = id;
    this.tokenizer = tokenizer;
    this.q = q;

    // wrap highlight spans around matching hits before creating the li
    this.li = new Element('li').update(this.highlighted_hits());

    Event.observe(this.li, 'click', function(){ this.tokenizer.item_selected(this); }.bind(this));
  },
  
  highlighted_hits : function() {
    // first remove duplicates from the search string (and make everything lowercase)
		var ss = $w(this.q).collect(function(s){ return s.toLowerCase(); }).uniq();
		
		// also remove substrings of other terms (the bigger term will dominate)
		ss = ss.collect(function(st){
			if (ss.any(function(s){ return (s.startsWith(st) && s != st); }))
				return null;
			else
				return st;
		}).compact();
	
		// split the name into words
		var name = $w(this.name);

		// scan through the words
		name = name.collect(function(word){
			// find the matching search term
			var matching = ss.detect(function(st){
				return word.toLowerCase().startsWith(st);
			});

			// match found?
			if (matching) {
				// wrap the start of the word in the highlighting span
				var regex = new RegExp('^' + matching, 'i') ;
				word = word.sub(regex, function(start){ return '<span class="term">'+start+'</span>'; });
			}
			return word;
		});
	
		// piece the name back together
    return name.join(' ');
  }

});


var Token = Class.create(
{
  
  initialize : function(tokenizer, store_name, id, name) {
    // store basic info
    this.tokenizer = tokenizer;
    this.id = id;
    this.name = name;
    this.store_name = store_name;

    // create the divs and everything
    this.token = new Element('div', { className : 'token' }).update(new Element('div', { className:'label' }).update(this.name));
    this.closer = new Element('div', { className:'closer' }).update('x');
    this.token.insert(this.closer);
    this.tokenizer.token_field.insert({ before:this.token });
    this.token.insert(new Element('input', { type:'hidden', name:this.store_name, value:this.id }));
    
    // attach the events for removing the items
    Event.observe(this.closer, 'click', this.remove.bind(this));
  },

  remove : function() {
    // we remove it from the interface
    this.token.remove();
    // but it also needs to be removed from the tokenizer
    this.tokenizer.kill_with_id(this.id);
  }
  
});
