/*
	***** BEGIN LICENSE BLOCK *****
	
	Copyright © 2024 Corporation for Digital Scholarship
					 Vienna, Virginia, USA
					 https://www.zotero.org
	
	This file is part of Zotero.
	
	Zotero is free software: you can redistribute it and/or modify
	it under the terms of the GNU Affero General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	Zotero is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Affero General Public License for more details.
	
	You should have received a copy of the GNU Affero General Public License
	along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
	
	***** END LICENSE BLOCK *****
*/


{
	let { isPaneCollapsed, setPaneCollapsed } = ChromeUtils.importESModule(
		'chrome://zotero/content/elements/utils/collapsiblePane.mjs'
	);

	class ContextPane extends XULElementBase {
		content = MozXULElement.parseXULToFragment(`
			<deck id="zotero-context-pane-deck" flex="1" selectedIndex="0">
				<deck id="zotero-context-pane-item-deck"></deck>
				<deck id="zotero-context-pane-notes-deck" class="notes-pane-deck" flex="1"></deck>
			</deck>
		`);

		get sidenav() {
			return this._sidenav;
		}

		set sidenav(sidenav) {
			this._sidenav = sidenav;
			// TODO: decouple sidenav and contextPane
			sidenav.contextNotesPane = this._notesPaneDeck;
		}

		get mode() {
			return ["item", "notes"][this._panesDeck.getAttribute('selectedIndex')];
		}

		set mode(mode) {
			let modeMap = {
				item: "0",
				notes: "1",
			};
			if (!(mode in modeMap)) {
				throw new Error(`ContextPane.mode must be one of ["item", "notes"], but got ${mode}`);
			}
			this._panesDeck.selectedIndex = modeMap[mode];
		}

		get activeEditor() {
			let currentContext = this._getCurrentNotesContext();
			return currentContext?._getCurrentEditor();
		}

		get collapsed() {
			return isPaneCollapsed(this);
		}

		set collapsed(val) {
			setPaneCollapsed(this, val);
		}

		static get observedAttributes() {
			return ['collapsed'];
		}

		attributeChangedCallback(name, oldValue, newValue) {
			switch (name) {
				case "collapsed": {
					this.handleCollapse(oldValue, newValue);
					break;
				}
			}
		}

		handleCollapse(prevState, newState) {
			if (prevState === "true" && (!newState || newState === "false")) {
				let itemContext = this._getItemContext(Zotero_Tabs.selectedID);
				itemContext?.render();
			}
		}

		init() {
			this._panesDeck = this.querySelector('#zotero-context-pane-deck');
			// Item pane deck
			this._itemPaneDeck = this.querySelector('#zotero-context-pane-item-deck');
			// Notes pane deck
			this._notesPaneDeck = this.querySelector('#zotero-context-pane-notes-deck');

			this._notifierIDs = [
				Zotero.Notifier.registerObserver(this, ['item'], 'contextPane'),
				// We want to be notified quickly about tab events
				Zotero.Notifier.registerObserver(this, ['tab'], 'contextPane', 20),
			];
		}

		destroy() {
			for (let id of this._notifierIDs) {
				Zotero.Notifier.unregisterObserver(id);
			}
		}

		notify(action, type, ids, extraData) {
			if (type == 'item') {
				this._handleItemUpdate(action, type, ids, extraData);
				return;
			}
			if (type == 'tab' && action == 'add') {
				this._handleTabAdd(action, type, ids, extraData);
				return;
			}
			if (type == 'tab' && action == 'close') {
				this._handleTabClose(action, type, ids, extraData);
				return;
			}
			if (type == 'tab' && ["select", "load"].includes(action)) {
				this._handleTabSelect(action, type, ids, extraData);
			}
		}

		_handleItemUpdate(action, type, ids, extraData) {
			// Update, remove or re-create item panes
			if (action === 'modify') {
				for (let itemDetails of Array.from(this._itemPaneDeck.children)) {
					let tabID = itemDetails.tabID;
					let tab = Zotero_Tabs._getTab(tabID).tab;
					let item = Zotero.Items.get(tab?.data.itemID);
					if ((item.parentID || itemDetails.parentID)
						&& item.parentID !== itemDetails.parentID) {
						this._removeItemContext(tabID);
						this._addItemContext(tabID, item.itemID, tab?.type);
					}
				}
			}

			// Update notes lists for affected libraries
			if (['add', 'delete', 'modify'].includes(action)) {
				let libraryIDs = [];
				for (let id of ids) {
					let item = Zotero.Items.get(id);
					if (item && (item.isNote() || item.isRegularItem())) {
						libraryIDs.push(item.libraryID);
					}
					else if (action == 'delete') {
						libraryIDs.push(extraData[id].libraryID);
					}
				}
				for (let context of Array.from(this._notesPaneDeck.children)) {
					if (libraryIDs.includes(context.libraryID)) {
						context.affectedIDs = new Set([...context.affectedIDs, ...ids]);
						context.update();
					}
				}
			}
		}

		_handleTabAdd(_action, _type, _ids, _extraData) {}

		_handleTabClose(action, type, ids) {
			for (let id of ids) {
				this._removeItemContext(id);
			}
			if (Zotero_Tabs.deck.children.length == 1) {
				Array.from(this._notesPaneDeck.children).forEach(x => x.notesList.expanded = false);
			}
			// Close tab specific notes if tab id no longer exists, but
			// do that only when unloaded tab is reloaded
			setTimeout(() => {
				let contextNodes = Array.from(this._notesPaneDeck.children);
				for (let contextNode of contextNodes) {
					let nodes = Array.from(contextNode.querySelector('.zotero-context-pane-tab-notes-deck').children);
					for (let node of nodes) {
						let tabID = node.getAttribute('data-tab-id');
						if (!document.getElementById(tabID)) {
							node.remove();
						}
					}
				}
				// For unknown reason fx102, unlike 60, sometimes doesn't automatically update selected index
				this._selectItemContext(Zotero_Tabs.selectedID);
			});
		}

		async _handleTabSelect(action, type, ids, extraData) {
			// TEMP: move these variables to ZoteroContextPane
			let _contextPaneSplitter = ZoteroContextPane.splitter;
			let _contextPane = document.getElementById('zotero-context-pane');
			let tabID = ids[0];
			let tabType = extraData[tabID].type;
			// It seems that changing `hidden` or `collapsed` values might
			// be related with significant slow down when there are too many
			// DOM nodes (i.e. 10k notes)
			if (tabType == 'library') {
				_contextPaneSplitter.setAttribute('hidden', true);
				_contextPane.setAttribute('collapsed', true);
				ZoteroContextPane.showLoadingMessage(false);
				this._sidenav.hidden = true;
			}
			else if (Zotero_Tabs.hasContextPane(tabType)
					// The reader tab load event is triggered asynchronously.
					// If the tab is no longer selected by the time the event is triggered,
					// we don't need to update the context pane, since it must already be
					// updated by another select tab event.
					&& (action === 'select'
						|| (action === 'load' && Zotero_Tabs.selectedID == tabID))) {
				if (Zotero_Tabs.hasNoteContext(tabType)) {
					this._setupNotesContext(tabID);
				}
				else {
					this._disableNotesContext();
				}
				_contextPaneSplitter.setAttribute('hidden', false);

				_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
				
				this._sidenav.hidden = false;

				let tab = Zotero_Tabs._getTab(tabID).tab;
				await this._addItemContext(ids[0], tab.data.itemID, tab.type);
	
				this._selectItemContext(tabID);
			}

			ZoteroContextPane.update();
			Zotero_Tabs.updateSidebarLayout();
		}

		async _setupNotesContext(tabID) {
			let { tab } = Zotero_Tabs._getTab(tabID);
			if (!tab || !tab.data.itemID) return;
			let attachment = await Zotero.Items.getAsync(tab.data.itemID);
			if (attachment) {
				this._selectNotesContext(attachment.libraryID);
				let notesContext = this._getNotesContext(attachment.libraryID);
				notesContext.updateNotesListFromCache();
			}
			let currentNoteContext = this._getCurrentNotesContext();
			// Always switch to the current selected tab, since the selection might have changed
			currentNoteContext.switchToTab(Zotero_Tabs.selectedID);
			this.sidenav.contextNotesPaneEnabled = true;
		}

		_disableNotesContext() {
			this.sidenav.contextNotesPaneEnabled = false;
		}

		_getCurrentNotesContext() {
			return this._notesPaneDeck.selectedPanel;
		}

		_getNotesContext(libraryID) {
			let context = Array.from(this._notesPaneDeck.children).find(x => x.libraryID == libraryID);
			if (!context) {
				context = this._addNotesContext(libraryID);
			}
			return context;
		}

		_addNotesContext(libraryID) {
			let context = document.createXULElement("notes-context");
			this._notesPaneDeck.append(context);
			context.libraryID = libraryID;
			return context;
		}

		_selectNotesContext(libraryID) {
			let context = this._getNotesContext(libraryID);
			this._notesPaneDeck.selectedPanel = context;
		}

		_removeNotesContext(libraryID) {
			let context = Array.from(this._notesPaneDeck.children).find(x => x.libraryID == libraryID);
			context?.remove();
		}

		_getItemContext(tabID) {
			return this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`);
		}

		_removeItemContext(tabID) {
			this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`)?.remove();
		}
	
		_selectItemContext(tabID) {
			let previousContainer = this._sidenav.container;
			let selectedPanel = this._getItemContext(tabID);
			if (selectedPanel) {
				this._itemPaneDeck.selectedPanel = selectedPanel;
				selectedPanel.sidenav = this._sidenav;
				// Inherits previous pinned states
				if (previousContainer) selectedPanel.pinnedPane = previousContainer.pinnedPane;
				selectedPanel.render();
			}
		}
	
		async _addItemContext(tabID, itemID, tabType = "") {
			if (this._getItemContext(tabID)) {
				return;
			}

			let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
			let library = Zotero.Libraries.get(libraryID);
			await library.waitForDataLoad('item');
	
			let item = Zotero.Items.get(itemID);
			if (!item) {
				return;
			}
			libraryID = item.libraryID;
			let parentID = item.parentID;
	
			let previousPinnedPane = this._sidenav.container?.pinnedPane || "";
			
			let targetItem;
			if (item.isNote()) {
				targetItem = item;
			}
			else {
				targetItem = parentID ? Zotero.Items.get(parentID) : item;
			}
			
			let editable = Zotero.Libraries.get(libraryID).editable
				// If the parent item or the attachment itself is in trash, itemPane is not editable
				&& !item.deleted && !targetItem.deleted;
	
			let itemDetails = document.createXULElement('item-details');
			itemDetails.id = tabID + '-context';
			itemDetails.dataset.tabId = tabID;
			itemDetails.className = 'zotero-item-pane-content';
			this._itemPaneDeck.appendChild(itemDetails);
	
			itemDetails.editable = editable;
			itemDetails.tabID = tabID;
			itemDetails.tabType = Zotero_Tabs.parseTabType(tabType).tabContentType;
			itemDetails.item = targetItem;
			// Manually cache parentID
			itemDetails.parentID = parentID;
			itemDetails.sidenav = this._sidenav;
			if (previousPinnedPane) itemDetails.pinnedPane = previousPinnedPane;
	
			// Make sure that the context pane of the selected tab is rendered
			if (tabID == Zotero_Tabs.selectedID) {
				this._selectItemContext(tabID);
			}
		}
	
		handleFocus() {
			if (!this.collapsed) {
				if (this.mode == "item") {
					let header = this._itemPaneDeck.selectedPanel.querySelector("item-pane-header");
					// Focus the first focusable node after header
					Services.focus.moveFocus(window, header, Services.focus.MOVEFOCUS_FORWARD, 0);
					return true;
				}
				else if (this.mode == "notes") {
					return this._getCurrentNotesContext().focus();
				}
			}
			return false;
		}
	}
	customElements.define("context-pane", ContextPane);
}
