hwm_build_manager

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==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
// @homepageURL https://github.com/betula/hwm_build_manager
// @include     http://*heroeswm.ru/*
// @include     http://178.248.235.15/*
// @include     http://*lordswm.com/*
// @version     1.0.2
// @grant       none
// ==/UserScript==


// Build Manager 1.0.2

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;
      }, (e) => {
        this._update();
        return Promise.reject(e);
      });
  }
  
  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', '/home.php').then((html) => {
        let m = html.match(/shop\.php\?b=reset_tube&reset=2&sign=[0-9a-f]+/);
        if (!m) return null;
        return this.cache.resetLink = '/' + m[0];
      })
    }

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

  _attribute(obj) {
    const getTotal = () => {
      return httpPlainRequest('GET', '/home.php').then((html) => {
        let m = html.match(/href="home\.php\?increase_all=knowledge"(?:.|\n)*?(\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: "piercing_luck", 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_conjuration", name: "Повелитель волшебства" },
        { id: "master_of_life", name: "Повелитель жизни" },
        { id: "master_of_obstacles", 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 mount() {
  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
  });
}

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

setTimeout(main);



// 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
}());