/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';

import {
  log as internalLogger,
  configs,
  wait,
  countMatched,
  dumpTab
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Constants from '/common/constants.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsUpdate from '/common/tabs-update.js';
import * as TreeBehavior from '/common/tree-behavior.js';

import { Tab } from '/common/TreeItem.js';

import * as TabsMove from './tabs-move.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';

function log(...args) {
  internalLogger('background/tabs-group', ...args);
}

export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerTabId, aliasTabId, replacedParentCount } = {}) {
  const url = new URL(Constants.kGROUP_TAB_URI);

  if (title)
    url.searchParams.set('title', title);

  if (temporaryAggressive)
    url.searchParams.set('temporaryAggressive', 'true');
  else if (temporary)
    url.searchParams.set('temporary', 'true');

  if (openerTabId)
    url.searchParams.set('openerTabId', openerTabId);

  if (aliasTabId)
    url.searchParams.set('aliasTabId', aliasTabId);

  if (replacedParentCount)
    url.searchParams.set('replacedParentCount', replacedParentCount);

  return url.href;
}

export function temporaryStateParams(state) {
  switch (state) {
    case Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE:
      return {
        temporary:           true,
        temporaryAggressive: false,
      };
    case Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE:
      return {
        temporary:           false,
        temporaryAggressive: true,
      };
    default:
      break;
  }
  return {
    temporary:           false,
    temporaryAggressive: false,
  };
}

export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...groupTabOptions } = {}) {
  const rootTabs = Tab.collectRootTabs(tabs);
  if (rootTabs.length <= 0)
    return null;

  log('groupTabs: ', () => tabs.map(dumpTab), { broadcast, parent, withDescendants });

  const uri = makeGroupTabURI({
    title:     browser.i18n.getMessage('groupTab_label', rootTabs[0].title),
    temporary: true,
    ...groupTabOptions
  });
  const groupTab = await TabsOpen.openURIInTab(uri, {
    windowId:     rootTabs[0].windowId,
    parent:       parent || rootTabs[0].$TST.parent,
    insertBefore: rootTabs[0],
    inBackground: true
  });

  if (!withDescendants) {
    const structure = TreeBehavior.getTreeStructureFromTabs(tabs);

    await Tree.detachTabsFromTree(tabs, {
      broadcast: !!broadcast,
    });

    log('structure: ', structure);
    await Tree.applyTreeStructureToTabs(tabs, structure, {
      broadcast: !!broadcast,
    });
  }

  await TabsMove.moveTabsAfter(tabs.slice(1), tabs[0], {
    broadcast: !!broadcast
  });
  for (const tab of rootTabs) {
    await Tree.attachTabTo(tab, groupTab, {
      forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
      dontMove:  true,
      broadcast: !!broadcast,
    });
  }
  return groupTab;
}

function reserveToCleanupNeedlessGroupTab(tabOrTabs) {
  const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
  for (const tab of tabs) {
    if (!TabsStore.ensureLivingItem(tab))
      continue;
    if (tab.$TST.temporaryMetadata.has('reservedCleanupNeedlessGroupTab'))
      clearTimeout(tab.$TST.temporaryMetadata.get('reservedCleanupNeedlessGroupTab'));
    tab.$TST.temporaryMetadata.set('reservedCleanupNeedlessGroupTab', setTimeout(() => {
      if (!tab.$TST)
        return;
      tab.$TST.temporaryMetadata.delete('reservedCleanupNeedlessGroupTab');
      cleanupNeedlssGroupTab(tab);
    }, 100));
  }
}

function cleanupNeedlssGroupTab(tabs) {
  if (!Array.isArray(tabs))
    tabs = [tabs];
  log('trying to clanup needless temporary group tabs from ', () => tabs.map(dumpTab));
  const tabsToBeRemoved = [];
  for (const tab of tabs) {
    if (tab.$TST.temporaryMetadata.has('movingAcrossWindows'))
      continue;

    if (tab.$TST.isTemporaryGroupTab) {
      if (tab.$TST.childIds.length > 1)
        break;
      const lastChild = tab.$TST.firstChild;
      if (lastChild &&
          !lastChild.$TST.isTemporaryGroupTab &&
          !lastChild.$TST.isTemporaryAggressiveGroupTab)
        break;
    }
    else if (tab.$TST.isTemporaryAggressiveGroupTab) {
      if (tab.$TST.childIds.length > 1)
        break;
    }
    else {
      break;
    }
    tabsToBeRemoved.push(tab);
  }
  log('=> to be removed: ', () => tabsToBeRemoved.map(dumpTab));
  TabsInternalOperation.removeTabs(tabsToBeRemoved, { keepDescendants: true });
}

export async function tryReplaceTabWithGroup(tab, { windowId, parent, children, insertBefore, newParent } = {}) {
  if (tab) {
    windowId     = tab.windowId;
    parent       = tab.$TST.parent;
    children     = tab.$TST.children;
    insertBefore = insertBefore || tab.$TST.unsafeNextTab;
  }

  if (children.length <= 1 ||
      countMatched(children,
                   tab => !tab.$TST.states.has(Constants.kTAB_STATE_TO_BE_REMOVED)) <= 1)
    return null;

  log('trying to replace the closing tab with a new group tab');

  const firstChild = children[0];
  const uri = makeGroupTabURI({
    title:     browser.i18n.getMessage('groupTab_label', firstChild.title),
    ...temporaryStateParams(configs.groupTabTemporaryStateForOrphanedTabs),
    replacedParentCount: (tab?.$TST?.replacedParentGroupTabCount || 0) + 1,
  });
  const win = TabsStore.windows.get(windowId);
  win.toBeOpenedTabsWithPositions++;
  const groupTab = await TabsOpen.openURIInTab(uri, {
    windowId,
    insertBefore,
    inBackground: true
  });
  log('group tab: ', dumpTab(groupTab));
  if (!groupTab) // the window is closed!
    return;
  if (newParent || parent)
    await Tree.attachTabTo(groupTab, newParent || parent, {
      dontMove:  true,
      broadcast: true
    });
  for (const child of children) {
    await Tree.attachTabTo(child, groupTab, {
      dontMove:  true,
      broadcast: true
    });
  }

  // This can be triggered on closing of multiple tabs,
  // so we should cleanup it on such cases for safety.
  // https://github.com/piroor/treestyletab/issues/2317
  wait(1000).then(() => reserveToCleanupNeedlessGroupTab(groupTab));

  return groupTab;
}


// ====================================================================
// init/update group tabs
// ====================================================================

/*
  To prevent the tab is closed by Firefox, we need to inject scripts dynamically.
  See also: https://github.com/piroor/treestyletab/issues/1670#issuecomment-350964087
*/
async function tryInitGroupTab(tab) {
  if (!tab.$TST.isGroupTab &&
      !tab.$TST.hasGroupTabURL)
    return;
  log('tryInitGroupTab ', tab);
  const v3Options = {
    target: { tabId: tab.id },
  };
  const v2Options = {
    runAt:           'document_start',
    matchAboutBlank: true
  };
  try {
    const getPageState = function getPageState() {
      return [window.prepared, document.documentElement.matches('.initialized')];
    };
    const [prepared, initialized, reloaded] = (browser.scripting ?
      browser.scripting.executeScript({ // Manifest V3
        ...v3Options,
        func: getPageState,
      }).then(results => results && results[0] && results[0].result || []) :
      browser.tabs.executeScript(tab.id, {
        ...v2Options,
        code: `(${getPageState.toString()})()`,
      }).then(results => results && results[0] || [])
    ).catch(error => {
      if (ApiTabs.isMissingHostPermissionError(error) &&
          tab.$TST.hasGroupTabURL) {
        log('  tryInitGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', tab.id);
        browser.tabs.reload(tab.id);
        return [[false, false, true]];
      }
      return ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError)(error);
    });
    log('  tryInitGroupTab: groupt tab state ', tab.id, { prepared, initialized, reloaded });
    if (reloaded) {
      log('  => reloaded ', tab.id);
      return;
    }
    if (prepared && initialized) {
      log('  => already initialized ', tab.id);
      return;
    }
  }
  catch(error) {
    log('  tryInitGroupTab: error while checking initialized: ', tab.id, error);
  }
  try {
    const getTitleExistence = function getState() {
      return !!document.querySelector('#title');
    };
    const titleElementExists = (browser.scripting ?
      browser.scripting.executeScript({ // Manifest V3
        ...v3Options,
        func: getTitleExistence,
      }).then(results => results && results[0] && results[0].result) :
      browser.tabs.executeScript(tab.id, {
        ...v2Options,
        code: `(${getTitleExistence.toString()})()`,
      }).then(results => results && results[0])
    ).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
    if (!titleElementExists && tab.status == 'complete') { // we need to load resources/group-tab.html at first.
      log('  => title element exists, load again ', tab.id);
      return browser.tabs.update(tab.id, { url: tab.url }).catch(ApiTabs.createErrorSuppressor());
    }
  }
  catch(error) {
    log('  tryInitGroupTab error while checking title element: ', tab.id, error);
  }

  (browser.scripting ?
    browser.scripting.executeScript({ // Manifest V3
      ...v3Options,
      files: [
        '/extlib/l10n-classic.js', // ES module does not supported as a content script...
        '/resources/group-tab.js',
      ],
    }) :
    Promise.all([
      browser.tabs.executeScript(tab.id, {
        ...v2Options,
        //file:  '/common/l10n.js'
        file: '/extlib/l10n-classic.js', // ES module does not supported as a content script...
      }).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
      browser.tabs.executeScript(tab.id, {
        ...v2Options,
        file: '/resources/group-tab.js',
      }).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
    ])
  ).then(() => {
    log('tryInitGroupTab completely initialized: ', tab.id);
  });

  if (tab.$TST.states.has(Constants.kTAB_STATE_UNREAD)) {
    tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
    SidebarConnection.sendMessage({
      type:     Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
      windowId: tab.windowId,
      tabId:    tab.id,
      removedStates: [Constants.kTAB_STATE_UNREAD]
    });
  }
}

function reserveToUpdateRelatedGroupTabs(tab, changedInfo) {
  const tabMetadata = tab.$TST.temporaryMetadata;
  const updatingTabs = tabMetadata.get('reserveToUpdateRelatedGroupTabsUpdatingTabs') || new Set();
  if (!tabMetadata.has('reserveToUpdateRelatedGroupTabsUpdatingTabs'))
    tabMetadata.set('reserveToUpdateRelatedGroupTabsUpdatingTabs', updatingTabs);

  const ancestorGroupTabs = [
    tab,
    tab.$TST.bundledTab,
    ...tab.$TST.ancestors,
    ...tab.$TST.ancestors.map(tab => tab.$TST.bundledTab),
  ].filter(tab => tab?.$TST.isGroupTab);
  for (const updatingTab of ancestorGroupTabs) {
    const updatingMetadata = updatingTab.$TST.temporaryMetadata;
    const reservedChangedInfo = updatingMetadata.get('reservedUpdateRelatedGroupTabChangedInfo') || new Set();
    for (const info of changedInfo) {
      reservedChangedInfo.add(info);
    }
    if (updatingTabs.has(updatingTab.id))
      continue;
    updatingTabs.add(updatingTab.id);
    const triggeredUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates') || new Set();
    triggeredUpdates.add(updatingTabs);
    updatingMetadata.set('reservedUpdateRelatedGroupTabTriggeredUpdates', triggeredUpdates);
    if (updatingMetadata.has('reservedUpdateRelatedGroupTab'))
      clearTimeout(updatingMetadata.get('reservedUpdateRelatedGroupTab'));
    updatingMetadata.set('reservedUpdateRelatedGroupTabChangedInfo', reservedChangedInfo);
    updatingMetadata.set('reservedUpdateRelatedGroupTab', setTimeout(() => {
      updatingMetadata.delete('reservedUpdateRelatedGroupTab');
      if (updatingTab.$TST) {
        try {
          if (reservedChangedInfo.size > 0)
            updateRelatedGroupTab(updatingTab, [...reservedChangedInfo]);
        }
        catch(_error) {
        }
        updatingMetadata.delete('reservedUpdateRelatedGroupTabChangedInfo');
      }
      setTimeout(() => {
        const triggerUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates')
        updatingMetadata.delete('reservedUpdateRelatedGroupTabTriggeredUpdates');
        if (!triggerUpdates)
          return;
        for (const updatingTabs of triggerUpdates) {
          updatingTabs.delete(updatingTab.id);
        }
      }, 100)
    }, 100));
  }
}

async function updateRelatedGroupTab(groupTab, changedInfo = []) {
  if (!TabsStore.ensureLivingItem(groupTab))
    return;

  await tryInitGroupTab(groupTab);
  if (changedInfo.includes('tree')) {
    try {
      await browser.tabs.sendMessage(groupTab.id, {
        type: 'treestyletab:update-tree',
      }).catch(error => {
        if (ApiTabs.isMissingHostPermissionError(error))
          throw error;
        return ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleUnloadedError)(error);
      });
    }
    catch(error) {
      if (ApiTabs.isMissingHostPermissionError(error)) {
        log('  updateRelatedGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', groupTab.id);
        browser.tabs.reload(groupTab.id);
        return;
      }
    }
  }

  const firstChild = groupTab.$TST.firstChild;
  if (!firstChild) // the tab can be closed while waiting...
    return;

  if (changedInfo.includes('title')) {
    let newTitle;
    if (Constants.kGROUP_TAB_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
      newTitle = browser.i18n.getMessage('groupTab_label', firstChild.title);
    }
    else if (Constants.kGROUP_TAB_FROM_PINNED_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
      const opener = groupTab.$TST.openerTab;
      if (opener) {
        if (opener &&
            opener.favIconUrl) {
          SidebarConnection.sendMessage({
            type:       Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
            windowId:   groupTab.windowId,
            tabId:      groupTab.id,
            favIconUrl: opener.favIconUrl
          });
        }
        newTitle = browser.i18n.getMessage('groupTab_fromPinnedTab_label', opener.title);
      }
    }

    if (newTitle && groupTab.title != newTitle) {
      browser.tabs.sendMessage(groupTab.id, {
        type:  'treestyletab:update-title',
        title: newTitle,
      }).catch(ApiTabs.createErrorHandler(
        ApiTabs.handleMissingTabError,
        ApiTabs.handleMissingHostPermissionError,
        _error => {
          // failed to update the title by group tab itself, so we try to update it from outside
          groupTab.title = newTitle;
          TabsUpdate.updateTab(groupTab, { title: newTitle });
        }
      ));
    }
  }
}

Tab.onRemoved.addListener((tab, _closeInfo = {}) => {
  const ancestors = tab.$TST.ancestors;
  wait(0).then(() => {
    reserveToCleanupNeedlessGroupTab(ancestors);
  });
});

Tab.onUpdated.addListener((tab, changeInfo) => {
  if ('url' in changeInfo ||
      'previousUrl' in changeInfo ||
      'state' in changeInfo) {
    const status = changeInfo.status || tab?.status;
    const url = changeInfo.url ? changeInfo.url :
      status == 'complete' && tab ? tab.url : '';
    if (tab &&
        status == 'complete') {
      if (url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
        tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
      }
      else if (!Constants.kSHORTHAND_ABOUT_URI.test(url)) {
        tab.$TST.getPermanentStates().then(async (states) => {
          if (url.indexOf(Constants.kGROUP_TAB_URI) == 0)
            return;
          // Detect group tab from different session - which can have different UUID for the URL.
          const PREFIX_REMOVER = /^moz-extension:\/\/[^\/]+/;
          const pathPart = url.replace(PREFIX_REMOVER, '');
          if (states.includes(Constants.kTAB_STATE_GROUP_TAB) &&
              pathPart.split('?')[0] == Constants.kGROUP_TAB_URI.replace(PREFIX_REMOVER, '')) {
            const parameters = pathPart.replace(/^[^\?]+\?/, '');
            const oldUrl = tab.url;
            await wait(100); // for safety
            if (tab.url != oldUrl)
              return;
            browser.tabs.update(tab.id, {
              url: `${Constants.kGROUP_TAB_URI}?${parameters}`
            }).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
            tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB);
          }
          else {
            tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
          }
        });
      }
    }
    // restored tab can be replaced with blank tab. we need to restore it manually.
    else if (changeInfo.url == 'about:blank' &&
             changeInfo.previousUrl &&
             changeInfo.previousUrl.indexOf(Constants.kGROUP_TAB_URI) == 0) {
      const oldUrl = tab.url;
      wait(100).then(() => { // redirect with delay to avoid infinite loop of recursive redirections.
        if (tab.url != oldUrl)
          return;
        browser.tabs.update(tab.id, {
          url: changeInfo.previousUrl
        }).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
        tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
      });
    }

    if (changeInfo.status ||
        changeInfo.url ||
        url.indexOf(Constants.kGROUP_TAB_URI) == 0)
      tryInitGroupTab(tab);
  }

  if ('title' in changeInfo) {
    const group = Tab.getGroupTabForOpener(tab);
    if (group)
      reserveToUpdateRelatedGroupTabs(group, ['title', 'tree']);
  }
});

Tab.onGroupTabDetected.addListener(tab => {
  tryInitGroupTab(tab);
});

Tab.onLabelUpdated.addListener(tab => {
  reserveToUpdateRelatedGroupTabs(tab, ['title', 'tree']);
});

Tab.onActivating.addListener((tab, _info = {}) => {
  tryInitGroupTab(tab);
});

// returns a boolean: need to reload or not.
export async function clearTemporaryState(tab) {
  if (!tab.$TST.isTemporaryGroupTab &&
      !tab.$TST.isTemporaryAggressiveGroupTab)
    return;

  const url = new URL(tab.url);
  url.searchParams.delete('temporary');
  url.searchParams.delete('temporaryAggressive');
  await Promise.all([
    browser.tabs.sendMessage(tab.id, {
      type: 'treestyletab:clear-temporary-state',
    }).catch(ApiTabs.createErrorHandler()),
    browser.tabs.executeScript(tab.id, { // failsafe
      runAt: 'document_start',
      code:  `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
    }).catch(ApiTabs.createErrorHandler()),
  ]);
  tab.url = url.href;
}

Tab.onPinned.addListener(async tab => {
  log('handlePinnedParentTab ', tab);

  await Tree.collapseExpandSubtree(tab, {
    collapsed: false,
    broadcast: true
  });

  log('  childIdsBeforeMoved: ', tab.$TST.temporaryMetadata.get('childIdsBeforeMoved'));
  log('  parentIdBeforeMoved: ', tab.$TST.temporaryMetadata.get('parentIdBeforeMoved'));

  const children = (
    tab.$TST.temporaryMetadata.has('childIdsBeforeMoved') ?
      tab.$TST.temporaryMetadata.get('childIdsBeforeMoved').map(id => Tab.get(id)) :
      tab.$TST.children
  ).filter(tab => TabsStore.ensureLivingItem(tab));
  const parent = TabsStore.ensureLivingItem(
    tab.$TST.temporaryMetadata.has('parentIdBeforeMoved') ?
      Tab.get(tab.$TST.temporaryMetadata.get('parentIdBeforeMoved')) :
      tab.$TST.parent
  );

  let openedGroupTab;
  const shouldGroupChildren = configs.autoGroupNewTabsFromPinned || tab.$TST.isGroupTab;
  if (shouldGroupChildren) {
    log(' => trying to group left tabs with a group: ', children);
    openedGroupTab = await groupTabs(children, {
      // If the tab is a group tab, the opened tab should be treated as an alias of the pinned group tab.
      // Otherwise it should be treated just as a temporary group tab to group children.
      title:       tab.$TST.isGroupTab ? tab.title : browser.i18n.getMessage('groupTab_fromPinnedTab_label', tab.title),
      temporary:   !tab.$TST.isGroupTab,
      openerTabId: tab.$TST.uniqueId.id,
      parent,
      withDescendants: true,
    });
    log(' openedGroupTab: ', openedGroupTab);
    // Tree structure of left tabs can be modified by someone like tryFixupTreeForInsertedTab@handle-moved-tabs.js.
    // On such cases we need to restore the original tree structure.
    const modifiedChildren = children.filter(child => children.includes(child.$TST.parent));
    log(' modifiedChildren: ', modifiedChildren);
    if (modifiedChildren.length > 0) {
      for (const child of modifiedChildren) {
        await Tree.detachTab(child, {
          broadcast: true,
        });
        await Tree.attachTabTo(child, openedGroupTab, {
          dontMove:  true,
          broadcast: true,
        });
      }
    }
  }
  else {
    log(' => no need to group left tabs, just detaching');
    await Tree.detachAllChildren(tab, {
      behavior: TreeBehavior.getParentTabOperationBehavior(tab, {
        context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
        preventEntireTreeBehavior: true,
      }),
      broadcast: true
    });
  }
  await Tree.detachTab(tab, {
    broadcast: true
  });

  // Such a group tab will be closed automatically when all children are detached.
  // To prevent the auto close behavior, the tab type need to be turned to permanent.
  await clearTemporaryState(tab);

  if (tab.$TST.isGroupTab && openedGroupTab) {
    const url = new URL(tab.url);
    url.searchParams.set('aliasTabId', openedGroupTab.$TST.uniqueId.id);
    await Promise.all([
      browser.tabs.sendMessage(tab.id, {
        type: 'treestyletab:replace-state-url',
        url: url.href,
      }).catch(ApiTabs.createErrorHandler()),
      browser.tabs.executeScript(tab.id, { // failsafe
        runAt: 'document_start',
        code:  `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
      }).catch(ApiTabs.createErrorHandler()),
    ]);
    await browser.tabs.sendMessage(tab.id, {
      type: 'treestyletab:update-tree',
      url: url.href,
    }).catch(ApiTabs.createErrorHandler());
    tab.url = url.href;
  }
});

Tree.onAttached.addListener((tab, _info = {}) => {
  reserveToUpdateRelatedGroupTabs(tab, ['tree']);
});

Tree.onDetached.addListener((_tab, detachInfo) => {
  if (!detachInfo.oldParentTab)
    return;
  if (detachInfo.oldParentTab.$TST.isGroupTab)
    reserveToCleanupNeedlessGroupTab(detachInfo.oldParentTab);
  reserveToUpdateRelatedGroupTabs(detachInfo.oldParentTab, ['tree']);
});

/*
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info) => {
  reserveToUpdateRelatedGroupTabs(tab);
});
*/
