/*** @using { core.RAIIEventEmitter } **/

var Modernizr = window.Modernizr

exports.capabilities = {
	csstransforms3d: Modernizr.csstransforms3d,
	csstransforms: Modernizr.csstransforms,
	csstransitions: Modernizr.csstransitions,
	mouseEnterLeaveSupported: _globals.core.os != "netcast"
}

var imageCache = null

exports.createAddRule = function(style) {
	if(! (style.sheet || {}).insertRule) {
		var sheet = (style.styleSheet || style.sheet)
		return function(name, rules) {
			try {
				sheet.addRule(name, rules)
			} catch(e) {
				log("AddRule failed:", e, ", sheet:", sheet, ", name:", name, ", rules:", rules)
			}
		}
	}
	else {
		var sheet = style.sheet
		return function(name, rules) {
			try {
				sheet.insertRule(name + '{' + rules + '}', sheet.cssRules.length)
			} catch(e) {
				log("InsertRule failed:", e, ", sheet:", sheet, ", name:", name, ", rules:", rules)
			}
		}
	}
}

var StyleCache = function() {
	this._cache = {}
}

var StyleCachePrototype = StyleCache.prototype
StyleCachePrototype.constructor = StyleCache

StyleCachePrototype.update = function(element, name, value) {
	//log('update', element._uniqueId, name, value)
	var cache = this._cache
	var id = element._uniqueId
	var entry = cache[id]
	if (entry !== undefined) {
		entry.data[name] = value
		++entry.size
	} else {
		var data = {}
		data[name] = value
		cache[id] = {data: data, element: element, size: 1}
	}
}

StyleCachePrototype.pop = function(element) {
	var id = element._uniqueId
	var data = this._cache[id]
	if (data === undefined)
		return

	delete this._cache[id]
	return data
}

StyleCachePrototype.apply = function() {
	var cache = this._cache
	this._cache = {}

	for(var id in cache) {
		var entry = cache[id]
		entry.element.updateStyle(entry)
	}
}

var StyleClassifier = function (prefix) {
	var style = document.createElement('style')
	style.type = 'text/css'
	document.head.appendChild(style)

	this.prefix = prefix + 'C-'
	this.style = style
	this.total = 0
	this.stats = {}
	this.classes = {}
	this.classes_total = 0
	this._addRule = exports.createAddRule(style)
}

var StyleClassifierPrototype = StyleClassifier.prototype
StyleClassifierPrototype.constructor = StyleClassifier

StyleClassifierPrototype.add = function(rule) {
	this.stats[rule] = (this.stats[rule] || 0) + 1
	++this.total
}

StyleClassifierPrototype.register = function(rules) {
	var rule = rules.join(';')
	var classes = this.classes
	var cls = classes[rule]
	if (cls !== undefined)
		return cls

	cls = classes[rule] = this.prefix + this.classes_total++
	this._addRule('.' + cls, rule)
	return cls
}

StyleClassifierPrototype.classify = function(rules) {
	var total = this.total
	if (total < 10) //fixme: initial population threshold
		return ''

	rules.sort() //mind vendor prefixes!
	var classified = []
	var hot = []
	var stats = this.stats
	rules.forEach(function(rule, idx) {
		var hits = stats[rule]
		var usage = hits / total
		if (usage > 0.05) { //fixme: usage threshold
			classified.push(rule)
			hot.push(idx)
		}
	})
	if (hot.length < 2)
		return ''
	hot.forEach(function(offset, idx) {
		rules.splice(offset - idx, 1)
	})
	return this.register(classified)
}

var _modernizrCache = {}
if (_globals.core.userAgent.toLowerCase().indexOf('webkit') >= 0)
	_modernizrCache['appearance'] = '-webkit-appearance'

var getPrefixedName = function(name) {
	var prefixedName = _modernizrCache[name]
	if (prefixedName === undefined)
		_modernizrCache[name] = prefixedName = window.Modernizr.prefixedCSS(name)
	return prefixedName
}

exports.getPrefixedName = getPrefixedName

var passiveListeners = ['touchstart', 'touchend', 'wheel', 'scroll']
var passiveArg = Modernizr.passiveeventlisteners ? {passive: true} : false

var registerGenericListener = function(target) {
	var storage = target.__domEventListeners
	if (storage === undefined)
		storage = target.__domEventListeners = {}

	target.onListener('',
		function(name) {
			//log('registering generic event', name)
			var callback = storage[name] = target._context.wrapNativeCallback(function() {
				target.emitWithArgs(name, arguments)
			})

			var args = [name, callback]
			if (passiveListeners.indexOf(name) >= 0)
				args.push(passiveArg)

			target.dom.addEventListener.apply(target.dom, args)
		},
		function(name) {
			//log('removing generic event', name)
			target.dom.removeEventListener(name, storage[name])
		}
	)
}

var _loadedStylesheets = {}

exports.loadExternalStylesheet = function(url) {
	if (!_loadedStylesheets[url]) {
		var link = document.createElement('link')
		link.setAttribute('rel', "stylesheet")
		link.setAttribute('href', url)
		document.head.appendChild(link)
		_loadedStylesheets[url] = true
	}
}

var lastId = 0

var nodesCache = {};

/**
 * @constructor
 */

function mangleClass(name) {
	return $manifest$html5$prefix + name
}

exports.Element = function(context, tag, cls) {
	if (typeof tag === 'string') {
		if (cls === undefined)
			cls = ''

		var key = tag + '.' + cls
		if (!nodesCache[key]) {
			var el = document.createElement(tag)
			if ($manifest$html5$prefix || cls)
				el.classList.add(mangleClass(cls))
			if ($manifest$html5$prefix && cls)
				el.classList.add(mangleClass('')) //base item style, fixme: pass array here?
			nodesCache[key] = el
		}
		this.dom = nodesCache[key].cloneNode(false);
	}
	else
		this.dom = tag

	_globals.core.RAIIEventEmitter.apply(this)
	this._context = context
	this._transitions = {}
	this._class = ''
	this._widthAdjust = 0
	this._uniqueId = (++lastId).toString(36)
	this._firstChildIndex = 0

	registerGenericListener(this)
}

var ElementPrototype = exports.Element.prototype = Object.create(_globals.core.RAIIEventEmitter.prototype)
ElementPrototype.constructor = exports.Element

ElementPrototype.addClass = function(cls) {
	this.dom.classList.add(cls)
}

ElementPrototype.appendChildren = function(children) {
	if (children.length > 0) {
		var fragment = document.createDocumentFragment()
		children.forEach(function(child) {
			fragment.appendChild(child)
		})
		this.dom.appendChild(fragment)
	}
}

ElementPrototype.removeChildren = function(ui) {
	var removedChildren = []

	var dom = this.dom
	ui.children.forEach(function(child) {
		var element = child.element
		if (element !== undefined) {
			var childNode = element.dom
			if (childNode.parentNode === dom) {
				dom.removeChild(childNode)
				removedChildren.push(childNode)
			}
		}
	})
	return removedChildren
}

ElementPrototype.setHtml = function(html, component) {
	this._widthAdjust = 0 //reset any text related rounding corrections
	var dom = this.dom
	var children
	if (component !== undefined)
		children = this.removeChildren(component)
	else
		children = []
	dom.innerHTML = html
	this.appendChildren(children)
}

ElementPrototype.width = function() {
	this.updateStyle()
	return this.dom.clientWidth - this._widthAdjust
}

ElementPrototype.height = function() {
	this.updateStyle()
	return this.dom.clientHeight
}

ElementPrototype.fullWidth = function() {
	this.updateStyle()
	return this.dom.scrollWidth - this._widthAdjust
}

ElementPrototype.fullHeight = function() {
	this.updateStyle()
	return this.dom.scrollHeight
}

ElementPrototype.style = function(name, style) {
	var cache = this._context._styleCache
	if (style !== undefined) {
		cache.update(this, name, style)
	} else if (typeof name === 'object') { //style({ }) assignment
		for(var k in name)
			cache.update(this, k, name[k])
	}
	else
		throw new Error('cache is write-only')
}

ElementPrototype.setAttribute = function(name, value) {
	this.dom.setAttribute(name, value)
}

/** @const */
var cssUnits = {
	'left': 'px',
	'top': 'px',
	'width': 'px',
	'height': 'px',

	'border-radius': 'px',
	'border-width': 'px',

	'margin-left': 'px',
	'margin-top': 'px',
	'margin-right': 'px',
	'margin-bottom': 'px',

	'padding-left': 'px',
	'padding-top': 'px',
	'padding-right': 'px',
	'padding-bottom': 'px',
	'padding': 'px'
}

ElementPrototype.forceLayout = function() {
	this.updateStyle()
	return this.dom.offsetWidth | this.dom.offsetHeight
}

ElementPrototype.updateStyle = function(updated) {
	var element = this.dom
	if (!element)
		return

	if (updated === undefined) {
		updated = this._context._styleCache.pop(this)
		if (updated === undefined) //no update at all
			return
	}

	var styles = updated.data
	var elementStyle = element.style

	//fixme: classifier is currently broken, restore rules processor
	//var rules = []
	for(var name in styles) {
		var value = styles[name]
		//log('updateStyle', this._uniqueId, name, value)

		var prefixedName = getPrefixedName(name)
		var ruleName = prefixedName !== false? prefixedName: name
		if (Array.isArray(value))
			value = value.join(',')

		if (typeof value === 'number') {
			if (name === 'width')
				value += this._widthAdjust
			var unit = cssUnits[name]
			if (unit !== undefined) {
				value += unit
			}
		}

		elementStyle[ruleName] = value
	}
/*
	var cache = this._context._styleClassifier
	var cls = cache? cache.classify(rules): ''
	if (cls !== this._class) {
		var classList = element.classList
		if (this._class !== '')
			classList.remove(this._class)
		this._class = cls
		if (cls !== '')
			classList.add(cls)
	}
*/
}

ElementPrototype.append = function(el) {
	this.dom.appendChild((el instanceof exports.Element)? el.dom: el)
}

ElementPrototype.discard = function() {
	_globals.core.RAIIEventEmitter.prototype.discard.apply(this)
	this.remove()
}

ElementPrototype.remove = function() {
	var dom = this.dom
	if (dom.parentNode)
		dom.parentNode.removeChild(dom)
}

exports.Document = function(context, dom) {
	_globals.core.RAIIEventEmitter.apply(this)
	this._context = context
	this.dom = dom

	registerGenericListener(this)
}

var DocumentPrototype = exports.Document.prototype = Object.create(_globals.core.RAIIEventEmitter.prototype)
DocumentPrototype.constructor = exports.Document

exports.Window = function(context, dom) {
	_globals.core.RAIIEventEmitter.apply(this)
	this._context = context
	this.dom = dom

	registerGenericListener(this)
}

var WindowPrototype = exports.Window.prototype = Object.create(_globals.core.RAIIEventEmitter.prototype)
WindowPrototype.constructor = exports.Window

WindowPrototype.width = function() {
	return this.dom.innerWidth
}

WindowPrototype.height = function() {
	return this.dom.innerHeight
}

WindowPrototype.scrollY = function() {
	return this.dom.scrollY
}

exports.getElement = function(ctx, tag) {
	var tags = document.getElementsByTagName(tag)
	if (tags.length != 1)
		throw new Error('no tag ' + tag + '/multiple tags')
	return new exports.Element(ctx, tags[0])
}

exports.init = function(ctx) {
	imageCache = new _globals.html5.cache.Cache(loadImage)

	ctx._styleCache = new StyleCache()
	var options = ctx.options
	var prefix = ctx._prefix
	var divId = options.id
	var tag = options.tag || 'div'

	if (prefix) {
		prefix += '-'
		log('Context: using prefix', prefix)
	}

	var doc = new _globals.html5.html.Document(ctx, document)
	ctx.document = doc

	var win = new _globals.html5.html.Window(ctx, window)
	ctx.window = win
	var w, h

	var html = exports
	var div = document.getElementById(divId)
	var topLevel = div === null
	if (!topLevel) {
		div = new html.Element(ctx, div)
		w = div.width()
		h = div.height()
		log('Context: found element by id, size: ' + w + 'x' + h)
		win.on('resize', function() { ctx.width = div.width(); ctx.height = div.height(); });
	} else {
		w = win.width();
		h = win.height();
		log("Context: window size: " + w + "x" + h);
		div = html.createElement(ctx, tag)
		div.dom.id = divId
		win.on('resize', function() { ctx.width = win.width(); ctx.height = win.height(); });
		var body = html.getElement(ctx, 'body')
		body.append(div);
	}

	if (Modernizr.canvastext) {
		ctx._textCanvas = html.createElement(ctx, 'canvas')
		ctx._textCanvas.style('width', 0)
		ctx._textCanvas.style('height', 0)
		div.append(ctx._textCanvas)
		ctx._textCanvasContext = ('getContext' in ctx._textCanvas.dom)? ctx._textCanvas.dom.getContext('2d'): null
	} else {
		ctx._textCanvasContext = null
	}

	ctx.element = div
	ctx.width = w
	ctx.height = h

	win.on('scroll', function(event) { ctx.scrollY = win.scrollY(); });

	var onFullscreenChanged = function(e) {
		var state = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
		log('fullscreen change event:', state)
		ctx.fullscreen = state
	}

	new Array('webkitfullscreenchange', 'mozfullscreenchange', 'fullscreenchange', 'MSFullscreenChange').forEach(function(name) {
		div.on(name, onFullscreenChanged, true)
	})

	win.on('keydown', function(event) {
		if (ctx.processKey(event))
			event.preventDefault()
	}) //fixme: add html.Document instead
	win.on('orientationchange', function(event) {
		log('orientation changed event')
		ctx.system.screenWidth = window.screen.width
		ctx.system.screenHeight = window.screen.height
	})

	ctx._styleClassifier = $manifest$cssAutoClassificator? new StyleClassifier(ctx._prefix): null; //broken beyond repair
}

//fixme: this is sorta hack, generalize that across other backends
exports.initSystem = function(system) {
	var win = system._context.window

	win.on('focus', function() { system.pageActive = true })
	win.on('blur', function() { system.pageActive = false })

	system.screenWidth = window.screen.width
	system.screenHeight = window.screen.height
}

exports.createElement = function(ctx, tag, cls) {
	return new exports.Element(ctx, tag, cls)
}

exports.initRectangle = function(rect) {
}

var ImageComponent = _globals.core.Image

var updateImage = function(image, metrics) {
	if (!metrics) {
		image.status = ImageComponent.Error
		return
	}

	var style = {'background-image': 'url("' + image.source + '")'}

	var natW = metrics.width, natH = metrics.height
	image.sourceWidth = natW
	image.sourceHeight = natH

	if (image.fillMode !== ImageComponent.PreserveAspectFit) {
		image.paintedWidth = image.width
		image.paintedHeight = image.height
	}

	switch(image.horizontalAlignment) {
		case ImageComponent.AlignHCenter:
			style['background-position-x'] = 'center'
			break;
		case ImageComponent.AlignLeft:
			style['background-position-x'] = 'left'
			break;
		case ImageComponent.AlignRight:
			style['background-position-x'] = 'right'
			break;
	}

	switch(image.verticalAlignment) {
		case ImageComponent.AlignVCenter:
			style['background-position-y'] = 'center'
			break;
		case ImageComponent.AlignTop:
			style['background-position-y'] = 'top'
			break;
		case ImageComponent.AlignBottom:
			style['background-position-y'] = 'bottom'
			break;
	}

	switch(image.fillMode) {
		case ImageComponent.Stretch:
			style['background-repeat'] = 'no-repeat'
			style['background-size'] = '100% 100%'
			break;
		case ImageComponent.TileVertically:
			style['background-repeat'] = 'repeat-y'
			style['background-size'] = '100% ' + natH + 'px'
			break;
		case ImageComponent.TileHorizontally:
			style['background-repeat'] = 'repeat-x'
			style['background-size'] = natW + 'px 100%'
			break;
		case ImageComponent.Tile:
			style['background-repeat'] = 'repeat-y repeat-x'
			style['background-size'] = 'auto'
			break;
		case ImageComponent.PreserveAspectCrop:
			style['background-repeat'] = 'no-repeat'
			style['background-size'] = 'cover'
			break;
		case ImageComponent.Pad:
			style['background-repeat'] = 'no-repeat'
			style['background-position'] = '0% 0%'
			style['background-size'] = 'auto'
			break;
		case ImageComponent.PreserveAspectFit:
			style['background-repeat'] = 'no-repeat'
			style['background-size'] = 'contain'
			var w = image.width, h = image.height
			var targetRatio = 0, srcRatio = natW / natH

			if (w && h)
				targetRatio = w / h

			if (srcRatio > targetRatio && w) { // img width aligned with target width
				image.paintedWidth = w;
				image.paintedHeight = w / srcRatio;
			} else {
				image.paintedHeight = h;
				image.paintedWidth = h * srcRatio;
			}
			break;
	}
	style['image-rendering'] = image.smooth? 'auto': 'pixelated'
	image.style(style)

	image.status = ImageComponent.Ready
	image._context._processActions()
}

var failImage = function(image) {
	image._onError()
	image._context._processActions()
}

var loadImage = function(url, callback) {
	var tmp = new Image()

	tmp.onerror = function() {
		tmp.onload = null
		tmp.onerror = null
		callback(null)
	}

	tmp.onload = function() {
		tmp.onload = null
		tmp.onerror = null
		callback({ width: tmp.naturalWidth, height: tmp.naturalHeight })
	}
	tmp.src = url
}

exports.initImage = function(image) {
}

exports.loadImage = function(image) {
	var callback = function(metrics) {
		updateImage(image, metrics)
	}

	if (image.source.indexOf('?') < 0) {
		imageCache.get(image.source, callback)
	} else {
		loadImage(image.source, callback)
	}
}

exports.initText = function(text) {
}

var layoutTextSetStyle = function(text, style) {
	switch(text.verticalAlignment) {
		case text.AlignTop:		text._topPadding = 0; break
		case text.AlignBottom:	text._topPadding = text.height - text.paintedHeight; break
		case text.AlignVCenter:	text._topPadding = (text.height - text.paintedHeight) / 2; break
	}
	style['padding-top'] = text._topPadding
	style['height'] = text.height - text._topPadding
	text.style(style)
}

exports.setText = function(text, html) {
	text.element.setHtml(html, text)
}

exports.layoutText = function(text) {
	var ctx = text._context
	var textCanvasContext = ctx._textCanvasContext
	var wrap = text.wrapMode !== _globals.core.Text.NoWrap
	var element = text.element

	var dom = element.dom

	var isHtml = text.textFormat === text.Html || text.text.search(/[\<\&]/) >= 0 //dubious check

	if (!wrap && textCanvasContext !== null && !isHtml) {
		var font = text.font
		var fontSize
		if (font.pointSize)
			fontSize = Math.round(font.pointSize * 96 / 72)
		else
			fontSize = font.pixelSize
		//log('fontSize = ', fontSize)

		textCanvasContext.font = fontSize + 'px ' + font.family

		//log('font from canvas:', textCanvasContext.font, font.family, font.pixelSize, font.pointSize, fontSize)
		var metrics = textCanvasContext.measureText(text.text)
		text.paintedWidth = metrics.width
		text.paintedHeight = fontSize * font.lineHeight
		//log('layoutText', text.text, text.paintedWidth, text.paintedHeight)
		layoutTextSetStyle(text, {})
		return
	}
	var removedChildren = element.removeChildren(text)

	if (!wrap)
		text.style({ width: 'auto', height: 'auto', 'padding-top': 0 }) //no need to reset it to width, it's already there
	else
		text.style({ 'height': 'auto', 'padding-top': 0})

	//this is the source of rounding error. For instance you have 186.3px wide text, this sets width to 186px and causes wrapping
	text.paintedWidth = element.fullWidth()
	text.paintedHeight = element.fullHeight()

	//this makes style to adjust width (by adding this value), and return back _widthAdjust less
	element._widthAdjust = 1

	var style
	if (!wrap)
		style = { width: text.width, height: text.height } //restore original width value (see 'if' above)
	else
		style = {'height': text.height }

	layoutTextSetStyle(text, style)
	element.appendChildren(removedChildren)
}

exports.run = function(ctx, onloadCallback) {
	ctx.window.on($manifest$expectRunContextEvent ? 'runContext' : 'load', function() {
		onloadCallback()
	})
}

exports.tick = function(ctx) {
	//log('tick')
	ctx._styleCache.apply()
}

///@private
var setTransition = function(component, name, animation) {
	var html5 = exports
	var transition = {
		property: html5.getPrefixedName('transition-property'),
		delay: html5.getPrefixedName('transition-delay'),
		duration: html5.getPrefixedName('transition-duration'),
		timing: html5.getPrefixedName('transition-timing-function')
	}
	var element = component.element
	element.forceLayout() //flush styles before setting transition

	name = html5.getPrefixedName(name) || name //replace transform: <prefix>rotate hack

	var transitions = element._transitions
	var property	= transitions[transition.property] || []
	var duration	= transitions[transition.duration] || []
	var timing		= transitions[transition.timing] || []
	var delay		= transitions[transition.delay] || []

	var idx = property.indexOf(name)
	if (idx === -1) { //if property not set
		if (animation) {
			property.push(name)
			duration.push(animation.duration + 'ms')
			timing.push(animation.easing)
			delay.push(animation.delay + 'ms')
		}
	} else { //property already set, adjust the params
		if (animation && animation.active()) {
			duration[idx] = animation.duration + 'ms'
			timing[idx] = animation.easing
			delay[idx] = animation.delay + 'ms'
		} else {
			property.splice(idx, 1)
			duration.splice(idx, 1)
			timing.splice(idx, 1)
			delay.splice(idx, 1)
		}
	}

	transitions[transition.property] = property
	transitions[transition.duration] = duration
	transitions[transition.timing] = timing
	transitions[transition.delay] = delay

	//FIXME: orsay animation is not working without this shit =(
	if (component._context.system.os === 'orsay' || component._context.system.os === 'netcast') {
		transitions["transition-property"] = property
		transitions["transition-duration"] = duration
		transitions["transition-delay"] = delay
		transitions["transition-timing-function"] = timing
	}
	component.style(transitions)
	return true
}

var cssMappings = {
	width: 'width', height: 'height',
	x: 'left', y: 'top', viewX: 'left', viewY: 'top',
	opacity: 'opacity',
	border: 'border',
	radius: 'border-radius',
	rotate: 'transform',
	boxshadow: 'box-shadow',
	transform: 'transform',
	visible: 'visibility', visibleInView: 'visibility',
	background: 'background',
	color: 'color',
	backgroundImage: 'background-image',
	font: 'font'
}

///@private tries to set animation on name using css transitions, returns true on success
exports.setAnimation = function (component, name, animation) {
	if (!exports.capabilities.csstransitions || $manifest$cssDisableTransitions || (animation && !animation.cssTransition))
		return false

	var css = cssMappings[name]
	return css !== undefined? setTransition(component, css, animation): false
}

exports.requestAnimationFrame = Modernizr.prefixed('requestAnimationFrame', window)	|| function(callback) { return setTimeout(callback, 0) }
exports.cancelAnimationFrame = Modernizr.prefixed('cancelAnimationFrame', window)	|| function(id) { return clearTimeout(id) }

exports.enterFullscreenMode = function(el) { return Modernizr.prefixed('requestFullscreen', el.dom)() }
exports.exitFullscreenMode = function() { return window.Modernizr.prefixed('exitFullscreen', document)() }
exports.inFullscreenMode = function () { return !!window.Modernizr.prefixed('fullscreenElement', document) }


'use strict';

var Benchmark = require('benchmark');

var sp, msgpack, protobuf, lzstring;

sp = require('./schemapack');
msgpack = require("msgpack-lite"); // Comment out if you didn't `npm install msgpack-lite`
protobuf = require("protobufjs"); // Comment out if you didn't `npm install protobufjs`
lzstring = require('lz-string'); // Comment out if you didn't `npm install lz-string`

function byteCount(testName, len, baseLen) {
  console.log(testName + " Byte Count: " + len + (baseLen ? ', ' + Math.round(len / baseLen * 100) + '%' : ''));
}

function runSuite(suite) {
  suite.on('cycle', function(event) {
    console.log(String(event.target));
  })
  .on('complete', function() {
    var fastest = this.filter('fastest');
    console.log('Fastest is ' + fastest.map('name'));
    var fhz = fastest.map('hz');
    Array.prototype.forEach.call(this, function(item) {
      console.log(item['name'] + ' : ' + Math.round(fhz / item['hz'] * 100) + '% slower');
    });
  })
  .run({ 'async': false, 'minTime': .01, 'minSamples': 0 });
}

exports.runBenchmark = function(schema, val) {
  console.log('###############################');
  console.log("Benchmark beginning..");

  var built = sp.build(schema);
  if (protobuf) {
    var pbJS = protobuf.loadJson(pbjsPlayerSchema);
    var pbMsg = pbJS.build("Message");
  }

  var playerFixture = require('./fixtures/player');

  var suiteEncode = new Benchmark.Suite;
  suiteEncode.add('JSON Encode', function() { JSON.stringify(val); });
  suiteEncode.add('SchemaPack Encode', function() { built.encode(val); });
  if (msgpack) suiteEncode.add('MsgPack Encode', function() { msgpack.encode(val); });
  if (protobuf) suiteEncode.add('ProtoBuf Encode', function() { pbMsg.encode(playerFixture.items[0]).toArrayBuffer(); });
  if (lzstring) suiteEncode.add('LZ-String Encode', function() { lzstring.compressToBase64(JSON.stringify(val)); });
  runSuite(suiteEncode);

  console.log("--------------------------------------");

  var spEncoded = built.encode(val);
  var jsonEncoded = JSON.stringify(val);
  if (msgpack) { var msgPackEncoded = msgpack.encode(val); }
  if (protobuf) { var protobufEncoded = pbMsg.encode(playerFixture.items[0]).toArrayBuffer(); }
  if (lzstring) { var lzstringEncoded = lzstring.compressToBase64(JSON.stringify(val)); }

  var suiteDecode = new Benchmark.Suite;
  suiteDecode.add('JSON Decode', function() { JSON.parse(jsonEncoded); });
  suiteDecode.add('SchemaPack Decode', function() { built.decode(spEncoded); });
  if (msgpack) suiteDecode.add('MsgPack Decode', function() { msgpack.decode(msgPackEncoded); });
  if (protobuf) suiteDecode.add('ProtoBuf Decode', function() { pbMsg.decode(protobufEncoded); });
  if (lzstring) suiteDecode.add('LZ-String Decode', function() { JSON.parse(lzstring.decompressFromBase64(lzstringEncoded)); });
  runSuite(suiteDecode);

  console.log("--------------------------------------");

  byteCount("JSON", jsonEncoded.length);
  byteCount("SchemaPack", spEncoded.length, jsonEncoded.length);
  if (msgpack) { byteCount("MsgPack", msgPackEncoded.length, jsonEncoded.length); }
  if (protobuf) { byteCount("ProtoBuf", protobufEncoded.byteLength, jsonEncoded.length); }
  if (lzstring) { byteCount("LZ-String", lzstringEncoded.length, jsonEncoded.length); }
}

// Used pbjs CLI to create this from .proto file for the player schema
var pbjsPlayerSchema = {
  "package": null,
  "messages": [{
    "name": "Message",
    "fields": [{
      "rule": "required",
      "type": "int32",
      "name": "health",
      "id": 1
    }, {
      "rule": "required",
      "type": "bool",
      "name": "jumping",
      "id": 2
    }, {
      "rule": "repeated",
      "type": "int32",
      "name": "position",
      "id": 3
    }, {
      "rule": "required",
      "type": "Attributes",
      "name": "attributes",
      "id": 4
    }],
    "messages": [{
      "name": "Attributes",
      "fields": [{
        "rule": "required",
        "type": "int32",
        "name": "str",
        "id": 1
      }, {
        "rule": "required",
        "type": "int32",
        "name": "agi",
        "id": 2
      }, {
        "rule": "required",
        "type": "int32",
        "name": "int",
        "id": 3
      }]
    }]
  }]
};


define(function () { 'use strict';

    /*! *****************************************************************************
    Copyright (c) Microsoft Corporation. All rights reserved.
    Licensed under the Apache License, Version 2.0 (the "License"); you may not use
    this file except in compliance with the License. You may obtain a copy of the
    License at http://www.apache.org/licenses/LICENSE-2.0

    THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
    WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
    MERCHANTABLITY OR NON-INFRINGEMENT.

    See the Apache Version 2.0 License for specific language governing permissions
    and limitations under the License.
    ***************************************************************************** */
    /* global Reflect, Promise */

    var extendStatics = function(d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };

    function __extends(d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    }

    var Event = /** @class */ (function () {
        function Event(type, target) {
            this.target = target;
            this.type = type;
        }
        return Event;
    }());
    var ErrorEvent = /** @class */ (function (_super) {
        __extends(ErrorEvent, _super);
        function ErrorEvent(error, target) {
            var _this = _super.call(this, 'error', target) || this;
            _this.message = error.message;
            _this.error = error;
            return _this;
        }
        return ErrorEvent;
    }(Event));
    var CloseEvent = /** @class */ (function (_super) {
        __extends(CloseEvent, _super);
        function CloseEvent(code, reason, target) {
            if (code === void 0) { code = 1000; }
            if (reason === void 0) { reason = ''; }
            var _this = _super.call(this, 'close', target) || this;
            _this.wasClean = true;
            _this.code = code;
            _this.reason = reason;
            return _this;
        }
        return CloseEvent;
    }(Event));

    /*!
     * Reconnecting WebSocket
     * by Pedro Ladaria <pedro.ladaria@gmail.com>
     * https://github.com/pladaria/reconnecting-websocket
     * License MIT
     */
    var getGlobalWebSocket = function () {
        if (typeof WebSocket !== 'undefined') {
            // @ts-ignore
            return WebSocket;
        }
    };
    /**
     * Returns true if given argument looks like a WebSocket class
     */
    var isWebSocket = function (w) { return typeof w === 'function' && w.CLOSING === 2; };
    var DEFAULT = {
        maxReconnectionDelay: 10000,
        minReconnectionDelay: 1000 + Math.random() * 4000,
        minUptime: 5000,
        reconnectionDelayGrowFactor: 1.3,
        connectionTimeout: 4000,
        maxRetries: Infinity,
        debug: false,
    };
    var ReconnectingWebSocket = /** @class */ (function () {
        function ReconnectingWebSocket(url, protocols, options) {
            if (options === void 0) { options = {}; }
            var _this = this;
            this._listeners = {
                error: [],
                message: [],
                open: [],
                close: [],
            };
            this._retryCount = -1;
            this._shouldReconnect = true;
            this._connectLock = false;
            this._binaryType = 'blob';
            this._closeCalled = false;
            this._messageQueue = [];
            /**
             * An event listener to be called when the WebSocket connection's readyState changes to CLOSED
             */
            this.onclose = undefined;
            /**
             * An event listener to be called when an error occurs
             */
            this.onerror = undefined;
            /**
             * An event listener to be called when a message is received from the server
             */
            this.onmessage = undefined;
            /**
             * An event listener to be called when the WebSocket connection's readyState changes to OPEN;
             * this indicates that the connection is ready to send and receive data
             */
            this.onopen = undefined;
            this._handleOpen = function (event) {
                _this._debug('open event');
                var _a = _this._options.minUptime, minUptime = _a === void 0 ? DEFAULT.minUptime : _a;
                clearTimeout(_this._connectTimeout);
                _this._uptimeTimeout = setTimeout(function () { return _this._acceptOpen(); }, minUptime);
                // @ts-ignore
                _this._ws.binaryType = _this._binaryType;
                // send enqueued messages (messages sent before websocket open event)
                _this._messageQueue.forEach(function (message) { return _this._ws.send(message); });
                _this._messageQueue = [];
                if (_this.onopen) {
                    _this.onopen(event);
                }
                _this._listeners.open.forEach(function (listener) { return _this._callEventListener(event, listener); });
            };
            this._handleMessage = function (event) {
                _this._debug('message event');
                if (_this.onmessage) {
                    _this.onmessage(event);
                }
                _this._listeners.message.forEach(function (listener) { return _this._callEventListener(event, listener); });
            };
            this._handleError = function (event) {
                _this._debug('error event', event.message);
                _this._disconnect(undefined, event.message === 'TIMEOUT' ? 'timeout' : undefined);
                if (_this.onerror) {
                    _this.onerror(event);
                }
                _this._debug('exec error listeners');
                _this._listeners.error.forEach(function (listener) { return _this._callEventListener(event, listener); });
                _this._connect();
            };
            this._handleClose = function (event) {
                _this._debug('close event');
                _this._clearTimeouts();
                if (_this._shouldReconnect) {
                    _this._connect();
                }
                if (_this.onclose) {
                    _this.onclose(event);
                }
                _this._listeners.close.forEach(function (listener) { return _this._callEventListener(event, listener); });
            };
            this._url = url;
            this._protocols = protocols;
            this._options = options;
            this._connect();
        }
        Object.defineProperty(ReconnectingWebSocket, "CONNECTING", {
            get: function () {
                return 0;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket, "OPEN", {
            get: function () {
                return 1;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket, "CLOSING", {
            get: function () {
                return 2;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket, "CLOSED", {
            get: function () {
                return 3;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "CONNECTING", {
            get: function () {
                return ReconnectingWebSocket.CONNECTING;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "OPEN", {
            get: function () {
                return ReconnectingWebSocket.OPEN;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "CLOSING", {
            get: function () {
                return ReconnectingWebSocket.CLOSING;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "CLOSED", {
            get: function () {
                return ReconnectingWebSocket.CLOSED;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "binaryType", {
            get: function () {
                return this._ws ? this._ws.binaryType : this._binaryType;
            },
            set: function (value) {
                this._binaryType = value;
                if (this._ws) {
                    // @ts-ignore
                    this._ws.binaryType = value;
                }
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "retryCount", {
            /**
             * Returns the number or connection retries
             */
            get: function () {
                return Math.max(this._retryCount, 0);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "bufferedAmount", {
            /**
             * The number of bytes of data that have been queued using calls to send() but not yet
             * transmitted to the network. This value resets to zero once all queued data has been sent.
             * This value does not reset to zero when the connection is closed; if you keep calling send(),
             * this will continue to climb. Read only
             */
            get: function () {
                var bytes = this._messageQueue.reduce(function (acc, message) {
                    if (typeof message === 'string') {
                        acc += message.length; // not byte size
                    }
                    else if (message instanceof Blob) {
                        acc += message.size;
                    }
                    else {
                        acc += message.byteLength;
                    }
                    return acc;
                }, 0);
                return bytes + (this._ws ? this._ws.bufferedAmount : 0);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "extensions", {
            /**
             * The extensions selected by the server. This is currently only the empty string or a list of
             * extensions as negotiated by the connection
             */
            get: function () {
                return this._ws ? this._ws.extensions : '';
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "protocol", {
            /**
             * A string indicating the name of the sub-protocol the server selected;
             * this will be one of the strings specified in the protocols parameter when creating the
             * WebSocket object
             */
            get: function () {
                return this._ws ? this._ws.protocol : '';
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "readyState", {
            /**
             * The current state of the connection; this is one of the Ready state constants
             */
            get: function () {
                return this._ws ? this._ws.readyState : ReconnectingWebSocket.CONNECTING;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ReconnectingWebSocket.prototype, "url", {
            /**
             * The URL as resolved by the constructor
             */
            get: function () {
                return this._ws ? this._ws.url : '';
            },
            enumerable: true,
            configurable: true
        });
        /**
         * Closes the WebSocket connection or connection attempt, if any. If the connection is already
         * CLOSED, this method does nothing
         */
        ReconnectingWebSocket.prototype.close = function (code, reason) {
            if (code === void 0) { code = 1000; }
            this._closeCalled = true;
            this._shouldReconnect = false;
            this._clearTimeouts();
            if (!this._ws) {
                this._debug('close enqueued: no ws instance');
                return;
            }
            if (this._ws.readyState === this.CLOSED) {
                this._debug('close: already closed');
                return;
            }
            this._ws.close(code, reason);
        };
        /**
         * Closes the WebSocket connection or connection attempt and connects again.
         * Resets retry counter;
         */
        ReconnectingWebSocket.prototype.reconnect = function (code, reason) {
            this._shouldReconnect = true;
            this._closeCalled = false;
            this._retryCount = -1;
            if (!this._ws || this._ws.readyState === this.CLOSED) {
                this._connect();
            }
            else {
                this._disconnect(code, reason);
                this._connect();
            }
        };
        /**
         * Enqueue specified data to be transmitted to the server over the WebSocket connection
         */
        ReconnectingWebSocket.prototype.send = function (data) {
            if (this._ws && this._ws.readyState === this.OPEN) {
                this._debug('send', data);
                this._ws.send(data);
            }
            else {
                this._debug('enqueue', data);
                this._messageQueue.push(data);
            }
        };
        /**
         * Register an event handler of a specific event type
         */
        ReconnectingWebSocket.prototype.addEventListener = function (type, listener) {
            if (this._listeners[type]) {
                // @ts-ignore
                this._listeners[type].push(listener);
            }
        };
        /**
         * Removes an event listener
         */
        ReconnectingWebSocket.prototype.removeEventListener = function (type, listener) {
            if (this._listeners[type]) {
                // @ts-ignore
                this._listeners[type] = this._listeners[type].filter(function (l) { return l !== listener; });
            }
        };
        ReconnectingWebSocket.prototype._debug = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            if (this._options.debug) {
                // not using spread because compiled version uses Symbols
                // tslint:disable-next-line
                console.log.apply(console, ['RWS>'].concat(args));
            }
        };
        ReconnectingWebSocket.prototype._getNextDelay = function () {
            var _a = this._options, _b = _a.reconnectionDelayGrowFactor, reconnectionDelayGrowFactor = _b === void 0 ? DEFAULT.reconnectionDelayGrowFactor : _b, _c = _a.minReconnectionDelay, minReconnectionDelay = _c === void 0 ? DEFAULT.minReconnectionDelay : _c, _d = _a.maxReconnectionDelay, maxReconnectionDelay = _d === void 0 ? DEFAULT.maxReconnectionDelay : _d;
            var delay = minReconnectionDelay;
            if (this._retryCount > 0) {
                delay =
                    minReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, this._retryCount - 1);
                if (delay > maxReconnectionDelay) {
                    delay = maxReconnectionDelay;
                }
            }
            this._debug('next delay', delay);
            return delay;
        };
        ReconnectingWebSocket.prototype._wait = function () {
            var _this = this;
            return new Promise(function (resolve) {
                setTimeout(resolve, _this._getNextDelay());
            });
        };
        ReconnectingWebSocket.prototype._getNextUrl = function (urlProvider) {
            if (typeof urlProvider === 'string') {
                return Promise.resolve(urlProvider);
            }
            if (typeof urlProvider === 'function') {
                var url = urlProvider();
                if (typeof url === 'string') {
                    return Promise.resolve(url);
                }
                if (url.then) {
                    return url;
                }
            }
            throw Error('Invalid URL');
        };
        ReconnectingWebSocket.prototype._connect = function () {
            var _this = this;
            if (this._connectLock || !this._shouldReconnect) {
                return;
            }
            this._connectLock = true;
            var _a = this._options, _b = _a.maxRetries, maxRetries = _b === void 0 ? DEFAULT.maxRetries : _b, _c = _a.connectionTimeout, connectionTimeout = _c === void 0 ? DEFAULT.connectionTimeout : _c, _d = _a.WebSocket, WebSocket = _d === void 0 ? getGlobalWebSocket() : _d;
            if (this._retryCount >= maxRetries) {
                this._debug('max retries reached', this._retryCount, '>=', maxRetries);
                return;
            }
            this._retryCount++;
            this._debug('connect', this._retryCount);
            this._removeListeners();
            if (!isWebSocket(WebSocket)) {
                throw Error('No valid WebSocket class provided');
            }
            this._wait()
                .then(function () { return _this._getNextUrl(_this._url); })
                .then(function (url) {
                // close could be called before creating the ws
                if (_this._closeCalled) {
                    _this._connectLock = false;
                    return;
                }
                _this._debug('connect', { url: url, protocols: _this._protocols });
                _this._ws = _this._protocols
                    ? new WebSocket(url, _this._protocols)
                    : new WebSocket(url);
                // @ts-ignore
                _this._ws.binaryType = _this._binaryType;
                _this._connectLock = false;
                _this._addListeners();
                _this._connectTimeout = setTimeout(function () { return _this._handleTimeout(); }, connectionTimeout);
            });
        };
        ReconnectingWebSocket.prototype._handleTimeout = function () {
            this._debug('timeout event');
            this._handleError(new ErrorEvent(Error('TIMEOUT'), this));
        };
        ReconnectingWebSocket.prototype._disconnect = function (code, reason) {
            if (code === void 0) { code = 1000; }
            this._clearTimeouts();
            if (!this._ws) {
                return;
            }
            this._removeListeners();
            try {
                this._ws.close(code, reason);
                this._handleClose(new CloseEvent(code, reason, this));
            }
            catch (error) {
                // ignore
            }
        };
        ReconnectingWebSocket.prototype._acceptOpen = function () {
            this._debug('accept open');
            this._retryCount = 0;
        };
        ReconnectingWebSocket.prototype._callEventListener = function (event, listener) {
            if ('handleEvent' in listener) {
                // @ts-ignore
                listener.handleEvent(event);
            }
            else {
                // @ts-ignore
                listener(event);
            }
        };
        ReconnectingWebSocket.prototype._removeListeners = function () {
            if (!this._ws) {
                return;
            }
            this._debug('removeListeners');
            this._ws.removeEventListener('open', this._handleOpen);
            this._ws.removeEventListener('close', this._handleClose);
            this._ws.removeEventListener('message', this._handleMessage);
            // @ts-ignore
            this._ws.removeEventListener('error', this._handleError);
        };
        ReconnectingWebSocket.prototype._addListeners = function () {
            if (!this._ws) {
                return;
            }
            this._debug('addListeners');
            this._ws.addEventListener('open', this._handleOpen);
            this._ws.addEventListener('close', this._handleClose);
            this._ws.addEventListener('message', this._handleMessage);
            // @ts-ignore
            this._ws.addEventListener('error', this._handleError);
        };
        ReconnectingWebSocket.prototype._clearTimeouts = function () {
            clearTimeout(this._connectTimeout);
            clearTimeout(this._uptimeTimeout);
        };
        return ReconnectingWebSocket;
    }());

    return ReconnectingWebSocket;

});


define([
	"constants",
	"mediator",
	"handlebars",
	"utils",
	"colorsets",
	"hbs!dataTabTemplate",
	"hbs!dataRowPartial"
], function(C, mediator, Handlebars, utils, COLORSETS, dataTabTemplate, dataRowPartial) {
	"use strict";

	var _MODULE_ID = "dataTab";
	var _$sortableDataList;
	var _openSections = {
		panelData: false,
		panelSmallSegmentGrouping: true
	};

	var _init = function() {
		Handlebars.registerPartial("data-row", dataRowPartial);
		mediator.register(_MODULE_ID);
	};

	var _render = function(tabEl, config) {
		var $dataTab = $(tabEl);

		$dataTab.html(dataTabTemplate({
			config: config,
			colorsets: COLORSETS,
			openSections: _openSections
		}));

		$(".panelDataSectionToggle").on("click", function() {
			var $sectionHeading = $(this);
			var section = $sectionHeading.data("section");
			var $el = $("#" + section);
			if ($sectionHeading.hasClass("expanded")) {
				$el.hide("blind", function() { $sectionHeading.removeClass("expanded"); });
				_openSections[section] = false;
			} else {
				$el.css("display", "none").removeClass("hidden").show("blind", function() { $sectionHeading.addClass("expanded"); });
				_openSections[section] = true;
			}
		});

		var groupSmallDataSegments = $("#groupSmallDataSegments");
		groupSmallDataSegments.on("click", function() {
			var section = $("#panelSmallSegmentGrouping");
			var labels      = section.find("label");
			var inputFields = section.find("input, select");
			if (this.checked) {
				labels.removeClass("disabled");
				inputFields.removeAttr("disabled");
			} else {
				labels.addClass("disabled");
				inputFields.attr("disabled", "disabled");
				$("#groupSmallDataSegments").removeAttr("disabled");
				$("#groupSmallDataSegmentsEnabled").removeClass("disabled");
			}
			mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
		});

		_$sortableDataList = $("#sortableDataList");
		_$sortableDataList.find(".segmentColor").colorpicker().on("changeColor", function(e) {
			$(e.target).css("background-color", e.color.toHex());
			mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
		});

		// make the data vertically sortable
		_$sortableDataList.sortable({
			handle: ".handle",
			axis: "y",
			update: function() {
				_updateCounter();
				mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
			}
		});

		_$sortableDataList.on("click", ".removeRow", _removeRow);
		$("#colorSets").on("change", _selectColorset);
		$("#shuffleColors").on("click", _randomizeColorset);
		$("#addRow").on("click", _addRow);
		_$sortableDataList.on("keyup", ".dataLabel", function() {
			mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
		});

		$("#smallSegmentGroupingValueType").on("change", function() {
			if (this.value === "percentage") {
				$("#smallSegmentPercentageUnit").show();
			} else {
				$("#smallSegmentPercentageUnit").hide();
			}
		});

		utils.addColorpicker("smallSegmentColor");
	};

	var _addRow = function(e) {
		e.preventDefault();
		_$sortableDataList.append(dataRowPartial({ color: "#efefef" }));
		_updateCounter();

		_$sortableDataList.children().last().find(".segmentColor").colorpicker().on("changeColor", function(e) {
			$(e.target).css("background-color", e.color.toHex());
			mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
		});
	};

	var _removeRow = function(e) {
		$(e.target).closest("li").remove();
		_updateCounter();
		mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
	};

	var _getTabData = function() {
		var data = [];
		var trs = _$sortableDataList.find("li");
		for (var i=0; i<trs.length; i++) {
			var row = $(trs[i]);
			data.push({
				label: row.find(".dataLabel").val(),
				value: parseFloat(row.find(".dataValue").val()),
				color: utils.rgb2hex(row.find(".segmentColor").css("background-color"))
			});
		}
		return {
			sortOrder: $("#dataSortOrder").val(),
			smallSegmentGrouping: {
				enabled:   $("#groupSmallDataSegments")[0].checked,
				value:     parseFloat($("#smallSegmentGroupingValue").val()),
				valueType: $("#smallSegmentGroupingValueType").val(),
				label:     $("#smallSegmentGroupingLabel").val(),
				color:     $("#smallSegmentColor").val()
			},
			content: data
		};
	};

	/**
	 * Randomizes whatever colors are currently being used.
	 * @private
	 */
	var _randomizeColorset = function() {
		var colors = []
		var trs = _$sortableDataList.find("li");
		for (var i=0; i<trs.length; i++) {
			colors.push(utils.rgb2hex($(trs[i]).find(".segmentColor").css("background-color")));
		}
		colors = utils.shuffleArray(colors);
		_setColors(colors);
	};

	var _selectColorset = function(e) {
		var parts = e.target.value.split("-");
		if (parts.length !== 2) {
			return;
		}
		var group = parts[0];
		var index = parts[1];

		var colors = [];
		if (group === "default") {
			colors = defaultSettings.misc.colors.segments;
		} else {
			colors = COLORSETS[group][index].colors;
		}
		_setColors(colors);

		mediator.publish(_MODULE_ID, C.EVENT.DEMO_PIE.RENDER.NO_ANIMATION);
	};

	var _setColors = function(colors) {
		// reset them all to black - we want any superfluous entries to be clearly shown to be black - users
		// can always change them
		_$sortableDataList.find(".segmentColor").css("backgroundColor", "#000000").attr("data-color", "#000000");

		var trs = _$sortableDataList.find("li");
		for (var i=0; i<trs.length; i++) {
			$(trs[i]).find(".segmentColor").colorpicker("setValue", colors[i]);
		}
	};

	var _updateCounter = function() {
		var trs = _$sortableDataList.find(".dataRowIndex");
		for (var i=0; i<trs.length; i++) {
			$(trs[i]).html(i+1);
		}
	};

	_init();

	return {
		render: _render,
		getTabData: _getTabData
	};
});


define([
	"constants"
], function(C) {
	"use strict";

	var _modules = {};

	var _start = function() {
		_initAll();
		_runAll();
	};

	var _initAll = function() {
		for (var i in _modules) {
			_modules[i].init();
		}
	};

	var _runAll = function() {
		for (var i in _modules) {
			_modules[i].run();
		}
	};

	var _register = function(moduleID, settings) {
		if (_modules.hasOwnProperty(moduleID)) {
			console.warn("A module with this ID is already registered: ", moduleID);
			return;
		}

		var moduleConfig = $.extend({
			init: function() { },
			run: function() { },
			subscriptions: { }
		}, settings);

		_modules[moduleID] = moduleConfig;

		if (C.DEBUG) {
			console.log("registered: ", moduleID);
		}
	};

	/**
	 * Publishes a message to anyone who's subscribed to it.
	 * @param moduleID
	 * @param message
	 * @param data
	 * @private
	 */
	var _publish = function(moduleID, message, data) {
		if (C.DEBUG) {
			console.log("[" + moduleID + "] publish(): ", message, data);
		}

		for (var i in _modules) {
			var subscriptions = _modules[i].subscriptions;

			// if this module has subscribed to this event, call the callback function
			if (subscriptions.hasOwnProperty(message)) {
				subscriptions[message]({
					sender: moduleID,
					data: data
				});
			}
		}
	};

	var _subscribe = function(MODULE_ID, subscriptions) {
		_modules[MODULE_ID].subscriptions = subscriptions;
	};

	return {
		start: _start,
		register: _register,
		publish: _publish,
		subscribe: _subscribe
	};
});


define([
	"constants",
	"mediator"
], function(C, mediator) {
	"use strict";

	var _MODULE_ID = "pageHelper";

	var _pages = ["about", "generator", "download", "help", "quickStart", "examples", "docs"];
	var _$topNav;
	var _currentPage = null;
	var _currentPageHash = null;

	/**
	 * Called once onload by app-start, it looks at the URL and figures out what page to show depending on
	 * whether there's a valid hash value.
	 * @private
	 */
	var _initPage = function() {
		_$topNav = $("#topNav");
		_initPageNavEventHandlers();
		_updateBodySizeClass();
		_selectPage(document.location.hash);
	};

	// this catches ALL nav clicks, not just in the main navbar
	var _initPageNavEventHandlers = function() {
		$(document).on("click", ".selectPage", function(e) {
			e.preventDefault();
			_selectPage(this.hash);
		});

		$(window).on("resize", function() {
			var width = $(window).width();
			var height = $(window).height();
			var breakPoint = _updateBodySizeClass(width);
			mediator.publish(_MODULE_ID, C.EVENT.PAGE.RESIZE, {
				width: width,
				height: height,
				breakPoint: breakPoint
			});
		});
	};

	/**
	 * This was added to get around some CSS nonsense with the homepage slider script. It adds a class to the body
	 * element that specifies the current viewport width.
	 * @private
	 */
	var _updateBodySizeClass = function(width) {
		var breakPointIndex = null;
		for (var i=0; i< C.OTHER.BREAKPOINTS.length; i++) {
			if (width >= C.OTHER.BREAKPOINTS[i]) {
				breakPointIndex = i;
			}
		}

		$("body").removeClass("size768 size992 size1200");
		var breakPoint = null;
		if (breakPointIndex !== null) {
			breakPoint = C.OTHER.BREAKPOINTS[breakPointIndex];
			$("body").addClass("size" + breakPoint);
		}
		return breakPoint;
	};

	/**
	 * Our main, hideous, navigation function. This accepts a page identifier (string) listed in the _pages array
	 * above. This function validates the page string and fade in/out the appropriate page.
	 * @param page
	 * @private
	 */
	var _selectPage = function(pageCandidate) {

		// remove any hashes that may be there
		var pageCandidateNoHash = pageCandidate.replace(/#/, "");

		var hashParts = pageCandidateNoHash.split("-");
		var page = hashParts[0];

		// check the page is valid. If not, default to the About page
		page = ($.inArray(page, _pages) !== -1) ? page : "about";
		if (pageCandidate === _currentPageHash) {
			return;
		}

		var publishData = { page: page, prevPage: _currentPage, pageHash: pageCandidateNoHash };
		if (page !== _currentPage) {

			// fade out the old page, if one was selected (not the case for first load)
			if (_currentPage) {

				_$topNav.find(".active").removeClass("active");
				$("#" + _currentPage).removeClass("hidden fadeIn").addClass("fadeOut");

				(function(cp) {
					setTimeout(function() {
						$("#" + cp).addClass("hidden").removeClass("fadeOut");

						// show the new one. Good lord. Nested setTimeouts? What the gibbering fuck...
						setTimeout(function() {
							_$topNav.find("a[href=#" + page + "]").closest("li").addClass("active"); // select the tab
							$("#" + page).removeClass("hidden fadeOut").addClass("fadeIn"); // select the page
							mediator.publish(_MODULE_ID, C.EVENT.PAGE.LOAD, publishData);
						}, 10);
					}, C.OTHER.PAGE_LOAD_SPEED);
				})(_currentPage);

			} else {
				_$topNav.find("a[href=#" + page + "]").closest("li").addClass("active"); // select the tab
				$("#" + page).removeClass("hidden fadeOut");

				// weird, we need the timeout for the initial page load otherwise it doesn't fade in
				setTimeout(function() {
					$("#" + page).addClass("fadeIn");
					mediator.publish(_MODULE_ID, C.EVENT.PAGE.LOAD, publishData);
				}, 10);
			}

		} else {
			mediator.publish(_MODULE_ID, C.EVENT.PAGE.LOAD, publishData);
		}

		window.location.hash = pageCandidate;

		// store the current page & full hash
		_currentPage = page;
		_currentPageHash = pageCandidate;
	};

	var _getDemoPieChartIndex = function(examplePies) {
		var index = 0;
		if (/#generator-start-pie\d/.test(document.location.hash)) {
			var pieChartNum = parseInt(document.location.hash.replace(/#generator-start-pie/, ""), 10);
			if ($.isNumeric(pieChartNum) && pieChartNum >= 1 && pieChartNum <= examplePies.length) {
				index = pieChartNum-1;
			}
		}
		return index;
	};

	return {
		initPage: _initPage,
		selectPage: _selectPage,
		getDemoPieChartIndex: _getDemoPieChartIndex
	};
});