"use strict";
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeViewsExtImpl = void 0;
// TODO: extract `@theia/util` for event, disposable, cancellation and common types
// don't use @theia/core directly from plugin host
const event_1 = require("@theia/core/lib/common/event");
const disposable_1 = require("@theia/core/lib/common/disposable");
const types_impl_1 = require("../types-impl");
const plugin_api_rpc_1 = require("../../common/plugin-api-rpc");
const common_1 = require("../../common");
const plugin_icon_path_1 = require("../plugin-icon-path");
const vscode_uri_1 = require("@theia/core/shared/vscode-uri");
class TreeViewsExtImpl {
    constructor(rpc, commandRegistry) {
        this.commandRegistry = commandRegistry;
        this.treeViews = new Map();
        this.proxy = rpc.getProxy(plugin_api_rpc_1.PLUGIN_RPC_CONTEXT.TREE_VIEWS_MAIN);
        commandRegistry.registerArgumentProcessor({
            processArgument: arg => {
                if (common_1.TreeViewItemReference.is(arg)) {
                    return this.toTreeElement(arg);
                }
                else if (Array.isArray(arg)) {
                    return arg.map(param => common_1.TreeViewItemReference.is(param) ? this.toTreeElement(param) : param);
                }
                else {
                    return arg;
                }
            }
        });
    }
    $dragStarted(treeViewId, treeItemIds, token) {
        return this.getTreeView(treeViewId).onDragStarted(treeItemIds, token);
    }
    $drop(treeViewId, treeItemId, dataTransferItems, token) {
        return this.getTreeView(treeViewId).handleDrop(treeItemId, dataTransferItems, token);
    }
    toTreeElement(treeViewItemRef) {
        var _a;
        return (_a = this.treeViews.get(treeViewItemRef.viewId)) === null || _a === void 0 ? void 0 : _a.getElement(treeViewItemRef.itemId);
    }
    registerTreeDataProvider(plugin, treeViewId, treeDataProvider) {
        const treeView = this.createTreeView(plugin, treeViewId, { treeDataProvider });
        return types_impl_1.Disposable.create(() => {
            this.treeViews.delete(treeViewId);
            treeView.dispose();
        });
    }
    createTreeView(plugin, treeViewId, options) {
        if (!options || !options.treeDataProvider) {
            throw new Error('Options with treeDataProvider is mandatory');
        }
        const treeView = new TreeViewExtImpl(plugin, treeViewId, options, this.proxy, this.commandRegistry.converter);
        this.treeViews.set(treeViewId, treeView);
        return {
            // tslint:disable:typedef
            get onDidExpandElement() {
                return treeView.onDidExpandElement;
            },
            get onDidCollapseElement() {
                return treeView.onDidCollapseElement;
            },
            get selection() {
                return treeView.selectedElements;
            },
            get onDidChangeSelection() {
                return treeView.onDidChangeSelection;
            },
            get visible() {
                return treeView.visible;
            },
            get onDidChangeVisibility() {
                return treeView.onDidChangeVisibility;
            },
            get message() {
                return treeView.message;
            },
            set message(message) {
                treeView.message = message;
            },
            get title() {
                return treeView.title;
            },
            set title(title) {
                treeView.title = title;
            },
            get description() {
                return treeView.description;
            },
            set description(description) {
                treeView.description = description;
            },
            get badge() {
                return treeView.badge;
            },
            set badge(badge) {
                treeView.badge = badge;
            },
            reveal: (element, revealOptions) => treeView.reveal(element, revealOptions),
            dispose: () => {
                this.treeViews.delete(treeViewId);
                treeView.dispose();
            }
        };
    }
    async $getChildren(treeViewId, treeItemId) {
        const treeView = this.getTreeView(treeViewId);
        return treeView.getChildren(treeItemId);
    }
    async $resolveTreeItem(treeViewId, treeItemId, token) {
        return this.getTreeView(treeViewId).resolveTreeItem(treeItemId, token);
    }
    async $hasResolveTreeItem(treeViewId) {
        return this.getTreeView(treeViewId).hasResolveTreeItem();
    }
    async $setExpanded(treeViewId, treeItemId, expanded) {
        const treeView = this.getTreeView(treeViewId);
        if (expanded) {
            return treeView.onExpanded(treeItemId);
        }
        else {
            return treeView.onCollapsed(treeItemId);
        }
    }
    async $setSelection(treeViewId, treeItemIds) {
        this.getTreeView(treeViewId).setSelection(treeItemIds);
    }
    async $setVisible(treeViewId, isVisible) {
        this.getTreeView(treeViewId).setVisible(isVisible);
    }
    getTreeView(treeViewId) {
        const treeView = this.treeViews.get(treeViewId);
        if (!treeView) {
            throw new Error(`No tree view with id '${treeViewId}' registered.`);
        }
        return treeView;
    }
}
exports.TreeViewsExtImpl = TreeViewsExtImpl;
class TreeViewExtImpl {
    constructor(plugin, treeViewId, options, proxy, commandsConverter) {
        var _a, _b, _c, _d, _e, _f;
        this.plugin = plugin;
        this.treeViewId = treeViewId;
        this.options = options;
        this.proxy = proxy;
        this.commandsConverter = commandsConverter;
        this.onDidExpandElementEmitter = new event_1.Emitter();
        this.onDidExpandElement = this.onDidExpandElementEmitter.event;
        this.onDidCollapseElementEmitter = new event_1.Emitter();
        this.onDidCollapseElement = this.onDidCollapseElementEmitter.event;
        this.onDidChangeSelectionEmitter = new event_1.Emitter();
        this.onDidChangeSelection = this.onDidChangeSelectionEmitter.event;
        this.onDidChangeVisibilityEmitter = new event_1.Emitter();
        this.onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event;
        this.nodes = new Map();
        this.pendingRefresh = Promise.resolve();
        this.localDataTransfer = new types_impl_1.DataTransfer();
        this.toDispose = new disposable_1.DisposableCollection(disposable_1.Disposable.create(() => this.clearAll()), this.onDidExpandElementEmitter, this.onDidCollapseElementEmitter, this.onDidChangeSelectionEmitter, this.onDidChangeVisibilityEmitter);
        this._message = '';
        this._title = '';
        this._description = '';
        this._badge = undefined;
        this.selectedItemIds = new Set();
        this._visible = false;
        // make copies of optionally provided MIME types:
        const dragMimeTypes = (_b = (_a = options.dragAndDropController) === null || _a === void 0 ? void 0 : _a.dragMimeTypes) === null || _b === void 0 ? void 0 : _b.slice();
        const dropMimeTypes = (_d = (_c = options.dragAndDropController) === null || _c === void 0 ? void 0 : _c.dropMimeTypes) === null || _d === void 0 ? void 0 : _d.slice();
        proxy.$registerTreeDataProvider(treeViewId, { canSelectMany: options.canSelectMany, dragMimeTypes, dropMimeTypes });
        this.toDispose.push(disposable_1.Disposable.create(() => this.proxy.$unregisterTreeDataProvider(treeViewId)));
        (_f = (_e = options.treeDataProvider).onDidChangeTreeData) === null || _f === void 0 ? void 0 : _f.call(_e, () => {
            this.pendingRefresh = proxy.$refresh(treeViewId);
        });
    }
    dispose() {
        this.toDispose.dispose();
    }
    async reveal(element, options) {
        await this.pendingRefresh;
        const elementParentChain = await this.calculateRevealParentChain(element);
        if (elementParentChain) {
            return this.proxy.$reveal(this.treeViewId, elementParentChain, Object.assign({ select: true, focus: false, expand: false }, options));
        }
    }
    get message() {
        return this._message;
    }
    set message(message) {
        this._message = message;
        this.proxy.$setMessage(this.treeViewId, this._message);
    }
    get title() {
        return this._title;
    }
    set title(title) {
        this._title = title;
        this.proxy.$setTitle(this.treeViewId, title);
    }
    get description() {
        return this._description;
    }
    set description(description) {
        this._description = description;
        this.proxy.$setDescription(this.treeViewId, this._description);
    }
    get badge() {
        return this._badge;
    }
    set badge(badge) {
        this._badge = badge;
        this.proxy.$setBadge(this.treeViewId, badge ? { value: badge.value, tooltip: badge.tooltip } : undefined);
    }
    getElement(treeItemId) {
        var _a;
        return (_a = this.nodes.get(treeItemId)) === null || _a === void 0 ? void 0 : _a.value;
    }
    /**
     * calculate the chain of node ids from root to element so that the frontend can expand all of them and reveal element.
     * this is needed as the frontend may not have the full tree nodes.
     * throughout the parent chain this.getChildren is called in order to fill this.nodes cache.
     *
     * returns undefined if wasn't able to calculate the path due to inconsistencies.
     *
     * @param element element to reveal
     */
    async calculateRevealParentChain(element) {
        var _a, _b, _c;
        if (!element) {
            // root
            return [];
        }
        const parent = (_c = await ((_b = (_a = this.options.treeDataProvider).getParent) === null || _b === void 0 ? void 0 : _b.call(_a, element))) !== null && _c !== void 0 ? _c : undefined;
        const chain = await this.calculateRevealParentChain(parent);
        if (!chain) {
            // parents are inconsistent
            return undefined;
        }
        const parentId = chain.length ? chain[chain.length - 1] : '';
        const treeItem = await this.options.treeDataProvider.getTreeItem(element);
        if (treeItem.id) {
            return chain.concat(treeItem.id);
        }
        const cachedParentNode = this.nodes.get(parentId);
        // first try to get children length from cache since getChildren disposes old nodes, which can cause a race
        // condition if command is executed together with reveal.
        // If not in cache, getChildren fills this.nodes and generate ids for them which are needed later
        const children = (cachedParentNode === null || cachedParentNode === void 0 ? void 0 : cachedParentNode.children) || await this.getChildren(parentId);
        if (!children) {
            // parent is inconsistent
            return undefined;
        }
        const candidateId = this.buildTreeItemId(parentId, treeItem);
        if (this.nodes.has(candidateId)) {
            return chain.concat(candidateId);
        }
        // couldn't calculate consistent parent chain and id
        return undefined;
    }
    getTreeItemLabel(treeItem) {
        const treeItemLabel = treeItem.label;
        return typeof treeItemLabel === 'object' ? treeItemLabel.label : treeItemLabel;
    }
    getTreeItemLabelHighlights(treeItem) {
        const treeItemLabel = treeItem.label;
        return typeof treeItemLabel === 'object' ? treeItemLabel.highlights : undefined;
    }
    getItemLabel(treeItem) {
        let idLabel = this.getTreeItemLabel(treeItem);
        // Use resource URI if label is not set
        if (idLabel === undefined && treeItem.resourceUri) {
            idLabel = treeItem.resourceUri.path.toString();
            idLabel = decodeURIComponent(idLabel);
            if (idLabel.indexOf('/') >= 0) {
                idLabel = idLabel.substring(idLabel.lastIndexOf('/') + 1);
            }
        }
        return idLabel;
    }
    buildTreeItemId(parentId, item) {
        // build tree id according to https://code.visualstudio.com/api/references/vscode-api#TreeItem
        // note: the front end tree implementation cannot handle reparenting items, hence the id is set to the "path" of individual ids
        let id = typeof item.id === 'string' ? item.id : this.getItemLabel(item);
        if (id) {
            // we use '' as the id of the root, we don't consider that a valid id
            // since '' is falsy, we'll never get '' in this branch
            id = TreeViewExtImpl.ID_ITEM + id;
        }
        else {
            id = TreeViewExtImpl.ID_COMPUTED + this.nextItemId++;
        }
        return `${parentId}/${id}`;
    }
    async getChildren(parentId) {
        const parentNode = this.nodes.get(parentId);
        const parent = parentNode === null || parentNode === void 0 ? void 0 : parentNode.value;
        if (parentId && !parent) {
            console.error(`No tree item with id '${parentId}' found.`);
            return [];
        }
        this.clearChildren(parentNode);
        // place root in the cache
        if (parentId === '') {
            const rootNodeDisposables = new disposable_1.DisposableCollection();
            this.nodes.set(parentId, { id: '', disposables: rootNodeDisposables, dispose: () => { rootNodeDisposables.dispose(); } });
        }
        // ask data provider for children for cached element
        const result = await this.options.treeDataProvider.getChildren(parent);
        if (result) {
            const treeItemPromises = result.map(async (value) => {
                // Ask data provider for a tree item for the value
                // Data provider must return theia.TreeItem
                const treeItem = await this.options.treeDataProvider.getTreeItem(value);
                // Convert theia.TreeItem to the TreeViewItem
                const label = this.getItemLabel(treeItem);
                const highlights = this.getTreeItemLabelHighlights(treeItem);
                // Generate the ID
                // ID is used for caching the element
                const id = this.buildTreeItemId(parentId, treeItem);
                const toDisposeElement = new disposable_1.DisposableCollection();
                const node = {
                    id,
                    pluginTreeItem: treeItem,
                    value,
                    disposables: toDisposeElement,
                    dispose: () => toDisposeElement.dispose()
                };
                if (parentNode) {
                    const children = parentNode.children || [];
                    children.push(node);
                    parentNode.children = children;
                }
                this.nodes.set(id, node);
                let icon;
                let iconUrl;
                let themeIcon;
                const { iconPath } = treeItem;
                if (typeof iconPath === 'string' && iconPath.indexOf('fa-') !== -1) {
                    icon = iconPath;
                }
                else if (types_impl_1.ThemeIcon.is(iconPath)) {
                    themeIcon = iconPath;
                }
                else {
                    iconUrl = plugin_icon_path_1.PluginIconPath.toUrl(iconPath, this.plugin);
                }
                const treeViewItem = {
                    id,
                    label,
                    highlights,
                    icon,
                    iconUrl,
                    themeIcon,
                    description: treeItem.description,
                    resourceUri: treeItem.resourceUri,
                    tooltip: treeItem.tooltip,
                    collapsibleState: treeItem.collapsibleState,
                    contextValue: treeItem.contextValue,
                    command: this.commandsConverter.toSafeCommand(treeItem.command, toDisposeElement),
                    accessibilityInformation: treeItem.accessibilityInformation
                };
                node.treeViewItem = treeViewItem;
                return treeViewItem;
            });
            return Promise.all(treeItemPromises);
        }
        else {
            return undefined;
        }
    }
    clearChildren(parentNode) {
        if (parentNode) {
            if (parentNode.children) {
                for (const child of parentNode.children) {
                    this.clear(child);
                }
            }
            delete parentNode['children'];
        }
        else {
            this.clearAll();
        }
    }
    clear(node) {
        if (node.children) {
            for (const child of node.children) {
                this.clear(child);
            }
        }
        this.nodes.delete(node.id);
        node.dispose();
    }
    clearAll() {
        this.nodes.forEach(node => node.dispose());
        this.nodes.clear();
    }
    async onExpanded(treeItemId) {
        // get element from a cache
        const cachedElement = this.getElement(treeItemId);
        // fire an event
        if (cachedElement) {
            this.onDidExpandElementEmitter.fire({
                element: cachedElement
            });
        }
    }
    async onCollapsed(treeItemId) {
        // get element from a cache
        const cachedElement = this.getElement(treeItemId);
        // fire an event
        if (cachedElement) {
            this.onDidCollapseElementEmitter.fire({
                element: cachedElement
            });
        }
    }
    async resolveTreeItem(treeItemId, token) {
        var _a;
        if (!this.options.treeDataProvider.resolveTreeItem) {
            return undefined;
        }
        const node = this.nodes.get(treeItemId);
        if (node && node.treeViewItem && node.pluginTreeItem && node.value) {
            const resolved = (_a = await this.options.treeDataProvider.resolveTreeItem(node.pluginTreeItem, node.value, token)) !== null && _a !== void 0 ? _a : node.pluginTreeItem;
            node.treeViewItem.command = this.commandsConverter.toSafeCommand(resolved.command, node.disposables);
            node.treeViewItem.tooltip = resolved.tooltip;
            return node.treeViewItem;
        }
        return undefined;
    }
    hasResolveTreeItem() {
        return !!this.options.treeDataProvider.resolveTreeItem;
    }
    get selectedElements() {
        const items = [];
        for (const id of this.selectedItemIds) {
            const item = this.getElement(id);
            if (item) {
                items.push(item);
            }
        }
        return items;
    }
    setSelection(selectedItemIds) {
        const toDelete = new Set(this.selectedItemIds);
        for (const id of selectedItemIds) {
            toDelete.delete(id);
            if (!this.selectedItemIds.has(id)) {
                this.doSetSelection(selectedItemIds);
                return;
            }
        }
        if (toDelete.size) {
            this.doSetSelection(selectedItemIds);
        }
    }
    doSetSelection(selectedItemIts) {
        this.selectedItemIds = new Set(selectedItemIts);
        this.onDidChangeSelectionEmitter.fire(Object.freeze({ selection: this.selectedElements }));
    }
    get visible() {
        return this._visible;
    }
    setVisible(visible) {
        if (visible !== this._visible) {
            this._visible = visible;
            this.onDidChangeVisibilityEmitter.fire(Object.freeze({ visible: this._visible }));
        }
    }
    async onDragStarted(treeItemIds, token) {
        var _a, _b;
        const treeItems = [];
        for (const id of treeItemIds) {
            const item = this.getElement(id);
            if (item) {
                treeItems.push(item);
            }
        }
        if ((_a = this.options.dragAndDropController) === null || _a === void 0 ? void 0 : _a.handleDrag) {
            this.localDataTransfer.clear();
            await this.options.dragAndDropController.handleDrag(treeItems, this.localDataTransfer, token);
            const uriList = await ((_b = this.localDataTransfer.get('text/uri-list')) === null || _b === void 0 ? void 0 : _b.asString());
            if (uriList) {
                return uriList.split('\n').map(str => vscode_uri_1.URI.parse(str));
            }
        }
        return undefined;
    }
    async handleDrop(treeItemId, dataTransferItems, token) {
        var _a;
        const treeItem = treeItemId ? this.getElement(treeItemId) : undefined;
        const dropTransfer = new types_impl_1.DataTransfer();
        if ((_a = this.options.dragAndDropController) === null || _a === void 0 ? void 0 : _a.handleDrop) {
            this.localDataTransfer.forEach((item, type) => {
                dropTransfer.set(type, item);
            });
            for (const [type, item] of dataTransferItems) {
                // prefer the item the plugin has set in `onDragStarted`;
                if (!dropTransfer.has(type)) {
                    if (typeof item === 'string') {
                        dropTransfer.set(type, new types_impl_1.DataTransferItem(item));
                    }
                    else {
                        const file = {
                            name: item.name,
                            data: () => this.proxy.$readDroppedFile(item.contentId).then(buffer => buffer.buffer),
                            uri: item.uri ? vscode_uri_1.URI.revive(item.uri) : undefined
                        };
                        const fileItem = new class extends types_impl_1.DataTransferItem {
                            asFile() {
                                return file;
                            }
                        }(file);
                        dropTransfer.set(type, fileItem);
                    }
                }
            }
            return this.options.dragAndDropController.handleDrop(treeItem, dropTransfer, token);
        }
    }
}
TreeViewExtImpl.ID_COMPUTED = 'c';
TreeViewExtImpl.ID_ITEM = 'i';
//# sourceMappingURL=tree-views.js.map