import { MODULE_ID } from './constants.js';

export function registerSidebarTab() {
  CONFIG.ui.sidebar.TABS.baileywiki = {
    icon: 'icon-bw-logo',
    tooltip: 'BaileyWiki',
    gmOnly: true,
  };
  ui.baileywiki = new BaileyWikiTab();
}

class BaileyWikiTab extends foundry.applications.api.HandlebarsApplicationMixin(
  foundry.applications.sidebar.AbstractSidebarTab,
) {
  /** @override */
  static tabName = 'baileywiki';

  /** @override */
  static DEFAULT_OPTIONS = {
    window: {
      title: 'BaileyWiki',
    },
    classes: ['bw-tab', 'flexcol'],
    actions: {
      clickMacro: BaileyWikiTab._onClickMacro,
      markRead: BaileyWikiTab._onMarkRead,
      markDisplay: BaileyWikiTab._onMarkDisplay,
      openTutorial: BaileyWikiTab._onOpenTutorial,
      openBrowser: BaileyWikiTab._onOpenBrowser,
      downloadScene: BaileyWikiTab._onDownloadScene,
      authenticate: BaileyWikiTab._onAuthenticate,
    },
  };

  /** @override */
  static PARTS = {
    info: {
      template: `modules/${MODULE_ID}/templates/tab/info.hbs`,
    },
    browsers: {
      template: `modules/${MODULE_ID}/templates/tab/browsers.hbs`,
    },
    utilities: {
      template: `modules/${MODULE_ID}/templates/tab/utilities.hbs`,
      scrollable: ['.macros'],
    },
    tutorials: {
      template: `modules/${MODULE_ID}/templates/tab/tutorials.hbs`,
      scrollable: ['.tutorials'],
    },
  };

  static _folderPartial = `modules/${MODULE_ID}/templates/tab/macro-folder-partial.hbs`;

  /** @override */
  _configureRenderParts(options) {
    const parts = super._configureRenderParts(options);
    parts.utilities.templates ??= [];
    parts.utilities.templates.push(this.constructor._folderPartial);
    return parts;
  }

  /** @override */
  async _preparePartContext(partId, context, options) {
    context.partId = partId;
    switch (partId) {
      case 'info':
        await this._prepareInfoContext(context, options);
        break;
      case 'utilities':
        await this._prepareUtilitiesContext(context, options);
        break;
      case 'browsers':
        await this._prepareBrowsersContext(context, options);
        break;
      case 'tutorials':
        await this._prepareTutorialsContext(context, options);
        break;
    }
    return context;
  }

  async _prepareInfoContext(context, options) {
    const modules = [];
    const dependenciesToDisplay = ['multi-token-edit', 'tokenmagic'];
    game.modules.forEach((m) => {
      if (m.active && (m.id.startsWith('baileywiki') || dependenciesToDisplay.includes(m.id)))
        modules.push({ label: m.title.replace('Baileywiki', '').trim(), version: m.version });
    });
    context.modules = modules;

    try {
      if (game.modules.get('baileywiki-premium-content')?.active) {
        context.displayAuthenticate = !game.modules.get('baileywiki-premium-content').api.IsAuthenticated();
        context.displaySceneDownload =
          !context.displayAuthenticate && canvas.scene?.getFlag('baileywiki-premium-content', 'failedDownload');
      }
    } catch (e) {
      console.error(e);
    }
    return context;
  }

  async _prepareBrowsersContext(context, options) {
    const browsersFolder = fromUuidSync(BW.CONFIG.tab.browserFolderUuid);

    // Assumes that the browser folder contains other folders with names matching
    // module ids, we only include entries of folders whose modules are active
    context.browsers = [];
    browsersFolder?.children.forEach((ch) => {
      if (game.modules.get(ch.folder.name)?.active) {
        ch.entries.forEach((entry) => {
          context.browsers.push({ label: entry.name, uuid: entry.uuid });
        });
      }
    });

    if (context.browsers.length) {
      if (context.browsers.length % 2 === 1) {
        context.browsers.push(null);
      }
      // Group into pairs
      context.browsers = context.browsers.reduce((acc, val, i) => {
        if (i % 2 === 0) acc.push([val]);
        else acc[acc.length - 1].push(val);
        return acc;
      }, []);
    } else delete context.browsers;
  }

  async _prepareUtilitiesContext(context, options) {
    context.folderPartial = this.constructor._folderPartial;

    const utilityFolder = fromUuidSync(BW.CONFIG.tab.macroFolderUuid);
    if (utilityFolder) {
      const topFolder = this._parseMacroFolder(utilityFolder);
      context.folders = topFolder.children;
      context.macros = topFolder.macros;
    } else {
      context.folders = [];
      context.macros = [];
    }
  }

  /**
   * Prepares a folder for rendering
   * @param {Folder} folder
   * @returns
   */
  _parseMacroFolder(folder) {
    return {
      name: folder.name,
      children: folder.children.map((ch) => this._parseMacroFolder(ch.folder)),
      macros: folder.contents,
    };
  }

  async _prepareTutorialsContext(context, options) {
    const readTutorials = game.settings.get(MODULE_ID, 'read-tutorials');
    const displayReadTutorials = game.settings.get(MODULE_ID, 'read-tutorials-display-category');

    const tutorialFolder = fromUuidSync(BW.CONFIG.tab.tutorialFolderUuid);
    if (!tutorialFolder) {
      context.categories = [];
      return;
    }

    const categories = tutorialFolder.children.map((ch) => {
      const display = Boolean(displayReadTutorials[ch.folder.id]);
      return {
        id: ch.folder.id,
        name: ch.folder.name,
        display,
        tutorials: ch.entries.map((entry) => {
          const read = Boolean(readTutorials[entry.uuid]);
          return { name: entry.name, read, hidden: read && !display, uuid: entry.uuid };
        }),
      };
    });

    context.categories = categories;
  }

  /** @override */
  _attachPartListeners(partId, element, options) {
    super._attachPartListeners(partId, element, options);
    switch (partId) {
      case 'utilities':
        for (const macro of element.querySelectorAll('.macro .name')) {
          macro.addEventListener('mouseover', this._onMacroHover.bind(this));
        }
        break;
    }
  }

  /**
   * Handle macro hover, dynamically setting the hover tooltip to the first comment within the macro
   * @param {MouseEvent} event
   */
  async _onMacroHover(event) {
    const element = event.target.closest('.macro');
    if (!element.dataset.tooltip) {
      const macro = await fromUuid(element.dataset.uuid);

      const firstLine = macro.command.slice(0, macro.command.indexOf('\n'));
      if (firstLine.startsWith('//')) element.setAttribute('data-tooltip', firstLine.substring(2));
      else element.setAttribute('data-tooltip', macro.name);

      element.dispatchEvent(new MouseEvent('pointerenter'));
    }
  }

  /**
   * Handle filtering of macros and macro folders
   * @param {Event} event
   * @param {string} query
   * @param {RegExp} rgx
   * @param {HTMLElement} html
   * @returns
   */
  _onSearchFilter(event, query, rgx, html) {
    if (!query) {
      html.querySelectorAll('.folder').forEach((folder) => {
        folder.style.display = '';
        folder.open = false;
      });
      html.querySelectorAll('.macro').forEach((macro) => {
        macro.style.display = '';
      });
      return;
    }

    query = query.toLocaleLowerCase();
    html.querySelectorAll(':scope > .folder').forEach((folder) => this._matchFolder(folder, query));
    html.querySelectorAll(':scope > .macro').forEach((macro) => this._matchMacro(macro, query));
  }

  /**
   * Match folder to a query
   * @param {HTMLElement} folder
   * @param {string} query
   * @param {boolean} parentMatch
   * @returns
   */
  _matchFolder(folder, query, parentMatch = false) {
    let nameMatch = folder.dataset.name.toLocaleLowerCase().includes(query);

    let childMatch = false;
    folder.querySelectorAll(':scope > .folder').forEach((folder) => {
      childMatch = this._matchFolder(folder, query, nameMatch || parentMatch) || childMatch;
    });

    let macroMatch = false;
    folder.querySelectorAll(':scope > .macro').forEach((folder) => {
      macroMatch = this._matchMacro(folder, query, nameMatch || parentMatch) || macroMatch;
    });

    if (childMatch || macroMatch || nameMatch || parentMatch) {
      folder.style.display = '';
      folder.open = childMatch || macroMatch;
    } else {
      folder.style.display = 'none';
    }
    return childMatch || macroMatch || nameMatch;
  }

  /**
   * Match macro to a query
   * @param {HTMLElement} macro
   * @param {string} query
   * @param {boolean} parentMatch
   * @returns
   */
  _matchMacro(macro, query, parentMatch = false) {
    const match = macro.querySelector('.name').textContent.toLocaleLowerCase().includes(query);
    macro.style.display = match || parentMatch ? '' : 'none';
    return match;
  }

  /**
   * Handles macro click to execute it
   * @param {*} event
   * @param {*} element
   */
  static async _onClickMacro(event, element) {
    const macro = await fromUuid(element.dataset.uuid);
    macro?.execute();
  }

  /**
   * Toggle tutorial as read
   * @param {MouseEvent} event
   * @param {HTMLElement} element
   */
  static async _onMarkRead(event, element) {
    const uuid = element.closest('.tutorial').dataset.uuid;

    const read = game.settings.get(MODULE_ID, 'read-tutorials');
    read[uuid] = !Boolean(read[uuid]);
    await game.settings.set(MODULE_ID, 'read-tutorials', read);

    this.render({ parts: ['tutorials'] });
  }

  /**
   * Toggle category/folder display of read tutorials
   * @param {MouseEvent} event
   * @param {HTMLElement} element
   */
  static async _onMarkDisplay(event, element) {
    const id = element.closest('.category').dataset.id;

    const display = game.settings.get(MODULE_ID, 'read-tutorials-display-category');
    display[id] = !Boolean(display[id]);
    await game.settings.set(MODULE_ID, 'read-tutorials-display-category', display);

    this.render({ parts: ['tutorials'] });
  }

  /**
   * Open clicked tutorial/journal
   * @param {MouseEvent} event
   * @param {HTMLElement} element
   */
  static async _onOpenTutorial(event, element) {
    fromUuid(element.dataset.uuid).then((journal) => journal?.sheet.render(true));
  }

  /**
   * Open clicked browser macro
   * @param {MouseEvent} event
   * @param {HTMLElement} element
   */
  static async _onOpenBrowser(event, element) {
    fromUuid(element.dataset.uuid).then((macro) => macro?.execute());
  }

  static async _onDownloadScene(event, element) {
    element.hidden = true;
    game.modules
      .get('baileywiki-premium-content')
      .api.DownloadScene()
      .then(() => BaileyWikiTab._onDisplaySceneDownloadButton());
  }

  static _onAuthenticate(event, element) {
    game.modules.get('baileywiki-premium-content').api.AuthenticationMenu();
  }

  /** @override */
  async _renderHTML(context, options) {
    if (this.isPopout) {
      options.parts = options.parts.filter((id) => id === 'utilities');
    }
    return super._renderHTML(context, options);
  }

  /** @override */
  async _onFirstRender(context, options) {
    await super._onFirstRender(context, options);
    this._createContextMenu(this._getMacroContextOptions, '.macro', {
      hookName: 'getBaileyWikiMacroContextOptions',
      parentClassHooks: false,
    });
    if (!BaileyWikiTab._registeredHooks) {
      BaileyWikiTab._registeredHooks = true;
      Hooks.on('canvasInit', () => BaileyWikiTab._onDisplaySceneDownloadButton());
      ['Login', 'Logout', 'AuthError'].forEach((hook) => {
        Hooks.on(`bwPremium.${hook}`, () => ui.baileywiki?.render({ parts: ['info'] }));
      });
    }
  }

  /**
   * Context menu options for Macros
   * @returns
   */
  _getMacroContextOptions() {
    return [
      {
        name: 'Open Macro',
        icon: '<i class="fa-solid fa-code"></i>',
        condition: game.user.isGM,
        callback: (li) => fromUuid(li.dataset.uuid).then((macro) => macro.sheet.render(true)),
      },
    ];
  }

  /**
   * Set data upon macro drag
   * @param {MouseEvent} event
   */
  _onDragMacroStart(event) {
    const target = event.currentTarget;
    event.dataTransfer.setData('text/plain', JSON.stringify({ type: 'Macro', uuid: target.dataset.uuid }));
  }

  static _onDisplaySceneDownloadButton(scene = canvas.scene) {
    if (scene && scene.id === canvas.scene?.id && game.modules.get('baileywiki-premium-content')?.active) {
      ui.baileywiki.render({ parts: ['info'] });
    }
  }

  /** @override */
  async _onRender(context, options) {
    await super._onRender(context, options);

    const { SearchFilter, DragDrop } = foundry.applications.ux;

    // Search macro
    this._search ??= new SearchFilter({
      inputSelector: 'input[type=search]',
      contentSelector: '.macros',
      callback: this._onSearchFilter.bind(this),
    });
    this._search.bind(this.element);

    // Drag macro
    new DragDrop.implementation({
      dragSelector: '.macro',
      permissions: {
        dragstart: true,
        drop: false,
      },
      callbacks: {
        dragstart: this._onDragMacroStart.bind(this),
      },
    }).bind(this.element);
  }
}
