// @requires Cocktail, Ingredient, Good

CocktailsPage =
{
	init: function (states, nodes, styles, cookies) {
		this.view       = new CocktailsView(states, nodes, styles)
		this.model      = new CocktailsModel(states, this.view)
		this.controller = new CocktailsController(states, cookies, this.model, this.view)
	}
}

$.onready(
	function () {
		var nodes = {
			bodyWrapper: cssQuery('#main-wrapper .body-wrapper')[0],
			resultsDisplay: $('results_display'),
			resultsRoot: $('surface'),
			pagerRoot: $('p-list'),
			
			bigNext: cssQuery(".pager-big .next")[0],
			bigPrev: cssQuery(".pager-big .prev")[0],
			
			alphabetRu: $('alphabetical-ru'),
			lettersAll: $('letters_all'),
			
			tagsList: $('tags_list'),
			strengthsList: $('strengths_list'),
			methodsList: $('methods_list'),
			
			searchByName: $('search_by_name'),
			searchByIngreds: $('search_by_ingreds'),
			searchByIngredsInput: cssQuery('#search_by_ingreds input')[0],
			searchByIngredsForm: cssQuery('#search_by_ingreds form')[0],
			searchByLetter: $('search_by_letter'),
			
			tagStrengthArea: $('b_search'),
			mainArea: $('b_content'),
			
			searchTabs: $('search_tabs'),
			ingredsView: cssQuery(".ingreds-list")[0],
			removeAllIngreds: cssQuery(".ingreds-list .rem")[0],
			searchesList: $('ingredients_list'),
			searchTips: $('search_tips'),
			
			ingredientsLink: $('all_list'),
			
			searchExampleIngredient: $('search_example_ingredient'),
			searchTipIngredient: $('search_tip_ingredient'),
			
			searchExampleName: $('search_example_name'),
			searchExampleNameEng: $('search_example_name_eng'),
			searchTipName: $('search_tip_name'),
			
			cartEmpty: $('cart_draghere'),
			cartFull: $('cart_contents'),
			
			spotlighted: $('spotlighted')
		}
		
		var styles = {
			selected: 'selected-button',
			disabled: 'disabled',
			point: 'point'
		}
		
		var cookies = {
			filter: 'filters',
			force: 'force',
			
			strengthState: 'strength_state',
			tagState: 'tag_state',
			methodState: 'method_state'
		}
		
		var states = {
			byName:        0,
			byLetter:      1,
			byIngredients: 2,
			
			defaultState:  0
		}
		
		CocktailsPage.init(states, nodes, styles, cookies)
		Calculator.init()
		Theme.bind()
	}
)

Element.prototype.removeClassName = Element.prototype.remClassName

Switcher =
{
	bind: function (main, buttons, tabs, names)
	{
		if (!main || !buttons || !tabs)
			throw new Error('main, buttons or tabs are not defined: ' + [!!main, !!buttons, !!tabs].join(', '))
		main.nodes = {buttons: Array.copy(buttons), tabs: Array.copy(tabs)}
		main.names = names || []
		
		main.onselect = function () {}
		main.setTabs = function (tabs) { this.nodes.tabs = tabs }
		main.setNames = function (names) { this.names = names }
		main.select = function (num)
		{
			if (typeof num != 'number')
				num = this.names.indexOf(num)
			
			if (num < 0 || this.onselect(num) === false)
				return
			
			this.drawSelected(num)
		}
		main.drawSelected = function (num)
		{
			if (typeof num != 'number')
				num = this.names.indexOf(num)
			
			if (num < 0)
				return
			
			var buttons = this.nodes.buttons
			for (var i = 0; i < buttons.length; i++)
				if (buttons[i])
					num == i ? buttons[i].addClassName('selected') : buttons[i].remClassName('selected')
			
			var tabs = this.nodes.tabs
			if (tabs && tabs[num])
				for (var i = 0; i < tabs.length; i++)
					if (tabs[i])
						num == i ? tabs[i].show() : tabs[i].hide()
		}
		
		function isParent (node, parent, root)
		{
			do
			{
				// log(node)
				if (node == parent)
					return true
				if (node == root)
					return false
			}
			while (node = node.parentNode)
			
			return false
		}
		
		function mouseSelect (e)
		{
			var buttons = this.nodes.buttons
			var num = -1
			for (var i = 0; i < buttons.length; i++)
				if (isParent(e.target, buttons[i], this))
					num = i
			
			return this.select(num)
		}
		main.addEventListener('mousedown', mouseSelect, false)
		
		return main
	}
}

;(function(){

var rex = /([\\\.\*\+\?\$\^\|\(\)\[\]\{\}])/g

if (!RegExp.escape)
RegExp.escape = function (str)
{
	return ('' + str).replace(rex, '\\$1')
}

})();

// NodesShortcut
;(function(){

var doc = document, undef, myName = 'NodesShortcut'

function T (text) { return doc.createTextNode(text) }
function N (tag, cn, text)
{
	var node = doc.createElement(tag)
	if (cn !== undef) node.className = cn
	if (text !== undef) node.appendChild(T(text))
	return node
}

function E (tag, cn, props)
{
	var node = doc.createElement(tag)
	if (cn !== undef) node.className = cn
	if (props)
		for (var i in props)
			node.setAttribute(i, props[i])
	return node
}

var code = 'var T=' + myName + '.T,N=' + myName + '.N,E=' + myName + '.E',
	Me = self[myName] = function () { return code }

Me.T = T
Me.N = N
Me.E = E

})();

// Class
;(function(){

var myName = 'Class'
var Me = self[myName] = function (name, sup)
{
	var klass = function ()
	{
		this.constructor = klass//arguments.callee
		this.initialize.apply(this, arguments)
		// try { this.initialize.apply(this, arguments) }
		// catch (ex) { throw new Error(name + ': ' + ex.message) }
	}
	
	klass.className = name || '[anonimous ' + myName + ']'
	klass.prototype = new (sup || Me.Object)()
	klass.constructor = Me
	
	return klass
}

Me.className = myName
Me.Object = function () {}
Me.Object.prototype =
{
	initialize: function () {},
	extend: function (s) { if (s) for (var p in s) this[p] = s[p]; return this }
}

// Me.supercall(this, 'initialize', [arg1, arg2, arg3...])
Function.prototype.supercall = function (o, n, a) { this.prototype.constructor.prototype[n].apply(o, a) }

})();



// Module
;(function(){

var myName = 'Module'
var Me = self[myName] = function (name, proto)
{
	var module = function () { throw new Error(myName + ': can`t create direct instances of myself') }
	module.className = name || '[anonimous ' + myName + ']'
	module.constructor = Me
	module.mix = function (cls) { Object.extend(cls.prototype, this.prototype); return this }
	
	if (proto)
		module.prototype = proto
	return module
}

Me.className = myName

Function.prototype.mixIn = function (module)
{
	if (module.constructor != Me)
		throw new Error('Function: can only mixIn modules')
	return module.mix(this)
}

})();

// MVC
;(function(){

var myName = 'MVC'
var ME = self[myName] = Class(myName)
ME.prototype.extend
({
	initialize: function ()
	{
		var model = this.model = new this.constructor.Model(),
			view = this.view = new this.constructor.View(),
			controller = this.controller = new this.constructor.Controller()
		
		model.view = controller.view = view
		model.controller = view.controller = controller
		view.model = controller.model = model
		model.parent = view.parent = controller.parent = this
		
		return this
	}
})

ME.Model = Class(myName + '.Model')
ME.View = Class(myName + '.View')
ME.Controller = Class(myName + '.Controller')

ME.create = function (name)
{
	var widget = Class(name, this)
	widget.Model = Class(name + '.Model', this.Model)
	widget.View = Class(name + '.View', this.View)
	widget.Controller = Class(name + '.Controller', this.Controller)
	
	return widget
}

})();

;(function(){

var myName = 'Autocompleter'
var Me = self[myName] = MVC.create(myName)

Me.prototype.extend
({
	bind: function (main, count)
	{
		this.view.bind({main:main})
		this.setCount(count === undefined ? 15 : count)
		return this
	},
	
	setDataSource: function (ds) { this.model.dataSource = ds },
	setCount: function (v) { this.model.setCount(v); this.view.setCount(v) },
	setInstant: function (v) { this.controller.instant = v },
	onconfirm: function () {}
})

// Me.mixIn(EventDriven)

eval(NodesShortcut())

Me.View.prototype.extend
({
	initialize: function ()
	{
		this.nodes = {}
		this.keyMap = {38:'goUp', 40:'goDown', 37:false, 39:false, 9:false, 16:false, 17:false, 18:false, 91:false, 13:'goEnter', 27:'goEscape'}
	},
	
	bind: function (nodes)
	{
		this.nodes = nodes
		var main = nodes.main
		main.setAttribute('autocomplete', 'off')
		
		var list = this.nodes.list = N('ul')
		list.className = 'autocomplete'
		main.parentNode.appendChild(list)
		
		var me = this
		main.addEventListener('blur', function (e) { me.onBlur(e) }, false)
		main.addEventListener('keypress', function (e) { me.onKeyPress(e) }, false)
	},
	
	onKeyPress: function (e)
	{
		var targ = e.target, controller = this.controller,
			action = this.keyMap[e.keyCode]
		// alert(e.keyCode)
		if (action === false)
			return
		else if (action)
		{
			if (controller[action](targ.value) === false)
			{
				e.preventDefault()
				e.stopPropagation()
			}
		}
		else
			setTimeout(function () { controller.goValue(targ.value) }, 1)
	},
	
	onBlur: function (e)
	{
		this.controller.goBlur()
	},
	
	onMouseMove: function (node, e)
	{
		this.controller.itemHovered(node.num)
	},
	
	onMouseDown: function (node, e)
	{
		this.controller.itemClicked(node.num)
	},
	
	setCount: function (count)
	{
		this.createItemsNodes(count)
	},
	
	renderVariant: function (str)
	{
		this.nodes.main.value = str
	},
	
	show: function ()
	{
		var nodes = this.nodes
		nodes.main.addClassName('autocompleting')
		nodes.list.show()
		this.active = true
	},
	
	hide: function ()
	{
		var nodes = this.nodes
		nodes.main.removeClassName('autocompleting')
		nodes.list.hide()
		this.active = false
	},
	
	createItemsNodes: function (count)
	{
		var list = this.nodes.list, items = this.nodes.items = []
		list.empty()
		
		var me = this
		function mousedown (e) { me.onMouseDown(this, e) }
		function mousemove (e) { me.onMouseMove(this, e) }
		
		for (var i = 0; i < count; i++)
		{
			var item = items[i] = N('li')
			item.className = 'item'
			item.hide()
			list.appendChild(item)
			item.num = i
			item.addEventListener('mousedown', mousedown, false)
			item.addEventListener('mousemove', mousemove, false)
		}
	},
	
	renderResults: function (results)
	{
		var items = this.nodes.items
		for (var i = 0; i < results.length && i < items.length; i++)
		{
			var r = results[i],
				item = items[i]
			item.empty()
			item.appendChild(r[1]) // [1] means a text representing node (or DocumentFragment)
			item.show()
		}
		
		for (; i < items.length; i++)
			items[i].hide()
	},
	
	selectItem: function (num)
	{
		if (this.selected === num)
			return
		
		var node, items = this.nodes.items
		
		if ((node = items[this.selected]))
			node.removeClassName('selected')
		
		if ((node = items[num]))
			node.addClassName('selected')
		
		this.selected = num
	}
})

Me.Controller.prototype.extend
({
	initialize: function ()
	{
		this.reset()
		this.value = undefined
	},
	
	reset: function ()
	{
		this.results = []
		this.selected = -1
		this.value = ''
	},
	
	begin: function ()
	{
		if (this.active)
			return
		// log('begin')
		
		this.active = true
		this.view.show()
	},
	
	end: function ()
	{
		if (!this.active)
			return
		// log('end')
		
		this.active = false
		this.reset()
		this.view.hide()
	},
	
	search: function ()
	{
		this.model.search(this.value)
	},
	
	setResults: function (results)
	{
		this.selected = -1
		this.results = results
		this.view.renderResults(results)
		this.view.selectItem(-1)
	},
	
	selectBy: function (dir)
	{
		var total = this.results.length,
			selected = this.selected
		
		selected += dir
		
		if (selected < -1)
			selected = total - 1
		else if (selected >= total)
			selected = -1
		
		this.select(selected)
	},
	
	select: function (num)
	{
		if (this.selected === num)
			return
		
		this.selected = num
		this.view.selectItem(num)
	},
	
	sendSelected: function ()
	{
		this.view.renderVariant(this.selectedValue())
	},
	
	selectedValue: function ()
	{
		var selected = this.selected
		return selected < 0 ? this.value : this.results[selected][0] // [0] means a text value
	},
	
	dispatchConfirm: function ()
	{
		return this.parent.onconfirm({type:'confirm', data: {value:this.selectedValue(), selected:this.selected, results:this.results}})
	},
	
	goValue: function (value)
	{
		if (this.value !== value)
		{
			this.value = value
			if (value !== '')
			{
				this.begin()
				this.search()
			}
			else
				this.end()
		}
	},
	
	goUp: function (value)
	{
		if (this.active)
		{
			this.selectBy(-1)
			this.sendSelected()
			return false // drop an event
		}
	},
	
	goDown: function (value)
	{
		if (this.active)
		{
			this.selectBy(1)
			this.sendSelected()
		}
		else
		{
			this.value = value
			if (value !== '')
			{
				this.begin()
				this.search()
			}
		}
		
		return false // drop an event
	},
	
	goEnter: function (value)
	{
		if (this.active)
		{
			if (this.dispatchConfirm() !== false)
				this.sendSelected()
			
			this.end()
			
			return this.instant || false
		}
	},
	
	goEscape: function (value)
	{
		if (this.active)
		{
			this.view.renderVariant(this.value)
			this.end()
		}
	},
	
	goBlur: function ()
	{
		if (this.active)
		{
			this.sendSelected()
			this.end()
		}
	},
	
	itemHovered: function (num)
	{
		this.select(num)
	},
	
	itemClicked: function (num)
	{
		this.select(num)
		if (this.dispatchConfirm() !== false)
			this.sendSelected()
		this.end()
	}
})

Me.Model.prototype.extend
({
	setCount: function (v) { this.count = v },
	search: function (value)
	{
		var ds = this.dataSource
		this.controller.setResults(ds ? ds.search(value, this.count) : [])
	}
})

})();

function CocktailsModel (states, view) {
	this.resultSet = [];
	
	this.filters = {
		name:        "",
		letter:      "",
		tag:         "",
		strength:    "",
		ingredients: [],
		page:        0,
		state:       states.defaultState
	};
	
	this.resultSet = [];
	
	
	this.initialize = function(filters) {
		this.filters = this.completeFilters(filters);
		var viewData = {}
		
		viewData.ingredients = Ingredient.getAllNames()
		viewData.tags = Cocktail.getTags()
		viewData.strengths = Cocktail.getStrengths()
		viewData.methods = Cocktail.getMethods()
		
		viewData.letters = Cocktail.getFirstLetters()
		viewData.names = Ingredient.getAllSecondNames()
		viewData.byName = Ingredient.getNameBySecondNameHash()
		view.initialize(viewData, this.filters.state);
		this.applyFilters();
	};
	
	this.randomIngredient = function(){
		var allNames = Ingredient.getAllNames()
		var num = Math.floor((allNames.length)*Math.random());
		return allNames[num];
	};
	
	this.randomCocktailNames = function(){
		var cocktails = Cocktail.getAll()
		var num = Math.floor((cocktails.length)*Math.random());
		var cocktail = cocktails[num];
		return [cocktail.name, cocktail.name_eng];
	};
	
	this.completeFilters = function(filters){
		if(!filters)             filters = {};
		if(!filters.name)        filters.name = "";
		if(!filters.letter)      filters.letter = "";
		if(!filters.tag)         filters.tag = "";
		if(!filters.strength)    filters.strength = "";
		if(!filters.method)      filters.method = "";
		if(!filters.page)        filters.page = 0;
		
		if(!filters.ingredients) filters.ingredients = [];
		else if(filters.ingredients.split) filters.ingredients = filters.ingredients.split(",");
		
		if (!filters.marks)
			filters.marks = []
		else if (filters.marks.split)
			filters.marks = filters.marks.split(',')
		
		if(!filters.state) filters.state = states.defaultState;
		
		if(filters.ingredients.length || filters.tag || filters.strength || filters.method) {
			filters.state = states.byIngredients;
		}
		
		return filters;
	};
	
	this.resetFilters = function(){
		this.filters.name = "";
		this.filters.letter = "";
		this.filters.tag = "";
		this.filters.strength = "";
		this.filters.method = "";
		this.filters.ingredients = [];
		this.filters.marks = []
		this.filters.page = 0;
		this.filters.state = states.defaultState;
	};
	
	this.filtersAreEmpty = function(){
		return (!this.filters.name && !this.filters.letter &&
				!this.filters.tag && !this.filters.strength &&
				!this.filters.method && !this.filters.ingredients.length)
	};
	
	
	this.uniqueTags = function(set){
		var res = [];
		for(var i = 0; i < set.length; i++){ res = res.concat(set[i].tags) }
		return res.uniq();
	};
	
	this.uniqueStrengths = function(set){
		var res = [];
		for(var i = 0; i < set.length; i++){ res.push(set[i].strength) }
		return res.uniq();
	};

	this.uniqueMethods = function(set){
		var res = [];
		for(var i = 0; i < set.length; i++){ res.push(set[i].method) }
		return res.uniq();
	};

	this.onStateChanged = function(state){
		this.resetFilters();
		this.filters.state = state;
		this.applyFilters();
	}
	
	this.onPageChanged = function(num){
		this.filters.page = num;
		view.controller.saveFilters(this.filters);
	};
	
	this.onLetterFilter = function(name, name_all) {
		if(name != this.filters.letter) {
			this.filters.ingredients = [];
			this.filters.tag         = "";
			this.filters.strength    = "";
			this.filters.method      = "";
			this.filters.page        = 0;
			
			if(name != name_all) {
				this.filters.letter    = name;
				Statistics.cocktailsFilterSelected(name)
			} else this.filters.letter = "";
			this.applyFilters();
		}
	};
	
	this.onNameFilter = function(name) {
		if(name != this.filters.name) {
			this.filters.ingredients = [];
			this.filters.tag         = "";
			this.filters.strength    = "";
			this.filters.method      = "";
			this.filters.page        = 0;
			this.filters.name        = name;
			this.applyFilters();
		}
	}
	
	this.onTagFilter = function(name) {
		if(name != this.filters.tag) {
			this.filters.letter  = "";
			this.filters.tag     = name;
			Statistics.cocktailsFilterSelected(name)
		} else this.filters.tag  = "";
		this.filters.method = "";
		this.filters.page = 0;
		this.applyFilters();
	};
	
	this.onStrengthFilter = function(name) {
		if(name != this.filters.strength) {
			this.filters.letter      = "";
			this.filters.strength    = name;
			Statistics.cocktailsFilterSelected(name)
		} else this.filters.strength = "";
		this.filters.page = 0;
		this.filters.tag = "";
		this.filters.method = "";
		this.applyFilters();
	};
	
	this.onMethodFilter = function(name) {
		if(name != this.filters.method) {
			this.filters.letter  = "";
			this.filters.method  = name;
			Statistics.cocktailsFilterSelected(name)
		} else this.filters.method = "";
		this.filters.page = 0;
		this.applyFilters();
	};	

	this.onIngredientFilter = function(name, remove) {
		this.filters.letter   = "";
		this.filters.page     = 0;
		this.filters.strength = "";
		this.filters.tag      = "";
		this.filters.method   = "";
		
		if (!name) // removing all
		{
			this.filters.ingredients = []
			this.filters.marks = []
			this.applyFilters()
			return
		}
		
		var idx = this.filters.marks.indexOf(name)
		if (idx >= 0)
		{
			this.filters.marks.splice(idx, 1)
			this.applyFilters()
			return
		}
		
		var idx = this.filters.ingredients.indexOf(name);
		if (remove) {
			this.filters.ingredients.splice(idx, 1);
		} else if (idx == -1){
			this.filters.ingredients.push(name);
			Statistics.ingredientSelected(Ingredient.getByName(name))
		} else return; // duplicate entry
		this.applyFilters();
	};
	
	this.onMarkAddFilter = function (name)
	{
		var idx = this.filters.marks.indexOf(name)
		if (idx < 0)
		{
			this.filters.marks.push(name)
			this.applyFilters()
			return
		}
	}
	
	// get states by current filters
	this.getGroupStates = function(){
		var set = [], groupStates = {};
		
		if (this.filtersAreEmpty())
		{
			var res = {}
			res.tags = Cocktail.getTags()
			res.strengths = Cocktail.getStrengths()
			res.methods = Cocktail.getMethods()
		}
		
		// strengths state - depends only on ingredients
		var rFilters = cloneObject(this.filters);
		rFilters.strength = "", rFilters.tag  = "", rFilters.method = "";
		groupStates.strengths = this.uniqueStrengths(this.getCocktailsByFilters(rFilters, states));
		
		// tags state - depends on ingredients and strength
		rFilters = cloneObject(this.filters);
		rFilters.tag = "", rFilters.method = "";
		groupStates.tags = this.uniqueTags(this.getCocktailsByFilters(rFilters, states));
		
		// methods state - depends on ingredients, strength and tag
		rFilters = cloneObject(this.filters);
		rFilters.method = "";
		groupStates.methods = this.uniqueMethods(this.getCocktailsByFilters(rFilters, states));
		
		return groupStates;
	};
	
	var getBySimilarNameCache = {},
		allCocktails = Cocktail.getAll()
	this.getBySimilarName = function (name)
	{
		if (getBySimilarNameCache[name])
			return getBySimilarNameCache[name]
			
		var words = name.split(/\s+/),
			res = [], db = allCocktails
		
		for (var i = 0; i < words.length; i++)
			words[i] = new RegExp('(?:^|\\s|-)' + RegExp.escape(words[i]), 'i')
		
		var first = words[0], jl = words.length
		SEARCH: for (var i = 0; i < db.length; i++)
		{
			var cocktail = db[i], name
			
			if (first.test(cocktail.name))
				name = cocktail.name
			else if (first.test(cocktail.name_eng))
				name = cocktail.name_eng
			else
				continue SEARCH
			
			for (var j = 1; j < jl; j++)
				if (!words[j].test(name))
					continue SEARCH
			
			res.push(cocktail)
		}
		return (getBySimilarNameCache[name] = res)
	},
	
	
	this.getCocktailsByFilters = function (filters, states)
	{
		var res = null
		
		if (filters.name)
			return this.getBySimilarName(filters.name)
		
		if (filters.letter)
			return Cocktail.getByLetter(filters.letter)
		
		if (filters.tag)
			res = Cocktail.getByTag(filters.tag)
		
		if (filters.strength)
			res = Cocktail.getByStrength(filters.strength, res)
		
		if (filters.method)
			res = Cocktail.getByMethod(filters.method, res)
		
		if (filters.marks && filters.marks.length)
		{
			var marks = filters.marks, ingredients = []
			for (var i = 0; i < marks.length; i++)
				ingredients.push(Ingredient.getByMark(marks[i]))
			
			// concat all the ingredients in one native operation just like SIMD ;)
			ingredients = Array.prototype.concat.apply([], ingredients)
			res = Cocktail.getByIngredients(ingredients, res, 1)
		}
		
		if (filters.ingredients && filters.ingredients.length)
			res = Cocktail.getByIngredientNames(filters.ingredients, res)
		
		if (!res)
		{
			if (filters.state == states.byName)
				res = Cocktail.getAll().shuffled()
			else
				res = Cocktail.getAll().sortedBy(Cocktail.nameSort)
		}
		
		return res
	}
	
	
	this.applyFilters = function()
	{
		var filters = this.filters
		view.onModelChanged(this.getCocktailsByFilters(filters, states), filters, this.getGroupStates());
	};
}

;(function(){

var myName = 'IngredientsSearcher', Me = self[myName] = Class(myName)

eval(NodesShortcut())

Me.prototype.extend
({
	initialize: function (ingredients, names)
	{
		this.ingredients = ingredients || []
		this.names = names || {}
		this.cache = {}
		this.withouts = {}
	},
	
	search: function (substr, count)
	{
		var cache = this.cache, res
		
		substr = substr.replace(/^\s+|\s+$/g, '') // trim
		if (substr === '')
			res = []
		else
		{
			if (!(res = cache[substr]))
				res = cache[substr] = this.searchInSet(this.ingredients, this.names, substr, count)
			
			var withouts = this.withouts,
				filtered = []
			
			for (var i = 0, il = res.length; i < il; i ++)
			{
				var row = res[i]
				if (!withouts[row[0]])
					filtered.push(row)
			}
			
			res = filtered
		}
		
		return res
	},
	
	searchInSet: function (set, names, substr, count)
	{
		var rex = new RegExp('(^|.*\\s)((' + RegExp.escape(substr) + ')(.*?))(\\s.*|$)', 'i'),
			matches = [], res = []
		
		for (var i = 0, il = set.length; i < il; i++)
		{
			var m, v = set[i]
			if (m = rex.exec(v))
			{
				// log(m)
				// matches.push([(10000 * m[2].length) + (100 * m[1].length) + v.length, v, m])
				matches.push([m[2].length, v, m])
			}
		}
		
		matches = matches.sort(this.sortByWeight)
		
		for (var i = 0, il = matches.length; i < il && count-- > 0; i++)
		{
			var v = matches[i], m = v[2]
			
			var text = N('span')
			if (m[1])
				text.appendChild(T(m[1]))
			// m[2] is used instead of substr because m[2] != substr when searching with "i"
			text.appendChild(N('span', 'substr', m[3]))
			if (m[3])
				text.appendChild(T(m[4] + m[5]))
			
			v = v[1]
			var name = names[v]
			if (name)
			{
				text.appendChild(T(' — это «' + name + '»'))
				v = name
			}
			
			res[i] = [v, text]
		}
		
		return res
	},
	
	sortByWeight: function (a, b) { return a[0] - b[0] }
})

})();
function remClass(elem, className) { if(elem) elem.remClassName(className) };

function CocktailsView (states, nodes, styles) {
	
	new Programica.RollingImagesLite(nodes.resultsDisplay, {animationType: 'easeInOutQuad', duration:0.75});
	
	this.filterElems   = { tag: null, strength: null, method: null, letter: null };
	this.perPage       = 16;
	this.np            = -1;
	this.renderedPages = {}
	this.nodeCache     = []
	
	
	this.riJustInited  = true;
	this.dropTargets   = [nodes.cartEmpty, nodes.cartFull];
	
	this.currentState;
	this.currentFilters;
	this.stateSwitcher;
	this.resultSet; // for caching purposes only
	
	this.initialize = function (viewData, state)
	{
		this.viewData = viewData
		
		var set = viewData.ingredients.slice()
		set.push.apply(set, viewData.names)
		set = set.sort()
		
		var searcher = this.searcher = new IngredientsSearcher(set, viewData.byName)
		var completer = this.completer = new Autocompleter().bind(nodes.searchByIngredsInput)
		completer.setDataSource(searcher)
		
		this.renderLetters(nodes.alphabetRu,     this.viewData.letters);
		this.renderGroupSet(nodes.tagsList,      this.viewData.tags);
		this.renderGroupSet(nodes.strengthsList, this.viewData.strengths);
		this.renderGroupSet(nodes.methodsList,   this.viewData.methods);
		
		this.bindEvents();
		this.turnToState(state);
	};
	
	this.bindEvents = function () {
		var self = this;
		
		var letterLinks = cssQuery("a", nodes.alphabetRu).concat(nodes.lettersAll);
		for(var i = 0; i < letterLinks.length; i++){
			letterLinks[i].addEventListener('mousedown', function(e){
				self.controller.onLetterFilter(e.target.innerHTML.toUpperCase(), 
											nodes.lettersAll.innerHTML.toUpperCase());
			}, false);
		}
		
		var tagLinks = cssQuery("dd", nodes.tagsList);
		for(var i = 0; i < tagLinks.length; i++){
			tagLinks[i].addEventListener('mousedown', function(num){ return function(){
				if(!tagLinks[num].hasClassName(styles.disabled)) {
					self.controller.onTagFilter(this.value)
				}
			}}(i), false);
		}
		
		var strengthLinks = cssQuery("dd", nodes.strengthsList);
		for(var i = 0; i < strengthLinks.length; i++){
			strengthLinks[i].addEventListener('mousedown', function(num){ return function(){
				if(!strengthLinks[num].hasClassName(styles.disabled)) {
					self.controller.onStrengthFilter(this.innerHTML.toLowerCase());
				}
			}}(i), false);
		}

		var methodLinks = cssQuery("dd", nodes.methodsList);
		for(var i = 0; i < methodLinks.length; i++){
			methodLinks[i].addEventListener('mousedown', function(num){ return function(){
				if(!methodLinks[num].hasClassName(styles.disabled)) {
					self.controller.onMethodFilter(this.innerHTML.toLowerCase());
				}
			}}(i), false);
		}
		
		var ril = nodes.resultsDisplay.RollingImagesLite;
		
		nodes.bigPrev.addEventListener('mousedown', function(e){ ril.goPrev() }, false);
		nodes.bigNext.addEventListener('mousedown', function(e){ ril.goNext() }, false);
		
		ril.onselect = function (node, num) {
			if (!self.riJustInited) {
				self.controller.onPageChanged(num);
				self.renderNearbyPages(num, 0)
			} else { self.riJustInited = false }
			
			// big pager buttons
			if(num == (self.np-1) || self.np == 1) nodes.bigNext.addClassName(styles.disabled);
			else nodes.bigNext.remClassName(styles.disabled);
			if(num == 0 || self.np == 1) nodes.bigPrev.addClassName(styles.disabled);
			else nodes.bigPrev.remClassName(styles.disabled);
		}
		
		nodes.searchExampleIngredient.addEventListener('mousedown', function(e){ self.onIngredientAdded(this.innerHTML) }, false);
		
		nodes.searchByName.getElementsByTagName("form")[0].addEventListener('submit', function(e) { e.preventDefault() }, false);
		var searchByNameInput = nodes.searchByName.getElementsByTagName("input")[0];
		searchByNameInput.addEventListener('keyup', function(e){ self.controller.onNameFilter(this.value) }, false);
		
		nodes.searchTipName.show = function () {
			this.style.display = "block";
			this.style.visibility = "visible";
			var names = self.controller.needRandomCocktailNames();
			nodes.searchExampleName.innerHTML = names[0];
			nodes.searchExampleNameEng.innerHTML = names[1];
		};
		
		nodes.removeAllIngreds.addEventListener('click', function(e){
				self.onAllIngredientsRemoved();
			}, false);
		
		nodes.searchTipIngredient.show = function () {
			this.style.display = "block";
			this.style.visibility = "visible";
			nodes.searchExampleIngredient.innerHTML = self.controller.needRandomIngredient();
		};
		
		nodes.ingredsView.show = function(){
			this.style.display = "block";
			this.style.visibility = "visible";
			nodes.searchTips.hide();
		}
		
		nodes.ingredsView.hide = function(){
			this.style.visibility = "hidden";
			this.style.display = "none";
			nodes.searchTips.show();
		}
		
		var nameSearchHandler = function (e) {
			searchByNameInput.value = this.innerHTML;
			self.controller.onNameFilter(this.innerHTML);
			nodes.searchTipName.hide();
		};
		
		nodes.searchExampleName.addEventListener('mousedown', nameSearchHandler, false);
		nodes.searchExampleNameEng.addEventListener('mousedown', nameSearchHandler, false);
		
		this.stateSwitcher = Switcher.bind(nodes.searchTabs, nodes.searchTabs.getElementsByTagName("li"),
						[nodes.searchByName, nodes.searchByLetter, nodes.searchByIngreds]);
		
		this.stateSwitcher.onselect = function (num) {
			self.turnToState(num);
			self.controller.onStateChanged(num);
		}
		
		function changeListener (e)
		{
			nodes.searchByIngredsInput.value = ''
			self.onIngredientAdded(e.data.value)
			return false // prevents input value blinking in FF
		}
		this.completer.onconfirm = changeListener
		nodes.searchByIngredsForm.addEventListener('submit', function (e) { e.preventDefault() }, false)
	};
	
	this.turnToState = function(state){
		this.currentState = state;
		this.stateSwitcher.drawSelected(state);
		
		var viewport = nodes.mainArea.getElementsByClassName("viewport")[0]; 
		
		var bodyWrapper = nodes.bodyWrapper
		for (var k in states)
			// toggleClassName(k, states[k] == state) must be used
			states[k] == state ? bodyWrapper.addClassName(k) : bodyWrapper.remClassName(k)
		
		if(state == states.byIngredients) {
			nodes.tagStrengthArea.show();
			this.perPage = 16;
		} else {
			nodes.tagStrengthArea.hide();
			this.perPage = 20;
		}
		
		nodes.ingredsView.hide();
		nodes.searchTipIngredient.setVisible(state == states.byIngredients);
		nodes.searchTipName.setVisible(state == states.byName);
		if(state != states.byName) cssQuery("input", nodes.searchByName)[0].value = "";
	};
	
	this.onAllIngredientsRemoved = function () {
		this.controller.onIngredientFilter();
	};
	
	this.onIngredientAdded = function(name)
	{
		var markToken = 'марка '
		if (name.indexOf(markToken) == 0)
			this.controller.onMarkAddFilter(name.substr(markToken.length), false)
		else
			this.controller.onIngredientFilter(name, false)
	}
	
	this.onIngredientRemoved = function(name) {
		this.controller.onIngredientFilter(name, true);
	};
	
	this.onModelChanged = function(resultSet, filters, groupStates) { // model
		this.currentFilters = filters;
		
		this.renderAllPages(resultSet, filters.page);
		this.renderFilters(this.currentFilters, groupStates.tags, groupStates.strengths, groupStates.methods);
		this.controller.saveFilters(this.currentFilters);
		
		var withouts = this.searcher.withouts = {},
			ingredients = filters.ingredients;
		
		for (var i = 0, il = ingredients.length; i < il; i ++){
			withouts[ingredients[i]] = true;
		}
	};
	
	this.renderFilters = function(filters, tagState, strengthState, methodState){
		remClass(this.filterElems.letter || nodes.lettersAll, styles.selected);
		if(filters.letter != "") {
			var letterElems = cssQuery("a", nodes.alphabetRu).concat(nodes.lettersAll);
			
			for(var i = 0; i < letterElems.length; i++) {
				if(letterElems[i].innerHTML == filters.letter.toLowerCase()){
					this.filterElems.letter = letterElems[i];
					break;
				}
			}   
		} else this.filterElems.letter = nodes.lettersAll;
		this.filterElems.letter.addClassName(styles.selected);
		
		// TODO: simplify this code with nodes[...] while avoiding the copy-paste
		var tagElems = nodes.tagsList.getElementsByTagName("dd");
		for(var i = 0; i < tagElems.length; i++) {
			var elemTxt = tagElems[i].value
			if(elemTxt == filters.tag) {
				this.filterElems.tag = tagElems[i];
				this.filterElems.tag.className = styles.selected;
			} else if(tagState.indexOf(elemTxt) == -1) {
				tagElems[i].className = styles.disabled;
			} else {
				tagElems[i].className = "";
			}
		}
		
		var strengthElems = nodes.strengthsList.getElementsByTagName("dd");
		for(var i = 0; i < strengthElems.length; i++) {
			var elemTxt = strengthElems[i].innerHTML.toLowerCase();
			if(elemTxt == filters.strength) {
				this.filterElems.strength = strengthElems[i]; 
				this.filterElems.strength.className = styles.selected;
			} else if(strengthState.indexOf(elemTxt) == -1) {
				strengthElems[i].className = styles.disabled
			} else {
				strengthElems[i].className = "";
			}
		}
		
		var methodElems = nodes.methodsList.getElementsByTagName("dd");
		for(var i = 0; i < methodElems.length; i++) {
			var elemTxt = methodElems[i].innerHTML.toLowerCase();
			if(elemTxt == filters.method) {
				this.filterElems.method = methodElems[i]; 
				this.filterElems.method.className = styles.selected;
			} else if(methodState.indexOf(elemTxt) == -1) {
				methodElems[i].className = styles.disabled
			} else {
				methodElems[i].className = "";
			}
		}
		
		var ingredientsParent = nodes.searchesList;
		ingredientsParent.empty();
		
		var words = filters.marks.concat(filters.ingredients)
		for (var i = 0, il = words.length; i < il; i++)
		{
			ingredientsParent.appendChild(this.createIngredientElement(words[i]));
			if (i != (il-1))
				ingredientsParent.appendChild(document.createTextNode(" + "));
		}
		
		if(this.currentState == states.byIngredients){
			nodes.searchTipIngredient.setVisible(words.length == 0)
			nodes.ingredsView.setVisible(words.length > 0)
		}
		
		if(filters.page > 0) {
			nodes.resultsDisplay.RollingImagesLite.goToNode($('page_'+filters.page), 'directJump');	
		}
		
		if (filters.name)
		{
			var input = cssQuery("input", nodes.searchByName)[0]
			if (input.value != filters.name)
				input.value = filters.name
		}
	},
	
	this.renderAllPages = function(resultSet, pageNum){
		this.resultSet = resultSet;
		this.np = this.getNumOfPages(resultSet, this.perPage);
		
		nodes.resultsRoot.empty();
		
		this.renderedPages = {}
		this.nodeCache     = []
		this.renderSkeleton(this.np);
		this.renderNearbyPages(pageNum);
		
		this.renderPager(this.np);
		nodes.resultsDisplay.RollingImagesLite.sync();
		nodes.resultsDisplay.RollingImagesLite.goInit();
	};
	
	this.renderSkeleton = function (count)
	{
		var parent = nodes.resultsRoot, pages = nodes.pages = []
		for (var i = 0; i < count; i++)
		{
			var page = pages[i] = document.createElement('ul')
			page.id = 'page_' + i
			page.className = 'point cocktails';
			parent.appendChild(page)
		}
	}
	
	this.renderNearbyPages = function (num, delta)
	{
		if (delta === undefined)
			delta = 1
		
		for (var i = num - delta; i <= num + delta; i++)
			if(i >= 0 && i < this.np && !this.renderedPages[i])
				this.renderPage(i)
	}
	
	this.renderGroupSet = function(parent, set){
		for(var i = 0; i < set.length; i++) {
			var dd = document.createElement("dd");
			dd.value = set[i]
			var span = document.createElement("span");
			var txt = document.createTextNode(set[i].capitalize());
			dd.appendChild(txt);
			parent.appendChild(dd);
		}		
	};
	
	this.renderLetters = function(parent, set){
		for(var i = 0; i < set.length; i++){
			var a = document.createElement("a");
			a.innerHTML = set[i];
			parent.appendChild(a);
		}
	},
	
	this.renderPage = function (num)
	{
		var cocktails = this.resultSet,
			node, cocktail, cache = this.nodeCache,
			parent = nodes.pages[num],
			end = (num + 1) * this.perPage,
			dropTargets = this.dropTargets
		
		for (var i = num * this.perPage; i < end; i++)
		{
			if (!(node = cache[i]))
			{
				if (!(cocktail = cocktails[i]))
					continue
				node = cache[i] = cocktail.getPreviewNode()
				node.img.__draggable = [cocktail.name, dropTargets]
			}
			parent.appendChild(node)
		}
		
		this.renderedPages[num] = true
	};
	
	this.createIngredientElement = function(name){
		var a = document.createElement("a");
		a.innerHTML = name;
		var self = this;
		a.addEventListener('click', function(e){
			self.onIngredientRemoved(name);
		}, false);
		return a;
	};
	
	this.getNumOfPages = function(resultSet, perPage) {
		if ((resultSet.length % perPage) == 0) return (resultSet.length/perPage);
		return parseInt(resultSet.length / perPage) + 1;
	};
	
	this.renderPager = function (numOfPages) {
		var span = nodes.pagerRoot;
		span.empty();
		for (var i = 1; i <= numOfPages; i++) {
			var a = document.createElement("a");
			a.className= i >= 10 ? "button two" : "button";
			a.appendChild(document.createTextNode(i));
			span.appendChild(a);
			span.appendChild(document.createTextNode(' '))
		}
	};
}

function keyForValue(hash, value) {
  for(var key in hash) if(hash[key] == value) return key
  return null
}

function CocktailsController (states, cookies, model, view) {
	this.model = model;
	this.view	= view;
	
	this.hashTimeout = null;
	
	this.initialize = function () {
		var filters = this.filtersFromRequest();
		var states = null;
		if(!filters) filters = this.filtersFromCookie();
		
		this.view.controller = this;
		this.model.initialize(filters);
		
		// fix for cocktails initialization issue
		this.currentHash = window.location.hash
		var me = this
		function checkHash ()
		{
			if (me.currentHash != window.location.hash)
				window.location.reload(true)
		}
		setInterval(checkHash, 250)
	};
	
	this.filtersFromRequest = function () {
		var address = window.location.href;
		var match = address.match(/.+\#(.+)/);
		if(match){
			var params = match[1].split("&");
			var filters = {};
			for(var i = 0; i < params.length; i++) {
				var pair = params[i].split("=");
				filters[pair[0]]=decodeURIComponent(pair[1]);
			}
            filters.state = states[filters.state];
			return filters;
		} else return null;
	};
	
	this.filtersFromCookie = function () {
		var cookie = Cookie.get(cookies.filter);
		if(cookie) return Object.parse(cookie);
		else return null;
	};
	
	this.saveFilters = function (filters) {
		var self = this;
		clearTimeout(this.hashTimeout);
		this.hashTimeout = setTimeout(function() { 
			self.updatePageHash(filters);
			Cookie.set(cookies.filter, Object.stringify(filters));
		} , 400);
	};
	
	this.updatePageHash = function(filters) {
		var pairs = [];
		for(var key in filters)
			if(filters[key] != "" || (filters[key] === 0 && key != "page")) {
				var value = filters[key];
				if(key == "state") value = keyForValue(states, value)
				pairs.push([key, value]);
			}
		
		var hash = [], encode = encodeURIComponent;
		for(var i = 0; i < pairs.length; i++) {
			hash[i] = encode(pairs[i][0]) + "=" + encode(pairs[i][1]);
		}
		if (hash)
		{
			window.location.hash = hash.join('&')
			this.currentHash = window.location.hash
		}
	};
	
	this.onLetterFilter = function(letter, all) {
		this.model.onLetterFilter(letter, all);
	};
	
	this.onTagFilter = function(tag) {
		this.model.onTagFilter(tag);
	};
	
    this.onMethodFilter = function(method) {
		this.model.onMethodFilter(method);
	};

	this.onStrengthFilter = function(strength) {
		this.model.onStrengthFilter(strength);
	};
	
	this.onIngredientFilter = function(name, remove) {
		this.model.onIngredientFilter(name, remove);
	};
	
	this.onMarkAddFilter = function(name, remove) {
		this.model.onMarkAddFilter(name, remove);
	};
	
	this.onNameFilter = function(name){
		this.model.onNameFilter(name);
	};
	
	this.onPageChanged = function(num){
		this.model.onPageChanged(num);
	};
	
	this.onStateChanged = function(num){
		this.model.onStateChanged(num);
	}
	
	this.needRandomIngredient = function(){
		return this.model.randomIngredient();
	};
	
	this.needRandomCocktailNames = function(){
		return this.model.randomCocktailNames();
	};
	
	this.initialize();
};

