const Template = {
  popup: document.querySelector('#tpl___popup'),
  tabs: document.querySelector('#tpl___tabs'),
  tabHeading: document.querySelector('#tpl___tabs__tab-heading'),
  tabPanels: document.querySelector('#tpl___tabs__panels'),
  tabPanel: document.querySelector('#tpl___tabs__tab-panel'),
  battleMonster: document.querySelector('#tpl___battle__monster'),
  battleHpBar: document.querySelector('#tpl___battle__hp-bar'),
  battleExpBar: document.querySelector('#tpl___battle__exp-bar'),
  battleActionFeedback: document.querySelector('#tpl___battle__action-feedback'),
  techniques: document.querySelector('#tpl___techniques'),
  technique: document.querySelector('#tpl___technique'),
  party: document.querySelector('#tpl___party'),
  partyMonster: document.querySelector('#tpl___party__monster'),
  monsterStats: document.querySelector('#tpl___monster-stats'),
  movesetList: document.querySelector('#tpl___moveset__list'),
  movesetItem: document.querySelector('#tpl___moveset__item'),
  areaSelection: document.querySelector('#tpl___area-selection'),
  areaSelectionItem: document.querySelector('#tpl___area-selection__item'),
  inventory: document.querySelector('#tpl___inventory'),
  inventoryItem: document.querySelector('#tpl___inventory__item'),
  menuJournal:  document.querySelector('#tpl___menu__journal'),
  dialogSave:  document.querySelector('#tpl___dialog__save'),
  dialogLoad:  document.querySelector('#tpl___dialog__load'),
  menuSettings: document.querySelector('#tpl___menu__settings'),
};
const UI = {
  elements: {
    sceneBattle: document.querySelector('#scene__battle'),
    sceneTown: document.querySelector('#scene__town'),
    battle: document.querySelector('#battle'),
    battleOpponent: document.querySelector('#battle__opponent'),
    battleOpponentSprite: null,
    battleOpponentAnimation: document.querySelector('.battle__technique-animation'),
    battlePlayer: document.querySelector('#battle__player'),
    techniques: document.querySelector('#techniques'),
    log: document.querySelector('#log'),
    status: document.querySelector('#status'),
    nextTrainer: document.querySelector('#status [data-template-slot="nextTrainer"]'),
    changeArea: document.querySelector('#status [data-template-slot="changeArea"]'),
    menuParty: document.querySelector('#menu__party'),
    menuInventory: document.querySelector('#menu__inventory'),
    menuCatch: document.querySelector('#menu__catch'),
    menuLog:  document.querySelector('#menu__log'),
    menuJournal: document.querySelector('#menu__journal'),
    menuSettings: document.querySelector('#menu__settings'),
  },
  events: document.createElement('div'),
  /**
   * @param {HTMLElement} template
   */
  createTemplate (template) {
    const templateBase = document.createElement('div');
    templateBase.innerHTML = template.innerHTML.trim();
    const templateNode = templateBase.firstChild;
    /**
     * @param {HTMLElement} targetElement
     */
    templateNode.appendTo = function (targetElement) {
      if (templateNode.dataset.templateType && templateNode.dataset.templateType === 'multiple') {
        for (const child of [...this.children]) {
          targetElement.appendChild(child);
        }
      } else {
        targetElement.appendChild(this);
      }
    };
    return templateNode;
  },
  /**
   * @returns {HTMLElement}
   */
  createPopup () {
    const popup = UI.createTemplate(Template.popup);
    popup.addEventListener('click', ({ target }) => {
      if (target === popup) {
        popup.dispatchEvent(new Event('close'));
        popup.remove();
      }
    });
    return popup;
  },
  /**
   * @typedef {Object} Tab
   * @property {HTMLElement} heading
   * @property {HTMLElement} content
   * @inner
   *
   * @param {Tab[]} tabs
   *
   * @returns {HTMLElement}
   */
  createTabs (tabs) {
    const wrap = UI.createTemplate(Template.tabs);
    const panelsNode = UI.createTemplate(Template.tabPanels);
    wrap.style.gridTemplateColumns = '1fr '.repeat(tabs.length);
    const name = randomString();
    for (const idx in tabs) {
      const tab = tabs[idx];
      const tabHeading = UI.createTemplate(Template.tabHeading);
      const tabPanel = UI.createTemplate(Template.tabPanel);
      const inputId = `${name}_${idx}`;
      const panelId = randomString();
      const tabHeadingInput = tabHeading.querySelector('[data-template-slot="input"]');
      tabHeadingInput.name = name;
      tabHeadingInput.id = inputId;
      tabHeadingInput.setAttribute('aria-controls', panelId);
      if (idx == 0) {
        tabHeadingInput.checked = true;
      }
      const tabHeadingLabel = tabHeading.querySelector('[data-template-slot="label"]');
      tabHeadingLabel.setAttribute('for', inputId);
      tabHeadingLabel.appendChild(tab.heading);
      tabPanel.id = panelId;
      tabPanel.appendChild(tab.content);
      tabHeading.appendTo(wrap);
      panelsNode.appendChild(tabPanel);
    }
    wrap.appendChild(panelsNode);
    return wrap;
  },
  /**
   * @param {HTMLElement} slotNode
   * @param {HTMLElement} replacingNode
   *
   * @returns {HTMLElement}
   */
  replaceTemplateSlot (slotNode, replacingNode) {
    replacingNode.dataset.templateSlot = slotNode.dataset.templateSlot;
    slotNode.replaceWith(replacingNode);
    return replacingNode;
  },
  /**
   * @param {HTMLElement} popup
   */
  drawPopup (popup) {
    const otherPopupExists = document.querySelector('.popup__overlay');
    if (otherPopupExists) {
      popup.classList.add('popup--is-multiple');
    }
    document.body.appendChild(popup);
  },
  /* Battle */
  techniqueAnimationIsRunning: false,
  techniqueAnimationNumber: 0,
  techniqueAnimationFps: 20,
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createHpBar (monster) {
    const template = UI.createTemplate(Template.battleHpBar);
    const bar = template.querySelector('[data-template-slot="bar"]');
    const text = template.querySelector('[data-template-slot="text"]');
    let barColor;
    const percentHp = (monster.hp / monster.stats.hp) * 100;
    if (percentHp > 60) {
      barColor = 'green';
    } else if (percentHp > 15) {
      barColor = 'rgb(240, 240, 100)';
    } else {
      barColor = 'red';
    }
    bar.style.backgroundColor = barColor;
    bar.style.width = `${percentHp}%`;
    text.textContent = `${monster.hp} / ${monster.stats.hp}`;
    return template;
  },
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createExpBar (monster) {
    const template = UI.createTemplate(Template.battleExpBar);
    const bar = template.querySelector('[data-template-slot="bar"]');
    const text = template.querySelector('[data-template-slot="text"]');
    const expToNextLevel = monster.getExperienceRequired() - monster.getExperienceRequired(-1);
    const currentRelativeExp = monster.exp - monster.getExperienceRequired(-1);
    const expPercent = (currentRelativeExp / expToNextLevel) * 100;
    bar.style.width = `${expPercent}%`;
    text.textContent = `${monster.exp} / ${monster.getExperienceRequired()}`;
    return template;
  },
  /**
   * @param {string} gender
   *
   * @returns {HTMLElement}
   */
  createGenderIcon (gender) {
    const icon = document.createElement('span');
    icon.textContent = gender === 'male' ? '♂' : gender === 'female' ? '♀' : '⚲';
    icon.title = slugToName(gender);
    icon.classList.add('gender-icon');
    return icon;
  },
  /**
   * @param {string} type
   *
   * @returns {HTMLElement}
   */
  createElementTypeIcon (type) {
    const img = document.createElement('img');
    img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/element/${type}_type.png`;
    img.title = translate(type) || slugToName(type);
    return img;
  },
  /**
   * @param {StatusEffect} statusEffect
   *
   * @returns {HTMLElement}
   */
  createStatusEffectIcon (statusEffect) {
    if (!statusEffect) {
      return document.createElement('i');
    }
    const img = document.createElement('img');
    img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${statusEffect.slug}.png`;
    img.title = statusEffect.name;
    return img;
  },
  /**
   * @param {Monster} monster
   */
  createBattleMonster (monster) {
    const template = UI.createTemplate(Template.battleMonster);
    template.querySelector('[data-template-slot="name"]').textContent = monster.name;
    template.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML;
    template.querySelector('[data-template-slot="level"]').textContent = monster.level;
    template.querySelector('[data-template-slot="statusEffect"]').innerHTML = UI.createStatusEffectIcon(monster.statusEffect).outerHTML;
    template.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`;
    UI.replaceTemplateSlot(template.querySelector('[data-template-slot="hpBar"]'), UI.createHpBar(monster));
    return template;
  },
  /**
   * @returns {HTMLElement}
   */
  createOpponentMonster () {
    const battleMonsterNode = UI.createBattleMonster(Memory.state.opponent.activeMonster);
    battleMonsterNode.classList.add('battle__monster--opponent');
    if (Game.isBattleType('trainer')) {
      battleMonsterNode.classList.add('battle__monster--is-trainer');
      battleMonsterNode.querySelector('[data-template-slot="trainerName"]').textContent = Memory.state.opponent.name;
      if (Memory.state.opponent.sprite) {
        battleMonsterNode.classList.add('battle__monster--has-trainer-sprite');
        battleMonsterNode.querySelector('[data-template-slot="trainerSprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/player/${Memory.state.opponent.sprite}`;
        battleMonsterNode.querySelector('[data-template-slot="trainerSprite"]').title = Memory.state.opponent.name;
      }
    }
    return battleMonsterNode;
  },
  /**
   * @returns {HTMLElement}
   */
  createActiveMonster () {
    const battleMonsterNode = UI.createBattleMonster(Memory.state.player.activeMonster);
    UI.replaceTemplateSlot(
      battleMonsterNode.querySelector('[data-template-slot="expBar"]'),
      UI.createExpBar(Memory.state.player.activeMonster)
    );
    battleMonsterNode.classList.add('battle__monster--player');
    battleMonsterNode.querySelector('[data-template-slot="sprite"]').addEventListener('click', () => {
      UI.openStatsMenu(Memory.state.player.activeMonster);
    });
    return battleMonsterNode;
  },
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createActiveTechniques (monster) {
    const template = UI.createTemplate(Template.techniques);
    for (const technique of monster.activeTechniques) {
      const techniqueNode = UI.createTemplate(Template.technique);
      techniqueNode.querySelector('[data-template-slot="name"]').textContent = technique.name;
      techniqueNode.querySelector('[data-template-slot="recharge"]').textContent = technique.rechargeLength;
      techniqueNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
      techniqueNode.querySelector('[data-template-slot="range"]').textContent = translate(technique.range) || slugToName(technique.range);
      techniqueNode.querySelector('[data-template-slot="power"]').textContent = technique.power;
      techniqueNode.querySelector('[data-template-slot="accuracy"]').textContent = technique.accuracy;
      if (technique.isRecharging()) {
        techniqueNode.setAttribute('disabled', true);
      }
      template.appendChild(techniqueNode);
    }
    return template;
  },
  /**
   * @returns {HTMLElement}
   */
  createPlayerDefeatedMonsterSelection () {
    const monsterSelectionNode = UI.createMonsterSelection(Memory.state.player.monsters.filter((monster) => monster.hp > 0));
    monsterSelectionNode.addEventListener('monster:selected', (event) => {
      Game.setActivePlayerMonster(event.detail.monster);
      Game.playerIsChoosingNextMonster = false;
    });
    return monsterSelectionNode;
  },
  /**
   * @param {HTMLElement} battleMonsterNode
   */
  drawOpponentMonster () {
    const battleMonsterNode = UI.createOpponentMonster();
    UI.elements.battleOpponentSprite = battleMonsterNode.querySelector('[data-template-slot="sprite"]');
    UI.elements.battleOpponentSprite.style.transitionDuration = `${UI.damageHighlightClickDuration}s`;
    // en/disable catch
    UI.drawActiveBall();
    const previousBattleMonsterNode = UI.elements.battleOpponent.querySelector('.battle__monster');
    if (previousBattleMonsterNode) {
      UI.elements.battleOpponentSprite.classList = previousBattleMonsterNode.querySelector('[data-template-slot="sprite"]').classList;
      UI.elements.battleOpponent.removeChild(previousBattleMonsterNode);
    }
    UI.elements.battleOpponent.appendChild(battleMonsterNode);
  },
  /**
   * @returns {void}
   */
  drawActiveMonster () {
    const battleMonsterNode = UI.createActiveMonster();
    const previousBattleMonsterNode = UI.elements.battlePlayer.querySelector('.battle__monster');
    if (previousBattleMonsterNode) {
      UI.elements.battlePlayer.removeChild(previousBattleMonsterNode);
    }
    UI.elements.battlePlayer.appendChild(battleMonsterNode);
  },
  /**
   * @returns {void}
   */
  drawActiveTechniques () {
    const activeTechniques = UI.createActiveTechniques(Memory.state.player.activeMonster);
    activeTechniques.id = 'techniques';
    document.querySelector('#techniques').innerHTML = activeTechniques.innerHTML;
  },
  /**
   * @param {Technique} technique
   *
   * @returns {void}
   */
  drawTechniqueAnimation (technique) {
    const animation = technique.animation;
    if (!(animation && DB.allAnimations[animation])) {
      return;
    }
    if (UI.techniqueAnimationIsRunning) {
      return;
    }
    UI.techniqueAnimationIsRunning = true;
    const x = UI.battleClickEvent.clientX;
    const y = UI.battleClickEvent.clientY;
    const techniqueAnimationLoop = () => {
      UI.elements.battleOpponentAnimation.src = `/modules/tuxemon/mods/tuxemon/animations/technique/${animation}_${("00" + UI.techniqueAnimationNumber).slice(-2)}.png`;
      UI.elements.battleOpponentAnimation.style.top = y - (UI.elements.battleOpponentAnimation.clientHeight / 2) + 'px';
      UI.elements.battleOpponentAnimation.style.left = x - (UI.elements.battleOpponentAnimation.clientWidth / 2) + 'px';
      // console.log(UI.elements.battleOpponentAnimation.src);
      UI.techniqueAnimationNumber++;
      if (UI.techniqueAnimationNumber >= DB.allAnimations[animation].length) {
        UI.techniqueAnimationIsRunning = false;
        UI.techniqueAnimationNumber = 0;
        UI.elements.battleOpponentAnimation.src = '';
        return;
      }
      setTimeout(() => requestAnimationFrame(techniqueAnimationLoop), 1000 / UI.techniqueAnimationFps);
    };
    requestAnimationFrame(techniqueAnimationLoop);
  },
  drawLog () {
    if (UI.elements.log.children.length > Memory.state.Settings.logMaxLength) {
      UI.elements.log.innerHTML = '';
    }
    for (const message of Game.logMessages) {
      const textNode = document.createElement('div');
      textNode.innerHTML = '> '.repeat(message.indentation) + message.message;
      if (message.style) {
        for (const property of Object.keys(message.style)) {
          const value = message.style[property];
          textNode.style[property] = value;
        }
      }
      UI.elements.log.appendChild(textNode);
      UI.elements.log.scrollTop = UI.elements.log.scrollHeight;
    }
    Game.logMessages = [];
  },
  toggleLog () {
    UI.elements.log.classList.toggle('log--is-hidden');
  },
  drawArea () {
    UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`;
  },
  progressTurn () {
    UI.drawOpponentMonster();
    UI.drawActiveMonster();
    UI.drawActiveTechniques();
    UI.drawStatus();
  },
  /**
   * @param {HTMLElement} monsterSelectionNode
   */
  openPlayerDefeatedMonsterSelection (monsterSelectionNode) {
    const popup = UI.createPopup().cloneNode(true); // remove event listeners
    monsterSelectionNode.addEventListener('monster:selected', () => {
      popup.remove();
    });
    popup.querySelector('[data-template-slot="content"]').appendChild(monsterSelectionNode);
    UI.drawPopup(popup);
  },
  /* Battle - Action Feedback */
  /**
   * @type {MouseEvent}
   */
  battleClickEvent: null,
  damageHighlightClickDuration: 0.1,
  damageHighlightClickTimeout: null,
  /**
   * @param {any} feedback
   *
   * @returns {HTMLElement}
   */
  createActionFeedback (feedback) {
    const feedbackNode = UI.createTemplate(Template.battleActionFeedback);
    feedbackNode.innerHTML = feedback;
    feedbackNode.style.top = UI.battleClickEvent.pageY - UI.elements.battleOpponent.offsetTop + (Math.random() * 40 - 20) + 'px';
    feedbackNode.style.left = UI.battleClickEvent.pageX - UI.elements.battleOpponent.offsetLeft + (Math.random() * 40 - 20) + 'px';
    feedbackNode.dataset.duration = 2;
    feedbackNode.style.animationDuration = `${feedbackNode.dataset.duration}s`;
    return feedbackNode;
  },
  /**
   * @param {number|string} damage
   *
   * @returns {HTMLElement}
   */
  createDamage (damage) {
    return UI.createActionFeedback(damage);
  },
  /**
   * @returns {HTMLElement}
   */
  createDamageMiss () {
    return UI.createActionFeedback('MISS!');
  },
  /**
   * @param {HTMLElement} damageNode
   * @param {number} multiplier
   *
   * @returns {HTMLElement}
   */
  applyMultiplierToDamage (damageNode, multiplier) {
    damageNode.style.fontSize = `${multiplier * 2}rem`;
    return damageNode;
  },
  /**
   * @param {HTMLElement} damageNode
   * @param {Technique} technique
   *
   * @returns {HTMLElement}
   */
  applyTechniqueToDamage (damageNode, technique) {
    damageNode.style.color = mixColors(
      ...technique.types.map((type) => standardizeColor(ElementTypeColor[type]))
    );
    return damageNode;
  },
  /**
   * @param {HTMLElement} damageNode
   * @param {StatusEffect} statusEffect
   *
   * @returns {HTMLElement}
   */
  applyStatusEffectToDamage (damageNode, statusEffect) {
    damageNode.style.color = StatusEffectTypeColor[statusEffect.slug];
    return damageNode;
  },
  /**
   * @param {HTMLElement} damageNode
   */
  drawActionFeedback (node) {
    UI.elements.battleOpponent.appendChild(node);
    setTimeout(() => node.remove(), (node.dataset.duration * 1000) - 500);
  },
  /**
   * @param {HTMLElement} damageNode
   */
  drawDamage (damageNode) {
    UI.drawActionFeedback(damageNode);
    UI.elements.battleOpponentSprite.classList.add('damaged');
    clearTimeout(UI.damageHighlightClickTimeout);
    UI.damageHighlightClickTimeout = setTimeout(() => UI.elements.battleOpponentSprite.classList.remove('damaged'), UI.damageHighlightClickDuration * 1000);
  },
  /**
   * @param {HTMLElement} damageNode
   */
  drawDamageMiss (damageNode) {
    UI.drawActionFeedback(damageNode);
  },
  /* Town */
  async drawTown () {
    const currentArea = Memory.state.currentArea;
    UI.elements.sceneTown.innerHTML = Memory.state.currentArea.map;
    for (const locationId of Object.keys(currentArea.locations)) {
      const location = currentArea.locations[locationId];
      UI.elements.sceneTown.querySelector(`[data-location="${locationId}"]`).addEventListener('click', () => {
        if (location.type === 'healingCenter') {
          UI.openHealingCenter(location);
        }
        else if (location.type === 'shop') {
          UI.openShop(location);
        }
      });
    }
  },
  openHealingCenter (healingCenter) {},
  async openShop (shop) {
    const popup = UI.createPopup();
    const template = document.createElement('div');
    for (const itemData of shop.items) {
      const item = await fetchItem(itemData.item_name);
      const itemNode = document.createElement('div');
      itemNode.innerHTML = ` `;
      itemNode.innerHTML += `${item.name}`;
      itemNode.innerHTML += `${itemData.price} ${DB.currencies.map[Memory.state.Settings.currency].symbol}`;
      template.appendChild(itemNode);
    }
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /* Menu */
  partySelectionMode: 'select',
  inventorySelectionMode: 'use',
  /**
   * @param {Monster[]} monsters
   *
   * @returns {HTMLElement}
   */
  createMonsterSelection (monsters) {
    const template = document.createElement('div');
    template.classList.add('monster-selection');
    for (const monster of monsters) {
      const monsterNode = UI.createMonsterSelectionMonster(monster);
      monsterNode.addEventListener('monster:selected', (event) => {
        template.dispatchEvent(new CustomEvent('monster:selected', {
          detail: {
            node: monsterNode,
            monster: event.detail.monster,
          },
        }));
      });
      template.appendChild(monsterNode);
    }
    return template;
  },
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createMonsterSelectionMonster (monster) {
    const template = UI.createPartyMonster(monster);
    template.addEventListener('click', () => {
      template.dispatchEvent(new CustomEvent('monster:selected', {
        detail: {
          monster: monster,
        },
      }));
    });
    return template;
  },
  drawStatus () {
    const currentArea = Memory.state.currentArea;
    UI.elements.status.querySelector('[data-template-slot="money"]').textContent =
      `${Memory.state.money.toFixed(DB.currencies.map[Memory.state.Settings.currency].decimals)}` +
      ' ' +
      `${DB.currencies.map[Memory.state.Settings.currency].symbol}`;
    UI.elements.status.querySelector('[data-template-slot="area"]').textContent = currentArea.name;
    UI.elements.status.querySelector('[data-template-slot="monsterProgress"]').textContent = `${currentArea.monsterProgress} / ${currentArea.requiredEncounters}`;
    UI.elements.status.querySelector('[data-template-slot="trainerProgress"]').textContent = `${currentArea.trainerProgress} / ${currentArea.trainers.length}`;
    const nextTrainerButton = UI.elements.nextTrainer;
    if (
      Memory.state.opponent.type === 'monster' &&
      currentArea.monsterProgress >= currentArea.requiredEncounters &&
      currentArea.trainerProgress < currentArea.trainers.length
    ) {
      nextTrainerButton.disabled = false;
    } else {
      nextTrainerButton.disabled = true;
    }
  },
  openAreaSelection () {
    const popup = UI.createPopup();
    const template = UI.createTemplate(Template.areaSelection);
    const currentArea = Memory.state.currentArea;
    for (const connectionSlug in currentArea.connections) {
      const connection = currentArea.connections[connectionSlug];
      const connectionNode = UI.createTemplate(Template.areaSelectionItem);
      connectionNode.querySelector('[data-template-slot="text"]').textContent = connection.name;
      let canGo = true;
      for (const condition of connection.conditions) {
        if (condition === 'encounters') {
          canGo = canGo && currentArea.monsterProgress >= currentArea.requiredEncounters;
        }
        else if (condition === 'trainers') {
          canGo = canGo && currentArea.trainerProgress >= currentArea.trainers.length;
        }
        else if (condition.startsWith('event.')) {
          canGo = false;
        }
      }
      if (!canGo) {
        connectionNode.setAttribute('disabled', true);
      }
      connectionNode.addEventListener('click', () => {
        if (canGo) {
          Game.goToArea(connectionSlug);
          popup.remove();
        } else {
          alert("Can\'t go there yet!");
        }
      });
      template.appendChild(connectionNode);
    }
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  openPartyMenu () {
    const popup = UI.createPopup();
    const party = UI.createTemplate(Template.party);
    party.id = 'party';
    for (const monsterIdx in Memory.state.player.monsters) {
      const monster = Memory.state.player.monsters[monsterIdx];
      const partyMonster = UI.createPartyMonster(monster);
      partyMonster.addEventListener('click', async (event) => {
        // bubble up to partyNode
        let target = event.target;
        while (target.parentNode.id !== party.id) {
          target = target.parentNode;
        }
        if (UI.partySelectionMode === 'select') {
          Game.setActivePlayerMonster(monster);
          popup.remove();
        }
        else if (UI.partySelectionMode === 'stats') {
          UI.openStatsMenu(monster);
        }
        else if (UI.partySelectionMode === 'techniques') {
          UI.openMovesetSelection(monster);
        }
        UI.events.dispatchEvent(new CustomEvent('party:monsterSelected', {
          detail: {
            monster: monster,
            mode: UI.partySelectionMode,
          },
        }));
      });
      party.querySelector('[data-template-slot="monsters"]').appendChild(partyMonster);
    }
    const selectionModesNode = party.querySelector('[data-template-slot="modes"]');
    const selectionModeNodes = selectionModesNode.querySelectorAll('[data-party-selection-mode]');
    selectionModeNodes.forEach((node) => {
      if (node.dataset.partySelectionMode === UI.partySelectionMode) {
        node.setAttribute('selected', true);
      }
      node.addEventListener('click', () => {
        selectionModesNode.querySelector(`[data-party-selection-mode="${UI.partySelectionMode}"]`).removeAttribute('selected');
        UI.partySelectionMode = node.dataset.partySelectionMode;
        node.setAttribute('selected', true);
      });
    });
    popup.querySelector('.popup').appendChild(party);
    UI.drawPopup(popup);
  },
  drawActiveBall () {
    if (Game.canCatchMonster()) {
      UI.elements.menuCatch.removeAttribute('disabled');
    } else {
      UI.elements.menuCatch.setAttribute('disabled', true);
    }
    if (Memory.state.activeBall) {
      UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/${Memory.state.activeBall.slug}.png`;
    } else {
      UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/tuxeball.png`;
    }
  },
  openInventoryMenu () {
    const popup = UI.createPopup();
    const inventory = UI.createTemplate(Template.inventory);
    inventory.id = 'inventory';
    const tabs = {
      heal: {
        heading: 'Heal',
        items: [],
      },
      stats: {
        heading: 'Stats',
        items: [],
      },
      balls: {
        heading: 'Balls',
        items: [],
      },
      techniques: {
        heading: 'Techniques',
        items: [],
      },
      other: {
        heading: 'Other',
        items: [],
      },
      keyItems: {
        heading: 'Key Items',
        items: [],
      },
    };
    for (const item of Memory.state.player.inventory) {
      const inventoryItemNode = UI.createInventoryItem(item);
      if (['potion', 'revive'].includes(item.category)) {
        tabs['heal'].items.push(inventoryItemNode);
      }
      else if (['stats'].includes(item.category)) {
        tabs['stats'].items.push(inventoryItemNode);
      }
      else if (['capture'].includes(item.category)) {
        tabs['balls'].items.push(inventoryItemNode);
      }
      else if (['technique'].includes(item.category)) {
        tabs['techniques'].items.push(inventoryItemNode);
      }
      else if (['KeyItem'].includes(item.type)) {
        tabs['keyItems'].items.push(inventoryItemNode);
      }
      else {
        tabs['other'].items.push(inventoryItemNode);
      }
    }
    const tabsNode = UI.createTabs(Object.values(tabs).map((tab) => {
      const content = document.createElement('div');
      content.classList.add('inventory__tab');
      for (const item of tab.items) {
        content.appendChild(item);
      }
      return {
        heading: document.createTextNode(tab.heading),
        content: content,
      };
    }));
    tabsNode.style.gridTemplateColumns = '1fr 1fr 1fr';
    inventory.querySelector('[data-template-slot="items"]').appendChild(tabsNode);
    const selectionModesNode = inventory.querySelector('[data-template-slot="modes"]');
    const selectionModeNodes = selectionModesNode.querySelectorAll('[data-selection-mode]');
    selectionModeNodes.forEach((node) => {
      if (node.dataset.selectionMode === UI.inventorySelectionMode) {
        node.setAttribute('selected', true);
      }
      node.addEventListener('click', () => {
        selectionModesNode.querySelector(`[data-selection-mode="${UI.inventorySelectionMode}"]`).removeAttribute('selected');
        UI.inventorySelectionMode = node.dataset.selectionMode;
        node.setAttribute('selected', true);
      });
    });
    popup.querySelector('.popup').appendChild(inventory);
    popup.classList.add('inventory__popup');
    UI.drawPopup(popup);
  },
  openJournalMenu () {
    const popup = UI.createPopup();
    const journal = UI.createTemplate(Template.menuJournal);
    journal.querySelector('[data-template-slot="save"]').addEventListener('click', () => {
      UI.openSaveDialog();
    });
    journal.querySelector('[data-template-slot="load"]').addEventListener('click', () => {
      UI.openLoadDialog();
    });
    popup.querySelector('.popup').appendChild(journal);
    UI.drawPopup(popup);
  },
  openSettingsMenu () {
    const popup = UI.createPopup();
    const template = UI.createTemplate(Template.menuSettings);
    /* Language */
    const languageSelectNode = template.querySelector('[data-template-slot="language"]');
    const languages = {
      'cs_CZ': 'Czech (Czech Republic)',
      'de_DE': 'German (Germany)',
      'en_US': 'English (United States)',
      'eo': 'Esperanto',
      'es_ES': 'Spanish (Spain)',
      'es_MX': 'Spanish (Mexico)',
      'fi': 'Finnish',
      'fr_FR': 'French (France)',
      'it_IT': 'Italian (Italy)',
      'ja': 'Japanese',
      'nb_NO': 'Norwegian Bokmål (Norway)',
      'pl': 'Polish',
      'pt_BR': 'Portuguese (Brazil)',
      'zh_CN': 'Chinese (China)',
    };
    for (const languageCode of Object.keys(languages)) {
      const languageName = languages[languageCode];
      const languageOptionNode = document.createElement('option');
      languageOptionNode.value = languageCode;
      languageOptionNode.textContent = languageName;
      if (languageCode === Memory.state.Settings.language) {
        languageOptionNode.selected = true;
      }
      languageSelectNode.appendChild(languageOptionNode);
    }
    languageSelectNode.addEventListener('change', async () => {
      const selected = [...languageSelectNode.children].find((node) => node.selected === true);
      Memory.state.Settings.language = selected.value;
      await fetchTranslation(Memory.state.Settings.language);
      UI.drawOpponentMonster();
      UI.drawActiveMonster();
      UI.drawActiveTechniques();
    });
    /* Currency */
    const currencySelectNode = template.querySelector('[data-template-slot="currency"]');
    const currencyCodeMap = Object.keys(DB.currencies.map).sort((a, b) => {
      const nameA = DB.currencies.map[a].name;
      const nameB = DB.currencies.map[b].name;
      return nameA > nameB ? 1 : -1;
    });
    for (const currencyCode of currencyCodeMap) {
      const currency = DB.currencies.map[currencyCode];
      const currencyOptionNode = document.createElement('option');
      currencyOptionNode.value = currencyCode,
      currencyOptionNode.textContent = `${currency.symbol} - ${currency.name}`;
      if (currencyCode === Memory.state.Settings.currency) {
        currencyOptionNode.selected = true;
      }
      currencySelectNode.appendChild(currencyOptionNode);
    }
    currencySelectNode.addEventListener('change', async () => {
      const selected = [...currencySelectNode.children].find((node) => node.selected === true);
      const previousCurrencyCode = Memory.state.Settings.currency;
      const newCurrencyCode = selected.value;
      Memory.state.Settings.currency = newCurrencyCode;
      // re-calculate money
      const previousCurrency = DB.currencies.map[previousCurrencyCode];
      const newCurrency = DB.currencies.map[newCurrencyCode];
      const baseRateMoney = Memory.state.money / previousCurrency.rate;
      const exchangedMoney = baseRateMoney * newCurrency.rate;
      Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals));
      UI.drawTown();
      UI.drawStatus();
    });
    template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated;
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /* Menu - Party */
  /**
   * @param {Monster}
   *
   * @returns {HTMLElement}
   */
  createPartyMonster (monster) {
    const partyMonster = UI.createTemplate(Template.partyMonster);
    partyMonster.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`;
    partyMonster.querySelector('[data-template-slot="name"]').textContent = monster.name;
    partyMonster.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML;
    partyMonster.querySelector('[data-template-slot="level"]').textContent = monster.level;
    partyMonster.querySelector('[data-template-slot="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`;
    return partyMonster;
  },
  /* Menu - Monster */
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createStatsMenu (monster) { // TODO
    const template = UI.createTemplate(Template.monsterStats);
    template.querySelector('[data-template-slot="name"]').textContent = monster.name;
    template.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML;
    template.querySelector('[data-template-slot="level"]').textContent = monster.level;
    template.querySelector('[data-template-slot="types"]').innerHTML = monster.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
    template.querySelector('[data-template-slot="stats.melee.name"]').textContent = translate(StatType.melee) || slugToName(StatType.melee);
    template.querySelector('[data-template-slot="stats.armour.name"]').textContent = translate(StatType.armour) || slugToName(StatType.armour);
    template.querySelector('[data-template-slot="stats.ranged.name"]').textContent = translate(StatType.ranged) || slugToName(StatType.ranged);
    template.querySelector('[data-template-slot="stats.dodge.name"]').textContent = translate(StatType.dodge) || slugToName(StatType.dodge);
    template.querySelector('[data-template-slot="stats.speed.name"]').textContent = translate(StatType.speed) || slugToName(StatType.speed);
    template.querySelector('[data-template-slot="stats.melee.value"]').textContent = monster.stats.melee;
    template.querySelector('[data-template-slot="stats.armour.value"]').textContent = monster.stats.armour;
    template.querySelector('[data-template-slot="stats.ranged.value"]').textContent = monster.stats.ranged;
    template.querySelector('[data-template-slot="stats.dodge.value"]').textContent = monster.stats.dodge;
    template.querySelector('[data-template-slot="stats.speed.value"]').textContent = monster.stats.speed;
    template.querySelector('[data-template-slot="techniques"]').addEventListener('click', () => UI.openMovesetSelection(monster));
    return template;
  },
  /**
   * @param {Monster} monster
   *
   * @returns {Promise}
   */
  async createMovesetSelection (monster) {
    const movesetListNode = UI.createTemplate(Template.movesetList);
    for (const move of monster.moveset) {
      const technique = await fetchTechnique(move.technique);
      const movesetItemNode = UI.createTemplate(Template.movesetItem);
      movesetItemNode.title = technique.description;
      movesetItemNode.querySelector('[data-template-slot="name"]').textContent = technique.name;
      movesetItemNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
      movesetItemNode.querySelector('[data-template-slot="power"]').textContent = technique.power;
      movesetItemNode.querySelector('[data-template-slot="level"]').textContent = move.level_learned;
      // disabled?
      if (monster.level < move.level_learned) {
        movesetItemNode.setAttribute('disabled', true);
      }
      // selected?
      if (monster.activeTechniques.find((item) => item.slug == technique.slug)) {
        movesetItemNode.toggleAttribute('selected');
      }
      // clicked
      movesetItemNode.addEventListener('click', () => {
        if (movesetItemNode.getAttribute('disabled')) {
          return false;
        }
        if (monster.activeTechniques.length === 4 && !movesetItemNode.hasAttribute('selected')) {
          return;
        }
        // un/select
        movesetItemNode.toggleAttribute('selected');
        const isSelected = movesetItemNode.hasAttribute('selected');
        if (isSelected) {
          monster.activeTechniques.push(technique);
        } else {
          const idxTechniqueToRemove = monster.activeTechniques.findIndex((item) => item.slug == technique.slug);
          if (idxTechniqueToRemove > -1) {
            monster.activeTechniques.splice(idxTechniqueToRemove, 1);
          }
        }
        const event = new CustomEvent('movesetSelection:moveSelected', {
          detail: {
            isSelected: movesetItemNode.hasAttribute('selected'),
            technique: technique,
          },
        });
        UI.events.dispatchEvent(event);
      });
      movesetListNode.appendChild(movesetItemNode);
    }
    return movesetListNode;
  },
  /**
   * @param {Monster} monster
   */
  openStatsMenu (monster) {
    const popup = UI.createPopup();
    const statusMenu = UI.createStatsMenu(monster);
    popup.querySelector('.popup').appendChild(statusMenu);
    UI.drawPopup(popup);
  },
  /**
   * @param {Monster} monster
   */
  async openMovesetSelection (monster) {
    const popup = UI.createPopup();
    const movesetSelection = await UI.createMovesetSelection(monster);
    popup.querySelector('.popup').appendChild(movesetSelection);
    popup.addEventListener('close', () => UI.drawActiveTechniques());
    UI.drawPopup(popup);
  },
  /* Menu - Inventory */
  /**
   * @param {InventoryItem} item
   *
   * @returns {HTMLElement}
   */
  createInventoryItem (item) {
    const inventoryItemNode = UI.createTemplate(Template.inventoryItem);
    inventoryItemNode.title = item.description;
    inventoryItemNode.dataset.inventoryItem = item.slug;
    inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/${item.sprite}`;
    inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name;
    inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity;
    inventoryItemNode.addEventListener('click', async () => {
      if (UI.inventorySelectionMode === 'use') {
        if (item.category === 'potion') {
          UI.openItemMonsterSelection(item);
        }
        else if (item.category === 'capture') {
          Game.useItem(item);
        }
      }
      else if (UI.inventorySelectionMode === 'info') {
        UI.openItemInfo(item);
      }
    });
    return inventoryItemNode;
  },
  /**
   * @param {InventoryItem} item
   */
  redrawInventoryItem (item) {
    const itemNode = document.querySelector(`#inventory *[data-inventory-item="${item.slug}"]`);
    if (item.quantity === 0) {
      itemNode.remove();
    }
    const newNode = UI.createInventoryItem(item);
    itemNode.replaceWith(newNode);
  },
  /**
   * @param {InventoryItem} item
   * @param {Monster[]} monsters
   *
   * @returns {HTMLElement}
   */
  createItemMonsterSelection (item, monsters) {
    const template = UI.createMonsterSelection(
      monsters.filter((monster) => Game.canUseItem(item, monster))
    );
    const onMonsterSelectd = async (event) => {
      const monster = event.detail.monster;
      const monsterNode = event.detail.node || event.target;
      await Game.useItem(item, monster);
      if (item.quantity === 0) {
        Game.removeItemFromInventory(Memory.state.player.inventory, item);
        template.dispatchEvent(new Event('item:isExhausted'));
      }
      else {
        const canStillUseItem = Game.canUseItem(item, monster);
        if (canStillUseItem) {
          const replacingMonsterNode = UI.createMonsterSelectionMonster(monster);
          replacingMonsterNode.addEventListener('monster:selected', onMonsterSelectd);
          monsterNode.replaceWith(replacingMonsterNode);
        } else {
          monsterNode.remove();
          template.dispatchEvent(new Event('monster:lostCondition'));
        }
      }
      UI.redrawInventoryItem(item);
    };
    template.addEventListener('monster:selected', onMonsterSelectd);
    return template;
  },
  /**
   * @param {InventoryItem} item
   */
  openItemMonsterSelection (item) {
    const popup = UI.createPopup();
    const template = UI.createItemMonsterSelection(item, Memory.state.player.monsters);
    template.classList.add('inventory__monster-selection');
    if (template.children.length === 0) {
      alert('No applicable monsters.');
      return;
    }
    template.addEventListener('item:isExhausted', () => popup.remove());
    template.addEventListener('monster:lostCondition', () => popup.remove());
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /**
   * @param {InventoryItem} item
   */
  openItemInfo (item) {
    const popup = UI.createPopup();
    const template = document.createElement('div');
    template.textContent = item.conditions + ' -- ' + item.effects + ' -- ' + item.description;
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /* Menu - Journal */
  openSaveDialog () {
    const popup = UI.createPopup();
    const dialog = UI.createTemplate(Template.dialogSave);
    const saveData = Memory.save();
    dialog.querySelector('[data-template-slot="saveData"]').value = saveData;
    dialog.querySelector('[data-template-slot="saveClipboard"]').addEventListener('click', async () => {
      if (navigator.clipboard) {
        await navigator.clipboard.writeText(saveData);
        alert('Saved to clipboard!');
      } else {
        alert('ERROR: Browser can\'t copy to clipboard! You have to do it manually.');
      }
    });
    popup.querySelector('.popup').appendChild(dialog);
    UI.drawPopup(popup);
  },
  openLoadDialog () {
    const popup = UI.createPopup();
    const dialog = UI.createTemplate(Template.dialogLoad);
    dialog.querySelector('[data-template-slot="load"]').addEventListener('click', () => {
      Memory.load(
        dialog.querySelector('[data-template-slot="saveData"]').value.trim()
      );
      document.querySelectorAll('.popup__overlay').forEach((element) => element.remove())
    });
    popup.querySelector('.popup').appendChild(dialog);
    UI.drawPopup(popup);
  },
};
// UI element click bindings
UI.elements.changeArea.addEventListener('click', UI.openAreaSelection);
UI.elements.menuParty.addEventListener('click', UI.openPartyMenu);
UI.elements.menuInventory.addEventListener('click', UI.openInventoryMenu);
UI.elements.menuLog.addEventListener('click', UI.toggleLog);
UI.elements.menuJournal.addEventListener('click', UI.openJournalMenu);
UI.elements.menuSettings.addEventListener('click', UI.openSettingsMenu);
`;
      itemNode.innerHTML += `${item.name}`;
      itemNode.innerHTML += `${itemData.price} ${DB.currencies.map[Memory.state.Settings.currency].symbol}`;
      template.appendChild(itemNode);
    }
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /* Menu */
  partySelectionMode: 'select',
  inventorySelectionMode: 'use',
  /**
   * @param {Monster[]} monsters
   *
   * @returns {HTMLElement}
   */
  createMonsterSelection (monsters) {
    const template = document.createElement('div');
    template.classList.add('monster-selection');
    for (const monster of monsters) {
      const monsterNode = UI.createMonsterSelectionMonster(monster);
      monsterNode.addEventListener('monster:selected', (event) => {
        template.dispatchEvent(new CustomEvent('monster:selected', {
          detail: {
            node: monsterNode,
            monster: event.detail.monster,
          },
        }));
      });
      template.appendChild(monsterNode);
    }
    return template;
  },
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createMonsterSelectionMonster (monster) {
    const template = UI.createPartyMonster(monster);
    template.addEventListener('click', () => {
      template.dispatchEvent(new CustomEvent('monster:selected', {
        detail: {
          monster: monster,
        },
      }));
    });
    return template;
  },
  drawStatus () {
    const currentArea = Memory.state.currentArea;
    UI.elements.status.querySelector('[data-template-slot="money"]').textContent =
      `${Memory.state.money.toFixed(DB.currencies.map[Memory.state.Settings.currency].decimals)}` +
      ' ' +
      `${DB.currencies.map[Memory.state.Settings.currency].symbol}`;
    UI.elements.status.querySelector('[data-template-slot="area"]').textContent = currentArea.name;
    UI.elements.status.querySelector('[data-template-slot="monsterProgress"]').textContent = `${currentArea.monsterProgress} / ${currentArea.requiredEncounters}`;
    UI.elements.status.querySelector('[data-template-slot="trainerProgress"]').textContent = `${currentArea.trainerProgress} / ${currentArea.trainers.length}`;
    const nextTrainerButton = UI.elements.nextTrainer;
    if (
      Memory.state.opponent.type === 'monster' &&
      currentArea.monsterProgress >= currentArea.requiredEncounters &&
      currentArea.trainerProgress < currentArea.trainers.length
    ) {
      nextTrainerButton.disabled = false;
    } else {
      nextTrainerButton.disabled = true;
    }
  },
  openAreaSelection () {
    const popup = UI.createPopup();
    const template = UI.createTemplate(Template.areaSelection);
    const currentArea = Memory.state.currentArea;
    for (const connectionSlug in currentArea.connections) {
      const connection = currentArea.connections[connectionSlug];
      const connectionNode = UI.createTemplate(Template.areaSelectionItem);
      connectionNode.querySelector('[data-template-slot="text"]').textContent = connection.name;
      let canGo = true;
      for (const condition of connection.conditions) {
        if (condition === 'encounters') {
          canGo = canGo && currentArea.monsterProgress >= currentArea.requiredEncounters;
        }
        else if (condition === 'trainers') {
          canGo = canGo && currentArea.trainerProgress >= currentArea.trainers.length;
        }
        else if (condition.startsWith('event.')) {
          canGo = false;
        }
      }
      if (!canGo) {
        connectionNode.setAttribute('disabled', true);
      }
      connectionNode.addEventListener('click', () => {
        if (canGo) {
          Game.goToArea(connectionSlug);
          popup.remove();
        } else {
          alert("Can\'t go there yet!");
        }
      });
      template.appendChild(connectionNode);
    }
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  openPartyMenu () {
    const popup = UI.createPopup();
    const party = UI.createTemplate(Template.party);
    party.id = 'party';
    for (const monsterIdx in Memory.state.player.monsters) {
      const monster = Memory.state.player.monsters[monsterIdx];
      const partyMonster = UI.createPartyMonster(monster);
      partyMonster.addEventListener('click', async (event) => {
        // bubble up to partyNode
        let target = event.target;
        while (target.parentNode.id !== party.id) {
          target = target.parentNode;
        }
        if (UI.partySelectionMode === 'select') {
          Game.setActivePlayerMonster(monster);
          popup.remove();
        }
        else if (UI.partySelectionMode === 'stats') {
          UI.openStatsMenu(monster);
        }
        else if (UI.partySelectionMode === 'techniques') {
          UI.openMovesetSelection(monster);
        }
        UI.events.dispatchEvent(new CustomEvent('party:monsterSelected', {
          detail: {
            monster: monster,
            mode: UI.partySelectionMode,
          },
        }));
      });
      party.querySelector('[data-template-slot="monsters"]').appendChild(partyMonster);
    }
    const selectionModesNode = party.querySelector('[data-template-slot="modes"]');
    const selectionModeNodes = selectionModesNode.querySelectorAll('[data-party-selection-mode]');
    selectionModeNodes.forEach((node) => {
      if (node.dataset.partySelectionMode === UI.partySelectionMode) {
        node.setAttribute('selected', true);
      }
      node.addEventListener('click', () => {
        selectionModesNode.querySelector(`[data-party-selection-mode="${UI.partySelectionMode}"]`).removeAttribute('selected');
        UI.partySelectionMode = node.dataset.partySelectionMode;
        node.setAttribute('selected', true);
      });
    });
    popup.querySelector('.popup').appendChild(party);
    UI.drawPopup(popup);
  },
  drawActiveBall () {
    if (Game.canCatchMonster()) {
      UI.elements.menuCatch.removeAttribute('disabled');
    } else {
      UI.elements.menuCatch.setAttribute('disabled', true);
    }
    if (Memory.state.activeBall) {
      UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/${Memory.state.activeBall.slug}.png`;
    } else {
      UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/tuxeball.png`;
    }
  },
  openInventoryMenu () {
    const popup = UI.createPopup();
    const inventory = UI.createTemplate(Template.inventory);
    inventory.id = 'inventory';
    const tabs = {
      heal: {
        heading: 'Heal',
        items: [],
      },
      stats: {
        heading: 'Stats',
        items: [],
      },
      balls: {
        heading: 'Balls',
        items: [],
      },
      techniques: {
        heading: 'Techniques',
        items: [],
      },
      other: {
        heading: 'Other',
        items: [],
      },
      keyItems: {
        heading: 'Key Items',
        items: [],
      },
    };
    for (const item of Memory.state.player.inventory) {
      const inventoryItemNode = UI.createInventoryItem(item);
      if (['potion', 'revive'].includes(item.category)) {
        tabs['heal'].items.push(inventoryItemNode);
      }
      else if (['stats'].includes(item.category)) {
        tabs['stats'].items.push(inventoryItemNode);
      }
      else if (['capture'].includes(item.category)) {
        tabs['balls'].items.push(inventoryItemNode);
      }
      else if (['technique'].includes(item.category)) {
        tabs['techniques'].items.push(inventoryItemNode);
      }
      else if (['KeyItem'].includes(item.type)) {
        tabs['keyItems'].items.push(inventoryItemNode);
      }
      else {
        tabs['other'].items.push(inventoryItemNode);
      }
    }
    const tabsNode = UI.createTabs(Object.values(tabs).map((tab) => {
      const content = document.createElement('div');
      content.classList.add('inventory__tab');
      for (const item of tab.items) {
        content.appendChild(item);
      }
      return {
        heading: document.createTextNode(tab.heading),
        content: content,
      };
    }));
    tabsNode.style.gridTemplateColumns = '1fr 1fr 1fr';
    inventory.querySelector('[data-template-slot="items"]').appendChild(tabsNode);
    const selectionModesNode = inventory.querySelector('[data-template-slot="modes"]');
    const selectionModeNodes = selectionModesNode.querySelectorAll('[data-selection-mode]');
    selectionModeNodes.forEach((node) => {
      if (node.dataset.selectionMode === UI.inventorySelectionMode) {
        node.setAttribute('selected', true);
      }
      node.addEventListener('click', () => {
        selectionModesNode.querySelector(`[data-selection-mode="${UI.inventorySelectionMode}"]`).removeAttribute('selected');
        UI.inventorySelectionMode = node.dataset.selectionMode;
        node.setAttribute('selected', true);
      });
    });
    popup.querySelector('.popup').appendChild(inventory);
    popup.classList.add('inventory__popup');
    UI.drawPopup(popup);
  },
  openJournalMenu () {
    const popup = UI.createPopup();
    const journal = UI.createTemplate(Template.menuJournal);
    journal.querySelector('[data-template-slot="save"]').addEventListener('click', () => {
      UI.openSaveDialog();
    });
    journal.querySelector('[data-template-slot="load"]').addEventListener('click', () => {
      UI.openLoadDialog();
    });
    popup.querySelector('.popup').appendChild(journal);
    UI.drawPopup(popup);
  },
  openSettingsMenu () {
    const popup = UI.createPopup();
    const template = UI.createTemplate(Template.menuSettings);
    /* Language */
    const languageSelectNode = template.querySelector('[data-template-slot="language"]');
    const languages = {
      'cs_CZ': 'Czech (Czech Republic)',
      'de_DE': 'German (Germany)',
      'en_US': 'English (United States)',
      'eo': 'Esperanto',
      'es_ES': 'Spanish (Spain)',
      'es_MX': 'Spanish (Mexico)',
      'fi': 'Finnish',
      'fr_FR': 'French (France)',
      'it_IT': 'Italian (Italy)',
      'ja': 'Japanese',
      'nb_NO': 'Norwegian Bokmål (Norway)',
      'pl': 'Polish',
      'pt_BR': 'Portuguese (Brazil)',
      'zh_CN': 'Chinese (China)',
    };
    for (const languageCode of Object.keys(languages)) {
      const languageName = languages[languageCode];
      const languageOptionNode = document.createElement('option');
      languageOptionNode.value = languageCode;
      languageOptionNode.textContent = languageName;
      if (languageCode === Memory.state.Settings.language) {
        languageOptionNode.selected = true;
      }
      languageSelectNode.appendChild(languageOptionNode);
    }
    languageSelectNode.addEventListener('change', async () => {
      const selected = [...languageSelectNode.children].find((node) => node.selected === true);
      Memory.state.Settings.language = selected.value;
      await fetchTranslation(Memory.state.Settings.language);
      UI.drawOpponentMonster();
      UI.drawActiveMonster();
      UI.drawActiveTechniques();
    });
    /* Currency */
    const currencySelectNode = template.querySelector('[data-template-slot="currency"]');
    const currencyCodeMap = Object.keys(DB.currencies.map).sort((a, b) => {
      const nameA = DB.currencies.map[a].name;
      const nameB = DB.currencies.map[b].name;
      return nameA > nameB ? 1 : -1;
    });
    for (const currencyCode of currencyCodeMap) {
      const currency = DB.currencies.map[currencyCode];
      const currencyOptionNode = document.createElement('option');
      currencyOptionNode.value = currencyCode,
      currencyOptionNode.textContent = `${currency.symbol} - ${currency.name}`;
      if (currencyCode === Memory.state.Settings.currency) {
        currencyOptionNode.selected = true;
      }
      currencySelectNode.appendChild(currencyOptionNode);
    }
    currencySelectNode.addEventListener('change', async () => {
      const selected = [...currencySelectNode.children].find((node) => node.selected === true);
      const previousCurrencyCode = Memory.state.Settings.currency;
      const newCurrencyCode = selected.value;
      Memory.state.Settings.currency = newCurrencyCode;
      // re-calculate money
      const previousCurrency = DB.currencies.map[previousCurrencyCode];
      const newCurrency = DB.currencies.map[newCurrencyCode];
      const baseRateMoney = Memory.state.money / previousCurrency.rate;
      const exchangedMoney = baseRateMoney * newCurrency.rate;
      Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals));
      UI.drawTown();
      UI.drawStatus();
    });
    template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated;
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /* Menu - Party */
  /**
   * @param {Monster}
   *
   * @returns {HTMLElement}
   */
  createPartyMonster (monster) {
    const partyMonster = UI.createTemplate(Template.partyMonster);
    partyMonster.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`;
    partyMonster.querySelector('[data-template-slot="name"]').textContent = monster.name;
    partyMonster.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML;
    partyMonster.querySelector('[data-template-slot="level"]').textContent = monster.level;
    partyMonster.querySelector('[data-template-slot="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`;
    return partyMonster;
  },
  /* Menu - Monster */
  /**
   * @param {Monster} monster
   *
   * @returns {HTMLElement}
   */
  createStatsMenu (monster) { // TODO
    const template = UI.createTemplate(Template.monsterStats);
    template.querySelector('[data-template-slot="name"]').textContent = monster.name;
    template.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML;
    template.querySelector('[data-template-slot="level"]').textContent = monster.level;
    template.querySelector('[data-template-slot="types"]').innerHTML = monster.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
    template.querySelector('[data-template-slot="stats.melee.name"]').textContent = translate(StatType.melee) || slugToName(StatType.melee);
    template.querySelector('[data-template-slot="stats.armour.name"]').textContent = translate(StatType.armour) || slugToName(StatType.armour);
    template.querySelector('[data-template-slot="stats.ranged.name"]').textContent = translate(StatType.ranged) || slugToName(StatType.ranged);
    template.querySelector('[data-template-slot="stats.dodge.name"]').textContent = translate(StatType.dodge) || slugToName(StatType.dodge);
    template.querySelector('[data-template-slot="stats.speed.name"]').textContent = translate(StatType.speed) || slugToName(StatType.speed);
    template.querySelector('[data-template-slot="stats.melee.value"]').textContent = monster.stats.melee;
    template.querySelector('[data-template-slot="stats.armour.value"]').textContent = monster.stats.armour;
    template.querySelector('[data-template-slot="stats.ranged.value"]').textContent = monster.stats.ranged;
    template.querySelector('[data-template-slot="stats.dodge.value"]').textContent = monster.stats.dodge;
    template.querySelector('[data-template-slot="stats.speed.value"]').textContent = monster.stats.speed;
    template.querySelector('[data-template-slot="techniques"]').addEventListener('click', () => UI.openMovesetSelection(monster));
    return template;
  },
  /**
   * @param {Monster} monster
   *
   * @returns {Promise}
   */
  async createMovesetSelection (monster) {
    const movesetListNode = UI.createTemplate(Template.movesetList);
    for (const move of monster.moveset) {
      const technique = await fetchTechnique(move.technique);
      const movesetItemNode = UI.createTemplate(Template.movesetItem);
      movesetItemNode.title = technique.description;
      movesetItemNode.querySelector('[data-template-slot="name"]').textContent = technique.name;
      movesetItemNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
      movesetItemNode.querySelector('[data-template-slot="power"]').textContent = technique.power;
      movesetItemNode.querySelector('[data-template-slot="level"]').textContent = move.level_learned;
      // disabled?
      if (monster.level < move.level_learned) {
        movesetItemNode.setAttribute('disabled', true);
      }
      // selected?
      if (monster.activeTechniques.find((item) => item.slug == technique.slug)) {
        movesetItemNode.toggleAttribute('selected');
      }
      // clicked
      movesetItemNode.addEventListener('click', () => {
        if (movesetItemNode.getAttribute('disabled')) {
          return false;
        }
        if (monster.activeTechniques.length === 4 && !movesetItemNode.hasAttribute('selected')) {
          return;
        }
        // un/select
        movesetItemNode.toggleAttribute('selected');
        const isSelected = movesetItemNode.hasAttribute('selected');
        if (isSelected) {
          monster.activeTechniques.push(technique);
        } else {
          const idxTechniqueToRemove = monster.activeTechniques.findIndex((item) => item.slug == technique.slug);
          if (idxTechniqueToRemove > -1) {
            monster.activeTechniques.splice(idxTechniqueToRemove, 1);
          }
        }
        const event = new CustomEvent('movesetSelection:moveSelected', {
          detail: {
            isSelected: movesetItemNode.hasAttribute('selected'),
            technique: technique,
          },
        });
        UI.events.dispatchEvent(event);
      });
      movesetListNode.appendChild(movesetItemNode);
    }
    return movesetListNode;
  },
  /**
   * @param {Monster} monster
   */
  openStatsMenu (monster) {
    const popup = UI.createPopup();
    const statusMenu = UI.createStatsMenu(monster);
    popup.querySelector('.popup').appendChild(statusMenu);
    UI.drawPopup(popup);
  },
  /**
   * @param {Monster} monster
   */
  async openMovesetSelection (monster) {
    const popup = UI.createPopup();
    const movesetSelection = await UI.createMovesetSelection(monster);
    popup.querySelector('.popup').appendChild(movesetSelection);
    popup.addEventListener('close', () => UI.drawActiveTechniques());
    UI.drawPopup(popup);
  },
  /* Menu - Inventory */
  /**
   * @param {InventoryItem} item
   *
   * @returns {HTMLElement}
   */
  createInventoryItem (item) {
    const inventoryItemNode = UI.createTemplate(Template.inventoryItem);
    inventoryItemNode.title = item.description;
    inventoryItemNode.dataset.inventoryItem = item.slug;
    inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/${item.sprite}`;
    inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name;
    inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity;
    inventoryItemNode.addEventListener('click', async () => {
      if (UI.inventorySelectionMode === 'use') {
        if (item.category === 'potion') {
          UI.openItemMonsterSelection(item);
        }
        else if (item.category === 'capture') {
          Game.useItem(item);
        }
      }
      else if (UI.inventorySelectionMode === 'info') {
        UI.openItemInfo(item);
      }
    });
    return inventoryItemNode;
  },
  /**
   * @param {InventoryItem} item
   */
  redrawInventoryItem (item) {
    const itemNode = document.querySelector(`#inventory *[data-inventory-item="${item.slug}"]`);
    if (item.quantity === 0) {
      itemNode.remove();
    }
    const newNode = UI.createInventoryItem(item);
    itemNode.replaceWith(newNode);
  },
  /**
   * @param {InventoryItem} item
   * @param {Monster[]} monsters
   *
   * @returns {HTMLElement}
   */
  createItemMonsterSelection (item, monsters) {
    const template = UI.createMonsterSelection(
      monsters.filter((monster) => Game.canUseItem(item, monster))
    );
    const onMonsterSelectd = async (event) => {
      const monster = event.detail.monster;
      const monsterNode = event.detail.node || event.target;
      await Game.useItem(item, monster);
      if (item.quantity === 0) {
        Game.removeItemFromInventory(Memory.state.player.inventory, item);
        template.dispatchEvent(new Event('item:isExhausted'));
      }
      else {
        const canStillUseItem = Game.canUseItem(item, monster);
        if (canStillUseItem) {
          const replacingMonsterNode = UI.createMonsterSelectionMonster(monster);
          replacingMonsterNode.addEventListener('monster:selected', onMonsterSelectd);
          monsterNode.replaceWith(replacingMonsterNode);
        } else {
          monsterNode.remove();
          template.dispatchEvent(new Event('monster:lostCondition'));
        }
      }
      UI.redrawInventoryItem(item);
    };
    template.addEventListener('monster:selected', onMonsterSelectd);
    return template;
  },
  /**
   * @param {InventoryItem} item
   */
  openItemMonsterSelection (item) {
    const popup = UI.createPopup();
    const template = UI.createItemMonsterSelection(item, Memory.state.player.monsters);
    template.classList.add('inventory__monster-selection');
    if (template.children.length === 0) {
      alert('No applicable monsters.');
      return;
    }
    template.addEventListener('item:isExhausted', () => popup.remove());
    template.addEventListener('monster:lostCondition', () => popup.remove());
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /**
   * @param {InventoryItem} item
   */
  openItemInfo (item) {
    const popup = UI.createPopup();
    const template = document.createElement('div');
    template.textContent = item.conditions + ' -- ' + item.effects + ' -- ' + item.description;
    popup.querySelector('.popup').appendChild(template);
    UI.drawPopup(popup);
  },
  /* Menu - Journal */
  openSaveDialog () {
    const popup = UI.createPopup();
    const dialog = UI.createTemplate(Template.dialogSave);
    const saveData = Memory.save();
    dialog.querySelector('[data-template-slot="saveData"]').value = saveData;
    dialog.querySelector('[data-template-slot="saveClipboard"]').addEventListener('click', async () => {
      if (navigator.clipboard) {
        await navigator.clipboard.writeText(saveData);
        alert('Saved to clipboard!');
      } else {
        alert('ERROR: Browser can\'t copy to clipboard! You have to do it manually.');
      }
    });
    popup.querySelector('.popup').appendChild(dialog);
    UI.drawPopup(popup);
  },
  openLoadDialog () {
    const popup = UI.createPopup();
    const dialog = UI.createTemplate(Template.dialogLoad);
    dialog.querySelector('[data-template-slot="load"]').addEventListener('click', () => {
      Memory.load(
        dialog.querySelector('[data-template-slot="saveData"]').value.trim()
      );
      document.querySelectorAll('.popup__overlay').forEach((element) => element.remove())
    });
    popup.querySelector('.popup').appendChild(dialog);
    UI.drawPopup(popup);
  },
};
// UI element click bindings
UI.elements.changeArea.addEventListener('click', UI.openAreaSelection);
UI.elements.menuParty.addEventListener('click', UI.openPartyMenu);
UI.elements.menuInventory.addEventListener('click', UI.openInventoryMenu);
UI.elements.menuLog.addEventListener('click', UI.toggleLog);
UI.elements.menuJournal.addEventListener('click', UI.openJournalMenu);
UI.elements.menuSettings.addEventListener('click', UI.openSettingsMenu);