// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {DetailsPanel} from '../public/details_panel';
import {TabDescriptor, TabManager} from '../public/tab';
import {
  CollapsiblePanelVisibility,
  toggleVisibility,
} from '../components/widgets/collapsible_panel';
import {raf} from './raf_scheduler';

export interface ResolvedTab {
  uri: string;
  tab?: TabDescriptor;
}

export type TabPanelVisibility = 'COLLAPSED' | 'VISIBLE' | 'FULLSCREEN';

/**
 * Stores tab & current selection section registries.
 * Keeps track of tab lifecycles.
 */
export class TabManagerImpl implements TabManager, Disposable {
  private _registry = new Map<string, TabDescriptor>();
  private _defaultTabs = new Set<string>();
  private _detailsPanelRegistry = new Set<DetailsPanel>();
  private _instantiatedTabs = new Map<string, TabDescriptor>();
  private _openTabs: string[] = []; // URIs of the tabs open.
  private _currentTab: string = 'current_selection';
  private _tabPanelVisibility = CollapsiblePanelVisibility.COLLAPSED;
  private _tabPanelVisibilityChanged = false;

  [Symbol.dispose]() {
    // Dispose of all tabs that are currently alive
    for (const tab of this._instantiatedTabs.values()) {
      this.disposeTab(tab);
    }
    this._instantiatedTabs.clear();
  }

  registerTab(desc: TabDescriptor): Disposable {
    this._registry.set(desc.uri, desc);
    return {
      [Symbol.dispose]: () => this._registry.delete(desc.uri),
    };
  }

  addDefaultTab(uri: string): Disposable {
    this._defaultTabs.add(uri);
    return {
      [Symbol.dispose]: () => this._defaultTabs.delete(uri),
    };
  }

  registerDetailsPanel(section: DetailsPanel): Disposable {
    this._detailsPanelRegistry.add(section);
    return {
      [Symbol.dispose]: () => this._detailsPanelRegistry.delete(section),
    };
  }

  resolveTab(uri: string): TabDescriptor | undefined {
    return this._registry.get(uri);
  }

  showCurrentSelectionTab(): void {
    this.showTab('current_selection');
  }

  showTab(uri: string): void {
    // Add tab, unless we're talking about the special current_selection tab
    if (uri !== 'current_selection') {
      // Add tab to tab list if not already
      if (!this._openTabs.some((x) => x === uri)) {
        this._openTabs.push(uri);
      }
    }
    this._currentTab = uri;

    // The first time that we show a tab, auto-expand the tab bottom panel.
    // However, if the user has later collapsed the panel (hence if
    // _tabPanelVisibilityChanged == true), don't insist and leave things as
    // they are.
    if (
      !this._tabPanelVisibilityChanged &&
      this._tabPanelVisibility === CollapsiblePanelVisibility.COLLAPSED
    ) {
      this.setTabPanelVisibility(CollapsiblePanelVisibility.VISIBLE);
    }

    raf.scheduleFullRedraw();
  }

  // Hide a tab in the tab bar pick a new tab to show.
  // Note: Attempting to hide the "current_selection" tab doesn't work. This tab
  // is special and cannot be removed.
  hideTab(uri: string): void {
    // If the removed tab is the "current" tab, we must find a new tab to focus
    if (uri === this._currentTab) {
      // Remember the index of the current tab
      const currentTabIdx = this._openTabs.findIndex((x) => x === uri);

      // Remove the tab
      this._openTabs = this._openTabs.filter((x) => x !== uri);

      if (currentTabIdx !== -1) {
        if (this._openTabs.length === 0) {
          // No more tabs, use current selection
          this._currentTab = 'current_selection';
        } else if (currentTabIdx < this._openTabs.length - 1) {
          // Pick the tab to the right
          this._currentTab = this._openTabs[currentTabIdx];
        } else {
          // Pick the last tab
          const lastTab = this._openTabs[this._openTabs.length - 1];
          this._currentTab = lastTab;
        }
      }
    } else {
      // Otherwise just remove the tab
      this._openTabs = this._openTabs.filter((x) => x !== uri);
    }
    raf.scheduleFullRedraw();
  }

  toggleTab(uri: string): void {
    return this.isOpen(uri) ? this.hideTab(uri) : this.showTab(uri);
  }

  isOpen(uri: string): boolean {
    return this._openTabs.find((x) => x == uri) !== undefined;
  }

  get currentTabUri(): string {
    return this._currentTab;
  }

  get openTabsUri(): string[] {
    return this._openTabs;
  }

  get tabs(): TabDescriptor[] {
    return Array.from(this._registry.values());
  }

  get defaultTabs(): string[] {
    return Array.from(this._defaultTabs);
  }

  get detailsPanels(): DetailsPanel[] {
    return Array.from(this._detailsPanelRegistry);
  }

  /**
   * Resolves a list of URIs to tabs and manages tab lifecycles.
   * @param tabUris List of tabs.
   * @returns List of resolved tabs.
   */
  resolveTabs(tabUris: string[]): ResolvedTab[] {
    // Refresh the list of old tabs
    const newTabs = new Map<string, TabDescriptor>();
    const tabs: ResolvedTab[] = [];

    tabUris.forEach((uri) => {
      const newTab = this._registry.get(uri);
      tabs.push({uri, tab: newTab});

      if (newTab) {
        newTabs.set(uri, newTab);
      }
    });

    // Call onShow() on any new tabs.
    for (const [uri, tab] of newTabs) {
      const oldTab = this._instantiatedTabs.get(uri);
      if (!oldTab) {
        this.initTab(tab);
      }
    }

    // Call onHide() on any tabs that have been removed.
    for (const [uri, tab] of this._instantiatedTabs) {
      const newTab = newTabs.get(uri);
      if (!newTab) {
        this.disposeTab(tab);
      }
    }

    this._instantiatedTabs = newTabs;

    return tabs;
  }

  setTabPanelVisibility(visibility: CollapsiblePanelVisibility): void {
    this._tabPanelVisibility = visibility;
    this._tabPanelVisibilityChanged = true;
  }

  toggleTabPanelVisibility(): void {
    toggleVisibility(this._tabPanelVisibility, (visibility) =>
      this.setTabPanelVisibility(visibility),
    );
  }

  get tabPanelVisibility() {
    return this._tabPanelVisibility;
  }

  /**
   * Call onShow() on this tab.
   * @param tab The tab to initialize.
   */
  private initTab(tab: TabDescriptor): void {
    tab.onShow?.();
  }

  /**
   * Call onHide() and maybe remove from registry if tab is ephemeral.
   * @param tab The tab to dispose.
   */
  private disposeTab(tab: TabDescriptor): void {
    // Attempt to call onHide
    tab.onHide?.();

    // If ephemeral, also unregister the tab
    if (tab.isEphemeral) {
      this._registry.delete(tab.uri);
    }
  }
}
