/*
# 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,
  wait,
  mapAndFilterUniq,
  countMatched,
  configs
} from '/common/common.js';
import * as Constants from '/common/constants.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TreeBehavior from '/common/tree-behavior.js';

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

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

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


Tab.onRemoving.addListener(async (tab, removeInfo = {}) => {
  log('Tabs.onRemoving ', dumpTab(tab), removeInfo);
  if (removeInfo.isWindowClosing)
    return;

  let closeParentBehavior;
  let newParent;
  const successor = tab.$TST.possibleSuccessorWithDifferentContainer;
  if (successor) {
    log('override closeParentBehaior with kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD for different container successor ', successor);
    closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
    // When a new tab is created with a different container and this tab
    // is removed immediately before the new tab is completely handled,
    // TST fails to detect the new tab as the successor of this tab. Thus,
    // we treat the new tab as the successor - the first child of this
    // (actually not attached to this tab yet).
    if (successor && successor != tab.$TST.firstChild)
      newParent = successor;
  }
  else {
    closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
      context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
      ...removeInfo,
    });
    log('detected closeParentBehaior: ', closeParentBehavior);
  }

  if (closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
      tab.$TST.subtreeCollapsed)
    Tree.collapseExpandSubtree(tab, {
      collapsed: false,
      justNow:   true,
      broadcast: false // because the tab is going to be closed, broadcasted Tree.collapseExpandSubtree can be ignored.
    });

  const postProcessParams = {
    windowId:     tab.windowId,
    removedTab:   tab.$TST.export(true),
    structure:    TreeBehavior.getTreeStructureFromTabs([tab, ...tab.$TST.descendants], {
      full:                 true,
      keepParentOfRootTabs: true
    }),
    insertBefore: tab, // not firstChild, because the "tab" is disappeared from tree.
    parent:       tab.$TST.parent,
    newParent,
    children:     tab.$TST.children,
    descendants:  tab.$TST.descendants,
    nearestFollowingRootTab: tab.$TST.nearestFollowingRootTab,
    closeParentBehavior
  };

  if (tab.$TST.subtreeCollapsed) {
    tryGrantCloseTab(tab, closeParentBehavior).then(async granted => {
      if (!granted)
        return;
      log('Tabs.onRemoving: granted to close ', dumpTab(tab));
      handleRemovingPostProcess(postProcessParams)
    });
    // First we always need to detach children from the closing parent.
    // They will be processed again after confirmation.
    Tree.detachAllChildren(tab, {
      newParent,
      behavior:         Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN,
      dontExpand:       true,
      dontUpdateIndent: true,
      broadcast:        true
    });
  }
  else {
    await handleRemovingPostProcess(postProcessParams)
  }

  const win = TabsStore.windows.get(tab.windowId);
  if (!win.internalClosingTabs.has(tab.$TST.parentId))
    Tree.detachTab(tab, {
      dontUpdateIndent: true,
      dontSyncParentToOpenerTab: true,
      broadcast:        true
    });
});
async function handleRemovingPostProcess({ closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure } = {}) {
  log('handleRemovingPostProcess ', { closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure });
  if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
    await closeChildTabs(descendants, {
      triggerTab:        removedTab,
      originalStructure: structure
    });

  const replacedGroupTab = (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
    await TabsGroup.tryReplaceTabWithGroup(null, { windowId, parent, children, insertBefore, newParent }) :
    null;

  if (!replacedGroupTab) {
    await Tree.detachAllChildren(null, {
      windowId,
      parent,
      newParent,
      children,
      descendants,
      nearestFollowingRootTab,
      behavior:  closeParentBehavior,
      broadcast: true
    });
  }
}

async function tryGrantCloseTab(tab, closeParentBehavior) {
  log('tryGrantClose: ', { alreadyGranted: configs.grantedRemovingTabIds, closing: dumpTab(tab) });
  const alreadyGranted = configs.grantedRemovingTabIds.includes(tab.id);
  configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
  if (!tab || alreadyGranted) {
    log(' => no need to confirm');
    return true;
  }

  const self = tryGrantCloseTab;

  self.closingTabIds.push(tab.id);
  if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
    self.closingDescendantTabIds = self.closingDescendantTabIds
      .concat(TreeBehavior.getClosingTabsFromParent(tab).map(tab => tab.id));
    self.closingDescendantTabIds = Array.from(new Set(self.closingDescendantTabIds));
  }

  if (self.promisedGrantedToCloseTabs) {
    log(' => have promisedGrantedToCloseTabs');
    return self.promisedGrantedToCloseTabs;
  }

  self.closingTabWasActive = self.closingTabWasActive || tab.active;

  let shouldRestoreCount;
  self.promisedGrantedToCloseTabs = wait(250).then(async () => {
    log(' => confirmation with delay');
    const closingTabIds = new Set(self.closingTabIds);
    let allClosingTabs = new Set();
    allClosingTabs.add(tab);
    self.closingTabIds = Array.from(closingTabIds);
    self.closingDescendantTabIds = mapAndFilterUniq(self.closingDescendantTabIds, id => {
      if (closingTabIds.has(id))
        return undefined;
      const tab = Tab.get(id);
      if (tab) // ignore already closed tabs
        allClosingTabs.add(tab);
      return id;
    });
    allClosingTabs = Array.from(allClosingTabs);
    shouldRestoreCount = self.closingTabIds.length;
    const restorableClosingTabsCount = countMatched(
      allClosingTabs,
      tab => tab.url != 'about:blank' &&
             !tab.$TST.isNewTabCommandTab
    );
    log(' => restorableClosingTabsCount: ', restorableClosingTabsCount);
    if (restorableClosingTabsCount > 0) {
      log('tryGrantClose: show confirmation for ', allClosingTabs);
      return Background.confirmToCloseTabs(allClosingTabs.slice(1).map(tab => tab.$TST.sanitized), {
        windowId:   tab.windowId,
        messageKey: 'warnOnCloseTabs_fromOutside_message',
        titleKey:   'warnOnCloseTabs_fromOutside_title',
        minConfirmCount: 0
      });
    }
    return true;
  })
    .then(async (granted) => {
      log(' => granted: ', granted);
      // remove the closed tab itself because it is already closed!
      configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
      if (granted)
        return true;
      log(`tryGrantClose: not granted, restore ${shouldRestoreCount} tabs`);
      // this is required to wait until the closing tab is stored to the "recently closed" list
      wait(0).then(async () => {
        const restoredTabs = await Commands.restoreTabs(shouldRestoreCount);
        log('tryGrantClose: restored ', restoredTabs);
      });
      return false;
    });

  const granted = await self.promisedGrantedToCloseTabs;
  self.closingTabIds              = [];
  self.closingDescendantTabIds    = [];
  self.closingTabWasActive        = false;
  self.promisedGrantedToCloseTabs = null;
  return granted;
}
tryGrantCloseTab.closingTabIds              = [];
tryGrantCloseTab.closingDescendantTabIds    = [];
tryGrantCloseTab.closingTabWasActive        = false;
tryGrantCloseTab.promisedGrantedToCloseTabs = null;

async function closeChildTabs(tabs, { triggerTab, originalStructure } = {}) {
  //if (!fireTabSubtreeClosingEvent(parent, tabs))
  //  return;

  //markAsClosedSet([parent].concat(tabs));
  // close bottom to top!
  await TabsInternalOperation.removeTabs(tabs.slice(0).reverse(), { triggerTab, originalStructure });
  //fireTabSubtreeClosedEvent(parent, tabs);
}

Tab.onRemoved.addListener((tab, info) => {
  log('Tabs.onRemoved: removed ', dumpTab(tab));
  configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);

  if (info.isWindowClosing)
    return;

  // The removing tab may be attached to another tab or
  // other tabs may be attached to the removing tab.
  // We need to detach such relations always on this timing.
  if (info.oldChildren.length > 0) {
    Tree.detachAllChildren(tab, {
      children:  info.oldChildren,
      parent:    info.oldParent,
      behavior:  Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD,
      broadcast: true
    });
  }
  if (info.oldParent) {
    Tree.detachTab(tab, {
      dontSyncParentToOpenerTab: true,
      parent:    info.oldParent,
      broadcast: true
    });
  }
});

browser.windows.onRemoved.addListener(windowId  => {
  const win = TabsStore.windows.get(windowId);
  if (!win)
    return;
  configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => !win.tabs.has(id));
});


Tab.onDetached.addListener((tab, info = {}) => {
  log('Tabs.onDetached ', dumpTab(tab));
  let closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
    context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
    ...info
  });
  if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
    closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;

  const dontSyncParentToOpenerTab = info.trigger == 'tabs.onDetached';
  Tree.detachAllChildren(tab, {
    dontSyncParentToOpenerTab,
    behavior:  closeParentBehavior,
    broadcast: true
  });
  //reserveCloseRelatedTabs(toBeClosedTabs);
  Tree.detachTab(tab, {
    dontSyncParentToOpenerTab,
    dontUpdateIndent: true,
    broadcast:        true
  });
  //restoreTabAttributes(tab, backupAttributes);
});
