hwm_build_manager

Менеджер билдов для HWM

Versión del día 19/06/2017. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        hwm_build_manager
// @author      Chie (http://www.heroeswm.ru/pl_info.php?id=645888)
// @icon        https://s.gravatar.com/avatar/5fd0059ad34d082dfbd50cfdeb9aab6a
// @description Менеджер билдов для HWM
// @namespace   https://github.com/betula/hwm_build_manager
// @homepage    https://github.com/betula/hwm_build_manager
// @include     http://*heroeswm.ru/*
// @include     http://178.248.235.15/*
// @include     http://*lordswm.com/*
// @encoding    utf-8
// @version     1.0.0
// @grant       none
// ==/UserScript==




// Mithrill 1.1.1

;(function() {
"use strict"
function Vnode(tag, key, attrs0, children, text, dom) {
	return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false}
}
Vnode.normalize = function(node) {
	if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
	if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined)
	return node
}
Vnode.normalizeChildren = function normalizeChildren(children) {
	for (var i = 0; i < children.length; i++) {
		children[i] = Vnode.normalize(children[i])
	}
	return children
}
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g
var selectorCache = {}
var hasOwn = {}.hasOwnProperty
function compileSelector(selector) {
	var match, tag = "div", classes = [], attrs = {}
	while (match = selectorParser.exec(selector)) {
		var type = match[1], value = match[2]
		if (type === "" && value !== "") tag = value
		else if (type === "#") attrs.id = value
		else if (type === ".") classes.push(value)
		else if (match[3][0] === "[") {
			var attrValue = match[6]
			if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\")
			if (match[4] === "class") classes.push(attrValue)
			else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true
		}
	}
	if (classes.length > 0) attrs.className = classes.join(" ")
	return selectorCache[selector] = {tag: tag, attrs: attrs}
}
function execSelector(state, attrs, children) {
	var hasAttrs = false, childList, text
	var className = attrs.className || attrs.class
	for (var key in state.attrs) {
		if (hasOwn.call(state.attrs, key)) {
			attrs[key] = state.attrs[key]
		}
	}
	if (className !== undefined) {
		if (attrs.class !== undefined) {
			attrs.class = undefined
			attrs.className = className
		}
		if (state.attrs.className != null) {
			attrs.className = state.attrs.className + " " + className
		}
	}
	for (var key in attrs) {
		if (hasOwn.call(attrs, key) && key !== "key") {
			hasAttrs = true
			break
		}
	}
	if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") {
		text = children[0].children
	} else {
		childList = children
	}
	return Vnode(state.tag, attrs.key, hasAttrs ? attrs : undefined, childList, text)
}
function hyperscript(selector) {
	// Because sloppy mode sucks
	var attrs = arguments[1], start = 2, children
	if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
		throw Error("The selector must be either a string or a component.");
	}
	if (typeof selector === "string") {
		var cached = selectorCache[selector] || compileSelector(selector)
	}
	if (attrs == null) {
		attrs = {}
	} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
		attrs = {}
		start = 1
	}
	if (arguments.length === start + 1) {
		children = arguments[start]
		if (!Array.isArray(children)) children = [children]
	} else {
		children = []
		while (start < arguments.length) children.push(arguments[start++])
	}
	var normalized = Vnode.normalizeChildren(children)
	if (typeof selector === "string") {
		return execSelector(cached, attrs, normalized)
	} else {
		return Vnode(selector, attrs.key, attrs, normalized)
	}
}
hyperscript.trust = function(html) {
	if (html == null) html = ""
	return Vnode("<", undefined, undefined, html, undefined, undefined)
}
hyperscript.fragment = function(attrs1, children) {
	return Vnode("[", attrs1.key, attrs1, Vnode.normalizeChildren(children), undefined, undefined)
}
var m = hyperscript
/** @constructor */
var PromisePolyfill = function(executor) {
	if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
	if (typeof executor !== "function") throw new TypeError("executor must be a function")
	var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
	var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
	var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
	function handler(list, shouldAbsorb) {
		return function execute(value) {
			var then
			try {
				if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
					if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
					executeOnce(then.bind(value))
				}
				else {
					callAsync(function() {
						if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value)
						for (var i = 0; i < list.length; i++) list[i](value)
						resolvers.length = 0, rejectors.length = 0
						instance.state = shouldAbsorb
						instance.retry = function() {execute(value)}
					})
				}
			}
			catch (e) {
				rejectCurrent(e)
			}
		}
	}
	function executeOnce(then) {
		var runs = 0
		function run(fn) {
			return function(value) {
				if (runs++ > 0) return
				fn(value)
			}
		}
		var onerror = run(rejectCurrent)
		try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)}
	}
	executeOnce(executor)
}
PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
	var self = this, instance = self._instance
	function handle(callback, list, next, state) {
		list.push(function(value) {
			if (typeof callback !== "function") next(value)
			else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)}
		})
		if (typeof instance.retry === "function" && state === instance.state) instance.retry()
	}
	var resolveNext, rejectNext
	var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
	handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
	return promise
}
PromisePolyfill.prototype.catch = function(onRejection) {
	return this.then(null, onRejection)
}
PromisePolyfill.resolve = function(value) {
	if (value instanceof PromisePolyfill) return value
	return new PromisePolyfill(function(resolve) {resolve(value)})
}
PromisePolyfill.reject = function(value) {
	return new PromisePolyfill(function(resolve, reject) {reject(value)})
}
PromisePolyfill.all = function(list) {
	return new PromisePolyfill(function(resolve, reject) {
		var total = list.length, count = 0, values = []
		if (list.length === 0) resolve([])
		else for (var i = 0; i < list.length; i++) {
			(function(i) {
				function consume(value) {
					count++
					values[i] = value
					if (count === total) resolve(values)
				}
				if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") {
					list[i].then(consume, reject)
				}
				else consume(list[i])
			})(i)
		}
	})
}
PromisePolyfill.race = function(list) {
	return new PromisePolyfill(function(resolve, reject) {
		for (var i = 0; i < list.length; i++) {
			list[i].then(resolve, reject)
		}
	})
}
if (typeof window !== "undefined") {
	if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill
	var PromisePolyfill = window.Promise
} else if (typeof global !== "undefined") {
	if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill
	var PromisePolyfill = global.Promise
} else {
}
var buildQueryString = function(object) {
	if (Object.prototype.toString.call(object) !== "[object Object]") return ""
	var args = []
	for (var key0 in object) {
		destructure(key0, object[key0])
	}
	return args.join("&")
	function destructure(key0, value) {
		if (Array.isArray(value)) {
			for (var i = 0; i < value.length; i++) {
				destructure(key0 + "[" + i + "]", value[i])
			}
		}
		else if (Object.prototype.toString.call(value) === "[object Object]") {
			for (var i in value) {
				destructure(key0 + "[" + i + "]", value[i])
			}
		}
		else args.push(encodeURIComponent(key0) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : ""))
	}
}
var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i")
var _8 = function($window, Promise) {
	var callbackCount = 0
	var oncompletion
	function setCompletionCallback(callback) {oncompletion = callback}
	function finalizer() {
		var count = 0
		function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()}
		return function finalize(promise0) {
			var then0 = promise0.then
			promise0.then = function() {
				count++
				var next = then0.apply(promise0, arguments)
				next.then(complete, function(e) {
					complete()
					if (count === 0) throw e
				})
				return finalize(next)
			}
			return promise0
		}
	}
	function normalize(args, extra) {
		if (typeof args === "string") {
			var url = args
			args = extra || {}
			if (args.url == null) args.url = url
		}
		return args
	}
	function request(args, extra) {
		var finalize = finalizer()
		args = normalize(args, extra)
		var promise0 = new Promise(function(resolve, reject) {
			if (args.method == null) args.method = "GET"
			args.method = args.method.toUpperCase()
			var useBody = (args.method === "GET" || args.method === "TRACE") ? false : (typeof args.useBody === "boolean" ? args.useBody : true)
			if (typeof args.serialize !== "function") args.serialize = typeof FormData !== "undefined" && args.data instanceof FormData ? function(value) {return value} : JSON.stringify
			if (typeof args.deserialize !== "function") args.deserialize = deserialize
			if (typeof args.extract !== "function") args.extract = extract
			args.url = interpolate(args.url, args.data)
			if (useBody) args.data = args.serialize(args.data)
			else args.url = assemble(args.url, args.data)
			var xhr = new $window.XMLHttpRequest(),
				aborted = false,
				_abort = xhr.abort
			xhr.abort = function abort() {
				aborted = true
				_abort.call(xhr)
			}
			xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
			if (args.serialize === JSON.stringify && useBody) {
				xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8")
			}
			if (args.deserialize === deserialize) {
				xhr.setRequestHeader("Accept", "application/json, text/*")
			}
			if (args.withCredentials) xhr.withCredentials = args.withCredentials
			for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
				xhr.setRequestHeader(key, args.headers[key])
			}
			if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr
			xhr.onreadystatechange = function() {
				// Don't throw errors on xhr.abort().
				if(aborted) return
				if (xhr.readyState === 4) {
					try {
						var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args))
						if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
							resolve(cast(args.type, response))
						}
						else {
							var error = new Error(xhr.responseText)
							for (var key in response) error[key] = response[key]
							reject(error)
						}
					}
					catch (e) {
						reject(e)
					}
				}
			}
			if (useBody && (args.data != null)) xhr.send(args.data)
			else xhr.send()
		})
		return args.background === true ? promise0 : finalize(promise0)
	}
	function jsonp(args, extra) {
		var finalize = finalizer()
		args = normalize(args, extra)
		var promise0 = new Promise(function(resolve, reject) {
			var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
			var script = $window.document.createElement("script")
			$window[callbackName] = function(data) {
				script.parentNode.removeChild(script)
				resolve(cast(args.type, data))
				delete $window[callbackName]
			}
			script.onerror = function() {
				script.parentNode.removeChild(script)
				reject(new Error("JSONP request failed"))
				delete $window[callbackName]
			}
			if (args.data == null) args.data = {}
			args.url = interpolate(args.url, args.data)
			args.data[args.callbackKey || "callback"] = callbackName
			script.src = assemble(args.url, args.data)
			$window.document.documentElement.appendChild(script)
		})
		return args.background === true? promise0 : finalize(promise0)
	}
	function interpolate(url, data) {
		if (data == null) return url
		var tokens = url.match(/:[^\/]+/gi) || []
		for (var i = 0; i < tokens.length; i++) {
			var key = tokens[i].slice(1)
			if (data[key] != null) {
				url = url.replace(tokens[i], data[key])
			}
		}
		return url
	}
	function assemble(url, data) {
		var querystring = buildQueryString(data)
		if (querystring !== "") {
			var prefix = url.indexOf("?") < 0 ? "?" : "&"
			url += prefix + querystring
		}
		return url
	}
	function deserialize(data) {
		try {return data !== "" ? JSON.parse(data) : null}
		catch (e) {throw new Error(data)}
	}
	function extract(xhr) {return xhr.responseText}
	function cast(type0, data) {
		if (typeof type0 === "function") {
			if (Array.isArray(data)) {
				for (var i = 0; i < data.length; i++) {
					data[i] = new type0(data[i])
				}
			}
			else return new type0(data)
		}
		return data
	}
	return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback}
}
var requestService = _8(window, PromisePolyfill)
var coreRenderer = function($window) {
	var $doc = $window.document
	var $emptyFragment = $doc.createDocumentFragment()
	var nameSpace = {
		svg: "http://www.w3.org/2000/svg",
		math: "http://www.w3.org/1998/Math/MathML"
	}
	var onevent
	function setEventCallback(callback) {return onevent = callback}
	function getNameSpace(vnode) {
		return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag]
	}
	//create
	function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) {
		for (var i = start; i < end; i++) {
			var vnode = vnodes[i]
			if (vnode != null) {
				createNode(parent, vnode, hooks, ns, nextSibling)
			}
		}
	}
	function createNode(parent, vnode, hooks, ns, nextSibling) {
		var tag = vnode.tag
		if (typeof tag === "string") {
			vnode.state = {}
			if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
			switch (tag) {
				case "#": return createText(parent, vnode, nextSibling)
				case "<": return createHTML(parent, vnode, nextSibling)
				case "[": return createFragment(parent, vnode, hooks, ns, nextSibling)
				default: return createElement(parent, vnode, hooks, ns, nextSibling)
			}
		}
		else return createComponent(parent, vnode, hooks, ns, nextSibling)
	}
	function createText(parent, vnode, nextSibling) {
		vnode.dom = $doc.createTextNode(vnode.children)
		insertNode(parent, vnode.dom, nextSibling)
		return vnode.dom
	}
	function createHTML(parent, vnode, nextSibling) {
		var match1 = vnode.children.match(/^\s*?<(\w+)/im) || []
		var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match1[1]] || "div"
		var temp = $doc.createElement(parent1)
		temp.innerHTML = vnode.children
		vnode.dom = temp.firstChild
		vnode.domSize = temp.childNodes.length
		var fragment = $doc.createDocumentFragment()
		var child
		while (child = temp.firstChild) {
			fragment.appendChild(child)
		}
		insertNode(parent, fragment, nextSibling)
		return fragment
	}
	function createFragment(parent, vnode, hooks, ns, nextSibling) {
		var fragment = $doc.createDocumentFragment()
		if (vnode.children != null) {
			var children = vnode.children
			createNodes(fragment, children, 0, children.length, hooks, null, ns)
		}
		vnode.dom = fragment.firstChild
		vnode.domSize = fragment.childNodes.length
		insertNode(parent, fragment, nextSibling)
		return fragment
	}
	function createElement(parent, vnode, hooks, ns, nextSibling) {
		var tag = vnode.tag
		var attrs2 = vnode.attrs
		var is = attrs2 && attrs2.is
		ns = getNameSpace(vnode) || ns
		var element = ns ?
			is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) :
			is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag)
		vnode.dom = element
		if (attrs2 != null) {
			setAttrs(vnode, attrs2, ns)
		}
		insertNode(parent, element, nextSibling)
		if (vnode.attrs != null && vnode.attrs.contenteditable != null) {
			setContentEditable(vnode)
		}
		else {
			if (vnode.text != null) {
				if (vnode.text !== "") element.textContent = vnode.text
				else vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
			}
			if (vnode.children != null) {
				var children = vnode.children
				createNodes(element, children, 0, children.length, hooks, null, ns)
				setLateAttrs(vnode)
			}
		}
		return element
	}
	function initComponent(vnode, hooks) {
		var sentinel
		if (typeof vnode.tag.view === "function") {
			vnode.state = Object.create(vnode.tag)
			sentinel = vnode.state.view
			if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
			sentinel.$$reentrantLock$$ = true
		} else {
			vnode.state = void 0
			sentinel = vnode.tag
			if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
			sentinel.$$reentrantLock$$ = true
			vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
		}
		vnode._state = vnode.state
		if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
		initLifecycle(vnode._state, vnode, hooks)
		vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode))
		if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
		sentinel.$$reentrantLock$$ = null
	}
	function createComponent(parent, vnode, hooks, ns, nextSibling) {
		initComponent(vnode, hooks)
		if (vnode.instance != null) {
			var element = createNode(parent, vnode.instance, hooks, ns, nextSibling)
			vnode.dom = vnode.instance.dom
			vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0
			insertNode(parent, element, nextSibling)
			return element
		}
		else {
			vnode.domSize = 0
			return $emptyFragment
		}
	}
	//update
	function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) {
		if (old === vnodes || old == null && vnodes == null) return
		else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
		else if (vnodes == null) removeNodes(old, 0, old.length, vnodes)
		else {
			if (old.length === vnodes.length) {
				var isUnkeyed = false
				for (var i = 0; i < vnodes.length; i++) {
					if (vnodes[i] != null && old[i] != null) {
						isUnkeyed = vnodes[i].key == null && old[i].key == null
						break
					}
				}
				if (isUnkeyed) {
					for (var i = 0; i < old.length; i++) {
						if (old[i] === vnodes[i]) continue
						else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling))
						else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
						else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns)
					}
					return
				}
			}
			recycling = recycling || isRecyclable(old, vnodes)
			if (recycling) {
				var pool = old.pool
				old = old.concat(old.pool)
			}
			var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
			while (oldEnd >= oldStart && end >= start) {
				var o = old[oldStart], v = vnodes[start]
				if (o === v && !recycling) oldStart++, start++
				else if (o == null) oldStart++
				else if (v == null) start++
				else if (o.key === v.key) {
					var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling)
					oldStart++, start++
					updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns)
					if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
				}
				else {
					var o = old[oldEnd]
					if (o === v && !recycling) oldEnd--, start++
					else if (o == null) oldEnd--
					else if (v == null) start++
					else if (o.key === v.key) {
						var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
						updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
						if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
						oldEnd--, start++
					}
					else break
				}
			}
			while (oldEnd >= oldStart && end >= start) {
				var o = old[oldEnd], v = vnodes[end]
				if (o === v && !recycling) oldEnd--, end--
				else if (o == null) oldEnd--
				else if (v == null) end--
				else if (o.key === v.key) {
					var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
					updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
					if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
					if (o.dom != null) nextSibling = o.dom
					oldEnd--, end--
				}
				else {
					if (!map) map = getKeyMap(old, oldEnd)
					if (v != null) {
						var oldIndex = map[v.key]
						if (oldIndex != null) {
							var movable = old[oldIndex]
							var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling)
							updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
							insertNode(parent, toFragment(movable), nextSibling)
							old[oldIndex].skip = true
							if (movable.dom != null) nextSibling = movable.dom
						}
						else {
							var dom = createNode(parent, v, hooks, ns, nextSibling)
							nextSibling = dom
						}
					}
					end--
				}
				if (end < start) break
			}
			createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
			removeNodes(old, oldStart, oldEnd + 1, vnodes)
		}
	}
	function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) {
		var oldTag = old.tag, tag = vnode.tag
		if (oldTag === tag) {
			vnode.state = old.state
			vnode._state = old._state
			vnode.events = old.events
			if (!recycling && shouldNotUpdate(vnode, old)) return
			if (typeof oldTag === "string") {
				if (vnode.attrs != null) {
					if (recycling) {
						vnode.state = {}
						initLifecycle(vnode.attrs, vnode, hooks)
					}
					else updateLifecycle(vnode.attrs, vnode, hooks)
				}
				switch (oldTag) {
					case "#": updateText(old, vnode); break
					case "<": updateHTML(parent, old, vnode, nextSibling); break
					case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break
					default: updateElement(old, vnode, recycling, hooks, ns)
				}
			}
			else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns)
		}
		else {
			removeNode(old, null)
			createNode(parent, vnode, hooks, ns, nextSibling)
		}
	}
	function updateText(old, vnode) {
		if (old.children.toString() !== vnode.children.toString()) {
			old.dom.nodeValue = vnode.children
		}
		vnode.dom = old.dom
	}
	function updateHTML(parent, old, vnode, nextSibling) {
		if (old.children !== vnode.children) {
			toFragment(old)
			createHTML(parent, vnode, nextSibling)
		}
		else vnode.dom = old.dom, vnode.domSize = old.domSize
	}
	function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) {
		updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns)
		var domSize = 0, children = vnode.children
		vnode.dom = null
		if (children != null) {
			for (var i = 0; i < children.length; i++) {
				var child = children[i]
				if (child != null && child.dom != null) {
					if (vnode.dom == null) vnode.dom = child.dom
					domSize += child.domSize || 1
				}
			}
			if (domSize !== 1) vnode.domSize = domSize
		}
	}
	function updateElement(old, vnode, recycling, hooks, ns) {
		var element = vnode.dom = old.dom
		ns = getNameSpace(vnode) || ns
		if (vnode.tag === "textarea") {
			if (vnode.attrs == null) vnode.attrs = {}
			if (vnode.text != null) {
				vnode.attrs.value = vnode.text //FIXME handle0 multiple children
				vnode.text = undefined
			}
		}
		updateAttrs(vnode, old.attrs, vnode.attrs, ns)
		if (vnode.attrs != null && vnode.attrs.contenteditable != null) {
			setContentEditable(vnode)
		}
		else if (old.text != null && vnode.text != null && vnode.text !== "") {
			if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text
		}
		else {
			if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)]
			if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
			updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns)
		}
	}
	function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) {
		if (recycling) {
			initComponent(vnode, hooks)
		} else {
			vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode))
			if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
			if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
			updateLifecycle(vnode._state, vnode, hooks)
		}
		if (vnode.instance != null) {
			if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling)
			else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns)
			vnode.dom = vnode.instance.dom
			vnode.domSize = vnode.instance.domSize
		}
		else if (old.instance != null) {
			removeNode(old.instance, null)
			vnode.dom = undefined
			vnode.domSize = 0
		}
		else {
			vnode.dom = old.dom
			vnode.domSize = old.domSize
		}
	}
	function isRecyclable(old, vnodes) {
		if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) {
			var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0
			var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0
			var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0
			if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) {
				return true
			}
		}
		return false
	}
	function getKeyMap(vnodes, end) {
		var map = {}, i = 0
		for (var i = 0; i < end; i++) {
			var vnode = vnodes[i]
			if (vnode != null) {
				var key2 = vnode.key
				if (key2 != null) map[key2] = i
			}
		}
		return map
	}
	function toFragment(vnode) {
		var count0 = vnode.domSize
		if (count0 != null || vnode.dom == null) {
			var fragment = $doc.createDocumentFragment()
			if (count0 > 0) {
				var dom = vnode.dom
				while (--count0) fragment.appendChild(dom.nextSibling)
				fragment.insertBefore(dom, fragment.firstChild)
			}
			return fragment
		}
		else return vnode.dom
	}
	function getNextSibling(vnodes, i, nextSibling) {
		for (; i < vnodes.length; i++) {
			if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom
		}
		return nextSibling
	}
	function insertNode(parent, dom, nextSibling) {
		if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling)
		else parent.appendChild(dom)
	}
	function setContentEditable(vnode) {
		var children = vnode.children
		if (children != null && children.length === 1 && children[0].tag === "<") {
			var content = children[0].children
			if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
		}
		else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted")
	}
	//remove
	function removeNodes(vnodes, start, end, context) {
		for (var i = start; i < end; i++) {
			var vnode = vnodes[i]
			if (vnode != null) {
				if (vnode.skip) vnode.skip = false
				else removeNode(vnode, context)
			}
		}
	}
	function removeNode(vnode, context) {
		var expected = 1, called = 0
		if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
			var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode)
			if (result != null && typeof result.then === "function") {
				expected++
				result.then(continuation, continuation)
			}
		}
		if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") {
			var result = vnode._state.onbeforeremove.call(vnode.state, vnode)
			if (result != null && typeof result.then === "function") {
				expected++
				result.then(continuation, continuation)
			}
		}
		continuation()
		function continuation() {
			if (++called === expected) {
				onremove(vnode)
				if (vnode.dom) {
					var count0 = vnode.domSize || 1
					if (count0 > 1) {
						var dom = vnode.dom
						while (--count0) {
							removeNodeFromDOM(dom.nextSibling)
						}
					}
					removeNodeFromDOM(vnode.dom)
					if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements
						if (!context.pool) context.pool = [vnode]
						else context.pool.push(vnode)
					}
				}
			}
		}
	}
	function removeNodeFromDOM(node) {
		var parent = node.parentNode
		if (parent != null) parent.removeChild(node)
	}
	function onremove(vnode) {
		if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode)
		if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode)
		if (vnode.instance != null) onremove(vnode.instance)
		else {
			var children = vnode.children
			if (Array.isArray(children)) {
				for (var i = 0; i < children.length; i++) {
					var child = children[i]
					if (child != null) onremove(child)
				}
			}
		}
	}
	//attrs2
	function setAttrs(vnode, attrs2, ns) {
		for (var key2 in attrs2) {
			setAttr(vnode, key2, null, attrs2[key2], ns)
		}
	}
	function setAttr(vnode, key2, old, value, ns) {
		var element = vnode.dom
		if (key2 === "key" || key2 === "is" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return
		var nsLastIndex = key2.indexOf(":")
		if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") {
			element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value)
		}
		else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value)
		else if (key2 === "style") updateStyle(element, old, value)
		else if (key2 in element && !isAttribute(key2) && ns === undefined && !isCustomElement(vnode)) {
			if (key2 === "value") {
				var normalized0 = "" + value // eslint-disable-line no-implicit-coercion
				//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
				if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === normalized0 && vnode.dom === $doc.activeElement) return
				//setting select[value] to same value while having select open blinks select dropdown in Chrome
				if (vnode.tag === "select") {
					if (value === null) {
						if (vnode.dom.selectedIndex === -1 && vnode.dom === $doc.activeElement) return
					} else {
						if (old !== null && vnode.dom.value === normalized0 && vnode.dom === $doc.activeElement) return
					}
				}
				//setting option[value] to same value while having select open blinks select dropdown in Chrome
				if (vnode.tag === "option" && old != null && vnode.dom.value === normalized0) return
			}
			// If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error0 will occur.
			if (vnode.tag === "input" && key2 === "type") {
				element.setAttribute(key2, value)
				return
			}
			element[key2] = value
		}
		else {
			if (typeof value === "boolean") {
				if (value) element.setAttribute(key2, "")
				else element.removeAttribute(key2)
			}
			else element.setAttribute(key2 === "className" ? "class" : key2, value)
		}
	}
	function setLateAttrs(vnode) {
		var attrs2 = vnode.attrs
		if (vnode.tag === "select" && attrs2 != null) {
			if ("value" in attrs2) setAttr(vnode, "value", null, attrs2.value, undefined)
			if ("selectedIndex" in attrs2) setAttr(vnode, "selectedIndex", null, attrs2.selectedIndex, undefined)
		}
	}
	function updateAttrs(vnode, old, attrs2, ns) {
		if (attrs2 != null) {
			for (var key2 in attrs2) {
				setAttr(vnode, key2, old && old[key2], attrs2[key2], ns)
			}
		}
		if (old != null) {
			for (var key2 in old) {
				if (attrs2 == null || !(key2 in attrs2)) {
					if (key2 === "className") key2 = "class"
					if (key2[0] === "o" && key2[1] === "n" && !isLifecycleMethod(key2)) updateEvent(vnode, key2, undefined)
					else if (key2 !== "key") vnode.dom.removeAttribute(key2)
				}
			}
		}
	}
	function isFormAttribute(vnode, attr) {
		return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement
	}
	function isLifecycleMethod(attr) {
		return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate"
	}
	function isAttribute(attr) {
		return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type"
	}
	function isCustomElement(vnode){
		return vnode.attrs.is || vnode.tag.indexOf("-") > -1
	}
	function hasIntegrationMethods(source) {
		return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove)
	}
	//style
	function updateStyle(element, old, style) {
		if (old === style) element.style.cssText = "", old = null
		if (style == null) element.style.cssText = ""
		else if (typeof style === "string") element.style.cssText = style
		else {
			if (typeof old === "string") element.style.cssText = ""
			for (var key2 in style) {
				element.style[key2] = style[key2]
			}
			if (old != null && typeof old !== "string") {
				for (var key2 in old) {
					if (!(key2 in style)) element.style[key2] = ""
				}
			}
		}
	}
	//event
	function updateEvent(vnode, key2, value) {
		var element = vnode.dom
		var callback = typeof onevent !== "function" ? value : function(e) {
			var result = value.call(element, e)
			onevent.call(element, e)
			return result
		}
		if (key2 in element) element[key2] = typeof value === "function" ? callback : null
		else {
			var eventName = key2.slice(2)
			if (vnode.events === undefined) vnode.events = {}
			if (vnode.events[key2] === callback) return
			if (vnode.events[key2] != null) element.removeEventListener(eventName, vnode.events[key2], false)
			if (typeof value === "function") {
				vnode.events[key2] = callback
				element.addEventListener(eventName, vnode.events[key2], false)
			}
		}
	}
	//lifecycle
	function initLifecycle(source, vnode, hooks) {
		if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode)
		if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode))
	}
	function updateLifecycle(source, vnode, hooks) {
		if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode))
	}
	function shouldNotUpdate(vnode, old) {
		var forceVnodeUpdate, forceComponentUpdate
		if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old)
		if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old)
		if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) {
			vnode.dom = old.dom
			vnode.domSize = old.domSize
			vnode.instance = old.instance
			return true
		}
		return false
	}
	function render(dom, vnodes) {
		if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
		var hooks = []
		var active = $doc.activeElement
		var namespace = dom.namespaceURI
		// First time0 rendering into a node clears it out
		if (dom.vnodes == null) dom.textContent = ""
		if (!Array.isArray(vnodes)) vnodes = [vnodes]
		updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
		dom.vnodes = vnodes
		for (var i = 0; i < hooks.length; i++) hooks[i]()
		if ($doc.activeElement !== active) active.focus()
	}
	return {render: render, setEventCallback: setEventCallback}
}
function throttle(callback) {
	//60fps translates to 16.6ms, round it down since setTimeout requires int
	var time = 16
	var last = 0, pending = null
	var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout
	return function() {
		var now = Date.now()
		if (last === 0 || now - last >= time) {
			last = now
			callback()
		}
		else if (pending === null) {
			pending = timeout(function() {
				pending = null
				callback()
				last = Date.now()
			}, time - (now - last))
		}
	}
}
var _11 = function($window) {
	var renderService = coreRenderer($window)
	renderService.setEventCallback(function(e) {
		if (e.redraw !== false) redraw()
	})
	var callbacks = []
	function subscribe(key1, callback) {
		unsubscribe(key1)
		callbacks.push(key1, throttle(callback))
	}
	function unsubscribe(key1) {
		var index = callbacks.indexOf(key1)
		if (index > -1) callbacks.splice(index, 2)
	}
	function redraw() {
		for (var i = 1; i < callbacks.length; i += 2) {
			callbacks[i]()
		}
	}
	return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
}
var redrawService = _11(window)
requestService.setCompletionCallback(redrawService.redraw)
var _16 = function(redrawService0) {
	return function(root, component) {
		if (component === null) {
			redrawService0.render(root, [])
			redrawService0.unsubscribe(root)
			return
		}
		
		if (component.view == null && typeof component !== "function") throw new Error("m.mount(element, component) expects a component, not a vnode")
		
		var run0 = function() {
			redrawService0.render(root, Vnode(component))
		}
		redrawService0.subscribe(root, run0)
		redrawService0.redraw()
	}
}
m.mount = _16(redrawService)
var Promise = PromisePolyfill
var parseQueryString = function(string) {
	if (string === "" || string == null) return {}
	if (string.charAt(0) === "?") string = string.slice(1)
	var entries = string.split("&"), data0 = {}, counters = {}
	for (var i = 0; i < entries.length; i++) {
		var entry = entries[i].split("=")
		var key5 = decodeURIComponent(entry[0])
		var value = entry.length === 2 ? decodeURIComponent(entry[1]) : ""
		if (value === "true") value = true
		else if (value === "false") value = false
		var levels = key5.split(/\]\[?|\[/)
		var cursor = data0
		if (key5.indexOf("[") > -1) levels.pop()
		for (var j = 0; j < levels.length; j++) {
			var level = levels[j], nextLevel = levels[j + 1]
			var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10))
			var isValue = j === levels.length - 1
			if (level === "") {
				var key5 = levels.slice(0, j).join()
				if (counters[key5] == null) counters[key5] = 0
				level = counters[key5]++
			}
			if (cursor[level] == null) {
				cursor[level] = isValue ? value : isNumber ? [] : {}
			}
			cursor = cursor[level]
		}
	}
	return data0
}
var coreRouter = function($window) {
	var supportsPushState = typeof $window.history.pushState === "function"
	var callAsync0 = typeof setImmediate === "function" ? setImmediate : setTimeout
	function normalize1(fragment0) {
		var data = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
		if (fragment0 === "pathname" && data[0] !== "/") data = "/" + data
		return data
	}
	var asyncId
	function debounceAsync(callback0) {
		return function() {
			if (asyncId != null) return
			asyncId = callAsync0(function() {
				asyncId = null
				callback0()
			})
		}
	}
	function parsePath(path, queryData, hashData) {
		var queryIndex = path.indexOf("?")
		var hashIndex = path.indexOf("#")
		var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length
		if (queryIndex > -1) {
			var queryEnd = hashIndex > -1 ? hashIndex : path.length
			var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd))
			for (var key4 in queryParams) queryData[key4] = queryParams[key4]
		}
		if (hashIndex > -1) {
			var hashParams = parseQueryString(path.slice(hashIndex + 1))
			for (var key4 in hashParams) hashData[key4] = hashParams[key4]
		}
		return path.slice(0, pathEnd)
	}
	var router = {prefix: "#!"}
	router.getPath = function() {
		var type2 = router.prefix.charAt(0)
		switch (type2) {
			case "#": return normalize1("hash").slice(router.prefix.length)
			case "?": return normalize1("search").slice(router.prefix.length) + normalize1("hash")
			default: return normalize1("pathname").slice(router.prefix.length) + normalize1("search") + normalize1("hash")
		}
	}
	router.setPath = function(path, data, options) {
		var queryData = {}, hashData = {}
		path = parsePath(path, queryData, hashData)
		if (data != null) {
			for (var key4 in data) queryData[key4] = data[key4]
			path = path.replace(/:([^\/]+)/g, function(match2, token) {
				delete queryData[token]
				return data[token]
			})
		}
		var query = buildQueryString(queryData)
		if (query) path += "?" + query
		var hash = buildQueryString(hashData)
		if (hash) path += "#" + hash
		if (supportsPushState) {
			var state = options ? options.state : null
			var title = options ? options.title : null
			$window.onpopstate()
			if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path)
			else $window.history.pushState(state, title, router.prefix + path)
		}
		else $window.location.href = router.prefix + path
	}
	router.defineRoutes = function(routes, resolve, reject) {
		function resolveRoute() {
			var path = router.getPath()
			var params = {}
			var pathname = parsePath(path, params, params)
			var state = $window.history.state
			if (state != null) {
				for (var k in state) params[k] = state[k]
			}
			for (var route0 in routes) {
				var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
				if (matcher.test(pathname)) {
					pathname.replace(matcher, function() {
						var keys = route0.match(/:[^\/]+/g) || []
						var values = [].slice.call(arguments, 1, -2)
						for (var i = 0; i < keys.length; i++) {
							params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
						}
						resolve(routes[route0], params, path, route0)
					})
					return
				}
			}
			reject(path, params)
		}
		if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute)
		else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute
		resolveRoute()
	}
	return router
}
var _20 = function($window, redrawService0) {
	var routeService = coreRouter($window)
	var identity = function(v) {return v}
	var render1, component, attrs3, currentPath, lastUpdate
	var route = function(root, defaultRoute, routes) {
		if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
		var run1 = function() {
			if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3)))
		}
		var bail = function(path) {
			if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
			else throw new Error("Could not resolve default route " + defaultRoute)
		}
		routeService.defineRoutes(routes, function(payload, params, path) {
			var update = lastUpdate = function(routeResolver, comp) {
				if (update !== lastUpdate) return
				component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
				attrs3 = params, currentPath = path, lastUpdate = null
				render1 = (routeResolver.render || identity).bind(routeResolver)
				run1()
			}
			if (payload.view || typeof payload === "function") update({}, payload)
			else {
				if (payload.onmatch) {
					Promise.resolve(payload.onmatch(params, path)).then(function(resolved) {
						update(payload, resolved)
					}, bail)
				}
				else update(payload, "div")
			}
		}, bail)
		redrawService0.subscribe(root, run1)
	}
	route.set = function(path, data, options) {
		if (lastUpdate != null) options = {replace: true}
		lastUpdate = null
		routeService.setPath(path, data, options)
	}
	route.get = function() {return currentPath}
	route.prefix = function(prefix0) {routeService.prefix = prefix0}
	route.link = function(vnode1) {
		vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href)
		vnode1.dom.onclick = function(e) {
			if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
			e.preventDefault()
			e.redraw = false
			var href = this.getAttribute("href")
			if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length)
			route.set(href, undefined, undefined)
		}
	}
	route.param = function(key3) {
		if(typeof attrs3 !== "undefined" && typeof key3 !== "undefined") return attrs3[key3]
		return attrs3
	}
	return route
}
m.route = _20(window, redrawService)
m.withAttr = function(attrName, callback1, context) {
	return function(e) {
		callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName))
	}
}
var _28 = coreRenderer(window)
m.render = _28.render
m.redraw = redrawService.redraw
m.request = requestService.request
m.jsonp = requestService.jsonp
m.parseQueryString = parseQueryString
m.buildQueryString = buildQueryString
m.version = "1.1.1"
m.vnode = Vnode
if (typeof module !== "undefined") module["exports"] = m
else window.m = m
}());




// Build Manager 1.0.0

class ServiceContainer {
  
  constructor() {
    this.instances = {};
  }
  
  _service(ctor) {
    let { name } = ctor;
    if (!this.instances[name]) {
      this.instances[name] = new ctor(this);
    }
    return this.instances[name];
  }
  
  get manager() { return this._service(ManagerService) }
  get fraction() { return this._service(FractionService) }
  get inventory() { return this._service(InventoryService) }
  get attribute() { return this._service(AttributeService) }
  get army() { return this._service(ArmyService) }
  get skill() { return this._service(SkillService) }
  get current() { return this._service(CurrentService) }
  get change() { return this._service(ChangeService) }
  get import() { return this._service(ImportService) }
  
}

class ManagerService {
  
  constructor(services) {
    this.services = services;
    this._storage = new LocalStorageArrayDriver('BM_MANAGER');
    this.items = [];
    
    this._restore();
  }
  
  createNew() {
    let nextNumber = 1;
    for (let item of this.items) {
      let match = item.name.match(/^Новый билд (\d+)$/);
      if (match) {
        nextNumber = Math.max(nextNumber, (parseInt(match[1]) || 0) + 1);
      }
    }

    let item = {
      id: uniqid(),
      name: `Новый билд ${nextNumber}`,
      fraction: this.services.fraction.default.id,
      inventory: this.services.inventory.default.id,
      attribute: this.services.attribute.default,
      army: this.services.army.default,
      skill: this.services.skill.default
    }

    this.items.push(item);
    this._store();

    return item;
  }
  
  remove(item) {
    let { founded, index } = this._searchByItem(item);
    if (!founded) return null;
    
    let items = this.items;
    items = items.slice(0, index).concat( items.slice(index + 1) );
    this.items = items;
    this._store();
    
    if (index === items.length) {
      return items[index - 1]
    }
    return items[index];
  }
  
  duplicate(item) {
    let { founded } = this._searchByItem(item);
    if (!founded) return null;

    let duplicate = deepCopy(item);
    duplicate.id = uniqid();
    duplicate.name += ' копия';
    
    this.items.push(duplicate);
    this._store()
    
    return duplicate;
  }
  
  update(updatedItem) {
    let { founded, index } = this._searchById(updatedItem.id);
    if (!founded) return null;

    this.items[index] = updatedItem;
    this._store();
    
    this.services.current.mayBeOnlyNameUpdated(updatedItem);

    return updatedItem;
  }
  
  searchEquals(item) {
    let { founded, index } = this._searchById(item.id);
    if (founded) {
      if (deepEquals(item, this.items[index])) {
        return {
          founded,
          index
        }
      }
    }
    return {
      founded: false
    }
  }

  serialize() {
    return JSON.stringify(this.items);
  }


  unserialize(value) {
    const INVALID = 'invalid';

    const Checker = {
      string(value) {
        if (typeof value !== 'string' || value.length === 0) throw INVALID;
      },
      number(value) {
        if (typeof value !== 'number') throw INVALID;
      },
      array(value) {
        if (!Array.isArray(value)) throw INVALID;
      },
      object(value) {
        if (!value || typeof value !== 'object') throw INVALID;
      },
      keys(value, keys) {
        if (!keys.every({}.hasOwnProperty.bind(value))) throw INVALID;
        for (let key of Object.keys(value)) {
          Checker.enum(key, keys);
        }
      },
      enum(value, values) {
        if (Array.isArray(value)) {
          for (let once of value) {
            Checker.enum(once, values);
          }
        } else {
          if (Array.isArray(values)) {
            if (values.indexOf(value) === -1) throw INVALID;
          } else {
            if (!values.hasOwnProperty(value)) throw INVALID;
          }
        }
      },
      length(value, len) {
        if (value.length !== len) throw INVALID;
      }

    };

    let items = [];

    try {
      items = JSON.parse(value);
      Checker.array(items);

      for (let item of items) {
        Checker.object(item);
        Checker.keys(item, [ 'id', 'name', 'fraction', 'inventory', 'attribute', 'army', 'skill' ]);

        let { id, name, fraction, inventory, attribute, army, skill } = item;

        Checker.string(id);
        Checker.string(name);
        Checker.enum(fraction, this.services.fraction.map);
        Checker.enum(inventory, this.services.inventory.map);

        Checker.object(attribute);
        Checker.keys(attribute, this.services.attribute.list);
        Object.values(attribute).forEach(Checker.number);

        Checker.array(army);
        Checker.length(army, 7);
        army.forEach(Checker.number);

        Checker.array(skill);
        Checker.enum(skill, this.services.skill.map);
      }

    } catch(e) {
      return false;
    }

    this.items = items;
    this._store();
    return true;
  }
  
  _searchById(id) {
    const items = this.items;
    let founded = false;   
    let index;
    for (index = 0; index < items.length; index++) {
      if (items[index].id === id) {
        founded = true;
        break;
      }
    }
    return founded ? { founded, index } : { founded: false };
  }
  
  _searchByItem(item) {
    let index = this.items.indexOf(item);
    return index != -1 
      ? { founded: true, index } 
      : { founded: false };
  }
  
  _restore() {
    this.items = this._storage.fetch();
  }
  
  _store() {
    this._storage.put(this.items);
  }
  
}

class CurrentService {

  constructor(services) {
    this.services = services;
    this._storage = new LocalStorageDriver('BM_CURRENT');
    this._restore();
  }

  get item() {
    return this._item;
  }

  mayBeOnlyNameUpdated(updatedItem) {
    let current = this._item;
    
    if (!current) return;
    if (current.id !== updatedItem.id) return;
    
    let clone = deepCopy(current);
    clone.name = updatedItem.name;
    if (!deepEquals(clone, updatedItem)) return;
    
    current.name = updatedItem.name;
    this._store();
  }
  
  isExpired() {
    if (!this._item) return false;
    return !this.services.manager.searchEquals(this._item).founded;
  }

  change(item, force) {
    item = deepCopy(item);

    const change = () => {
      if (!item) return item;
      if (force || !this._item) {
        return this.services.change.force(item);
      }
      return this.services.change.diff(this._item, item);
    };

    return this.changeQueue = (this.changeQueue || Promise.resolve())
      .then(change, change)
      .then((item) => {
        this._update(item);
        return item;
      }, () => {
        this._update();
        return Promise.reject();
      });
  }
  
  equals(item) {
    return deepEquals(this._item, item);
  }
  
  _update(item = null) {
    this._item = item;
    this._store();
  }
  
  _restore() {
    this._item = this._storage.fetch();
  }
  
  _store() {
    this._storage.put(this._item);
  }

}

class ChangeService {

  constructor(services) {
    this.services = services;
    this.cache = {};
  }

  force(to) {
    const promise = Promise.resolve()
      .then(() => this._fraction(to.fraction))
      .then(() => this._skill(to.skill))
      .then(() => this._army(to.army))
      .then(() => this._inventory(to.inventory))
      .then(() => this._reset())
      .then(() => this._attribute(to.attribute))
      .then(() => to);
      
    return PromiseMDecorator(promise);
  }

  diff(from, to) {    
    const fractionChanged = from.fraction !== to.fraction;
    const skillChanged = fractionChanged || !deepEquals(from.skill, to.skill);
    const armyChanged = fractionChanged || !deepEquals(from.army, to.army);
    const inventoryChanged = from.inventory !== to.inventory;
    const attributeChanged = !deepEquals(from.attribute, to.attribute);

    let serial = Promise.resolve();
    if (fractionChanged) {
      serial = serial.then(() => this._fraction(to.fraction, from.fraction))
    }
    if (skillChanged) {
      serial = serial.then(() => this._skill(to.skill))
    }
    if (armyChanged) {
      serial = serial.then(() => this._army(to.army))
    }
    if (inventoryChanged) {
      serial = serial.then(() => this._inventory(to.inventory))
    }
    if (attributeChanged) {
      serial = serial
        .then(() => this._reset())
        .then(() => this._attribute(to.attribute))
    }
    serial = serial.then(() => to);
    
    return PromiseMDecorator(serial);
  }

  _fraction(to, from) {
    to = this.services.fraction.map[to];
    
    let prepare = Promise.resolve();

    if (from) {
      from = this.services.fraction.map[from];
    } else {
      prepare = this.services.import.getFraction().then((id) => {
        from = this.services.fraction.map[id];
      })
    }

    const change = (data) => {
      return httpPlainRequest('FORM', '/castle.php', data)
    }

    return prepare.then(() => {

      if (from.fract !== to.fract) {
        let promise = change({ fract: to.fract });
        if (to.classid !== '0') {
          return promise.then(() => {
            return change({ classid: to.classid })
          });
        }
        return promise
      }

      if (from.classid !== to.classid) {
        return change({ classid: to.classid })
      }

    })
  }

  _skill(list) {
    let data = {
      rand: Math.random(),
      setstats: 1,
    }

    for (let i = 0; i < list.length; i++) {
      data[`param${i}`] = list[i];
    }

    return httpPlainRequest('FORM', '/skillwheel.php', data);
  }

  _army(list) {
    let data = {
      rand: Math.random(),
    }

    for (let i = 0; i < list.length; i++) {
      data[`countv${i+1}`] = list[i];
    }

    return httpPlainRequest('FORM', '/army_apply.php', data);
  }

  _inventory(id) {
    let struct = this.services.inventory.map[id];
    let data = {
      r: Date.now() + String(Math.random()).slice(2)
    };
    data[struct.type] = struct.value;
    return httpPlainRequest('GET', '/inventory.php', data);
  }

  _reset() {
    let resetLinkPromise = Promise.resolve(this.cache.resetLink);

    if (!this.cache.resetLink) {
      resetLinkPromise = httpPlainRequest('GET', '/shop.php', { cat: 'potions' }).then((html) => {
        let m = html.match(/\/shop\.php\?b=reset_tube&reset=1&cat=potions&sign=[0-9a-f]+/);
        if (!m) return null;
        return this.cache.resetLink = m[0];
      })
    }

    return resetLinkPromise.then((url) => httpPlainRequest('GET', url));
  }

  _attribute(obj) {

    const getTotal = () => {
      return httpPlainRequest('GET', '/home.php').then((html) => {
        let m = html.match(/href="home\.php\?increase_all=knowledge".*?(\d+)\<\/td/);
        if (!m) return null;
        return parseInt(m[1]) || null;
      });
    }

    const increase = (name, count) => {
      let serial = Promise.resolve();
      for (let i = 0; i < count; i++) {
        serial = serial.then(() => httpPlainRequest('GET', `/home.php?increase=${name}`));
      }
      return serial;
    }

    const increaseAll = (name) => {
      return httpPlainRequest('GET', `/home.php?increase_all=${name}`);
    }

    const distribute = (total) => {
      total = total || 0;

      let list = [];
      for (let name of Object.keys(obj)) {
        list.push({ name, value: obj[name] })
      }
      list.sort((a, b) => a.value - b.value);

      let serial = Promise.resolve();

      let used = 0;
      list.slice(0, -1).forEach(({ name, value }) => {
        if (used >= total) return;
        if (value === 0) return;

        let v = Math.min(total - used, value);
        used += value;
        serial = serial.then(() => increase(name, v));
      });

      if (total > used) {
        let { name, value } = list[ list.length - 1 ];
        if (value > 0) {
          if (value < total - used) {
            serial = serial.then(() => increase(name, value)); 
          } else {
            serial = serial.then(() => increaseAll(name));
          } 
        }       
      }

      return serial;      
    }

    return getTotal().then(distribute);
  }

}

class ImportService {

  constructor(services) {
    this.services = services;
  }

  getInventoryNamesIfAvailable() {
    if (location.pathname.match(/^\/inventory\.php/)) {
      let list = [];
      let nodes = document.querySelectorAll('a[href*="inventory.php?all_on"]');
      for (let node of nodes) {
        let [ _, type, value ] = node.getAttribute('href').match(/(all_on)=(\d)/);
        let name = node.innerText;
        list.push({
          type,
          value,
          name
        });
      }
      return list;
    }
  }

  getArmy() {
    return httpPlainRequest('GET', '/army.php').then((html) => {
      let m = html.match(/\<param name="FlashVars" value="param=\d+\|M([^"]+)/);
      if (!m) return null;
      
      let chunks = m[1].split(';M');
      let items = [];

      for (let chunk of chunks) {
        items.push( parseInt(chunk.split(':')[1].substr(57,3)) || 0 );
      }

      for (let i = items.length; i < this.services.army.iterator.length; i++) {
        items.push(0);
      }

      return items;
    });
  }

  getFraction() {
    return httpPlainRequest('GET', '/castle.php').then((html) => {
      let dict = {};
      for (let { fract, classid } of this.services.fraction.list) {
        if (!dict[fract]) {
          dict[fract] = {};
        }
        dict[fract][classid] = true;
      }

      const extractFract = () => {
        let m = html.match(/\<select name='fract'((?:.|[\r\n])+?)\<\/select/);
        if (!m) return null;

        let available = {};
        m[1].replace(/value=(\d)/g, (_, id) => {
          available[id] = true;
        });

        for (let id of Object.keys(dict)) {
          if (!available[id]) return id;
        }
      }

      const extractClassid = (fract) => {
        let m = html.match(/\<select name='classid'((?:.|[\r\n])+?)\<\/select/);
        if (!m) return null;

        let available = {};
        m[1].replace(/value=(\d)/g, (_, id) => {
          available[id] = true;
        });

        for (let id of Object.keys(dict[fract])) {
          if (!available[id]) return id;
        }
      }

      let fract = extractFract();
      if (!fract) return null;

      let classidList = Object.keys(dict[fract]);
      let classid;

      if (classidList.length === 1) {
        classid = classidList[0];

      } else {
        classid = extractClassid(fract);
        if (!classid) return null;
      }

      return `${fract}${classid}`
    })
  }

  getSkill() {
    return httpPlainRequest('GET', '/skillwheel.php').then((html) => {
      let m = html.match(/\<param name="FlashVars" value='param=.+?;builds=([^']+)/);
      if (!m) return null;
      
      let rows = m[1].split('$');
      let items = [];

      for (let r of rows) {
        let row = r.split('|');
        if (row.length != 10) continue;

        let id = row[0];
        let has = row[8];

        if (has === '1') {
          items.push(id);
        }
      }

      return items;
    })
  }

}

class FractionService {
  
  constructor() {
    this.list = [
      { fract: '1', classid: '0', name: 'Рыцарь' },
      { fract: '1', classid: '1', name: 'Рыцарь света' },
      { fract: '2', classid: '0', name: 'Некромант' },
      { fract: '2', classid: '1', name: 'Некромант - повелитель смерти' },
      { fract: '3', classid: '0', name: 'Маг' },
      { fract: '3', classid: '1', name: 'Маг-разрушитель' },
      { fract: '4', classid: '0', name: 'Эльф' },
      { fract: '4', classid: '1', name: 'Эльф-заклинатель' },
      { fract: '5', classid: '0', name: 'Варвар' },
      { fract: '5', classid: '1', name: 'Варвар крови' },
      { fract: '5', classid: '2', name: 'Варвар-шаман' },
      { fract: '6', classid: '0', name: 'Темный эльф' },
      { fract: '6', classid: '1', name: 'Темный эльф-укротитель' },
      { fract: '7', classid: '0', name: 'Демон' },
      { fract: '7', classid: '1', name: 'Демон тьмы' },
      { fract: '8', classid: '0', name: 'Гном' },
      { fract: '9', classid: '0', name: 'Степной варвар' }
    ];
    
    this.map = {};
    
    for (let item of this.list) {
      item.id = item.fract + item.classid;
      this.map[item.id] = item;
    }
  }
  
  get default() {
    return this.list[0];
  }
  
}

class InventoryService {
  
  constructor(services) {
    this.services = services;
    this._storage = new LocalStorageArrayDriver('BM_INVENTORY');
    this.loaded = false;
    
    this.list = [
      { type: 'all_off', value: '100', name: 'Снять все' },
      { type: 'all_on', value: '1', name: 'Набор 1' },
      { type: 'all_on', value: '2', name: 'Набор 2' },
      { type: 'all_on', value: '3', name: 'Набор 3' },
      { type: 'all_on', value: '4', name: 'Набор 4' },
      { type: 'all_on', value: '5', name: 'Набор 5' }
    ]
    
    this.map = {};
    
    for (let item of this.list) {
      item.id = item.type + item.value;
      this.map[item.id] = item;
    }
    
    this._restore();
  }
  
  _restore() {
    let list = this._storage.fetch();
    
    for (let item of list) {
      let id = item.type + item.value;
      if (this.map[id]) {
        this.map[id].name = item.name;
      }
    }
  }
  
  syncNamesIfAvailable() {
    let list = this.services.import.getInventoryNamesIfAvailable();
    if (list) {
      for (let { type, value, name } of list) {
        let id = type + value;
        if (this.map[id]) {
          this.map[id].name = name;
        }    
      }
      this._storage.put(list);
    }
  }
  
  get default() {
    return this.list[0];
  }
  
}

class AttributeService {
  
  get list() {
    return Object.keys(this.default);
  }
  
  get default() {
    return {
      attack: 0,
      defence: 0,
      power: 0,
      knowledge: 0
    }
  }
  
}

class ArmyService {
  
  get iterator() {
    return this.default;
  }
  
  get default() {
    return [ 0, 0, 0, 0, 0, 0, 0 ]
  }
  
}



class SkillService {
  
  constructor() {
    this.table = [
      { id: "attack", name: "Нападение", list: [
        { id: "attack1", name: "Основы нападения", main: true },
        { id: "attack2", name: "Развитое нападение", main: true },
        { id: "attack3", name: "Искусное нападение", main: true },
        { id: "battle_frenzy", name: "Боевое безумие" },
        { id: "retribution", name: "Воздаяние" },
        { id: "nature_wrath", name: "Лесная ярость" },
        { id: "power_of_speed", name: "Мастерство скорости" },
        { id: "excruciating_strike", name: "Мощный удар" },
        { id: "archery", name: "Стрельба" },
        { id: "tactics", name: "Тактика" },
        { id: "cold_steel", name: "Холодная сталь" },
      ]}, { id: "defense", name: "Защита", list: [
        { id: "defense1", name: "Основы защиты", main: true },
        { id: "defense2", name: "Развитая защита", main: true },
        { id: "defense3", name: "Искусная защита", main: true },
        { id: "last_stand", name: "Битва до последнего" },
        { id: "stand_your_ground", name: "Глухая оборона" },
        { id: "preparation", name: "Готовность" },
        { id: "power_of_endurance", name: "Сила камня" },
        { id: "resistance", name: "Сопротивление" },
        { id: "protection", name: "Сопротивление магии" },
        { id: "vitality", name: "Стойкость" },
        { id: "evasion", name: "Уклонение" },
      ]}, { id: "luck", name: "Удача", list: [
        { id: "luck1", name: "Призрачная удача", main: true },
        { id: "luck2", name: "Большая удача", main: true },
        { id: "luck3", name: "Постоянная удача", main: true },
        { id: "magic_resistance", name: "Магическое сопротивление" },
        { id: "soldier_luck", name: "Солдатская удача" },
        { id: "warlock_luck", name: "Удачливый чародей" },
        { id: "swarming_gate", name: "Широкие врата ада" },
        { id: "elven_luck", name: "Эльфийская удача" },
      ]}, { id: "leadership", name: "Лидерство", list: [
        { id: "leadership1", name: "Основы лидерства", main: true },
        { id: "leadership2", name: "Развитое лидерство", main: true },
        { id: "leadership3", name: "Искусное лидерство", main: true },
        { id: "aura_of_swiftness", name: "Аура скорости" },
        { id: "divine_guidance", name: "Воодушевление" },
        { id: "battle_commander", name: "Лесной лидер" },
        { id: "recruitment", name: "Сбор войск" },
        { id: "empathy", name: "Сопереживание" },
      ]}, { id: "enlightenment", name: "Образование", list: [
        { id: "enlightenment1", name: "Начальное образование", main: true },
        { id: "enlightenment2", name: "Среднее образование", main: true },
        { id: "enlightenment3", name: "Высшее образование", main: true },
        { id: "graduate", name: "Выпускник" },
        { id: "wizard_reward", name: "Колдовская награда" },
        { id: "know_your_enemy", name: "Лесное коварство" },
        { id: "lord_of_the_undead", name: "Повелитель мёртвых" },
        { id: "intelligence", name: "Притяжение маны" },
        { id: "dark_revelation", name: "Тёмное откровение" },
        { id: "arcane_exaltation", name: "Хранитель тайного" },
      ]}, { id: "dark", name: "Магия Тьмы", list: [
        { id: "dark1", name: "Основы магии Тьмы", main: true },
        { id: "dark2", name: "Сильная магия Тьмы", main: true },
        { id: "dark3", name: "Искусная магия Тьмы", main: true },
        { id: "weakening_strike", name: "Ослабляющий удар" },
        { id: "fallen_knight", name: "Падший рыцарь" },
        { id: "master_of_pain", name: "Повелитель боли" },
        { id: "master_of_curses", name: "Повелитель проклятий" },
        { id: "master_of_mind", name: "Повелитель разума" },
      ]}, { id: "destructive", name: "Магия Хаоса", list: [
        { id: "destructive1", name: "Основы магии Хаоса", main: true },
        { id: "destructive2", name: "Сильная магия Хаоса", main: true },
        { id: "destructive3", name: "Искусная магия Хаоса", main: true },
        { id: "searing_fires", name: "Иссушающее пламя" },
        { id: "sap_magic", name: "Истощение магии" },
        { id: "fiery_wrath", name: "Огненная ярость" },
        { id: "master_of_storms", name: "Повелитель бурь" },
        { id: "master_of_fire", name: "Повелитель огня" },
        { id: "master_of_ice", name: "Повелитель холода" },
        { id: "secrets_of_destruction", name: "Тайны хаоса" },
      ]}, { id: "light", name: "Магия Света", list: [
        { id: "light1", name: "Основы магии Света", main: true },
        { id: "light2", name: "Сильная магия Света", main: true },
        { id: "light3", name: "Искусная магия Света", main: true },
        { id: "master_of_blessings", name: "Дарующий благословение" },
        { id: "master_of_abjuration", name: "Дарующий защиту" },
        { id: "fire_resistance", name: "Защита от огня" },
        { id: "master_of_wrath", name: "Повелитель ярости" },
        { id: "twilight", name: "Сумерки" },
        { id: "refined_mana", name: "Тайны света" },
      ]}, { id: "summon", name: "Магия Природы", list: [
        { id: "summon1", name: "Основы магии Природы", main: true },
        { id: "summon2", name: "Сильная магия Природы", main: true },
        { id: "summon3", name: "Искусная магия Природы", main: true },
        { id: "master_of_life", name: "Повелитель жизни" },
      ]}, { id: "sorcery", name: "Чародейство", list: [
        { id: "sorcery1", name: "Основы чародейства", main: true },
        { id: "sorcery2", name: "Развитое чародейство", main: true },
        { id: "sorcery3", name: "Искусное чародейство", main: true },
        { id: "mana_regeneration", name: "Восполнение маны" },
        { id: "boneward", name: "Защита от магии хаоса" },
        { id: "erratic_mana", name: "Изменчивая мана" },
        { id: "magic_insight", name: "Мудрость" },
        { id: "arcane_brillance", name: "Тайное откровение" },
        { id: "arcane_excellence", name: "Тайное преимущество" },
        { id: "arcane_training", name: "Тайные знания" },
      ]}, { id: "special", name: "Фракция", list: [
        { id: "hellfire", name: "Адское пламя" },
        { id: "magic_mirror", name: "Волшебное зеркало" },
        { id: "runeadv", name: "Дополнительные руны" },
        { id: "necr_soul", name: "Духовная связь" },
        { id: "zakarrow", name: "Заколдованная стрела" },
        { id: "nomagicdamage", name: "Контроль магии" },
        { id: "elf_shot", name: "Ливень из стрел" },
        { id: "benediction", name: "Молитва" },
        { id: "knight_mark", name: "Надзор" },
        { id: "memoryblood", name: "Память нашей Крови" },
        { id: "cre_master", name: "Повелитель существ" },
        { id: "consumecorpse", name: "Поглощение трупов" },
        { id: "barb_skill", name: "Пробивающая мощь" },
        { id: "powerraise", name: "Совершенное Поднятие мертвецов" },
        { id: "dark_blood", name: "Тёмная кровь" },
        { id: "dark_power", name: "Тёмная сила" },
        { id: "save_rage", name: "Упорство ярости" },
      ]} 
    ];
    
    this.map = {};
    for (let section of this.table) {
      for (let item of section.list) {
        this.map[item.id] = item;
      }
    }
  }
  
  get default() {
    return [];
  }
  
}


styles(`
.mb-editor-name__box {
  margin: 5px 0 0 0;
}
.mb-editor-name__block-label {
  display: inline-block;
  width: 110px;
}
.mb-editor-name__block-input {
  width: 526px;
}
`);
class EditorNameComponent {
  
  view({ attrs: { value, onchange } }) {

    return m('.mb-editor-name__box', [
      m('.mb-editor-name__block', [
        m('.mb-editor-name__block-label', 'Название:'),
        m('input.mb-editor-name__block-input', { oninput: m.withAttr('value', onchange), value })
      ])
    ])
  }
}


styles(`
.mb-editor-fraction__box {
  margin: 5px 0;
}
.mb-editor-fraction__block-label {
  display: inline-block;
  width: 110px;
}
.mb-editor-fraction__import-button {
  display: inline-block;
  margin-left: 4px;
  cursor: pointer;
}
.mb-editor-fraction__import-button:hover {
  text-decoration: underline;
}
.mb-editor-fraction__import-button--importing {
  animation: mb-fraction-import-animation 1s infinite linear;
  cursor: wait;
}
.mb-editor-fraction__import-button--importing:hover {
  text-decoration: none;
}
@keyframes mb-fraction-import-animation {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(359deg); }
}
`);
class EditorFractionComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }

  view({ attrs: { value, onchange } }) {

    const importAction = () => {
      if (this.importing) return;
      
      this.importing = true;
      
      const finish = () => { this.importing = false };
      
      this.services.import.getFraction()
        .then((val) => {
          if (val) {
            onchange(val);
          }
        })
        .then(finish, finish);
    };

    const importButton = () => {
      return m('.mb-editor-fraction__import-button', { onclick: importAction, class: this.importing ? 'mb-editor-fraction__import-button--importing' : '' }, '<')
    };

    return m('.mb-editor-fraction__box', [
      m('.mb-editor-fraction__block', [
        m('.mb-editor-fraction__block-label', 'Фракция:'),
        m('select', 
          { oninput: m.withAttr('value', onchange), value: value },
          this.services.fraction.list.map(({ id, name }) => {
            return m('option', { key: id, value: id }, name);
          })),
        importButton()
      ])
    ])
  }
}

styles(`
.mb-editor-inventory__box {
  margin: 5px 0;
}
.mb-editor-inventory__block-label {
  display: inline-block;
  width: 110px;
}
`);
class EditorInventoryComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }

  view({ attrs: { value, onchange } }) {

    return m('.mb-editor-inventory__box', [
      m('.mb-editor-inventory__block', [
        m('.mb-editor-inventory__block-label', 'Набор оружия:'),
        m('select', 
          { oninput: m.withAttr('value', onchange), value: value },
          this.services.inventory.list.map(({ id, name }) => {
            return m('option', { key: id, value: id }, name);
          }))
      ])
    ])
  }
}

styles(`
.mb-editor-attribute__box {
  margin: 5px 0;
}
.mb-editor-attribute__block-label {
  display: inline-block;
  width: 110px;
}
.mb-editor-attribute__block-input {
  width: 20px;
  display: inline-block;
}
`);
class EditorAttributeComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }
  
  view({ attrs: { value, onchange } }) {
    
    let changeAction = (name, val) => {
      onchange(Object.assign({}, value, { [name]: parseInt(val) || 0 }));
    }
    
    return m('.mb-editor-attribute__box', [
      m('.mb-editor-attribute__block', [
        m('.mb-editor-attribute__block-label', 'Аттрибуты:'),
        this.services.attribute.list.map((name) => {
          return m('input.mb-editor-attribute__block-input', 
            { key: name, oninput: m.withAttr('value', (value) => { changeAction(name, value) }), value: value[name] || 0 });
        })
      ])
    ])
  }
}

styles(`
.mb-editor-army__box {
  margin: 5px 0;
}
.mb-editor-army__block-label {
  display: inline-block;
  width: 110px;
}
.mb-editor-army__block-controls {
  display: inline-block;
}
.mb-editor-army__block-input {
  width: 24px;
  display: inline-block;
}
.mb-editor-army__block-input:nth-child(1), 
.mb-editor-army__block-input:nth-child(2),
.mb-editor-army__block-input:nth-child(3),
.mb-editor-army__block-input:nth-child(4){
  width: 30px;
}
.mb-editor-army__block-import-button {
  margin-left: 4px;
  display: inline-block;
  cursor: pointer;
}
.mb-editor-army__block-import-button:hover {
  text-decoration: underline;
}
.mb-editor-army__block-import-button--importing {
  animation: mb-army-import-animation 1s infinite linear;
  cursor: wait;
}
.mb-editor-army__block-import-button--importing:hover {
  text-decoration: none;
}
@keyframes mb-army-import-animation {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(359deg); }
}
`);
class EditorArmyComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }
  
  view({ attrs: { value, onchange } }) {
    
    const changeAction = (index, val) => {
      let data = value.slice();
      data[index] = parseInt(val) || 0;
      onchange(data);
    };
    
    const importAction = () => {
      if (this.importing) return;
      
      this.importing = true;
      
      const finish = () => { this.importing = false };
      
      this.services.import.getArmy()
        .then((val) => {
          if (val) {
            onchange(val);
          }
        })
        .then(finish, finish);
    };
    
    const importButton = () => {
      return m('.mb-editor-army__block-import-button', { onclick: importAction, class: this.importing ? 'mb-editor-army__block-import-button--importing' : '' }, '<')
    };
    
    return m('.mb-editor-army__box', [
      m('.mb-editor-army__block', [
        m('.mb-editor-army__block-label', 'Армия:'),
        m('.mb-editor-army__block-controls', 
          this.services.army.iterator.map((_, index) => {
            return m('input.mb-editor-army__block-input', 
              { key: index, oninput: m.withAttr('value', (value) => { changeAction(index, value) }), value: value[index] || 0 })
          })
        ),
        importButton()
      ])
    ])
  }
}

styles(`
.mb-editor-skill__box {
  margin: 5px 0;
}
.mb-editor-skill__select {
  display: inline-block;
}
.mb-editor-skill__option--main {
  font-weight: bold;
}
.mb-editor-skill__list-item-name {
  display: inline-block;
  margin-right: 4px;
}
.mb-editor-skill__list-item-button {
  display: inline-block;
  cursor: pointer;
}
.mb-editor-skill__list-item-button:hover {
  text-decoration: underline;
}
.mb-editor-skill__import-button {
  display: inline-block;
  margin-left: 4px;
  cursor: pointer;
}
.mb-editor-skill__import-button:hover {
  text-decoration: underline;
}
.mb-editor-skill__import-button--importing {
  animation: mb-skill-import-animation 1s infinite linear;
  cursor: wait;
}
.mb-editor-skill__import-button--importing:hover {
  text-decoration: none;
}
@keyframes mb-skill-import-animation {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(359deg); }
}
`);
class EditorSkillComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }
  
  view({ attrs: { value, onchange } }) {
    
    const removeAction = (id) => {
      let p = value.indexOf(id);
      onchange(value.slice(0, p).concat( value.slice(p + 1) ));
    };
    
    const importAction = () => {
      if (this.importing) return;
      
      this.importing = true;
      
      const finish = () => { this.importing = false };
      
      this.services.import.getSkill()
        .then((val) => {
          if (val) {
            onchange(val);
          }
        })
        .then(finish, finish);
    };
    
    const importButton = () => {
      return m('.mb-editor-skill__import-button', { onclick: importAction, class: this.importing ? 'mb-editor-skill__import-button--importing' : '' }, '<')
    };
    
    const list = () => {
      return m('.mb-editor-skill__list', value.map((id) => {
        return m('.mb-editor-skill__list-item', [
          m('.mb-editor-skill__list-item-name', this.services.skill.map[id].name),
          m('.mb-editor-skill__list-item-button', { onclick: () => { removeAction(id) } }, '[х]')
        ])
      }))
    }
    
    const select = () => {
      return m('select.mb-editor-skill__select', 
        { oninput: m.withAttr('value', (id) => { onchange(value.concat(id)) }) }, [
          m('option', 'Навыки:'),
          this.services.skill.table.map(({ id, name, list }) => {
            return m('optgroup', { key: id, label: name }, list.map(({ id, name, main }) => {
              if (value.indexOf(id) !== -1) return null;
              return m('option', { key: id, value: id, class: main ? 'mb-editor-skill__option--main': '' }, name)
            }))
          })
        ])
    };
    
    return m('.mb-editor-skill__box', [
      m('.mb-editor-skill__select-block', [
        select(),
        importButton()
      ]),
      list()
    ])
  }
}


styles(`
.mb-editor__section {
  padding: 5px 6px;
  display: table;
}
.mb-editor__buttons {
  padding: 3px 5px 4px 5px;
  border-top: 1px #5D413A solid;
  background: #F5F3EA;
  height: 16px;
}
.mb-editor__save-button {
  font-weight: bold;
  cursor: pointer;
  display: inline-block;
  margin-right: 8px;
}
.mb-editor__cancel-button {
  cursor: pointer;
  display: inline-block;
}
.mb-editor__close-button {
  cursor: pointer;
  display: inline-block;
}
.mb-editor__save-button:hover, 
.mb-editor__cancel-button:hover,
.mb-editor__close-button:hover {
  text-decoration: underline;
}
.mb-editor__section-column {
  float: left;
  margin-right: 30px;
}
.mb-editor__section-column:last-child {
  margin-right: 0;
}
`);
class EditorComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }
  
  _updateOriginItem(item) {
    if (this.originItem !== item) {
      this.originItem = item;
      this.item = deepCopy(item);
    }
  }

  cancel() {
    this.item = deepCopy(this.originItem);
  }
  
  view({ attrs: { item: originItem, onchange, onclose } }) {
    this._updateOriginItem(originItem);

    let item = this.item;
    let services = this.services;
    
    const closeAction = () => {
      onclose();
    }
    
    const buttons = () => {
      let controls;
      if (deepEquals(this.item, originItem)) {
        controls =  m('.mb-editor__close-button', { onclick: closeAction }, 'Закрыть');

      } else {
        controls = [
          m('.mb-editor__save-button', { onclick: () => { onchange(item) }}, 'Сохранить'),
          m('.mb-editor__cancel-button', { onclick: this.cancel.bind(this) }, 'Отменить')
        ];
      }
      return m('.mb-editor__buttons', controls);
    };
    
    
    return m('.mb-editor__box', [
      m('.mb-editor__section', [
        m(EditorNameComponent, { value: item.name, onchange: (value) => { item.name = value } }),
        m('.mb-editor__section-column', [
          m(EditorFractionComponent, { services, value: item.fraction, onchange: (value) => { item.fraction = value } }),
          m(EditorInventoryComponent, { services, value: item.inventory, onchange: (value) => { item.inventory = value } }),
          m(EditorAttributeComponent, { services, value: item.attribute, onchange: (value) => { item.attribute = value } }),
          m(EditorArmyComponent, { services, value: item.army, onchange: (value) => { item.army = value } }),          
        ]),
        m('.mb-editor__section-column', [
          m(EditorSkillComponent, { services, value: item.skill, onchange: (value) => { item.skill = value } }),          
        ])
      ]),
      buttons()
    ])
  }
  
}




styles(`
.mb-export-popup__box {
  position: absolute;
  top: 2px;
  left: 2px;
  border: 1px #5D413A solid;
}
.mb-export-popup__header {
  background: #F5F3EA;
  border-bottom: 1px #5D413A solid;
  text-align: right;
}
.mb-export-popup__close-button {
  display: inline-block;
  cursor: pointer;
  color: rgb(89, 44, 8);
  padding: 4px 6px;
}
.mb-export-popup__close-button:hover {
  text-decoration: underline;
}
.mb-export-popup__body {  
  background: #fff;
}
.mb-export-popup__body textarea {
  resize: none;
  box-sizing: border-box;
  width: 580px;
  height: 300px;
  font-size: 11px;
  padding: 3px 5px;
  margin: 0;
  border: none;
}
.mb-export-popup__footer {
  padding: 3px 5px 4px 5px;
  border-top: 1px #5D413A solid;
  background: #F5F3EA;
  height: 16px;
}
`);
class ExportPopup {
  
  constructor({ attrs: { services }}) {
    this.services = services;
  }

  oncreate({ dom, attrs: { onclose } }) {
    this.releaseClickOutEventListener = nextTickAddClickOutEventListener(dom, onclose);
  }
  
  onbeforeremove() {
    this.releaseClickOutEventListener();
  }

  view({ attrs: { onclose }}) {
    return m('.mb-export-popup__box', [
      m('.mb-export-popup__header', [
        m('.mb-export-popup__close-button', { onclick: onclose }, 'Закрыть')
      ]),
      m('.mb-export-popup__body', [
        m('textarea', { readonly: true, value: this.services.manager.serialize() })
      ]),
      m('.mb-export-popup__footer')
    ])
  }

};

styles(`
.mb-import-popup__box {
  position: absolute;
  top: 2px;
  left: 2px;
  border: 1px #5D413A solid;
}
.mb-import-popup__header {
  background: #F5F3EA;
  border-bottom: 1px #5D413A solid;
  text-align: right;
}
.mb-import-popup__close-button,
.mb-import-popup__import-button {
  display: inline-block;
  cursor: pointer;
  color: rgb(89, 44, 8);
  padding: 4px 6px;
}
.mb-import-popup__close-button:hover,
.mb-import-popup__import-button:hover {
  text-decoration: underline;
}
.mb-import-popup__import-button {
  font-weight: bold;
}
.mb-import-popup__body {  
  background: #fff;
}
.mb-import-popup__body textarea {
  resize: none;
  box-sizing: border-box;
  width: 580px;
  height: 300px;
  font-size: 11px;
  padding: 3px 5px;
  margin: 0;
  border: none;
}
.mb-import-popup__footer {
  border-top: 1px #5D413A solid;
  background: #F5F3EA;
}
.mb-import-popup__import-error-message {
  color: red;
  margin-left: 10px;
  display: inline-block;
  padding: 4px 6px;
}
`);
class ImportPopup {

  constructor({ attrs: { services }}) {
    this.services = services;
    this.invalid = false;
  }

  oncreate({ dom, attrs: { onclose } }) {
    this.releaseClickOutEventListener = nextTickAddClickOutEventListener(dom, onclose);
  }
  
  onbeforeremove() {
    this.releaseClickOutEventListener();
  }

  view({ attrs: { onclose, onimport }}) {

    const onchange = (value) => {
      this.data = value;
      this.invalid = false;
    }

    const importAction = () => {
      if (this.services.manager.unserialize(this.data)) {
        this.data = null;
        onimport();
      } else {
        this.invalid = true;
      }
    }

    return m('.mb-import-popup__box', [
      m('.mb-import-popup__header', [
        m('.mb-import-popup__close-button', { onclick: onclose }, 'Закрыть')
      ]),
      m('.mb-import-popup__body', [
        m('textarea', { value: this.data, oninput: m.withAttr('value', onchange) })
      ]),
      m('.mb-import-popup__footer', [
        m('.mb-import-popup__import-button', { onclick: importAction }, 'Импорт'),
        this.invalid 
          ? m('.mb-import-popup__import-error-message', 'Некорректный формат данных')
          : null
      ])
    ])
  }

};

styles(`
.mb-manager__box {
  width: 650px;
  border: 1px #5D413A solid;
  background: #fff;
  position: absolute;
  left: 3px;
  top: 3px;
  z-index: 3;
  box-sizing: border-box;
}
.mb-manager__header {
  background: #F5F3EA;
  border-bottom: 1px #5D413A solid;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.mb-manager__header-button {
  display: inline-block;
  cursor: pointer;
  color: rgb(89, 44, 8);
  padding: 4px 0 4px 6px;
}
.mb-manager__header-button:last-child {
  padding-right: 6px;
}
.mb-manager__header-button:hover {
  text-decoration: underline;
}
.mb-manager__header-space {
  width: 20px;
  display: inline-block;
}
.mb-manager__list {
  max-height: 72px;
  overflow: auto;
  border-bottom: 1px #5D413A solid;
  margin-bottom: -1px;
  white-space: nowrap;
}
.mb-manager__list-item {
  padding: 0 6px;
  cursor: pointer;
  margin: 4px 0;
  display: table;
}
.mb-manager__list-item:hover {
  text-decoration: underline;
}
.mb-manager__list-item--selected {
  color: rgb(255, 0, 0);
  text-decoration: underline;
}
.mb-manager__list-empty {
  text-align: center;
  border-bottom: 1px #5D413A solid;
  padding: 15px 0;
  margin-bottom: -1px;
}
.mb-manager__confirm-remove {
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}
.mb-manager__confirm-remove-buttons {
  margin-top: 7px;
}
.mb-manager__confirm-remove-button {
  display: inline-block;
  margin: 4px 4px;
  cursor: pointer;
}
.mb-manager__confirm-remove-button:hover {
  text-decoration: underline;
}
.mb-manager__confirm-remove-button--no {
  font-weight: bold;
}
.mb-manager__confirm-remove-button:hover:not(.mb-manager__confirm-remove-button--no) {
  color: red;
}
.mg-manager__body {
  margin-top: 1px;
}
`);
class ManagerComponent {

  constructor({ attrs: { services }}) {
    this.services = services;
    
    this._initSelected();
    this.exportPopup = false;
    this.importPopup = false;
  }
  
  _initSelected() {
    let current = this.services.current.item;
    if (current) {
      let { founded, index } = this.services.manager.searchEquals(current);
      if (founded) {
        this.selected = this.services.manager.items[index];
        return;
      }
    }
    this.selected = this.services.manager.items[0];
  }

  createNew() {
    this.selected = this.services.manager.createNew();
  }
  
  selectItem(item) {
    this.selected = item;
  }
  
  removeSelected() {
    this.confirmRemove = true;
  }
  
  confirmRemoveOk() {
    this.selected = this.services.manager.remove(this.selected);
    this.confirmRemove = false;
  }
  
  confirmRemoveCancel() {
    this.confirmRemove = false;
  }
  
  duplicateSelected() {
    this.selected = this.services.manager.duplicate(this.selected);
  }
  
  updateItem(item) {
    this.selected = this.services.manager.update(item);
  }
  
  view({ attrs: { onclose } }) {
    
    const closeAction = () => {
      onclose();
    };
    
    const exportCloseAction = () => {
      this.exportPopup = false;
    };

    const importCloseAction = () => {
      this.importPopup = false;
    };

    const importAction = () => {
      this.importPopup = false;
      this.selected = null;
    };

    const headerLeft = () => {
      let controls = [];
      
      if (!this.confirmRemove) {
        controls.push(
          m('.mb-manager__header-button', { onclick: this.createNew.bind(this) }, 'Новый')
        );

        if (this.selected) {
          controls.push(
            m('.mb-manager__header-button', { onclick: this.duplicateSelected.bind(this) }, 'Копия'),
            m('.mb-manager__header-button', { onclick: this.removeSelected.bind(this) }, 'Удалить')
          )
        }
      }
      
      return m('.mb-manager__header-left', controls);
    };
    
    const headerRight = () => {
      let controls = [];
      if (!this.confirmRemove) {
        if (this.services.manager.items.length > 0) {
          controls.push(
            m('.mb-manager__header-button', { onclick: () => { this.exportPopup = true } }, 'Экспорт')
          )
        }
        controls.push(
          m('.mb-manager__header-button', { onclick: () => { this.importPopup = true } }, 'Импорт'),
          m('.mb-manager__header-space')
        )
      }
      controls.push(
        m('.mb-manager__header-button', { onclick: closeAction }, 'Закрыть')
      )

      return m('.mb-manager__header-right', controls);
    };
    
    const confirmRemove = () => {
      if (!this.confirmRemove) return null;
      return m('.mb-manager__confirm-remove', [
        m('.mb-manager__confirm-remove-message', [
          `Удалить "${this.selected.name}"?`
        ]),
        m('.mb-manager__confirm-remove-buttons', [
          m('.mb-manager__confirm-remove-button.mb-manager__confirm-remove-button--no', { onclick: this.confirmRemoveCancel.bind(this) }, 'Нет'),
          m('.mb-manager__confirm-remove-button', { onclick: this.confirmRemoveOk.bind(this) }, 'Да')
        ]),
      ])
    };
    
    const list = () => {      
      if (this.confirmRemove) return null;
      let items = this.services.manager.items;
      
      if (items.length === 0) {
        return m('.mb-manager__list-empty', 'Список пуст')
      }
      return m('.mb-manager__list', items.map((item) => {
        return m('.mb-manager__list-item', {
          key: item.id,
          class: (this.selected || {}).id === item.id ? 'mb-manager__list-item--selected' : '', 
          onclick: () => { this.selectItem(item) }
        }, item.name)
      }))
    };
    
    const body = () => {
      if (this.confirmRemove) return null;
      if (!this.selected) return null;
     
      return m('.mb-manager__body', [
        m(EditorComponent, { services: this.services, item: this.selected, onchange: this.updateItem.bind(this), onclose: closeAction })
      ]);
    };
    
    const popups = () => {
      if (this.confirmRemove) return null;

      return [
        this.exportPopup 
          ? m(ExportPopup, { services: this.services, onclose: exportCloseAction })
          : null,
        this.importPopup
          ? m(ImportPopup, { services: this.services, onclose: importCloseAction, onimport: importAction })
          : null
      ]
    }
    
    return m('.mb-manager__box', [
      m('.mb-manager__header', [
        headerLeft(),
        headerRight()
      ]),
      confirmRemove(),
      list(),
      body(),
      popups(),
    ])
  }
}

styles(`
.mb-selector__handler {
  display: flex;
  cursor: pointer;
}
.mb-selector__handler--changing {
  cursor: wait;
}
.mb-selector__info {
  padding: 2px 6px 4px 5px;
  background: #6b6b69;
  color: #f5c137;
  white-space: nowrap;
  border: 1px solid #f5c137;
  border-left: none;
}
.mb-selector__info-error {
  color: red;
}
.mb-selector__triangle-box {
  background: #6b6b69;
  color: #f5c137;
  border: 1px solid #f5c137;
  border-left: none;
  padding: 2px 8px 4px 5px;
  box-sizing: border-box;
  position: relative;
}
.mb-selector__triangle-box:before {
  content: "\\00a0";
}
.mb-selector__triangle {
  width: 0; 
  height: 0; 
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid #f5c137;
  position: absolute;
  left: 3px;
  top: 8px;
}
.mb-selector__triangle--up {
  transform: rotate(180deg);
}
.mb-selector__list-handler {
  z-index: 3;
  position: relative;
}
.mb-selector__list-box {
  position: absolute;
  top: 0;
  left: 0;
  border: 1px #5D413A solid;
  background: #fff;
}
.mb-selector__list-item {
  padding: 0 6px;
  cursor: pointer;
  margin: 4px 0;
  display: table;
  white-space: nowrap;
}
.mb-selector__list-item:hover .mb-selector__list-item-name {
  text-decoration: underline;
}
.mb-selector__list-item-name--current {
  color: rgb(255, 0, 0);
  text-decoration: underline;
  cursor: default;
}
.mb-selector__list-item-name {
  display: inline-block;
}
.mb-selector__list-item-force {
  display: inline-block;
  margin-left: 5px;
}
.mb-selector__list-item-force:hover {
  text-decoration: underline;
}
.mb-selector__list-footer {
  display: block;
  border-top: 1px #5D413A solid;
  padding: 4px 6px;
  margin: 0;
  background: #F5F3EA;
}
.mb-selector__list-button-cancel {
  cursor: pointer;
  display: inline-block;
}
.mb-selector__list-button-cancel:hover {
  text-decoration: underline;
}
`);
class SelectorComponent {
  
  constructor({ attrs: { services }}) {
    this.services = services;
    this.dropped = false;
    this.changing = false;
    this.error = false;

  }

  oncreate({ dom }) {
    this.releaseClickOutEventListener = addClickOutEventListener(dom, () => {
      this.dropped = false;
    });
  }
  
  onbeforeremove() {
    this.releaseClickOutEventListener();
  }

  drop() {
    if (this.changing) return;
    this.dropped = !this.dropped;
  }
  
  view() {
    let items = this.services.manager.items;
    if (!items.length) return null;
    
    let current = this.services.current.item;
    
    const selectAction = (item, force) => {
      if (item === current && !force) return;
      
      this.dropped = false;
      this.changing = true;
      
      this.services.current.change(item, force)
        .then(() => {
          this.changing = false;
          this.error = false;
        }, (e) => {
          console.error(e);
          this.changing = false;
          this.error = true;
        });
    };
    
    const list = () => {
      if (!this.dropped) return null;
      
      const box = m('.mb-selector__list-box', [
        m('.mb-selector__list', 
          items.map((item) => {
            return m('.mb-selector__list-item', [
              m('.mb-selector__list-item-name', 
                this.services.current.equals(item) 
                  ? { class: 'mb-selector__list-item-name--current' }
                  : { onclick: () => { selectAction(item) } }, 
                item.name),
              m('.mb-selector__list-item-force', { onclick: () => { selectAction(item, true) }}, '[*]')
            ])
          })),
        current 
          ? m('.mb-selector__list-footer', 
              m('.mb-selector__list-button-cancel', { onclick: () => { selectAction(null); } }, 'Сбросить'))
          : null,
      ]);
      
      return m('.mb-selector__list-handler', box);
    };
    
    const info = () => {
      if (!this.changing && !current && !this.error) return null;
      
      const text = () => {
        if (this.changing) {
          return 'Смена билда...'
        } else if (this.error) {
          return m('.mb-selector__info-error', 'Ошибка смены билда!')
        } else {
          return [
            this.services.current.isExpired() ? '*' : '',
            current.name
          ]
        }
      };
      
      return m('.mb-selector__info', text());
    };
    
    return m('.mb-selector__box', [
      m('.mb-selector__handler', { onclick: this.drop.bind(this), class: this.changing ? 'mb-selector__handler--changing' : '' }, [
        info(),
        m('.mb-selector__triangle-box', 
          m('.mb-selector__triangle', { class: this.dropped ? 'mb-selector__triangle--up' : '' }))
      ]),
      list()
    ])
  }
  
}


styles(`
.mb-app__handler {
  display: flex;
}
.mb-app__handler-editor-button {
  background: #6b6b69;
  color: #f5c137;
  border: 1px solid #f5c137;
  padding: 2px 6px 4px 6px;
  cursor: pointer;
}
`);
class AppComponent {
  
  constructor() {
    this.manager = false;
    this.services = new ServiceContainer();
    
    this.services.inventory.syncNamesIfAvailable();
  }
  
  view() {
    return m('.mb-app__box', [
      m('.mb-app__handler', [
        m('.mb-app__handler-editor-button', 
          { onclick: () => { this.manager = true } }, 
          '+'),
        m(SelectorComponent, { services: this.services })
      ]),
      this.manager 
        ? m(ManagerComponent, { services: this.services, onclose: () => { this.manager = false } })
        : null
    ]);
  }
}


function main() {
  let container = document.querySelector('body table table td');
  if (!container) return
  
  let checkAllowedPage = document.querySelector('a[href*="home.php"]');
  if (!checkAllowedPage) return;
  
  let root = document.createElement('div');
  root.style.position = 'absolute';
  root.style.top = '0';
  root.style.left = '0';
  
  container.style.position = 'relative';
  container.appendChild(root);
  m.mount(root, AppComponent);
}

function styles(content = null, flush = false) {
  let task = styles.task || {};
  
  if (content) {
    if (task.scheduled) {
      task.content += content;
    }
    else {
      let task = styles.task = {
        content,
        scheduled: true
      }
      task.timer = setTimeout(finish, 0, task)
    }    
  }
  
  if (flush && task.scheduled) {
    clearInterval(task.timer);
    finish(task);
  }
  
  function finish(task) {
    let head = document.querySelector('head');
    head.insertAdjacentHTML('beforeEnd', 
      `<style type="text/css">${task.content}</style>`);
    task.scheduled = false; 
  }
}

function uniqid() {
  return Date.now().toString(36) + Math.random().toString(36).slice(1);
}

function deepCopy(value) {
  if (!value) return value;
  if (value instanceof Array) {
    return value.map(deepCopy);
  }
  if (typeof value === 'object') {
    let obj = {};
    for (let key of Object.keys(value)) {
      obj[key] = deepCopy(value[key])
    }
    return obj;
  }
  return value;
}

function deepEquals(a, b) {
  if (a === b) return true;
  if (a instanceof Array && b instanceof Array) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!deepEquals(a[i], b[i])) return false;
    }
    return true;
  }
  if (!a || !b) return false;
  if (typeof a === 'object' && typeof b === 'object') {
    let keys = Object.keys(a);
    if (keys.length !== Object.keys(b).length) return false;
    for (let key of keys) {
      if (!b.hasOwnProperty(key)) return false;
      if (!deepEquals(a[key], b[key])) return false;
    }
    return true;
  }
  return false;
}

function addClickOutEventListener(dom, fn) {
  const listener = (event) => {    
    let node = event.target;
    while(node && node.parentNode) {
      if (node === dom) {
        return;
      }
      node = node.parentNode;
    }
    
    fn();
    m.redraw();
  };
  
  const body = document.body;
  body.addEventListener('click', listener);
  return () => {
    body.removeEventListener('click', listener);
  }
  
}

function nextTickAddClickOutEventListener(dom, fn) {

  let releaseEventListener = null;
  let timeout = setTimeout(() => {
    timeout = null;
    releaseEventListener = addClickOutEventListener(dom, fn);
  });

  return () => {
    if (timeout) clearTimeout(timeout);
    if (releaseEventListener) releaseEventListener();
  }
}

class LocalStorageDriver {
  
  constructor(key) {
    this.key = key;
  }
  
  fetch() {
    let text = localStorage.getItem(this.key);
    let data;
    try {
      data = JSON.parse(text);
    }
    catch(e) {
      data = null;
    }
    return data;
  }
  
  put(data) {
    localStorage.setItem(this.key, JSON.stringify(data));
  }
  
}

class LocalStorageArrayDriver extends LocalStorageDriver {
  
  fetch() {
    let data = super.fetch();
    if (!Array.isArray(data)) {
      data = [];
    }
    return data;    
  }
  
}

function PromiseMDecorator(promise) {
  const proxyWithRedraw = (data) => {
    setTimeout(m.redraw.bind(m));
    return data;
  }
  return promise.then(
    proxyWithRedraw, 
    (data) => proxyWithRedraw(Promise.reject(data))
  );
}

function httpPlainRequest(method, url, data) {
  if (method === 'FORM') {
    let form = new FormData();
    for (let key of Object.keys(data)) {
      form.append(key, data[key]);
    }
    data = form;
    method = 'POST';
  }

  return m.request({ method, url, data,
    extract: ({ responseText }) => responseText
  });
}

try {
  styles(null, true);
  main();
}
catch(e) {
  console.error(e);
}