hwm_build_manager

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

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