/*
# 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,
  wait,
  mapAndFilterUniq,
  configs,
  loadUserStyleRules,
  doProgressively,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Bookmark from '/common/bookmark.js';
import * as BrowserTheme from '/common/browser-theme.js';
import * as Constants from '/common/constants.js';
import * as ContextualIdentities from '/common/contextual-identities.js';
import * as Permissions from '/common/permissions.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsUpdate from '/common/tabs-update.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.js';

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

import * as Background from './background.js';
import * as Commands from './commands.js';
import * as Migration from './migration.js';
import * as TabsGroup from './tabs-group.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';

function log(...args) {
  internalLogger('background/handle-misc', ...args);
}


/* message observer */

// We cannot start listening of messages of browser.runtime.onMessage(External)
// at here and wait processing until promises are resolved like ApiTabsListener
// and BackgroundConnection, because making listeners asynchornous (async
// functions) will break things - those listeners must not return Promise for
// unneeded cases.
// So we simply ignore messages delivered before completely initialized, for now.
// See also: https://github.com/piroor/treestyletab/issues/2200

const PHASE_LOADING                = 0;
const PHASE_BACKGROUND_INITIALIZED = 1;
const PHASE_BACKGROUND_BUILT       = 2;
const PHASE_BACKGROUND_READY       = 3;
let mInitializationPhase = PHASE_LOADING;

Background.onInit.addListener(() => {
  mInitializationPhase = PHASE_BACKGROUND_INITIALIZED;
});

Background.onBuilt.addListener(() => {
  mInitializationPhase = PHASE_BACKGROUND_BUILT;
});

Background.onReady.addListener(() => {
  mInitializationPhase = PHASE_BACKGROUND_READY;
});

if (browser.sidebarAction)
  (browser.action || browser.browserAction)?.onClicked.addListener(onToolbarButtonClick);
browser.commands.onCommand.addListener(onShortcutCommand);
browser.runtime.onMessage.addListener(onMessage);
TSTAPI.onMessageExternal.addListener(onMessageExternal);


Background.onReady.addListener(() => {
  Bookmark.startTracking();
});

Background.onDestroy.addListener(() => {
  browser.runtime.onMessage.removeListener(onMessage);
  TSTAPI.onMessageExternal.removeListener(onMessageExternal);
  if (browser.sidebarAction)
    (browser.action || browser.browserAction)?.onClicked.removeListener(onToolbarButtonClick);
});


function onToolbarButtonClick(tab) {
  if (mInitializationPhase < PHASE_BACKGROUND_INITIALIZED ||
      Permissions.requestPostProcess()) {
    return;
  }

  if (Migration.isInitialStartup()) {
    Migration.openInitialStartupPage();
    return;
  }

  if (typeof browser.sidebarAction.toggle == 'function')
    browser.sidebarAction.toggle();
  else if (SidebarConnection.isSidebarOpen(tab.windowId))
    browser.sidebarAction.close();
  else
    browser.sidebarAction.open();
}

async function onShortcutCommand(command) {
  if (mInitializationPhase < PHASE_BACKGROUND_INITIALIZED)
    return;

  let activeTabs = command.tab ? [command.tab] : await browser.tabs.query({
    active:        true,
    currentWindow: true,
  }).catch(ApiTabs.createErrorHandler());
  if (activeTabs.length == 0)
    activeTabs = await browser.tabs.query({
      currentWindow: true,
    }).catch(ApiTabs.createErrorHandler());
  const activeTab = Tab.get(activeTabs[0].id);
  const selectedTabs = activeTab.$TST.multiselected ? Tab.getSelectedTabs(activeTab.windowId) : [activeTab];
  log('onShortcutCommand ', { command, activeTab, selectedTabs });

  switch (command) {
    case '_execute_browser_action':
      return;

    case 'reloadTree':
      Commands.reloadTree(selectedTabs);
      return;
    case 'reloadDescendants':
      Commands.reloadDescendants(selectedTabs);
      return;
    case 'toggleMuteTree':
      Commands.toggleMuteTree(selectedTabs);
      return;
    case 'toggleMuteDescendants':
      Commands.toggleMuteDescendants(selectedTabs);
      return;
    case 'closeTree':
      Commands.closeTree(selectedTabs);
      return;
    case 'closeDescendants':
      Commands.closeDescendants(selectedTabs);
      return;
    case 'closeOthers':
      Commands.closeOthers(selectedTabs);
      return;
    case 'toggleSticky':
      Commands.toggleSticky(selectedTabs);
      return;
    case 'collapseTree':
      Commands.collapseTree(selectedTabs);
      return;
    case 'collapseTreeRecursively':
      Commands.collapseTree(selectedTabs, { recursively: true });
      return;
    case 'collapseAll':
      Commands.collapseAll(activeTab.windowId);
      return;
    case 'expandTree':
      Commands.expandTree(selectedTabs);
      return;
    case 'expandTreeRecursively':
      Commands.expandTree(selectedTabs, { recursively: true });
      return;
    case 'expandAll':
      Commands.expandAll(activeTab.windowId);
      return;
    case 'bookmarkTree':
      Commands.bookmarkTree(selectedTabs);
      return;

    case 'newIndependentTab':
      Commands.openNewTabAs({
        baseTab: activeTab,
        as:      Constants.kNEWTAB_OPEN_AS_ORPHAN
      });
      return;
    case 'newChildTab':
      Commands.openNewTabAs({
        baseTab: activeTab,
        as:      Constants.kNEWTAB_OPEN_AS_CHILD
      });
      return;
    case 'newChildTabTop':
      Commands.openNewTabAs({
        baseTab: activeTab,
        as:      Constants.kNEWTAB_OPEN_AS_CHILD_TOP
      });
      return;
    case 'newChildTabEnd':
      Commands.openNewTabAs({
        baseTab: activeTab,
        as:      Constants.kNEWTAB_OPEN_AS_CHILD_END
      });
      return;
    case 'newSiblingTab':
      Commands.openNewTabAs({
        baseTab: activeTab,
        as:      Constants.kNEWTAB_OPEN_AS_SIBLING
      });
      return;
    case 'newNextSiblingTab':
      Commands.openNewTabAs({
        baseTab: activeTab,
        as:      Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING
      });
      return;

    case 'newContainerTab':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SHOW_CONTAINER_SELECTOR,
        windowId: activeTab.windowId
      });
      return;

    case 'tabMoveUp':
      Commands.moveUp(activeTab, { followChildren: false });
      return;
    case 'treeMoveUp':
      Commands.moveUp(activeTab, { followChildren: true });
      return;
    case 'tabMoveDown':
      Commands.moveDown(activeTab, { followChildren: false });
      return;
    case 'treeMoveDown':
      Commands.moveDown(activeTab, { followChildren: true });
      return;

    case 'focusPrevious':
      focusPrevious(activeTab);
      return;
    case 'focusPreviousSilently':
      focusPreviousSilently(activeTab);
      return;
    case 'focusNext':
      focusNext(activeTab);
      return;
    case 'focusNextSilently':
      focusNextSilently(activeTab);
      return;
    case 'focusParent':
      TabsInternalOperation.activateTab(activeTab.$TST.parent);
      return;
    case 'focusParentOrCollapse':
      collapseOrFocusToParent(activeTab);
      return;
    case 'focusFirstChild':
      TabsInternalOperation.activateTab(activeTab.$TST.firstChild);
      return;
    case 'focusFirstChildOrExpand':
      expandOrFocusToFirstChild(activeTab);
      return;
    case 'focusLastChild':
      TabsInternalOperation.activateTab(activeTab.$TST.lastChild);
      return;
    case 'focusPreviousSibling':
      TabsInternalOperation.activateTab(
        activeTab.$TST.previousSiblingTab ||
          (activeTab.$TST.parent ?
            activeTab.$TST.parent.$TST.lastChild :
            Tab.getLastRootTab(activeTab.windowId))
      );
      return;
    case 'focusNextSibling':
      TabsInternalOperation.activateTab(
        activeTab.$TST.nextSiblingTab ||
          (activeTab.$TST.parent ?
            activeTab.$TST.parent.$TST.firstChild :
            Tab.getFirstVisibleTab(activeTab.windowId))
      );
      return;

    case 'simulateUpOnTree':
      if (SidebarConnection.isOpen(activeTab.windowId)) {
        if (configs.faviconizePinnedTabs &&
            (activeTab.pinned ||
             activeTab == Tab.getFirstNormalTab(activeTab.windowId))) {
          const nextActiveId = await browser.runtime.sendMessage({
            type:     Constants.kCOMMAND_GET_ABOVE_TAB,
            windowId: activeTab.windowId,
            tabId:    activeTab.id,
          });
          log(`simulateUpOnTree: nextActiveId = ${nextActiveId}`);
          const nextActive = (
            Tab.get(nextActiveId) ||
            Tab.getLastVisibleTab(activeTab.windowId)
          );
          TabsInternalOperation.activateTab(nextActive, {
            silently: true,
          });
        }
        else {
          focusPreviousSilently(activeTab);
        }
      }
      else {
        focusPrevious(activeTab);
      }
      return;
    case 'simulateDownOnTree':
      if (SidebarConnection.isOpen(activeTab.windowId)) {
        if (configs.faviconizePinnedTabs &&
            activeTab.pinned) {
          const nextActiveId = await browser.runtime.sendMessage({
            type:     Constants.kCOMMAND_GET_BELOW_TAB,
            windowId: activeTab.windowId,
            tabId:    activeTab.id,
          });
          log(`simulateDownOnTree: nextActiveId = ${nextActiveId}`);
          const nextActive = (
            Tab.get(nextActiveId) ||
            Tab.getFirstNormalTab(activeTab.windowId)
          );
          TabsInternalOperation.activateTab(nextActive, {
            silently: true,
          });
        }
        else {
          focusNextSilently(activeTab);
        }
      }
      else {
        focusNext(activeTab);
      }
      return;
    case 'simulateLeftOnTree':
      if (SidebarConnection.isOpen(activeTab.windowId)) {
        if (configs.faviconizePinnedTabs &&
            activeTab.pinned) {
          const nextActiveId = await browser.runtime.sendMessage({
            type:     Constants.kCOMMAND_GET_LEFT_TAB,
            windowId: activeTab.windowId,
            tabId:    activeTab.id,
          });
          log(`simulateLeftOnTree: nextActiveId = ${nextActiveId}`);
          TabsInternalOperation.activateTab(Tab.get(nextActiveId), {
            silently: true,
          });
        }
        else if (await isSidebarRightSide(activeTab.windowId)) {
          expandOrFocusToFirstChild(activeTab);
        }
        else {
          collapseOrFocusToParent(activeTab);
        }
      }
      else {
        focusPrevious(activeTab);
      }
      return;
    case 'simulateRightOnTree':
      if (SidebarConnection.isOpen(activeTab.windowId)) {
        if (configs.faviconizePinnedTabs &&
            activeTab.pinned) {
          const nextActiveId = await browser.runtime.sendMessage({
            type:     Constants.kCOMMAND_GET_RIGHT_TAB,
            windowId: activeTab.windowId,
            tabId:    activeTab.id,
          });
          log(`simulateRightOnTree: nextActiveId = ${nextActiveId}`);
          TabsInternalOperation.activateTab(Tab.get(nextActiveId), {
            silently: true,
          });
        }
        else if (await isSidebarRightSide(activeTab.windowId)) {
          collapseOrFocusToParent(activeTab);
        }
        else {
          expandOrFocusToFirstChild(activeTab);
        }
      }
      else {
        focusNext(activeTab);
      }
      return;

    case 'tabbarUp':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SCROLL_TABBAR,
        windowId: activeTab.windowId,
        by:       'lineup'
      });
      return;
    case 'tabbarPageUp':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SCROLL_TABBAR,
        windowId: activeTab.windowId,
        by:       'pageup'
      });
      return;
    case 'tabbarHome':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SCROLL_TABBAR,
        windowId: activeTab.windowId,
        to:       'top'
      });
      return;

    case 'tabbarDown':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SCROLL_TABBAR,
        windowId: activeTab.windowId,
        by:       'linedown'
      });
      return;
    case 'tabbarPageDown':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SCROLL_TABBAR,
        windowId: activeTab.windowId,
        by:       'pagedown'
      });
      return;
    case 'tabbarEnd':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SCROLL_TABBAR,
        windowId: activeTab.windowId,
        to:       'bottom'
      });
      return;

    case 'toggleTreeCollapsed':
      if (activeTab.$TST.subtreeCollapsed)
        Commands.expandTree(selectedTabs);
      else
        Commands.collapseTree(selectedTabs);
      return;
    case 'toggleTreeCollapsedRecursively':
      if (activeTab.$TST.subtreeCollapsed)
        Commands.expandTree(selectedTabs, { recursively: true });
      else
        Commands.collapseTree(selectedTabs, { recursively: true });
      return;

    case 'toggleSubPanel':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_TOGGLE_SUBPANEL,
        windowId: activeTab.windowId
      });
      return;
    case 'switchSubPanel':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_SWITCH_SUBPANEL,
        windowId: activeTab.windowId
      });
      return;
    case 'increaseSubPanel':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_INCREASE_SUBPANEL,
        windowId: activeTab.windowId
      });
      return;
    case 'decreaseSubPanel':
      SidebarConnection.sendMessage({
        type:     Constants.kCOMMAND_DECREASE_SUBPANEL,
        windowId: activeTab.windowId
      });
      return;
  }
}

function focusPrevious(activeTab) {
  const nextActive = activeTab.$TST.nearestVisiblePrecedingTab ||
    (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.previousTab) ||
    Tab.getLastVisibleTab(activeTab.windowId);
  TabsInternalOperation.activateTab(nextActive);
}

function focusPreviousSilently(activeTab) {
  const nextActive = activeTab.$TST.nearestVisiblePrecedingTab ||
    (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.previousTab) ||
    Tab.getLastVisibleTab(activeTab.windowId);
  TabsInternalOperation.activateTab(nextActive, {
    silently: true,
  });
}

function focusNext(activeTab) {
  const nextActive = activeTab.$TST.nearestVisibleFollowingTab ||
    (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.nextTab) ||
    Tab.getFirstVisibleTab(activeTab.windowId);
  TabsInternalOperation.activateTab(nextActive);
}

function focusNextSilently(activeTab) {
  const nextActive = activeTab.$TST.nearestVisibleFollowingTab ||
    (!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.nextTab) ||
    Tab.getFirstVisibleTab(activeTab.windowId);
  TabsInternalOperation.activateTab(nextActive, {
    silently: true,
  });
}

async function isSidebarRightSide(windowId) {
  const position = await browser.runtime.sendMessage({
    type: Constants.kCOMMAND_GET_SIDEBAR_POSITION,
    windowId,
  });
  return position == Constants.kTABBAR_POSITION_RIGHT;
}

function collapseOrFocusToParent(activeTab) {
  if (!activeTab.$TST.subtreeCollapsed && activeTab.$TST.hasChild)
    Commands.collapseTree(activeTab);
  else
    TabsInternalOperation.activateTab(activeTab.$TST.parent);
}

function expandOrFocusToFirstChild(activeTab) {
  if (activeTab.$TST.subtreeCollapsed && activeTab.$TST.hasChild)
    Commands.expandTree(activeTab);
  else
    TabsInternalOperation.activateTab(activeTab.$TST.firstChild, {
      silently: true,
    });
}

// This must be synchronous and return Promise on demando, to avoid
// blocking to other listeners.
function onMessage(message, sender) {
  if (mInitializationPhase < PHASE_BACKGROUND_BUILT ||
      !message ||
      typeof message.type != 'string' ||
      message.type.indexOf('treestyletab:') != 0)
    return;

  //log('onMessage: ', message, sender);
  switch (message.type) {
    case Constants.kCOMMAND_GET_INSTANCE_ID:
      return Promise.resolve(Background.instanceId);

    case Constants.kCOMMAND_RELOAD:
      return Background.reload({ all: message.all });

    case Constants.kCOMMAND_REQUEST_UNIQUE_ID:
      return (async () => {
        if (!Tab.get(message.tabId))
          await Tab.waitUntilTracked(message.tabId);
        const tab = Tab.get(message.tabId);
        return tab ? tab.$TST.promisedUniqueId : {};
      })();

    case Constants.kCOMMAND_GET_THEME_DECLARATIONS:
      return browser.theme.getCurrent().then(theme => BrowserTheme.generateThemeDeclarations(theme));

    case Constants.kCOMMAND_GET_CONTEXTUAL_IDENTITIES_COLOR_INFO:
      return Promise.resolve(ContextualIdentities.getColorInfo());

    case Constants.kCOMMAND_GET_CONFIG_VALUE:
      if (Array.isArray(message.keys)) {
        const values = {};
        for (const key of message.keys) {
          values[key] = configs[key];
        }
        return Promise.resolve(values);
      }
      return Promise.resolve(configs[message.key]);

    case Constants.kCOMMAND_SET_CONFIG_VALUE:
      return Promise.resolve(configs[message.key] = message.value);

    case Constants.kCOMMAND_GET_USER_STYLE_RULES:
      return Promise.resolve(loadUserStyleRules());

    case Constants.kCOMMAND_PING_TO_BACKGROUND: // return tabs as the pong, to optimizie further initialization tasks in the sidebar
      TabsUpdate.completeLoadingTabs(message.windowId); // don't wait here for better perfomance
      return Promise.resolve(TabsStore.windows.get(message.windowId).export(true));

    case Constants.kCOMMAND_PULL_TABS:
      if (message.windowId) {
        TabsUpdate.completeLoadingTabs(message.windowId); // don't wait here for better perfomance
        return Promise.resolve(TabsStore.windows.get(message.windowId).export(true).tabs);
      }
      return Promise.resolve(message.tabIds.map(id => {
        const tab = Tab.get(id);
        return tab?.$TST.export(true);
      }));

    case Constants.kCOMMAND_PULL_TABS_ORDER:
      return Promise.resolve(TabsStore.windows.get(message.windowId).order);

    case Constants.kCOMMAND_PULL_TREE_STRUCTURE:
      return (async () => {
        while (mInitializationPhase < PHASE_BACKGROUND_READY) {
          await wait(10);
        }
        const structure = TreeBehavior.getTreeStructureFromTabs(
          message.windowId ?
            Tab.getAllTabs(message.windowId) :
            message.tabIds.map(id => Tab.get(id))
        );
        return { structure };
      })();

    case Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED:
      return (async () => {
        const grantedPermission = JSON.stringify(message.permissions);
        if (grantedPermission == JSON.stringify(Permissions.ALL_URLS)) {
          const tabs = await browser.tabs.query({}).catch(ApiTabs.createErrorHandler());
          await Tab.waitUntilTracked(tabs.map(tab => tab.id));
          for (const tab of tabs) {
            Background.tryStartHandleAccelKeyOnTab(Tab.get(tab.id));
          }
        }
        else if (grantedPermission == JSON.stringify(Permissions.BOOKMARKS)) {
          Migration.migrateBookmarkUrls();
          Bookmark.startTracking();
        }
      })();

    case Constants.kCOMMAND_SIMULATE_SIDEBAR_MESSAGE: {
      SidebarConnection.onMessage.dispatch(message.message.windowId, message.message);
    }; break;

    case Constants.kCOMMAND_CONFIRM_TO_CLOSE_TABS:
      log('kCOMMAND_CONFIRM_TO_CLOSE_TABS: ', { message });
      return Background.confirmToCloseTabs(message.tabs, message);

    default:
      if (TSTAPI.INTERNAL_CALL_PREFIX_MATCHER.test(message.type)) {
        return onMessageExternal({
          ...message,
          type: message.type.replace(TSTAPI.INTERNAL_CALL_PREFIX_MATCHER, ''),
        }, sender);
      }
      break;
  }
}

// This must be synchronous and return Promise on demando, to avoid
// blocking to other listeners.
function onMessageExternal(message, sender) {
  if (mInitializationPhase < PHASE_BACKGROUND_INITIALIZED)
    return;

  switch (message.type) {
    case TSTAPI.kGET_VERSION:
      return Promise.resolve(browser.runtime.getManifest().version);

    case TSTAPI.kGET_TREE:
      return (async () => {
        const tabs = await (message.rendered ?
          TSTAPI.getTargetRenderedTabs(message, sender) :
          TSTAPI.getTargetTabs(message, sender));
        const cache = {};
        const treeItems = Array.from(tabs, tab => TSTAPI.exportTab(tab, {
          interval: message.interval,
          cache,
        }));
        const result = TSTAPI.formatTabResult(
          treeItems,
          {
            ...message,
            // This must return an array of root tabs if just the window id is specified.
            // See also: https://github.com/piroor/treestyletab/issues/2763
            ...((message.window || message.windowId) && !message.tab && !message.tabs ? { tab: '*' } : {})
          },
          sender.id
        );
        TSTAPI.clearCache(cache);
        return result;
      })();

    case TSTAPI.kGET_LIGHT_TREE:
      return (async () => {
        const tabs = await (message.rendered ?
          TSTAPI.getTargetRenderedTabs(message, sender) :
          TSTAPI.getTargetTabs(message, sender));
        const cache = {};
        const treeItems = Array.from(tabs, tab => TSTAPI.exportTab(tab, {
          light:    true,
          interval: message.interval,
          cache,
        }));
        const result = TSTAPI.formatTabResult(
          treeItems,
          {
            ...message,
            // This must return an array of root tabs if just the window id is specified.
            // See also: https://github.com/piroor/treestyletab/issues/2763
            ...((message.window || message.windowId) && !message.tab && !message.tabs ? { tab: '*' } : {})
          },
          sender.id
        );
        TSTAPI.clearCache(cache);
        return result;
      })();

    case TSTAPI.kSTICK_TAB:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await doProgressively(
          tabs,
          tab => Commands.toggleSticky(tab, true),
          message.interval
        );
        return true;
      })();

    case TSTAPI.kUNSTICK_TAB:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await doProgressively(
          tabs,
          tab => Commands.toggleSticky(tab, false),
          message.interval
        );
        return true;
      })();

    case TSTAPI.kTOGGLE_STICKY_STATE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        let firstTabIsSticky = undefined;
        await doProgressively(
          tabs,
          tab => {
            if (firstTabIsSticky === undefined)
              firstTabIsSticky = tab.$TST.sticky;
            Commands.toggleSticky(tab, !firstTabIsSticky);
          },
          message.interval
        );
        return true;
      })();

    case TSTAPI.kCOLLAPSE_TREE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await doProgressively(
          tabs,
          tab => Commands.collapseTree(tab, {
            recursively: !!message.recursively
          }),
          message.interval
        );
        return true;
      })();

    case TSTAPI.kEXPAND_TREE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await doProgressively(
          tabs,
          tab => Commands.expandTree(tab, {
            recursively: !!message.recursively
          }),
          message.interval
        );
        return true;
      })();

    case TSTAPI.kTOGGLE_TREE_COLLAPSED:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await doProgressively(
          tabs,
          tab => Tree.collapseExpandSubtree(tab, {
            collapsed: !tab.$TST.subtreeCollapsed,
            broadcast: true
          }),
          message.interval
        );
        return true;
      })();

    case TSTAPI.kATTACH:
      return (async () => {
        await Tab.waitUntilTracked([
          message.child,
          message.parent,
          message.insertBefore,
          message.insertAfter
        ]);
        const child  = Tab.get(message.child);
        const parent = Tab.get(message.parent);
        if (!child ||
            !parent ||
            child.windowId != parent.windowId)
          return false;
        await Tree.attachTabTo(child, parent, {
          broadcast:    true,
          insertBefore: Tab.get(message.insertBefore),
          insertAfter:  Tab.get(message.insertAfter)
        });
        if (child.$TST.collapsed &&
            !parent.$TST.collapsed &&
            !parent.$TST.subtreeCollapsed) {
          await Tree.collapseExpandTabAndSubtree(child, {
            collapsed: false,
            bradcast:  true
          });
        }
        return true;
      })();

    case TSTAPI.kDETACH:
      return (async () => {
        await Tab.waitUntilTracked(message.tab);
        const tab = Tab.get(message.tab);
        if (!tab)
          return false;
        await Tree.detachTab(tab, {
          broadcast: true
        });
        if (tab.$TST.collapsed) {
          await Tree.collapseExpandTabAndSubtree(tab, {
            collapsed: false,
            bradcast:  true
          });
        }
        return true;
      })();

    case TSTAPI.kINDENT:
    case TSTAPI.kDEMOTE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const results = await doProgressively(
          tabs,
          tab => Commands.indent(tab, message),
          message.interval
        );
        return TSTAPI.formatResult(results, message);
      })();

    case TSTAPI.kOUTDENT:
    case TSTAPI.kPROMOTE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const results = await doProgressively(
          tabs,
          tab => Commands.outdent(tab, message),
          message.interval
        );
        return TSTAPI.formatResult(results, message);
      })();

    case TSTAPI.kMOVE_UP:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const results = await doProgressively(
          tabs,
          tab => Commands.moveUp(tab, message),
          message.interval
        );
        return TSTAPI.formatResult(results, message);
      })();

    case TSTAPI.kMOVE_TO_START:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await Commands.moveTabsToStart(Array.from(tabs));
        return true;
      })();

    case TSTAPI.kMOVE_DOWN:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const results = await doProgressively(
          tabs,
          tab => Commands.moveDown(tab, message),
          message.interval
        );
        return TSTAPI.formatResult(results, message);
      })();

    case TSTAPI.kMOVE_TO_END:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await Commands.moveTabsToEnd(Array.from(tabs));
        return true;
      })();

    case TSTAPI.kMOVE_BEFORE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const results = await doProgressively(
          tabs,
          tab => Commands.moveBefore(tab, message),
          message.interval
        );
        return TSTAPI.formatResult(results, message);
      })();

    case TSTAPI.kMOVE_AFTER:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const results = await doProgressively(
          tabs,
          tab => Commands.moveAfter(tab, message),
          message.interval
        );
        return TSTAPI.formatResult(results, message);
      })();

    case TSTAPI.kFOCUS:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const tabsArray = await doProgressively(
          tabs,
          tab => {
            TabsInternalOperation.activateTab(tab, {
              silently: message.silently
            });
            return tab;
          },
          message.interval
        );
        return TSTAPI.formatResult(tabsArray.map(() => true), message);
      })();

    case TSTAPI.kCREATE:
      return (async () => {
        const windowId = message.params.windowId;
        const win = TabsStore.windows.get(windowId);
        if (!win)
          throw new Error(`invalid windowId ${windowId}: it must be valid window id`);
        win.bypassTabControlCount++;
        const tab = await TabsOpen.openURIInTab(message.params, { windowId });
        return TSTAPI.exportTab(tab, { addonId: sender.id });
      })();

    case TSTAPI.kDUPLICATE:
      return (async () => {
        const tabs   = await TSTAPI.getTargetTabs(message, sender);
        let behavior = configs.autoAttachOnDuplicated;
        switch (String(message.as || 'sibling').toLowerCase()) {
          case 'child':
            behavior = behavior == Constants.kNEWTAB_OPEN_AS_CHILD_TOP ?
              Constants.kNEWTAB_OPEN_AS_CHILD_TOP :
              behavior == Constants.kNEWTAB_OPEN_AS_CHILD_END ?
                Constants.kNEWTAB_OPEN_AS_CHILD_END :
                Constants.kNEWTAB_OPEN_AS_CHILD;
            break;
          case 'first-child':
            behavior = Constants.kNEWTAB_OPEN_AS_CHILD_TOP;
            break;
          case 'last-child':
            behavior = Constants.kNEWTAB_OPEN_AS_CHILD_END;
            break;
          case 'sibling':
            behavior = Constants.kNEWTAB_OPEN_AS_SIBLING;
            break;
          case 'nextsibling':
            behavior = Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING;
            break;
          case 'orphan':
            behavior = Constants.kNEWTAB_OPEN_AS_ORPHAN;
            break;
          default:
            break;
        }
        const tabsArray = await doProgressively(
          tabs,
          async tab => {
            return Commands.duplicateTab(tab, {
              destinationWindowId: tab.windowId,
              behavior,
              multiselected: false
            });
          },
          message.interval
        );
        return TSTAPI.formatResult(tabsArray.map(() => true), message);
      })();

    case TSTAPI.kGROUP_TABS:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const temporaryStateParams = (message.temporary && !message.temporaryAggressive) ?
          {
            temporary:           true,
            temporaryAggressive: false,
          } :
          (!message.temporary && message.temporaryAggressive) ?
            {
              temporary:           false,
              temporaryAggressive: true,
            } :
            (message.temporaryAggressive === false && message.temporary === false) ?
              {
                temporary:           false,
                temporaryAggressive: false,
              } :
              {};
        const tab = await TabsGroup.groupTabs(Array.from(tabs), {
          title:     message.title,
          broadcast: true,
          ...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForAPI),
          ...temporaryStateParams,
        });
        if (!tab)
          return null;
        return TSTAPI.exportTab(tab, { addonId: sender.id });
      })();

    case TSTAPI.kOPEN_IN_NEW_WINDOW:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const windowId = await Commands.openTabsInWindow(Array.from(tabs), {
          multiselected: false
        });
        return windowId;
      })();

    case TSTAPI.kREOPEN_IN_CONTAINER:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        const reopenedTabs = await Commands.reopenInContainer(
          Array.from(tabs),
          message.containerId || 'firefox-default'
        );
        const cache = {};
        const result = await TSTAPI.formatTabResult(
          reopenedTabs.map(tab => TSTAPI.exportTab(tab, {
            interval: message.interval,
            addonId:  sender.id,
            cache,
          })),
          message
        );
        TSTAPI.clearCache(cache);
        return result;
      })();

    case TSTAPI.kGET_TREE_STRUCTURE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        return Promise.resolve(TreeBehavior.getTreeStructureFromTabs(Array.from(tabs)));
      })();

    case TSTAPI.kSET_TREE_STRUCTURE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        await Tree.applyTreeStructureToTabs(
          Array.from(tabs),
          message.structure,
          { broadcast: true }
        );
        return Promise.resolve(true);
      })();

    case TSTAPI.kADD_TAB_STATE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        let states = message.state || message.states;
        if (!Array.isArray(states))
          states = [states];
        states = mapAndFilterUniq(states, state => state && String(state) || undefined);
        if (states.length == 0)
          return true;
        const tabsArray = await doProgressively(
          tabs,
          tab => {
            for (const state of states) {
              tab.$TST.addState(state);
            }
            return tab;
          },
          message.interval
        );
        Tab.broadcastState(tabsArray, {
          add: states
        });
        return true;
      })();

    case TSTAPI.kREMOVE_TAB_STATE:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        let states = message.state || message.states;
        if (!Array.isArray(states))
          states = [states];
        states = mapAndFilterUniq(states, state => state && String(state) || undefined);
        if (states.length == 0)
          return true;
        const tabsArray = await doProgressively(
          tabs,
          tab => {
            for (const state of states) {
              tab.$TST.removeState(state);
            }
            return tab;
          },
          message.interval
        );
        Tab.broadcastState(tabsArray, {
          remove: states
        });
        return true;
      })();

    case TSTAPI.kGRANT_TO_REMOVE_TABS:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        configs.grantedRemovingTabIds = mapAndFilterUniq(configs.grantedRemovingTabIds.concat(tabs), tab => {
          tab = TabsStore.ensureLivingItem(tab);
          return tab?.id || undefined;
        });
        return true;
      })();

    case TSTAPI.kSTART_CUSTOM_DRAG:
      return (async () => {
        SidebarConnection.sendMessage({
          type:     Constants.kNOTIFY_TAB_MOUSEDOWN_EXPIRED,
          windowId: message.windowId || (await browser.windows.getLastFocused({ populate: false }).catch(ApiTabs.createErrorHandler())).id,
          button:   message.button || 0
        });
      })();

    case TSTAPI.kOPEN_ALL_BOOKMARKS_WITH_STRUCTURE:
      return Commands.openAllBookmarksWithStructure(
        message.id || message.bookmarkId,
        { discarded: message.discarded }
      );

    case TSTAPI.kSET_TOOLTIP_TEXT:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        for (const tab of tabs) {
          tab.$TST.registerTooltipText(sender.id, message.text || '', !!message.force);
        }
        return true;
      })();

    case TSTAPI.kCLEAR_TOOLTIP_TEXT:
      return (async () => {
        const tabs = await TSTAPI.getTargetTabs(message, sender);
        for (const tab of tabs) {
          tab.$TST.unregisterTooltipText(sender.id);
        }
        return true;
      })();

    case TSTAPI.kREGISTER_AUTO_STICKY_STATES:
      TreeItem.registerAutoStickyState(sender.id, message.state || message.states);
      break;

    case TSTAPI.kUNREGISTER_AUTO_STICKY_STATES:
      TreeItem.unregisterAutoStickyState(sender.id, message.state || message.states);
      break;
  }
}

Tab.onStateChanged.addListener((tab, state, added) => {
  switch (state) {
    case Constants.kTAB_STATE_STICKY:
      if (TSTAPI.hasListenerForMessageType(TSTAPI.kNOTIFY_TAB_STICKY_STATE_CHANGED)) {
        TSTAPI.broadcastMessage({
          type:   TSTAPI.kNOTIFY_TAB_STICKY_STATE_CHANGED,
          tab,
          sticky: !!added,
        }, { tabProperties: ['tab'] }).catch(_error => {});
      }
      break;
  }
});
