/*
# 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,
  dumpTab,
  configs,
  isFirefoxViewTab,
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.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/handle-new-tabs', ...args);
}


Tab.onBeforeCreate.addListener(async (tab, info) => {
  const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);

  // Special case, when all these conditions are true:
  // 1) A new blank tab is configured to be opened as a child of the active tab.
  // 2) The active tab is pinned.
  // 3) Tabs opened from a pinned parent are configured to be placed near the
  //    opener pinned tab.
  // then we fakely attach the new blank tab to the active pinned tab.
  // See also https://github.com/piroor/treestyletab/issues/3296
  const shouldAttachToPinnedOpener = (
    !tab.openerTabId &&
    !tab.pinned &&
    tab.$TST.isNewTabCommandTab &&
    Constants.kCONTROLLED_NEWTAB_POSITION.has(configs.autoAttachOnNewTabCommand) &&
    (
      (activeTab?.pinned &&
       Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromPinnedTabAt)) ||
      (isFirefoxViewTab(activeTab) &&
       Constants.kCONTROLLED_INSERTION_POSITION.has(configs.insertNewTabFromFirefoxViewAt))
    )
  );
  if (shouldAttachToPinnedOpener)
    tab.openerTabId = activeTab.id;
});

// this should return false if the tab is / may be moved while processing
Tab.onCreating.addListener((tab, info = {}) => {
  if (info.duplicatedInternally)
    return true;

  log('Tabs.onCreating ', dumpTab(tab), tab.openerTabId, info);

  const activeTab = info.activeTab || Tab.getActiveTab(tab.windowId);
  const opener = tab.$TST.openerTab;
  if (opener) {
    tab.$TST.setAttribute(Constants.kPERSISTENT_ORIGINAL_OPENER_TAB_ID, opener.$TST.uniqueId.id);
    if (!info.bypassTabControl)
      TabsStore.addToBeGroupedTab(tab);
  }
  else {
    let dontMove = false;
    if (!info.maybeOrphan &&
        !info.bypassTabControl &&
        activeTab &&
        !info.restored) {
      let autoAttachBehavior = configs.autoAttachOnNewTabCommand;
      if (tab.$TST.nextTab &&
          activeTab == tab.$TST.previousTab) {
        // New tab opened with browser.tabs.insertAfterCurrent=true may have
        // next tab. In this case the tab is expected to be placed next to the
        // active tab always, so we should change the behavior specially.
        // See also:
        //   https://github.com/piroor/treestyletab/issues/2054
        //   https://github.com/piroor/treestyletab/issues/2194#issuecomment-505272940
        dontMove = true;
        switch (autoAttachBehavior) {
          case Constants.kNEWTAB_OPEN_AS_ORPHAN:
          case Constants.kNEWTAB_OPEN_AS_SIBLING:
          case Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING:
            if (activeTab.$TST.hasChild)
              autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_CHILD;
            else
              autoAttachBehavior = Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING;
            break;

          case Constants.kNEWTAB_OPEN_AS_CHILD:
          default:
            break;
        }
      }
      if (tab.$TST.isNewTabCommandTab) {
        if (!info.positionedBySelf) {
          log('behave as a tab opened by new tab command');
          return handleNewTabFromActiveTab(tab, {
            activeTab,
            autoAttachBehavior,
            dontMove,
            openedWithCookieStoreId: info.openedWithCookieStoreId,
            inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
            context: TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
          }).then(moved => !moved);
        }
        return false;
      }
      else if (activeTab != tab) {
        tab.$TST.temporaryMetadata.set('possibleOpenerTab', activeTab.id);
      }
      if (!info.fromExternal)
        tab.$TST.temporaryMetadata.set('isNewTab', true);
    }
    if (info.fromExternal &&
        !info.bypassTabControl) {
      log('behave as a tab opened from external application');
      // we may need to reopen the tab with loaded URL
      if (configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
        tab.$TST.temporaryMetadata.set('fromExternal', true);
      return notifyToTryHandleNewTab(tab, {
        context: TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
        activeTab,
      }).then(allowed => {
        if (!allowed) {
          log(' => handling is canceled by someone');
          return true;
        }
        return Tree.behaveAutoAttachedTab(tab, {
          baseTab:   activeTab,
          behavior:  configs.autoAttachOnOpenedFromExternal,
          dontMove,
          broadcast: true
        }).then(moved => !moved);
      });
    }
    log('behave as a tab opened with any URL ', tab.title, tab.url);
    if (!info.restored &&
        !info.positionedBySelf &&
        !info.bypassTabControl &&
        configs.autoAttachOnAnyOtherTrigger != Constants.kNEWTAB_DO_NOTHING) {
      if (configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
        tab.$TST.temporaryMetadata.set('anyOtherTrigger', true);
      log('controlled as a new tab from other unknown trigger');
      return notifyToTryHandleNewTab(tab, {
        context: TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
        activeTab,
      }).then(allowed => {
        if (!allowed) {
          log(' => handling is canceled by someone');
          return true;
        }
        return Tree.behaveAutoAttachedTab(tab, {
          baseTab:   activeTab,
          behavior:  configs.autoAttachOnAnyOtherTrigger,
          dontMove,
          broadcast: true
        }).then(moved => !moved);
      });
    }
    if (info.positionedBySelf)
      tab.$TST.temporaryMetadata.set('positionedBySelf', true);
    return true;
  }

  log(`opener: ${dumpTab(opener)}, positionedBySelf = ${info.positionedBySelf}`);
  if (!info.bypassTabControl &&
      opener &&
      (opener.pinned || isFirefoxViewTab(opener)) &&
      opener.windowId == tab.windowId) {
    return handleTabsFromPinnedOpener(tab, opener, { activeTab }).then(moved => !moved);
  }
  else if (!info.maybeOrphan || info.bypassTabControl) {
    if (info.fromExternal &&
        !info.bypassTabControl &&
        configs.inheritContextualIdentityToTabsFromExternalMode != Constants.kCONTEXTUAL_IDENTITY_DEFAULT)
      tab.$TST.temporaryMetadata.set('fromExternal', true);
    return notifyToTryHandleNewTab(tab, {
      context: info.fromExternal && !info.bypassTabControl ?
        TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL :
        info.duplicated ?
          TSTAPI.kNEWTAB_CONTEXT_DUPLICATED :
          TSTAPI.kNEWTAB_CONTEXT_WITH_OPENER,
      activeTab,
      openerTab: opener,
    }).then(allowed => {
      if (!allowed) {
        log(' => handling is canceled by someone');
        return true;
      }
      const behavior = info.fromExternal && !info.bypassTabControl ?
        configs.autoAttachOnOpenedFromExternal :
        info.duplicated ?
          configs.autoAttachOnDuplicated :
          configs.autoAttachOnOpenedWithOwner;
      return Tree.behaveAutoAttachedTab(tab, {
        baseTab:   opener,
        behavior,
        dontMove:  info.positionedBySelf || info.mayBeReplacedWithContainer,
        broadcast: true
      }).then(moved => !moved);
    });
  }
  return true;
});

async function notifyToTryHandleNewTab(tab, { context, activeTab, openerTab } = {}) {
  const cache = {};
  const result = TSTAPI.tryOperationAllowed(
    TSTAPI.kNOTIFY_TRY_HANDLE_NEWTAB,
    { tab,
      activeTab,
      openerTab,
      context },
    { tabProperties: ['tab', 'activeTab', 'openerTab'], cache }
  );
  TSTAPI.clearCache(cache);
  return result;
}

async function handleNewTabFromActiveTab(tab, { url, activeTab, autoAttachBehavior, dontMove, openedWithCookieStoreId, inheritContextualIdentityMode, context } = {}) {
  log('handleNewTabFromActiveTab: activeTab = ', dumpTab(activeTab), { url, activeTab, autoAttachBehavior, dontMove, inheritContextualIdentityMode, context });
  if (activeTab &&
      activeTab.$TST.ancestors.includes(tab)) {
    log(' => ignore restored ancestor tab');
    return false;
  }

  const allowed = await notifyToTryHandleNewTab(tab, {
    context,
    activeTab,
  });
  if (!allowed) {
    log(' => handling is canceled by someone');
    return false;
  }

  const moved = await Tree.behaveAutoAttachedTab(tab, {
    baseTab:   activeTab,
    behavior:  autoAttachBehavior,
    broadcast: true,
    dontMove:  dontMove || false
  });
  if (openedWithCookieStoreId) {
    log('handleNewTabFromActiveTab: do not reopen tab opened with contextual identity explicitly');
    return moved;
  }
  if (tab.cookieStoreId && tab.cookieStoreId != 'firefox-default') {
    log('handleNewTabFromActiveTab: do not reopen tab opened with non-default contextual identity ', tab.cookieStoreId);
    return moved;
  }

  const parent = tab.$TST.parent;
  let cookieStoreId = null;
  switch (inheritContextualIdentityMode) {
    case Constants.kCONTEXTUAL_IDENTITY_FROM_PARENT:
      if (parent)
        cookieStoreId = parent.cookieStoreId;
      break;

    case Constants.kCONTEXTUAL_IDENTITY_FROM_LAST_ACTIVE:
      cookieStoreId = activeTab.cookieStoreId
      break;

    default:
      return moved;
  }
  if ((tab.cookieStoreId || 'firefox-default') == (cookieStoreId || 'firefox-default')) {
    log('handleNewTabFromActiveTab: no need to reopen with inherited contextual identity ', cookieStoreId);
    return moved;
  }

  if (!configs.inheritContextualIdentityToUnopenableURLTabs &&
      !TabsOpen.isOpenable(url)) {
    log('handleNewTabFromActiveTab: not openable URL, skip reopening ', cookieStoreId, url);
    return moved;
  }

  log('handleNewTabFromActiveTab: reopen with inherited contextual identity ', cookieStoreId);
  // We need to prevent grouping of this original tab and the reopened tab
  // by the "multiple tab opened in XXX msec" feature.
  const win = TabsStore.windows.get(tab.windowId);
  win.openedNewTabs.delete(tab.id);
  await TabsOpen.openURIInTab(url || null, {
    windowId: activeTab.windowId,
    parent,
    insertBefore: tab,
    active: tab.active,
    cookieStoreId
  });
  TabsInternalOperation.removeTab(tab);
  return moved;
}

async function handleTabsFromPinnedOpener(tab, opener, { activeTab } = {}) {
  const allowed = await notifyToTryHandleNewTab(tab, {
    context: TSTAPI.kNEWTAB_CONTEXT_FROM_PINNED,
    activeTab,
    openerTab: opener,
  });
  if (!allowed) {
    log('handleTabsFromPinnedOpener: handling is canceled by someone');
    return false;
  }

  const parent = Tab.getGroupTabForOpener(opener);
  if (parent) {
    log('handleTabsFromPinnedOpener: attach to corresponding group tab');
    tab.$TST.setAttribute(Constants.kPERSISTENT_ALREADY_GROUPED_FOR_PINNED_OPENER, true);
    tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
    // it could be updated already...
    const lastRelatedTab = opener.$TST.lastRelatedTabId == tab.id ?
      opener.$TST.previousLastRelatedTab :
      opener.$TST.lastRelatedTab;
    // If there is already opened group tab, it is more natural that
    // opened tabs are treated as a tab opened from unpinned tabs.
    const insertAt = configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_NEXT_TO_LAST_RELATED_TAB ?
      Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB :
      configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_TOP ?
        Constants.kINSERT_TOP :
        configs.autoAttachOnOpenedWithOwner == Constants.kNEWTAB_OPEN_AS_CHILD_END ?
          Constants.kINSERT_END :
          undefined;
    return Tree.attachTabTo(tab, parent, {
      lastRelatedTab,
      insertAt,
      forceExpand:    true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
      broadcast:      true
    });
  }

  if ((configs.autoGroupNewTabsFromPinned ||
       configs.autoGroupNewTabsFromFirefoxView) &&
      tab.$TST.needToBeGroupedSiblings.length > 0) {
    log('handleTabsFromPinnedOpener: controlled by auto-grouping');
    return false;
  }

  switch (isFirefoxViewTab(opener) ? configs.insertNewTabFromFirefoxViewAt : configs.insertNewTabFromPinnedTabAt) {
    case Constants.kINSERT_NEXT_TO_LAST_RELATED_TAB: {
      // it could be updated already...
      const lastRelatedTab = opener.$TST.lastRelatedTab != tab ?
        opener.$TST.lastRelatedTab :
        opener.$TST.previousLastRelatedTab;
      if (lastRelatedTab) {
        log(`handleTabsFromPinnedOpener: place after last related tab ${dumpTab(lastRelatedTab)}`);
        tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
        return TabsMove.moveTabAfter(tab, lastRelatedTab.$TST.lastDescendant || lastRelatedTab, {
          delayedMove: true,
          broadcast:   true
        });
      }
      const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
      if (lastPinnedTab) {
        log(`handleTabsFromPinnedOpener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
        tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
        return TabsMove.moveTabAfter(tab, lastPinnedTab, {
          delayedMove: true,
          broadcast:   true
        });
      }
      const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
      if (firstNormalTab) {
        log(`handleTabsFromPinnedOpener: place before first unpinned tab ${dumpTab(firstNormalTab)}`);
        tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
        return TabsMove.moveTabBefore(tab, firstNormalTab, {
          delayedMove: true,
          broadcast:   true
        });
      }
    };
    case Constants.kINSERT_TOP: {
      const lastPinnedTab = Tab.getLastPinnedTab(tab.windowId);
      if (lastPinnedTab) {
        log(`handleTabsFromPinnedOpener: opened from pinned opener: place after last pinned tab ${dumpTab(lastPinnedTab)}`);
        tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
        return TabsMove.moveTabAfter(tab, lastPinnedTab, {
          delayedMove: true,
          broadcast:   true
        });
      }
      const firstNormalTab = Tab.getFirstNormalTab(tab.windowId);
      if (firstNormalTab) {
        log(`handleTabsFromPinnedOpener: opened from pinned opener: place before first pinned tab ${dumpTab(firstNormalTab)}`);
        tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
        return TabsMove.moveTabBefore(tab, firstNormalTab, {
          delayedMove: true,
          broadcast:   true
        });
      }
    }; break;

    case Constants.kINSERT_END: {
      const lastTab = Tab.getLastTab(tab.windowId);
      log('handleTabsFromPinnedOpener: opened from pinned opener: place after the last tab ', lastTab);
      tab.$TST.temporaryMetadata.set('alreadyMovedAsOpenedFromPinnedOpener', true);
      return TabsMove.moveTabAfter(tab, lastTab, {
        delayedMove: true,
        broadcast:   true
      });
    };
  }

  return Promise.resolve(false);
}

Tab.onCreated.addListener((tab, info = {}) => {
  if (!info.duplicated ||
      info.bypassTabControl)
    return;
  const original = info.originalTab;
  log('duplicated ', dumpTab(tab), dumpTab(original));
  if (info.duplicatedInternally) {
    log('duplicated by internal operation');
    tab.$TST.addState(Constants.kTAB_STATE_DUPLICATING, { broadcast: true });
    TabsStore.addDuplicatingTab(tab);
  }
  else {
    // On old versions of Firefox, duplicated tabs had no openerTabId so they were
    // not handled by Tab.onCreating listener. Today they are already handled before
    // here, so this is just a failsafe (or for old versions of Firefox).
    // See also: https://github.com/piroor/treestyletab/issues/2830#issuecomment-831414189
    Tree.behaveAutoAttachedTab(tab, {
      baseTab:   original,
      behavior:  configs.autoAttachOnDuplicated,
      dontMove:  info.positionedBySelf || info.movedBySelfWhileCreation || info.mayBeReplacedWithContainer,
      broadcast: true
    });
  }
});

Tab.onUpdated.addListener((tab, changeInfo) => {
  if ('openerTabId' in changeInfo &&
      configs.syncParentTabAndOpenerTab &&
      !tab.$TST.updatingOpenerTabIds.includes(changeInfo.openerTabId) /* accept only changes from outside of TST */) {
    Tab.waitUntilTrackedAll(tab.windowId).then(() => {
      const parent = tab.$TST.openerTab;
      if (!parent ||
          parent.windowId != tab.windowId ||
          parent == tab.$TST.parent)
        return;
      Tree.attachTabTo(tab, parent, {
        insertAt:    Constants.kINSERT_NEAREST,
        forceExpand: tab.active,
        broadcast:   true
      });
    });
  }

  if (tab.$TST.temporaryMetadata.has('openedCompletely') &&
      tab.windowId == tab.$windowIdOnCreated && // Don't treat tab as "opened from active tab" if it is moved across windows while loading
      (changeInfo.url || changeInfo.status == 'complete') &&
      (tab.$TST.temporaryMetadata.has('isNewTab') ||
       tab.$TST.temporaryMetadata.has('fromExternal') ||
       tab.$TST.temporaryMetadata.has('anyOtherTrigger'))) {
    log('loaded tab ', dumpTab(tab), {
      isNewTab: tab.$TST.temporaryMetadata.has('isNewTab'),
      fromExternal: tab.$TST.temporaryMetadata.has('fromExternal'),
      anyOtherTrigger: tab.$TST.temporaryMetadata.has('anyOtherTrigger'),
    });
    tab.$TST.temporaryMetadata.delete('isNewTab');
    const possibleOpenerTab = Tab.get(tab.$TST.temporaryMetadata.get('possibleOpenerTab'));
    tab.$TST.temporaryMetadata.delete('possibleOpenerTab');
    log('possibleOpenerTab ', dumpTab(possibleOpenerTab));

    if (tab.$TST.temporaryMetadata.has('fromExternal')) {
      tab.$TST.temporaryMetadata.delete('fromExternal');
      log('behave as a tab opened from external application (delayed)');
      handleNewTabFromActiveTab(tab, {
        url:                           tab.url,
        activeTab:                     possibleOpenerTab,
        autoAttachBehavior:            configs.autoAttachOnOpenedFromExternal,
        inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromExternalMode,
        context:                       TSTAPI.kNEWTAB_CONTEXT_FROM_EXTERNAL,
      });
      return;
    }

    if (tab.$TST.temporaryMetadata.has('anyOtherTrigger')) {
      tab.$TST.temporaryMetadata.delete('anyOtherTrigger');
      log('behave as a tab opened from any other trigger (delayed)');
      handleNewTabFromActiveTab(tab, {
        url:                           tab.url,
        activeTab:                     possibleOpenerTab,
        autoAttachBehavior:            configs.autoAttachOnAnyOtherTrigger,
        inheritContextualIdentityMode: configs.inheritContextualIdentityToTabsFromAnyOtherTriggerMode,
        context:                       TSTAPI.kNEWTAB_CONTEXT_UNKNOWN,
      });
      return;
    }

    const win = TabsStore.windows.get(tab.windowId);
    log('win.openedNewTabs ', win.openedNewTabs);
    if (tab.$TST.parent ||
        !possibleOpenerTab ||
        win.openedNewTabs.has(tab.id) ||
        tab.$TST.temporaryMetadata.has('openedWithOthers') ||
        tab.$TST.temporaryMetadata.has('positionedBySelf')) {
      log(' => no need to control ', {
        parent: tab.$TST.parent,
        possibleOpenerTab,
        openedNewTab: win.openedNewTabs.has(tab.id),
        openedWithOthers: tab.$TST.temporaryMetadata.has('openedWithOthers'),
        positionedBySelf: tab.$TST.temporaryMetadata.has('positionedBySelf')
      });
      return;
    }

    if (tab.$TST.isNewTabCommandTab) {
      log('behave as a tab opened by new tab command (delayed)');
      tab.$TST.addState(Constants.kTAB_STATE_NEW_TAB_COMMAND_TAB);
      handleNewTabFromActiveTab(tab, {
        activeTab:                     possibleOpenerTab,
        autoAttachBehavior:            configs.autoAttachOnNewTabCommand,
        inheritContextualIdentityMode: configs.inheritContextualIdentityToChildTabMode,
        context:                       TSTAPI.kNEWTAB_CONTEXT_NEWTAB_COMMAND,
      });
      return;
    }

    const siteMatcher  = /^\w+:\/\/([^\/]+)(?:$|\/.*$)/;
    const openerTabSite = possibleOpenerTab.url.match(siteMatcher);
    const newTabSite    = tab.url.match(siteMatcher);
    if (openerTabSite &&
        newTabSite &&
        tab.url != possibleOpenerTab.url && // It may be opened by "Duplciate Tab" or "Open in New Container Tab" if the URL is completely same.
        openerTabSite[1] == newTabSite[1]) {
      log('behave as a tab opened from same site (delayed)');
      tab.$TST.addState(Constants.kTAB_STATE_OPENED_FOR_SAME_WEBSITE);
      handleNewTabFromActiveTab(tab, {
        url:                           tab.url,
        activeTab:                     possibleOpenerTab,
        autoAttachBehavior:            configs.autoAttachSameSiteOrphan,
        inheritContextualIdentityMode: configs.inheritContextualIdentityToSameSiteOrphanMode,
        context:                       TSTAPI.kNEWTAB_CONTEXT_WEBSITE_SAME_TO_ACTIVE_TAB,
      });
      return;
    }

    log('checking special openers (delayed)', { opener: possibleOpenerTab.url, child: tab.url });
    for (const rule of Constants.kAGGRESSIVE_OPENER_TAB_DETECTION_RULES_WITH_URL) {
      if (rule.opener.test(possibleOpenerTab.url) &&
          rule.child.test(tab.url)) {
        log('behave as a tab opened from special opener (delayed)', { rule });
        handleNewTabFromActiveTab(tab, {
          url:                tab.url,
          activeTab:          possibleOpenerTab,
          autoAttachBehavior: configs.autoAttachOnOpenedWithOwner,
          context:            TSTAPI.kNEWTAB_CONTEXT_FROM_ABOUT_ADDONS,
        });
        return;
      }
    }
  }
});


Tab.onAttached.addListener(async (tab, attachInfo = {}) => {
  if (!attachInfo.windowId)
    return;

  const parentTabOperationBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
    context:  Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
    ...attachInfo,
  });
  if (parentTabOperationBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
    return;

  log('Tabs.onAttached ', dumpTab(tab), attachInfo);

  log('descendants of attached tab: ', () => attachInfo.descendants.map(dumpTab));
  const movedTabs = await Tree.moveTabs(attachInfo.descendants, {
    destinationWindowId: tab.windowId,
    insertAfter:         tab
  });
  log('moved descendants: ', () => movedTabs.map(dumpTab));
  if (attachInfo.descendants.length == movedTabs.length) {
    await Tree.applyTreeStructureToTabs(
      [tab, ...movedTabs],
      attachInfo.structure
    );
  }
  else {
    for (const movedTab of movedTabs) {
      Tree.attachTabTo(movedTab, tab, {
        broadcast: true,
        dontMove:  true
      });
    }
  }
});
