/*
# 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 EventListenerManager from '/extlib/EventListenerManager.js';

import {
  log as internalLogger,
  dumpTab,
  configs
} from './common.js';

import * as TabsStore from './tabs-store.js';

function log(...args) {
  internalLogger('common/Window', ...args);
}

export default class Window {
  constructor(windowId, tabGroups) {
    const alreadyTracked = TabsStore.windows.get(windowId);
    if (alreadyTracked)
      return alreadyTracked;

    log(`window ${windowId} is newly tracked`);

    this.id    = windowId;
    this.tabs  = new Map();
    this.tabGroups = new Map();
    if (tabGroups) {
      this.initTabGroups(tabGroups);
    }
    this.order = [];

    this.containerElement = null;
    this.containerClassList = null;
    this.pinnedContainerElement = null;

    this.internalMovingTabs  = new Map();
    this.alreadyMovedTabs    = new Map();
    this.internalClosingTabs = new Set();
    this.keepDescendantsTabs = new Set();
    this.highlightingTabs    = new Set();
    this.tabsToBeHighlightedAlone = new Set();
    this.internallyMovingTabsForUpdatedNativeTabGroups = new Set();

    this.subTreeMovingCount =
      this.subTreeChildrenMovingCount =
      this.doingIntelligentlyCollapseExpandCount =
      this.duplicatingTabsCount = 0;

    this.internallyFocusingTabs = new Set();
    this.internallyFocusingByMouseTabs = new Set();
    this.internallyFocusingSilentlyTabs = new Set();

    this.preventToDetectTabBunchesUntil = Date.now() + configs.tabBunchesDetectionDelayOnNewWindow;

    this.openingTabs   = new Set();

    this.openedNewTabs = new Map();

    this.bypassTabControlCount       = 0;
    this.toBeOpenedNewTabCommandTab  = 0;
    this.toBeOpenedTabsWithPositions = 0;
    this.toBeOpenedTabsWithCookieStoreId = 0;
    this.toBeOpenedOrphanTabs        = 0;

    this.toBeAttachedTabs = new Set();
    this.toBeDetachedTabs = new Set();

    this.lastRelatedTabs = new Map();
    this.previousLastRelatedTabs = new Map();

    TabsStore.windows.set(windowId, this);
    TabsStore.prepareIndexesForWindow(windowId);

    // We should initialize private properties with blank value for better performance with a fixed shape.
    this.delayedDestroy = null;
  }
  initTabGroups(tabGroups) {
    log(`initializing tabGroups of window ${this.id}: `, tabGroups);
    this.tabGroups = new Map((tabGroups || []).map(group => [group.id, group]));
  }

  destroy() {
    for (const tab of this.tabs.values()) {
      if (tab.$TST)
        tab.$TST.destroy();
    }
    this.tabs.clear();
    this.tabGroups.clear();
    TabsStore.windows.delete(this.id);
    TabsStore.unprepareIndexesForWindow(this.id);

    if (this.containerElement) {
      const element = this.containerElement;
      if (element.parentNode && !element.hasChildNodes())
        element.parentNode.removeChild(element);
    }
    if (this.pinnedContainerElement) {
      const element = this.element;
      if (element.parentNode && !element.hasChildNodes())
        element.parentNode.removeChild(element);
    }
    this.unbindElements();

    this.tabs = null;
    this.tabGroups = null;
    this.order = null;
    this.id = null;
  }

  clear() {
    this.tabs.clear();
    this.order = [];
    TabsStore.unprepareIndexesForWindow(this.id);
    TabsStore.prepareIndexesForWindow(this.id);
    this.clearLastRelatedTabs();
  }

  clearLastRelatedTabs() {
    for (const openerId of this.lastRelatedTabs.keys()) {
      const opener = this.tabs.get(openerId);
      if (!opener)
        continue;
      opener.$TST.newRelatedTabsCount = 0;
    }
    this.lastRelatedTabs.clear();
    this.previousLastRelatedTabs.clear();
  }

  bindContainerElement(element) {
    this.containerElement = element;
    this.containerClassList = element.classList;
  }

  bindPinnedContainerElement(element) {
    this.pinnedContainerElement = element;
    this.pinnedContainerClassList = element.classList;
  }

  unbindElements() {
    this.containerElement = null;
    this.containerClassList = null;
    this.pinnedContainerElement = null;
    this.pinnedContainerClassList = null;
  }

  getOrderedTabs(startId, endId, tabs) {
    const orderedIds = this.sliceOrder(startId, endId);
    tabs = tabs || this.tabs;
    return (function*() {
      for (const id of orderedIds) {
        const tab = tabs.get(id);
        if (tab)
          yield tab;
      }
    }).call(this);
  }

  getReversedOrderedTabs(startId, endId, tabs) {
    const orderedIds = this.sliceOrder(startId, endId, this.order.slice(0).reverse());
    tabs = tabs || this.tabs;
    return (function*() {
      for (const id of orderedIds) {
        const tab = tabs.get(id);
        if (tab)
          yield tab;
      }
    }).call(this);
  }

  sliceOrder(startId, endId, orderedIds) {
    if (!orderedIds)
      orderedIds = this.order;
    if (startId) {
      if (!this.tabs.has(startId))
        return [];
      orderedIds = orderedIds.slice(orderedIds.indexOf(startId));
    }
    if (endId) {
      if (!this.tabs.has(endId))
        return [];
      orderedIds = orderedIds.slice(0, orderedIds.indexOf(endId) + 1);
    }
    return orderedIds;
  }

  trackTab(tab) {
    const alreadyTracked = TabsStore.tabs.get(tab.id);
    if (alreadyTracked)
      tab = alreadyTracked;

    if (this.delayedDestroy) {
      clearTimeout(this.delayedDestroy);
      this.delayedDestroy = null;
    }

    const order = this.order;
    if (this.tabs.has(tab.id)) { // already tracked: update
      const prevState = tab.reindexedBy;
      const index = order.indexOf(tab.id);
      order.splice(index, 1);
      order.splice(tab.index, 0, tab.id);
      for (let i = Math.min(index, tab.index), maxi = Math.min(Math.max(index, tab.index) + 1, order.length); i < maxi; i++) {
        const tab = this.tabs.get(order[i]);
        if (!tab)
          throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
        tab.index = i;
        tab.reindexedBy = `Window.property.trackTab/update (${tab.index})`;
        tab.$TST.invalidateCache();
      }
      const parent = tab.$TST.parent;
      if (parent) {
        parent.$TST.sortAndInvalidateChildren();
        parent.$TST.invalidateCachedAncestors();
      }
      log(`tab ${dumpTab(tab)} is re-tracked under the window ${this.id}: `, prevState, index, '=>', tab.reindexedBy, order.join(', '));
    }
    else { // not tracked yet: add
      this.tabs.set(tab.id, tab);
      order.splice(tab.index, 0, tab.id);
      for (let i = tab.index + 1, maxi = order.length; i < maxi; i++) {
        const tab = this.tabs.get(order[i]);
        if (!tab)
          throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
        tab.index = i;
        tab.reindexedBy = `Window.property.trackTab/new (${tab.index})`;
        tab.$TST.invalidateCache();
      }
      log(`tab ${dumpTab(tab)} is newly tracked under the window ${this.id}: `, order);
    }
    TabsStore.updateIndexesForTab(tab);
    tab.$TST.invalidateCache();
    return tab;
  }

  detachTab(tabId) {
    const tab = TabsStore.tabs.get(tabId);
    if (!tab)
      return;

    TabsStore.removeTabFromIndexes(tab);

    tab.$TST.detach();
    this.tabs.delete(tabId);
    const order = this.order;
    const index = order.indexOf(tab.id);
    if (index < 0) // the tab is not tracked yet!
      return;
    order.splice(index, 1);
    if (this.tabs.size == 0) {
      if (!TabsStore.getCurrentWindowId()) { // only in the background page - the sidebar has no need to destroy itself manually.
        // the last tab can be removed with browser.tabs.closeWindowWithLastTab=false,
        // so we should not destroy the window immediately.
        if (this.delayedDestroy)
          clearTimeout(this.delayedDestroy);
        this.delayedDestroy = setTimeout(() => {
          if (this.tabs &&
            this.tabs.size == 0)
            this.destroy();
        }, (configs.collapseDuration, 1000) * 5);
      }
    }
    else {
      for (let i = index, maxi = order.length; i < maxi; i++) {
        this.tabs.get(order[i]).index = i;
      }
    }
    return tab;
  }

  untrackTab(tabId) {
    const tab = this.detachTab(tabId);
    if (tab)
      tab.$TST.destroy();
  }

  export(full) {
    const tabs = [];
    for (const tab of this.getOrderedTabs()) {
      tabs.push(tab.$TST.export(full));
    }
    return {
      tabs,
      tabGroups: [...this.tabGroups.values()].map(group => group.$TST.sanitized),
    };
  }
}

Window.onInitialized = new EventListenerManager();

Window.init = (windowId, tabGroups) => {
  const win = TabsStore.windows.get(windowId) || new Window(windowId, tabGroups);
  if (tabGroups && tabGroups.size != win.tabGroups.size) {
    win.initTabGroups(tabGroups);
  }
  Window.onInitialized.dispatch(win);
  return win;
}
