/**********************************************************************
* 
* Simple Snake 
*
* This code is designed to illustrate the non-intuitive approach to an
* implementation, building a snake game as a cellular automaton rather
* than the more obvious, set of entities (OOP) or a number of sets 
* of procedures and data structures, directly emulating the "tactile" 
* perception of the game, i.e. independent field, snakes, walls, apples
* and their interactions.
*
* In this approach there are no entities, no snakes, no apples, no 
* walls, just a set of cells in a field and cell behaviours per game 
* step:
* 	- empty cells, apples and walls just sit there
* 	- "snake" cells:
* 		- decrement age
* 		- if age is 0 clear cell
* 		- if cell has direction (i.e. snake head)
* 			- if target cell is red (apple) increment age
* 			- color new cell in direction:
* 				- set age on to current age + 1
* 				- set direction to current
* 			- clear direction
*
* NOTE: that in the above description some details are omitted for 
* 		clarity...
*
*
* This code is structured in a scalable and introspective way:
* 	- Snake object is reusable as a prototype enabling multiple games
* 		to run at the same time
* 	- Snake implements an open external control scheme, i.e. it does not 
* 		impose a specific way to implementing the way to control the game
* 	- Simple (but not trivial) code and code structure
* 	- Introspective: no hidden/masked state or functionality
* 	- No external dependencies
*
*
* Goals:
* 	- Show that the "intuitive" is not the only approach
* 	- Show that the "intuitive" is not allways the simplest
* 	- Show one approach to a scalable yet simple architecture
*
*
*
**********************************************************************/
function makeEvent(handler_attr){
	return function(func){
		if(func === null){
			delete this[handler_attr]
		} else if(func instanceof Function){
			var handlers = this[handler_attr] = this[handler_attr] || []
			handlers.push(func)
		} else {
			var that = this
			var args = [].slice.call(arguments)
			this[handler_attr]
				&& this[handler_attr]
					.forEach(function(handler){ handler.apply(that, args) })
		}
		return this
	}
}
var Snake = {
	config: {
		field_size: 32,
		interval: 150,
	},
	_field: null,
	_cells: null,
	players: null,
	field_size: null,
	get random_point(){
		var cells = this._cells
		var l = cells.length
		var w = this.field_size.width
		do {
			var i = Math.floor(Math.random() * l)
		} while(cells[i].classList.length > 0)
		return {
			x: i%w,
			y: Math.floor(i/w),
		}
	},
	get random_direction(){
		return ('nesw')[Math.floor(Math.random() * 4)] },
	// utils...
	normalize_point: function(point){
		point = point || {}
		var w = this.field_size.width
		var x = point.x % w
		x = x < 0 ? (x + w) : x
		var h = this.field_size.height
		var y = point.y % h
		y = y < 0 ? (y + h) : y
		return { x: x, y: y }
	},
	_make_field: function(w){
		var l = []
		l.length = w || this.config.field_size
		l.fill('
')
		this._field.innerHTML = 
			` | \n${ 
				l.map(function(){ 
					return `   ${ l.join('') } 
` 
				}).join('\n') 
			}\n
`
	},
	_tick: function(){
		var that = this
		var l = this._cells.length
		var w = this.field_size.width
		var h = this.field_size.height
		var tick = this.__tick = (this.__tick + 1 || 0)
		var directions = 'neswn'
		this._cells.forEach(function(cell, i){
			var color = cell.style.backgroundColor
			// skip cells we touched on this tick...
			if(cell.tick == tick){
				return
			}
			// snake...
			if(cell.age != null){
				// handle cell age...
				if(cell.age == 0){
					delete cell.age
					cell.classList.remove('snake')
					cell.style.backgroundColor = ''
				} else {
					cell.age -= 1
				}
				// snake head -> move...
				var direction = cell.direction
				if(directions.indexOf(direction) >= 0){
					// turn...
					if(that.players[color] != ''){
						var turn = that.players[color] || ''
						var j = turn == 'left' ? directions.indexOf(direction) - 1
							: directions.indexOf(direction) + 1
						j = j < 0 ? 3 : j
						direction = directions[j]
						that.players[color] = ''
					}
					// get next cell index...
					var next = 
						direction == 'n' ? 
							(i < w ? l - w + i : i - w)
						: direction == 's' ? 
							(i > (l-w-1) ? i - (l-w) : i + w)
						: direction == 'e' ? 
							((i+1)%w == 0 ? i - (w-1) : i + 1)
						: (i%w == 0 ? i + (w-1) : i - 1)
					next = that._cells[next]
					var age = cell.age
					var move = false
					// special case: other snake's head -> kill both...
					if(next.direction){
						var other = next.style.backgroundColor
						next.classList.remove('snake')
						next.style.backgroundColor = ''
						// NOTE: we are not deleteing .direction here as 
						//		we can have upto 4 snakes colliding...
						next.direction = ''
						that.snakeKilled(other, next.age+1)
						that.snakeKilled(color, age+2)
						delete next.age
					// apple -> increment age...
					} else if(next.classList.contains('apple')){
						age += 1
						move = true
						next.classList.remove('apple')
						that.appleEaten(color, age+2)
					// empty -> just move...
					} else if(next.classList.length == 0){
						move = true
					// other -> kill...
					} else {
						that.snakeKilled(color, age+2)
					}
					// do the move...
					if(move){
						next.tick = tick
						next.style.backgroundColor = color
						next.classList.add('snake')
						next.age = age + 1
						next.direction = direction
					}
					delete cell.direction
				}
			}
			cell.tick = tick
		})
		this.tick(tick)
	},
	// constructors...
	snake: function(color, age, point, direction){
		point = this.normalize_point(point || this.random_point)
		var head = this._cells[point.x + point.y * this.field_size.width]
		head.style.backgroundColor = color
		head.classList.add('snake')
		head.direction = direction || this.random_direction
		head.age = (age || 5) - 1
		this.players[color] = ''
		return this
	},
	apple: function(point){
		point = this.normalize_point(point || this.random_point)
		var c = this._cells[point.x + point.y * this.field_size.width]
		c.classList.add('apple')
		c.style.backgroundColor = ''
		return this
	},
	wall: function(point, direction, length){
		direction = direction || this.random_direction
		point = this.normalize_point(point || this.random_point)
		var x = point.x
		var y = point.y
		length = length || 1
		while(length > 0){
			this._cells[x + y * this.field_size.width].classList.add('wall')
			x += direction == 'e' ? 1
				: direction == 'w' ? -1
				: 0
			x = x < 0 ? this.field_size.width + x
				: x % this.field_size.width
			y += direction == 'n' ? -1
				: direction == 's' ? 1
				: 0
			y = y < 0 ? this.field_size.height + y
				: y % this.field_size.height
			length -= 1
		}
		return this
	},
	// events...
	appleEaten: makeEvent('__appleEatenHandlers'),
	snakeKilled: makeEvent('__killHandlers'),
	tick: makeEvent('__tickHandlers'),
	// actions...
	setup: function(field, size){
		this.config.field_size = size || this.config.field_size
		field = field || this._field
		field = this._field = typeof(field) == typeof('str') ? document.querySelector(field)
			: field
		this._make_field()
		this._cells = [].slice.call(field.querySelectorAll('td'))
		this.field_size = {
			width: field.querySelector('tr').querySelectorAll('td').length,
			height: field.querySelectorAll('tr').length,
		}
		this.players = {}
		return this
			.appleEaten(null)
			.snakeKilled(null)
	},
	start: function(t){
		this._field.classList.remove('paused')
		this.__timer = this.__timer 
			|| setInterval(this._tick.bind(this), t || this.config.interval || 200)
		// reset player control actions...
		var that = this
		Object.keys(this.players)
			.forEach(function(k){ that.players[k] = '' })
		this.tick()
		return this
	},
	stop: function(){
		this._field.classList.add('paused')
		clearInterval(this.__timer)
		delete this.__timer
		delete this.__tick
		return this
	},
	pause: function(){
		return this.__timer ? this.stop() : this.start() },
	left: function(color){ 
		this.players[color || Object.keys(this.players)[0]] = 'left' 
		return this
	},
	right: function(color){
		this.players[color || Object.keys(this.players)[0]] = 'right' 
		return this
	},
	// levels...
	randomLevel: function(){
		var a = Math.round(this.field_size.width/8)
		return this
			.wall(null, null, a*6)
			.wall(null, null, a*6)
			.wall(null, null, a*6) },
}
/*********************************************************************/
//var BG_ANIMATION = true
var __CACHE_UPDATE_CHECK = 10*60*1000
var __HANDLER_SET = false
var __DEBOUNCE_TIMEOUT = 100
var __DEBOUNCE = false
var KEY_CONFIG = {
	' ': ['pause'],
	ArrowLeft: ['left'],
	ArrowRight: ['right'], 
	// IE compatibility...
	Left: ['left'],
	Right: ['right'],
}
function makeKeyboardHandler(snake){
	return function(event){
		clearHints()
		var action = KEY_CONFIG[event.key]
		action 
			&& action[0] in snake 
			&& snake[action[0]].apply(snake, action.slice(1)) }}
function makeTapHandler(snake){
	return function(event){
		// prevent clicks and touches from triggering the same action 
		// twice -- only handle the first one within timeout...
		// NOTE: this should not affect events of the same type...
		if(__DEBOUNCE && event.type != __DEBOUNCE){ return }
		__DEBOUNCE = event.type
		setTimeout(function(){ __DEBOUNCE = false }, __DEBOUNCE_TIMEOUT)
		clearHints()
		// top of screen (1/8)...
		;(event.clientY || event.changedTouches[0].pageY) <= (document.body.clientHeight / 8) ? 
			setup()
		// bottom of screen 1/8...
		: (event.clientY || event.changedTouches[0].pageY) >= (document.body.clientHeight / 8)*8 ? 
			Snake.pause()
		// left/right of screen...
		: (event.clientX || event.changedTouches[0].pageX) <= (document.body.clientWidth / 2) ? 
			Snake.left() 
			: Snake.right() }}
function clearHints(){
	document.body.classList.contains('hints')
		&& document.body.classList.remove('hints') }
//---------------------------------------------------------------------
// XXX need to place the snake with some headroom in the direction of 
//		travel...
function setup(snake, timer, size){
	snake = snake || Snake
	// setup event handlers (only once)...
	if(!__HANDLER_SET){
		// control handlers...
		document.addEventListener('keydown', makeKeyboardHandler(snake))
		document.addEventListener('touchstart', makeTapHandler(snake))
		//document.addEventListener('mousedown', makeTapHandler(snake))
		// cache updater...
		var appCache = window.applicationCache
		if(appCache
				&& appCache.status != appCache.UNCACHED){
			appCache.addEventListener('updateready', function(){
				if(appCache.status == appCache.UPDATEREADY){
					console.log('CACHE: new version available...')
					appCache.swapCache()
					confirm('New version ready, reload?')
						&& location.reload()
				}
			})
			setInterval(function(){ appCache.update() }, __CACHE_UPDATE_CHECK)
		}
		__HANDLER_SET = true
	}
	function showScore(color, age){
		score = snake.__top_score = 
			(!snake.__top_score || snake.__top_score.score < age) ?
				{
					color: color || '',
					score: age || 0,
				}
				: snake.__top_score
		snake._field.setAttribute('score', score.score)
		snake._field.setAttribute('snake', score.color)
		snake._field.setAttribute('state', (
			score.score == age && score.color == color) ? '(current)' : '')
	}
	// setup the game...
	return snake
		.setup('.simplesnake', size)
		.randomLevel()
		.start(timer)
		.pause()
		// game events...
		.appleEaten(function(color, age){ 
			this.apple() 
			showScore(color, age)
		})
		.snakeKilled(function(color, age){ 
			this
				.pause()
				.snake(color, 3) 
			showScore(color, 3)
		})
		.tick(function(){
			// digital noise effect...
			window.BG_ANIMATION
				&& (!snake.__tick || snake.__tick % 2 == 0)
				&& this._cells.forEach(function(c){
					var v = Math.floor(Math.random() * 6)
					c.classList.length == 0 ?
						(c.style.backgroundColor = 
							`rgb(${255 - v}, ${255 - v}, ${255 - v})`)
					: c.classList.contains('wall') ?
						(c.style.backgroundColor = 
							`rgb(${220 - v*2}, ${220 - v*2}, ${220 - v*2})`)
					: null })
		})
		// game eleemnts...
		.apple()
		.apple()
		.snake('blue', 3)
}
/**********************************************************************
* vim:set ts=4 sw=4 :                                                */