
var Collection = Class.create(Enumerable, {
  initialize: function(opts) {
    Object.Event.extend(this);
		this.removedEvent = "item:removed";
		this.addedEvent = "item:added";
		this.removeAllEvent = "item:removeAll";
		this.equality = function(a, b) { a === b };
		this.unique = false;
		Object.extend(this, opts);
    this.items = new Array();
  },
	
  add: function(item, opts) {
		if(this.unique) {
      var existing_items = this.find(item);
      if(existing_items.length != 0)
      {
        return existing_items.first();
      }
		}
    this.items.push(item);
    this.notify(this.addedEvent, item);
		return false;
  },
	
	remove: function(item) {
		i = this.indexOf(item);
    if(i != -1)
    {
      this.items.splice(i, 1);
      this.size--;
      this.notify(this.removedEvent, item, i);
      return item;
    } else {
      return null;
    }
	},
  
  clear: function() {
		this.size = 0;
		this.items.clear();
		this.notify(this.removeAllEvent, this);
  },
  
  _each: function(iterator) {
    this.items._each(iterator);
  },

  indexOf: function(item) {
		return this.items.indexOf(item);
	},
	
	find: function(needle) {
		return this.find_by_predicate(function(candidate) {
			return this.equality(needle, candidate);
		}.bind(this));
	},
	
	find_by_predicate: function(predicate) {
    var found = new Array();
    this._each(function(o) {
      if (predicate(o)) {
        found.push(o);
      }
    });
    return found;
  },
	
	remove_by_predicate: function(predicate) {
		this.find_by_predicate(predicate).each(this.remove.bind(this));
	},
	
	attrib_selector: function(attribs) {
    var f = function(o, j) {
      if (j == attribs.length)
        return o;
      else {
        // and a sprinkling of javascript's y combinator
        return arguments.callee(o[attribs[j]], j + 1);
      }
    };

    return function(o) {
      return f(o, 0);
    };
  },
	
	find_by_attribute: function(attribs_array, value) {
    var selector = this.attrib_selector(attribs_array);
    
    var predicate = function(o) {
      return selector(o) === value;
    };
    
    return this.find_by_predicate(predicate);
  }
});



function binary_search(array, array_start, array_end, item_sought, comparator)
{
        var pivot_index;
        var pivot_value;

        while(array_end > array_start) {
                pivot_index = (array_start + (array_end - array_start) / 2) | 0;
                pivot_value = array[pivot_index];
                
                c = comparator(item_sought, pivot_value);

                if (c > 0) {
                        array_start = pivot_index + 1;
                } else if(c < 0) {
                        array_end = pivot_index;
                } else {
                        return pivot_index;
                }
        }

        return -array_start - 1;
}

var SortedCollection = Class.create(Collection, {
  initialize: function($super, opts) {
    this.comparator = function(a, b) {
			if(a < b) {
				return -1;
			}
			if(b > a) {
				return 1;
			}
			return 0;
		};
    $super(opts);
  },
	
	add: function($super, item, opts) {
    var index = binary_search(this.items, 0, this.items.length, item, this.comparator);
		
		if(index >= 0) {
			if(this.unique) {
				return this.items[index];
			}
		} else {
			index = -(index + 1);
		}
    this.items.splice(index, 0, item);
    this.notify(this.addedEvent, item, index);
    return false;
  },
	
	indexOf: function($super, item) {
    var index = binary_search(this.items, 0, this.items.length, item, this.comparator);
		
		if(index < 0)
		{
			return -1;
		} else {
			return index;
		}
  },
});



Ajax.PeriodicalRequest = Class.create(Ajax.PeriodicalUpdater, {
	initialize: function($super, url, options) {
		$super(undefined, url, options);
	},
	
	onTimerEvent: function() {
		this.updater = new Ajax.Request(this.url, this.options);
	}
});



Controllers = Class.create({
});
$c = new Controllers();

Models = Class.create({
	initialize: function() {
		this.layout = true;
		Object.Event.extend(this);
		this.undo = new Collection();
  },
	
	setUser: function(u) {
		if (u != this.user) {
		  this.user = u;
			this.notify("user:changed", this.user);
		}
	},
	
	suspendLayout: function() {
		this.layout = false;
		this.notify("layout:suspend");
	},
	
	resumeLayout: function() {
		this.layout = true;
		this.notify("layout:resume");
	}
});
$m = new Models();

Viewers = Class.create({
	
});
$v = new Viewers();



var MessageTicker = Class.create({
  initialize: function(id, template_class, model){
		this.id = id;
		this.template_class = template_class;
		this.model = model;
		this.model.observe("item:added", this.itemAdded.bind(this));
		this.model.observe("item:removed", this.itemRemoved.bind(this));
		this.display();
  },
	
	display: function() {
		var el = $$('.view_templates .' + this.template_class)[0].cloneNode(true);
		this.element().replace(el);
		el.writeAttribute('id', this.id);
		
		this.model.each(function(m) {
			this.renderMessage(m);
		});
	},
	
	element: function() {
		return $(this.id);
	},
	
	messageElements: function(m) {
		return this.element().select('#' + m.id);
	},
	
	itemAdded: function(i) {
		this.renderMessage(i);
	},
	
	itemRemoved: function(i) {
		this.messageElements(i).each(function(el) {
			el.remove();
		});
	},
	
	renderMessage: function(m) {
		var message_template_class = m.level || 'info';
		var el = $$('.view_templates .message_templates .' + message_template_class)[0].cloneNode(true);
		el.writeAttribute('id', m.id);
		m.insert(el);
		this.element().insert(el);
	}
});


var UndoTicker = Class.create({
  initialize: function(id, template_class, model, i18n_module){
    this.id = id;
		this.i18n_module = i18n_module;
    this.template_class = template_class;
    this.model = model;
    this.model.observe("item:added", this.itemAdded.bind(this));
    this.model.observe("item:removed", this.itemRemoved.bind(this));
		
		this.setElement($$('.view_templates .' + this.template_class)[0].cloneNode(true));
		this.message_template = $$('.view_templates .undo_message')[0];
		
    this.display();
  },
  
  display: function() {
		this.element().update('');

		actions = new Array();
    
    this.model.each(function(m) {
      actions.push(m.action);
    });
		
		actions.uniq().each(function(a) {
			this.renderUndoAction(a);
		}.bind(this));
  },
	
	i18n: function(name, is_singular, context) {
		var plurality = is_singular ? 'singular' : 'plural';
		var key = 'undo.' + plurality + '.' + name;
		return $c.opinionsController.t(key, context);
	},
	
	renderUndoAction: function(a) {
		var items = this.model.find_by_predicate(function (u) {
			return u.action == a;
		});
		
	  var e = this.message_template.cloneNode(true);
		var context = {
			count: items.length
		};
		var text = this.i18n(a, items.length == 1, context);

		e.select('.message').invoke('update', text);
		
		e.select('.link a').each(function(link) {
		  link.observe('click', function() {
  			items.each(function(u) {
				  u.undo();
				  this.model.remove(u);
			  }.bind(this));
		  }.bind(this));
		}.bind(this));
		
		this.element().insert(e);
	},
  
  element: function() {
    return this.ticker_element;
  },
	
	setElement: function(el) {
		this.ticker_element = el;
		$(this.id).replace(el);
    el.writeAttribute('id', this.id);
	},
  
  messageElements: function(m) {
    return this.element().select('#' + m.id);
  },
  
  itemAdded: function(i) {
    this.display();
  },
  
  itemRemoved: function(i) {
    this.display();
  },
  
  renderMessage: function(m) {
    var message_template_class = m.level || 'info';
    var el = $$('.view_templates .message_templates .' + message_template_class)[0].cloneNode(true);
    el.writeAttribute('id', m.id);
    m.insert(el);
    this.element().insert(el);
  }
});


var Artist = Class.create({
  
  initialize: function(j) {
		var a;
    if("artist" in j) {
      a = j.artist;
    } else {
      a = j;
    }
    Object.Event.extend(this);
    Object.extend(this, a);
  }
 
});



var Album = Class.create({
  
  initialize: function(j) {
		var a;
		if("album" in j) {
			a = j.album;
		} else {
			a = j;
		}
    Object.Event.extend(this);
    Object.extend(this, a);

		if ("artist" in a && a.artist) {
			this.artist = new Artist(a.artist);
		} else {
			this.artist = false;
		}
  }
 
});



var PlayableTrack = Class.create({
  
  initialize: function(j) {
		var t;
    if("playable_track" in j) {
      t = j.playable_track;
	  } else if ("track" in j) {
			t = j.track;
    } else {
      t = j;
    }
    Object.Event.extend(this);
    Object.extend(this, t);
		if("album" in t && t.album) {
			this.album = new Album(t.album);
		}
		if("artist" in t && t.artist) {
			this.artist = new Artist(t.artist);
		}
  },
 
});



var User = Class.create({
  initialize: function(u) {
		this.id = u.id;
  }
});



var Opinion = Class.create({
	
	initialize: function(j) {
		if("opinion" in j) {
			o = j.opinion;
		} else {
			o = j;
		}
		Object.Event.extend(this);

		this.copyProperties(o);
	},
	
	bumpVersion: function() {
		if (!("version" in this)) {
      this.version = 1;
	  } else {
		  this.version = this.version + 1;
	  }
		return this.version;
	},
	
	copyProperties: function(o) {
		this.opinionable_type = o.opinionable_type;
		this.opinionable_id = o.opinionable_id;
		this.creator_id = o.creator_id;
		this.id = o.id;
		this.created_at = o.created_at;
		this.updated_at = o.updated_at;
		this.setLiked(o.liked);

		if ("artist" in o && o.artist) {
      this.artist = new Artist(o.artist);
    } else {
			this.artist = false;
		}
    if ("album" in o && o.album) {
      this.album = new Album(o.album);
    } else {
			this.album = false;
		}
    if ("track" in o && o.track) {
      this.track = new PlayableTrack(o.track);
    } else {
			this.track = false;
		}
	},
	
	setLiked: function(l) {
		if (this.liked != l) {
		  this.liked = l;
			this.notify("opinion:likeChanged", this);
    }
	},
	
	deleteOpinion: function() {
		this.notify("opinion:deleted", this);
	}
});



var OpinionsCollection = Class.create(SortedCollection, {
	initialize: function($super) {
		$super({
			removedEvent: "opinion:removed",
			addedEvent: "opinion:added",
			removeAllEvent: "opinion:removeAll",
			unique: true,
			equality: function(a, b) {
        return a.opinionable_type === b.opinionable_type && a.opinionable_id === b.opinionable_id;
      },
			comparator: function(a, b) {
				var a_s = a.opinionable_type + '_' + a.opinionable_id;
				var b_s = b.opinionable_type + '_' + b.opinionable_id;
				if(a_s < b_s) {
					return -1;
				}
				if(b_s < a_s) {
					return 1;
				}
				return 0;
			}
		});
  },
	
	addOpinion: function(opinion) {
		this.add(opinion);
	},
	
	add: function($super, opinion) {
		var existing = $super(opinion);
		if(existing) {
			existing.copyProperties(opinion);
			return existing;
		} else {
      return opinion;
		}
	},
	
	removeOpinion: function(opinion) {
    return this.remove(opinion);
	},

  find_by_opinionable: function(op_type, op_id) {
		var predicate = function(o) {
			return o.opinionable_type === op_type && o.opinionable_id === op_id;
		};
		
		return this.find_by_predicate(predicate);
	},
	
	find_by_associated_id: function(attribs_array, i) {
		return this.find_by_attribute(attribs_array, i);
	}
});

$m.trackOpinionsModel = new OpinionsCollection();
$m.artistOpinionsModel = new OpinionsCollection();

$m.find_by_opinionable = function(opinionable_type, opinionable_id) {
	switch(opinionable_type)
	{
		case 'Track':
		  return $m.trackOpinionsModel.find_by_opinionable(opinionable_type, opinionable_id);
		case 'Artist':
		  return $m.artistOpinionsModel.find_by_opinionable(opinionable_type, opinionable_id);
	}
}



var OpinionPage = Class.create({
  
  initialize: function() {
    Object.Event.extend(this);
		this.sortOrder = 'artist';
		this.showAll = true;
  },
  
  setLetter: function(l) {
    if (this.letter != l) {
      this.letter = l;
      this.notify("opinionPage:letterChanged", this);
    }
  },
  
	setSortOrder: function(s) {
		if (this.sortOrder != s) {
      this.sortOrder = s;
      this.notify("opinionPage:sortOrderChanged", this);
    }
	},
	
	setShowAll: function(b) {
		if(this.showAll != b) {
			this.showAll = b;
			this.notify("opinionPage:showAllChanged", this);
		}
	},

	getComparator: function() {
		switch(this.sortOrder) {
			case 'track':
			  selector = function(o) { return o.track.title };
			break;
			case 'artist':
			  selector = function(o) { return o.artist.name };
			break;
			case 'album':
			 selector = function(o) { return o.album.title };
			break;
		}
		
		select_sort_value = function(o) {
			return selector(o) + o.artist.name + o.album.title + o.track.title;
		};
		
		return function(a, b) {
      var val_a = select_sort_value(a).toLowerCase();
      var val_b = select_sort_value(b).toLowerCase();
    
      if (val_a < val_b) {
        return -1;
      }
      if (val_a > val_b) {
        return 1;
      }
      return 0;
    };
	}
});

$m.opinionPage = new OpinionPage();



var Progress = Class.create({
  initialize: function() {
    Object.Event.extend(this);
  },
	
	setProgress: function(p) {
		this.progress = p;
		this.notify("progress:changed", this);
  },
	
	start: function() {
		this.notify("progress:started", this);
		this.setProgress(0);
	},
	
	finish: function() {
		this.setProgress(100);
		this.notify("progress:finished", this);
	}
});



var OpinionsController = Class.create({
	initialize: function() {
		$m.opinionPage.observe("opinionPage:letterChanged", this.onPageChange.bind(this));
		$m.opinionPage.observe("opinionPage:sortOrderChanged", this.onPageChange.bind(this));
		$m.observe("user:changed", this.onUserChanged.bind(this));
		this.loaded = false;
		this.undo_counters = new Object();
		this.onUserChanged($m.user);
	},
	
	onUserChanged: function(user) {
		this.readonly = !(user ? user.has_premium : false);
	},
	
	clear: function() {
		this.debug("clearing model");
		$m.trackOpinionsModel.clear();
    $m.artistOpinionsModel.clear();
		this.loaded = false;
	},
	
	bumpUndoCounter: function(name) {
		if(name in this.undo_counters) {
			this.undo_counters[name]++;
		} else {
			this.undo_counters[name] = 1;
		}
		return this.undo_counters[name];
	},
	
	recordUndoableAction: function(action) {
		version = this.bumpUndoCounter(action.action);
		$m.undo.add(action);
		setTimeout("$c.opinionsController.removeUndo('" + action.action + "'," + version + ")", 30000);
	},
	
	removeUndo: function(action_name, version) {
		if(this.undo_counters[action_name] != version)
		{
			return;
		}
		
		$m.undo.remove_by_predicate(function(u) {
			return u.action == action_name;
		});
	},
	
	onPageChange: function() {
		if (this.lastLetter !== $m.opinionPage.letter || this.lastSort !== $m.opinionPage.sortOrder) {
		  this.loadOpinions();
	  }
	},
	
	onOpinionDelete: function(opinion) {
		if(this.readonly) {
			this.permissionDenied('delete', opinion);
			return;
		}
		
		this.debug("deleting opinion");

    $c.opinionsController.removeTrackOpinion(opinion);

    var ctrlr = this;

		// DELETE /api/users/:user_id/opinions/:id(.:format) {:controller=>"api/opinions", :action=>"destroy"}
    new Ajax.Request('/api/users/' + $m.user.id + '/opinions/remove_opinion.json', {
      method: 'post',
      parameters: {
				opinionable_id: opinion.opinionable_id,
				opinionable_type: opinion.opinionable_type,
				authenticity_token: encodeURIComponent(window._token),
			},
      onException: function(a, e) {
        
      },
      onSuccess: function(response) {
				ctrlr.processChanges(response.responseJSON);
				ctrlr.reloadAttempts = 0;
				ctrlr.recordUndoableAction({
			    action: 'removed',
			    item: opinion,
					undo: function() {
						ctrlr.addOpinion(opinion);
						ctrlr.saveOpinion(opinion);
					}
		    });
      },
			onFailure: function(response) {
				ctrlr.debug('save opinion request: ' + response.statusText);
				if (response.status == 403) {
		      ctrlr.addTrackOpinion(opinion);
					ctrlr.permissionDenied('delete', opinion);
		    } else {
			    ctrlr.recover(response);
		    }
			}
    });
	},
	
	onOpinionLoveBanToggle: function(opinion) {
		if(this.readonly) {
      this.permissionDenied('modify', opinion);
      return;
    }
		
		opinion.setLiked(!opinion.liked);

    this.saveOpinion(opinion);
	},
	
	onBanArtistToggle: function(opinion) {
		if(this.readonly) {
      this.permissionDenied('modify', opinion);
      return;
    }
		
		this.debug('toggling artist');
		
		var artist_opinion;
		var os = $m.artistOpinionsModel.find_by_opinionable("Artist", opinion.track.artist_id);

		if (os.length == 0) {
			artist_opinion = new Opinion({
        opinion: {
          liked: false,
          opinionable_id: opinion.track.artist_id,
          opinionable_type: 'Artist',
          creator_id: $m.user.id
        }
      });
      $c.opinionsController.addOpinion(artist_opinion);
	  } else {
			artist_opinion = os.first();
			artist_opinion.setLiked(!artist_opinion.liked);
		}

		this.saveOpinion(artist_opinion);
	},
	
	like: function(opinionable_type, opinionable_id) {
    var os = $m.find_by_opinionable(opinionable_type, opinionable_id);
    
    if(os.length != 0)
    {
      opinion = os.first();
      opinion.liked = true;
    } else {
      opinion = new Opinion({
        opinion: {
          liked: true,
          opinionable_id: opinionable_id,
          opinionable_type: opinionable_type,
          creator_id: $m.user.id
        }
      });
    }
    
		this.reloadOpinion(opinion);
  },
	
	dislike: function(opinionable_type, opinionable_id) {
    var os = $m.find_by_opinionable(opinionable_type, opinionable_id);
    
    if(os.length != 0)
    {
			opinion = os.first();
      opinion.liked = false;
    } else {
			opinion = new Opinion({
        opinion: {
          liked: false,
          opinionable_id: opinionable_id,
          opinionable_type: opinionable_type,
          creator_id: $m.user.id
        }
      });
		}
		
    this.reloadOpinion(opinion);
  },
	
	reloadOpinion: function(opinion) {
		this.debug('reloading opinion');
		
		var version = opinion.bumpVersion();
		var ctrlr = this;
		
		// query_opinion_api_user_opinions GET    /api/users/:user_id/opinions/query_opinion(.:format) {:controller=>"api/opinions", :action=>"query_opinion"}
		new Ajax.Request('/api/users/' + $m.user.id + '/opinions/query_opinion.json', {
      method: 'get',
      parameters: {
        opinionable_id: opinion.opinionable_id,
        opinionable_type: opinion.opinionable_type
      },
      onException: function(a, e) {
        
      },
      onSuccess: function(response) {
				if (version == opinion.version) {
					response_opinion = new Opinion(response.responseJSON);
			    $c.opinionsController.addOpinion(response_opinion);
		    }
      },
      onFailure: function(response) {
				ctrlr.debug('reload opinion request: ' + response.statusText);
				if (version == opinion.version) {
          if (response.status == 404) {
		        ctrlr.removeOpinion(opinion);
		      } else {
		  	    ctrlr.recover(response);
		      } 
        }
      }
    });
	},
	
	processChanges: function(response_opinions) {
    response_opinions.created.each(function(o) {
      this.addOpinion(new Opinion(o));  
    }.bind(this));
    response_opinions.updated.each(function(o) {
      this.addOpinion(new Opinion(o)); 
    }.bind(this));
    response_opinions.deleted.each(function(o) {
      this.removeOpinion(new Opinion(o)); 
    }.bind(this));
  },
	
	saveOpinion: function(opinion, supplied_opts) {
		if(this.readonly) {
      this.permissionDenied('modify', opinion);
      return;
    }
		
		this.debug('saving opinion');
		
	  var version = opinion.bumpVersion();
		
    opts = {
		  callback: this.processChanges.bind(this),
		  errback: function() {
				this.reloadOpinion(opinion);
				this.permissionDenied('modify', opinion);
			}.bind(this),
			recover: this.recover.bind(this)
	  }
		Object.extend(opts, supplied_opts || {})

    var ctrlr = this;

	  //$m.requestProgress.start();
    // POST   /api/users/:user_id/opinions/create_or_update(.:format) {:controller=>"api/opinions", :action=>"create_or_update"}
    // PUT    /api/users/:user_id/opinions/:id(.:format) {:controller=>"api/opinions", :action=>"update"}
    new Ajax.Request('/api/users/' + $m.user.id + '/opinions/create_or_update.json', {
      method: 'post',
      parameters: {
        authenticity_token: encodeURIComponent(window._token),
        id: opinion.id,
			  liked: opinion.liked,
			  opinionable_id: opinion.opinionable_id,
			  opinionable_type: opinion.opinionable_type,
			  creator_id: $m.user.id
      },
      onException: function(a, e) {
      },
      onSuccess: function(response) {
				this.reloadAttempts = 0;
        response_opinions = response.responseJSON;
        if (version == opinion.version) {
          opts.callback(response_opinions);
		    }
      },
			onFailure: function(response) {
				ctrlr.debug('save opinion request: ' + response.statusText);
				if (version == opinion.version) {
			    if (response.status == 403) {
				    opts.errback(response);
			    } else {
				    opts.recover(response);
			    }
		    }
			}
    });
	},
	
	setPageLetter: function(letter) {
		$m.opinionPage.setLetter(letter);
	},
	
	setPageSort: function(sort_type) {
		$m.opinionPage.setSortOrder(sort_type);
	},
	
	addOpinion: function(opinion) {
		switch(opinion.opinionable_type) {
			case 'Artist':
			  this.addArtistOpinion(opinion);
				break;
		  case 'PlayableTrack':
			  this.addTrackOpinion(opinion);
				break;
		}
	},
	
	removeOpinion: function(opinion) {
		switch(opinion.opinionable_type) {
      case 'Artist':
        this.removeArtistOpinion(opinion);
        break;
      case 'PlayableTrack':
        this.removeTrackOpinion(opinion);
        break;
    }
	},
	
	addTrackOpinion: function(opinion) {
		$m.trackOpinionsModel.addOpinion(opinion);
	},
	
	addArtistOpinion: function(opinion) {
		$m.artistOpinionsModel.addOpinion(opinion);
	},
	
	removeTrackOpinion: function(opinion) {
		$m.trackOpinionsModel.removeOpinion(opinion);
	},
	
	removeArtistOpinion: function(opinion) {
		$m.artistOpinionsModel.removeOpinion(opinion);
	},
	
	start: function() {
		this.cancel();
		
		if(!this.loaded) {
		  this.loadOpinions();
		}
	},
	
	loadOpinions: function() {
		var letterOpt = ($m.opinionPage.letter == undefined ? '' : ('letter=' + $m.opinionPage.letter + '&')); 
    var sortOpt = ($m.opinionPage.sortOrder == undefined ? '' : ('sort=' + $m.opinionPage.sortOrder + '&'));
    var ctrlr = this;

		$m.requestProgress.start();
		//new Ajax.PeriodicalRequest(
		// GET    /api/users/:user_id/opinions(.:format) {:controller=>"api/opinions", :action=>"index"}
		new Ajax.Request('/api/users/' + $m.user.id + '/opinions.json', {
			frequency: 4,
			decay: 1,
      method: 'get',
      parameters: sortOpt + letterOpt + 'authenticity_token=' + encodeURIComponent(window._token),
      onComplete: function() {
        $m.requestProgress.finish();
      },
      onLoading: function() {
        $m.requestProgress.setProgress($m.requestProgress.progress + 10);
      },
      onException: function(a, e) {
        
      },
      onSuccess: function(response) {
				$m.suspendLayout();
				try {
			    ctrlr.reloadAttempts = 0;
			    ctrlr.clear();
					if (response.responseJSON.letter) {
		  	    ctrlr.lastLetter = response.responseJSON.letter.toUpperCase();
		      }
					ctrlr.lastSort = response.responseJSON.sort
				  $m.opinionPage.setLetter(ctrlr.lastLetter);
					$m.opinionPage.setSortOrder(ctrlr.lastSort);
					if (letterOpt == '') {
            $m.opinionPage.setShowAll(response.responseJSON.can_show_all);
		      }
			    response.responseJSON.artist_opinions.each(function(result){
				    opinion = new Opinion(result);
				    ctrlr.addArtistOpinion(opinion);
			    });
			    response.responseJSON.track_opinions.each(function(result){
				    opinion = new Opinion(result);
				    ctrlr.addTrackOpinion(opinion);
			    });
			    ctrlr.loaded = true;
			    ctrlr.clearErrors();
		    } finally {
			    $m.resumeLayout();
		    }
      },
			onFailure: function(response) {
				ctrlr.debug('load opinions: ' + response.statusText);
			  $c.opinionsController.recover(response);
			}
    });
	},
	
	reloadPartial: function() {
		return false;
	},
	
	recover: function(response) {
		if(response.status == 401) {
			if(/user_id does not match OAuth/.match(response.responseText)) {
				// logged in as different user to $m.user				
				this.reloadAttempts = (this.reloadAttemps || 0) + 1;
				if (this.reloadPartial(this.reloadAttempts)) {
          // nothing
				} else {
					this.error(this.t('reload_prompt'));
				}
			} else {
				// logged out?
				this.error(this.t('login_prompt'));
			}
		}
	},
	
  permissionDenied: function(action, opinion) {
		this.clearInfos();
		this.info(this.t('you_need_premium'));
	},
	
	debug: function(s) {

	},
	
	info: function(s, opts) {
    if(opts == undefined) {
			opts = {}
		}
    m = this.msg(new Message(s, { level: 'info' }));
		if (!("timeout" in opts)) {
			opts.timeout = 15000;
		}
		setTimeout("$c.opinionsController.removeMsg('" + m.id + "')", opts.timeout);
  },
	
	error: function(s) {
    this.msg(new Message(s, { level: 'error' }));
  },
	
	removeMsg: function(id) {
		$m.opinionsMessages.remove_by_predicate(function(m) {
      return m.id == id;
    });
	},
	
	t: function(key, subst) {
		if (subst == undefined) {
			subst = {};
		}
		var full_key = '$m.i18n.loves_and_bans.' + key;
		var value = eval(full_key);
		if (value == undefined) {
			return full_key;
		}
		return value.gsub(/\{\{(\w+)\}\}/, function(match) {
			return subst[match[1]];
		});
	},
	
	clearInfos: function() {
    $m.opinionsMessages.remove_by_predicate(function(m) {
      return m.level == 'info';
    });
  },
	
	clearErrors: function() {
		$m.opinionsMessages.remove_by_predicate(function(m) {
			return m.level == 'error';
		});
	},
	
	msg: function(m) {
		$m.opinionsMessages.add(m);
		return m;
	},
  
	cancel: function() {
		if (this.request) {
		  this.request.stop();
	  }
	},
});

$m.requestProgress = new Progress();
$c.opinionsController = new OpinionsController($m.opinionsModel);

$m.opinionsMessages = new Collection({
  equality: function(a,b) {
    return a.level === b.level && a.msg === b.msg && a.element === b.element;
  },
  unique: true
});



OpinionsTable = Class.create({
	initialize: function(id) {
		view = this;

		this.id = id;
		$m.trackOpinionsModel.observe("opinion:added", this.onOpinionAdded.bind(this));
		$m.trackOpinionsModel.observe("opinion:removed", this.onOpinionRemoved.bind(this));
		
		$m.artistOpinionsModel.observe("opinion:added", this.onArtistOpinionAdded.bind(this));
    $m.artistOpinionsModel.observe("opinion:removed", this.onArtistOpinionRemoved.bind(this));
		
		$m.trackOpinionsModel.observe("opinion:removeAll", this.onOpinionsCleared.bind(this));
		$m.artistOpinionsModel.observe("opinion:removeAll", this.onOpinionsCleared.bind(this));
		
		$m.trackOpinionsModel.each(function(opinion, index) {
      opinion.observe("opinion:likeChanged", this.onOpinionLikeChanged.bind(this));
    }.bind(this));
    
    $m.artistOpinionsModel.each(function(opinion, index) {
      opinion.observe("opinion:likeChanged", this.onArtistOpinionLikeChanged.bind(this));
    }.bind(this));
		
		$m.requestProgress.observe("progress:changed", this.onRequestProgressChanged.bind(this));
		
		$m.opinionPage.observe("opinionPage:sortOrderChanged", this.onPageSortOrderChanged.bind(this));
		
		$m.observe("layout:resume", this.onLayoutResume.bind(this));
		
		this.row_template = $$('.view_templates .opinion')[0];
		this.setElement($$('.view_templates .opinions')[0].cloneNode(true));
    
    if ($m.opinionPage.sortOrder) {
      this.element().select('th.' + $m.opinionPage.sortOrder).invoke('addClassName', 'selected');
    }

		var scrollbar_id = this.element().select('.scrollable .scrollable_content')[0].identify();
		var scrollbar_track_id = this.element().select('.scrollable .scrollbar_track')[0].identify();
		
		this.scrollbar = new Control.ScrollBar(scrollbar_id, scrollbar_track_id);
		
		this.needsDisplay = false;
		this.display();
		
		this.scrollbar.recalculateLayout();
	},
	
	display: function() {
		if (!$m.layout) {
			this.needsDisplay = true;
			return;
		}
		
		this.tbodyElement().update('');
			
	  var sorted = new SortedCollection({
		  comparator: $m.opinionPage.getComparator()
		});
			
		$m.trackOpinionsModel.each(function(opinion, index) {
		  sorted.add(opinion);
		}.bind(this));
			
		sorted.each(function(opinion, index) {
			this.renderOpinion(opinion, index);
		}.bind(this));
	},
	
	onOpinionsCleared: function() {
		this.display();
	},
	
	onLayoutResume: function() {
		if(this.needsDisplay) {
			this.display();
		}
		this.needsDisplay = false;
		this.scrollbar.recalculateLayout();
	},
	
	onPageSortOrderChanged: function() {
		if ($m.opinionPage.sortOrder) {
			table = this.element();
			table.select('th').invoke('removeClassName', 'selected');
      table.select('th.' + $m.opinionPage.sortOrder).invoke('addClassName', 'selected');
    }
	},
	
	onRequestProgressChanged: function(p) {
		if(p.progress < 100)
		{
			this.element().setOpacity(0.5);
		} else {
			this.element().setOpacity(1.0);
		}
	},
	
	onOpinionLikeChanged: function(o) {
		this.row_element(o).each(function(el) {
      links = el.select('a.loveBanLink');
      links.each(function(link) {
        this.renderLoveBanLink(link, o);
      }.bind(this));
	  }.bind(this));
	},
	
	onArtistOpinionLikeChanged: function(o) {
		this.row_elements_by_artist(o.opinionable_id).each(function(el) {
			links = el.select('a.banArtistLink');
			links.each(function(link) {
				this.renderArtistBanLink(link, o.liked);
			}.bind(this));
		}.bind(this));
	},
	
	renderRelatedTrackOpinions: function(artist_opinion, artist_liked) {
		$m.trackOpinionsModel.find_by_associated_id(['track', 'artist_id'], artist_opinion.opinionable_id).each(function (o) {
			this.row_element(o).each(function(el) {
			  artistLinks = el.select('a.banArtistLink');
        artistLinks.each(function(link) {
          this.renderArtistBanLink(link, artist_liked);
        }.bind(this));	
			}.bind(this));
		}.bind(this));
		
	},
	
	onOpinionAdded: function(o, index) {
		o.observe("opinion:likeChanged", this.onOpinionLikeChanged.bind(this));
		this.renderOpinion(o);
		if ($m.layout) {
		  this.scrollbar.recalculateLayout();
	  }
	},
	
	onOpinionRemoved: function(o, index) {
		this.row_element(o).invoke('remove');
		if ($m.layout) {
      this.scrollbar.recalculateLayout();
    }
	},
	
	onArtistOpinionAdded: function(o) {
		o.observe("opinion:likeChanged", this.onArtistOpinionLikeChanged.bind(this));
    this.renderRelatedTrackOpinions(o, o.liked);
  },
  
  onArtistOpinionRemoved: function(o) {
    this.renderRelatedTrackOpinions(o, true);
  },
	
	element: function() {
		return this.table_element;
	},
	
	setElement: function(el) {
		this.table_element = el
    $(this.id).replace(this.table_element);
    this.table_element.setAttribute('id', this.id);
	},
	
	tbodyElement: function() {
		return this.element().select('tbody')[0];
	},
	
	row_element: function(opinion) {
    return this.element().select('[opinionable_id="' + opinion.opinionable_id + '"][opinionable_type="' + opinion.opinionable_type + '"]');
	},
	
	row_elements_by_artist: function(artist_id) {
		return this.element().select('[artist_id="' + artist_id + '"]');
	},
	
	select_value: function(el, name) {
		var selector = '.' + name;
		return el.select(selector)[0].innerHTML;
	},
	
	select_sort_value: function(el) {
		return this.select_value(el, $m.opinionPage.sortOrder) + this.select_value(el, 'artist') + this.select_value(el, 'album') + this.select_value(el, 'track');
	},
	
	compare_elements: function(a, b) {
		var val_a = this.select_sort_value(a).toLowerCase();
		var val_b = this.select_sort_value(b).toLowerCase();
		
		if (val_a < val_b) {
			return -1;
		}
		if (val_a > val_b) {
			return 1;
		}
		return 0;
	},
	
	insert_opinion_element: function(element, index) {
		tbody = this.tbodyElement();
		children = tbody.childElements();
		
		if (index == undefined) {
		  index = binary_search(children, 0, children.length, element, this.compare_elements.bind(this));
	  }
		
		if(index < 0) {
			index = -(index + 1);
		}

		if(index < children.length) {
			children[index].insert({before: element});
		} else {
			tbody.insert({bottom: element});
	  }
	},
	
	renderOpinion: function(opinion, index) {
		if (!$m.layout) {
      this.needsDisplay = true;
      return;
    }
		
		row = this.row_template.cloneNode(true);
		
		row.setAttribute('opinionable_type', opinion.opinionable_type);
		row.setAttribute('opinionable_id', opinion.opinionable_id);
		row.setAttribute('artist_id', opinion.artist.id);
		row.select('.artist').invoke('update', opinion.artist.name);
		row.select('.track').invoke('update', opinion.track.title);
		row.select('.album').invoke('update', opinion.album.title);

		this.createDeleteLink(row, opinion);
		this.createLoveBanLink(row, opinion);
		this.createArtistBanLink(row, opinion);

		this.insert_opinion_element(row, index);
	},
	
	createDeleteLink: function(el, opinion) {
		el.select('a.deleteLink').each(function(a) {
      a.observe('click', function() {
        $c.opinionsController.onOpinionDelete(opinion);
      });
      this.renderDeleteLink(a, opinion);
    }.bind(this));
	},
	
	renderDeleteLink: function(el, opinion) {
		
	},
	
	createLoveBanLink: function(el, opinion) {
		el.select('a.loveBanLink').each(function(a) {
      a.observe('click', function() {
        $c.opinionsController.onOpinionLoveBanToggle(opinion);
      });
      this.renderLoveBanLink(a, opinion);
    }.bind(this));
	},
	
	renderLoveBanLink: function(el, opinion) {
		if(opinion.liked) {
			el.removeClassName('banned');
			el.addClassName('liked');
		}	else {
			el.removeClassName('liked');
			el.addClassName('banned');
		}
	},
	
	createArtistBanLink: function(el, opinion) {
		el.select('a.banArtistLink').each(function(a) {
      a.observe('click', function() {
        $c.opinionsController.onBanArtistToggle(opinion);
      });
      this.renderArtistBanLinkFromTrackOpinion(a, opinion);
    }.bind(this));
	},
	
	renderArtistBanLinkFromTrackOpinion: function(el, track_opinion) {
		var os = $m.artistOpinionsModel.find_by_opinionable("Artist", track_opinion.track.artist_id);
		var liked = true;

		if (os.length == 1 && !os.first().liked) {
      liked = false;
    }

		this.renderArtistBanLink(el, liked);
	},
	
	renderArtistBanLink: function(el, liked) {
    if(liked) {
      el.removeClassName('banned');
      el.addClassName('liked');
    } else {
      el.removeClassName('liked');
      el.addClassName('banned');
    }
  }
});



OpinionsPageSelector = Class.create({
  initialize: function(id) {
    this.id = id;
		
		$m.opinionPage.observe("opinionPage:letterChanged", this.onPageChanged.bind(this));
		$m.opinionPage.observe("opinionPage:showAllChanged", this.onShowAllChanged.bind(this));

    this.display();
	},
	
	onShowAllChanged: function(page) {
		if (!page.showAll) {
		  this.element().select('.all_link').invoke('hide');
	  } else {
			this.element().select('.all_link').invoke('show');
		}
	},
	
	onPageChanged: function(page) {
		this.currentPageLetterElements().each(function(el) {
			el.removeClassName('selected');
		});
		
		this.currentPageLetter = page.letter;
		
		this.currentPageLetterElements().each(function(el) {
			if (el.readAttribute('letter') != '_') {
	  	  el.addClassName('selected');
	    }
		});
	},
	
	element: function() {
		return $(this.id);
	},
	
	currentPageLetterElements: function() {
		if(this.currentPageLetter != undefined) {
			return this.element().select('.page_link[letter=' + this.currentPageLetter + ']');
		} else {
			return new Array();
		}
	},
	
	display: function() {
		var letters = $$('.view_templates .opinion_page_links')[0].cloneNode(true);
		
		this.element().update();
		this.element().insert(letters);
		
		this.onPageChanged($m.opinionPage);
		
		this.element().select('.page_link').each(function(a) {
			a.observe('click', function() {
				var letter = a.readAttribute('letter');
        $c.opinionsController.setPageLetter(letter);
      });
		}.bind(this));
	}
});


OpinionsSortSelector = Class.create({
  initialize: function(id) {
    this.id = id;
    
    $m.opinionPage.observe("opinionPage:sortOrderChanged", this.onSortChanged.bind(this));

    this.display();
		this.onSortChanged($m.opinionPage);
  },
	
	element: function() {
		return $(this.id);
	},
	
	selectorElement: function() {
		return this.element().select('select')[0];
	},
	
	display: function() {
		chooser = $$('.view_templates .opinions_sort_selector')[0].cloneNode(true);
		this.element().replace(chooser);
		chooser.setAttribute('id', this.id);
		
		this.selectorElement().observe('change', function() {
			var el = this.selectorElement();
			var i = 0;
			if (el.selectedIndex >= 0) {
				i = el.selectedIndex;
			} else {
				el.selectedIndex = i;
			}
			$c.opinionsController.setPageSort(el.options[i].value);
		}.bind(this));
	},
	
	onSortChanged: function(pager) {
		el = this.selectorElement();
		if(pager.sortOrder != el.options[el.selectedIndex].value) {
			for(var i = 0; i < el.options.length; i++)
			{
				if(el.options[i].value == pager.sortOrder) {
					el.selectedIndex = i;
					break;
				}
			}
		}
	}
});



function like(opionionable_type, opinionable_id) {
	try {
  	$c.opinionsController.like(opionionable_type, opinionable_id);
  } catch (e) {
	}
}

function dislike(opinionable_type, opinionable_id) {
	try {
  	$c.opinionsController.dislike(opinionable_type, opinionable_id);
  } catch (e) {
	}
}

var LogoutLogin = Class.create({
  initialize: function(opts) {
		opts = opts || {};
    Object.Event.extend(this);
		this.loginFormCompleteHandler = this.loginFormComplete.bind(this);
		this.logoutFormCompleteHandler = this.logoutFormComplete.bind(this);
		this.do_logout = opts.logout || Prototype.emptyFunction;
		this.do_login = opts.login || Prototype.emptyFunction;
  },
    
  // actions
  login: function(login, password, remember_me) {
    this.credentials = {
      login: login,
      password: password,
      remember_me: remember_me
    };

    this.do_login(this.credentials.login, this.credentials.password, this.credentials.remember_me);
  },
  logout: function() {
		this.credentials = {};
    this.do_logout();
  },
    
  loginFormComplete: function(response) {
    if (this.credentials.login) {
			if (response.status == 200) {
				$m.setUser(new User({id: response.responseJSON.user_id}));
	  	  this.notify("login:success");
	    } else {
				$m.setUser(false);
				this.notify("login:failure");
			}
    }
  },
  logoutFormComplete: function(response) {
		if (!this.credentials.login) {
			$m.setUser(false);
		  this.notify("logout:success");
	  }
  }
});

$m.msg_id = 0;
function next_msg_id() {
	return 'msg_' + (++$m.msg_id);
}

var Message = Class.create({
	initialize: function(msg, opts){
		Object.extend(this, opts || {});
		this.id = next_msg_id();
		this.element = new Element('span');
		if(msg) {
		  this.element.update(msg);
		}
	  if ("element" in opts) {
		  this.element.insert(opts.element);
	  }
  },
	
	insert: function(el) {
    el.insert(this.element);
	},
});


