import { Subscription } from 'rxjs';
import { isNil, isNumber } from 'lodash-es';
import { distinct } from '@vscode/monaco-editor/esm/vs/base/common/arrays';
import { IDisposable, DisposableStore } from '@vscode/monaco-editor/esm/vs/base/common/lifecycle';
import { ITreeViewDescriptor, ITreeView, ITreeItem, ITreeViewDataProvider, IRevealOptions } from '@workbench-stack/core';
import { Connection, ExtHostTreeViewsShape, createProxyAndHandleRequests, Disposable } from "@workbench-stack/extension-api";
import { ExtHostContext as ServerExtHostContext, MainThreadTreeViewsShape } from '@workbench-stack/extension-api/lib/protocol/server.protocol';

import { SubscriptionMap } from '../common';
import { ViewProviderRegistry } from '../../services/view-service';

class TreeViewDataProvider implements ITreeViewDataProvider {

    private readonly itemsMap: Map</* TreeItemHandle */any, ITreeItem> = new Map<any /* TreeItemHandle*/, ITreeItem>();

    constructor(private readonly treeViewId: string,
        private readonly _proxy: ExtHostTreeViewsShape,
        // private readonly notificationService: INotificationService,
    ) { }

    public getChildren(treeItem?: ITreeItem): Promise<ITreeItem[]> {
        return Promise.resolve(this._proxy.$getChildren(this.treeViewId, treeItem ? treeItem.handle : undefined)
            .then(
                children => this.postGetChildren(children),
                err => {
                    // this.notificationService.error(err);
                    return [];
                }));
    }

    public getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] {
        const itemsToRefresh: ITreeItem[] = [];
        if (itemsToRefreshByHandle) {
            for (const treeItemHandle of Object.keys(itemsToRefreshByHandle)) {
                const currentTreeItem = this.getItem(treeItemHandle);
                if (currentTreeItem) { // Refresh only if the item exists
                    const treeItem = itemsToRefreshByHandle[treeItemHandle];
                    // Update the current item with refreshed item
                    this.updateTreeItem(currentTreeItem, treeItem);
                    if (treeItemHandle === treeItem.handle) {
                        itemsToRefresh.push(currentTreeItem);
                    } else {
                        // Update maps when handle is changed and refresh parent
                        this.itemsMap.delete(treeItemHandle);
                        this.itemsMap.set(currentTreeItem.handle, currentTreeItem);
                        const parent = treeItem.parentHandle ? this.itemsMap.get(treeItem.parentHandle) : null;
                        if (parent) {
                            itemsToRefresh.push(parent);
                        }
                    }
                }
            }
        }
        return itemsToRefresh;
    }

    public getItem(treeItemHandle: string): ITreeItem | undefined {
        return this.itemsMap.get(treeItemHandle);
    }

    public isEmpty(): boolean {
        return this.itemsMap.size === 0;
    }

    private postGetChildren(elements: ITreeItem[]): ITreeItem[] {
        const result: ITreeItem[] = [];
        if (elements) {
            for (const element of elements) {
                this.itemsMap.set(element.handle, element);
                result.push(element);
            }
        }
        return result;
    }

    private updateTreeItem(current: ITreeItem, treeItem: ITreeItem): void {
        treeItem.children = treeItem.children ? treeItem.children : undefined;
        if (current) {
            const properties = distinct([...Object.keys(current), ...Object.keys(treeItem)]);
            for (const property of properties) {
                (<any>current)[property] = (<any>treeItem)[property];
            }
        }
    }
}

export class ClientTreeViews extends Disposable implements MainThreadTreeViewsShape {
    private proxy: ExtHostTreeViewsShape;
    private serverProxy: ExtHostTreeViewsShape;
    private readonly _store = new DisposableStore();

    private subscriptions = new Subscription();
    private registrations = new SubscriptionMap();

    private readonly _dataProviders: Map<string, any> = new Map<string, any/* TreeViewDataProvider */>();

    constructor(
        connection: Connection,
        public registry: ViewProviderRegistry,
    ) {
        super(() => { });

        this.proxy = createProxyAndHandleRequests('treeViews', connection, this);
        this.serverProxy = createProxyAndHandleRequests(ServerExtHostContext.ExtHostLanguageFeatures.sid, connection, this);
    }

    public unsubscribe(): void {
        this.subscriptions.unsubscribe();
    }

    public dispose(): void {
        this._dataProviders.forEach((dataProvider, treeViewId) => {
            const treeView = this.getTreeView(treeViewId);
            if (treeView) {
                treeView.dataProvider = undefined;
            }
        });
        this._dataProviders.clear();
        super.dispose();
    }

    public async $refresh(treeViewId: string, itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): Promise<void> {
        const viewer = this.getTreeView(treeViewId);
        const dataProvider = this._dataProviders.get(treeViewId);

        if (viewer && dataProvider) {
            const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle);
            return viewer.refresh(itemsToRefresh.length ? itemsToRefresh : undefined);
        }
        return Promise.resolve();
    }

    private getTreeView(treeViewId: string): any | ITreeView | null {
        const viewDescriptor: any /*ITreeViewDescriptor */ = this.registry.getView(treeViewId);
        return viewDescriptor ? viewDescriptor.treeView : null;
    }

    public $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }) {
        const dataProvider = new TreeViewDataProvider(treeViewId, this.proxy /* this.notificationService */);
        this._dataProviders.set(treeViewId, dataProvider);

        const viewer = this.getTreeView(treeViewId);

        if (viewer) {
            // Order is important here. The internal tree isn't created until the dataProvider is set.
            // Set all other properties first!
            viewer.showCollapseAllAction = !!options.showCollapseAll;
            viewer.canSelectMany = !!options.canSelectMany;
            viewer.dataProvider = dataProvider;
            this.registerListeners(treeViewId, viewer);
            this.proxy.$setVisible(treeViewId, viewer.visible);
        } else {
            // this.notificationService.error('No view is registered with id: ' + treeViewId);
        }
    }

    private registerListeners(treeViewId: string, treeView: any | ITreeView): void {
        this._register(treeView.onDidExpandItem(item => this.proxy.$setExpanded(treeViewId, item.handle, true)));
        this._register(treeView.onDidCollapseItem(item => this.proxy.$setExpanded(treeViewId, item.handle, false)));
        this._register(treeView.onDidChangeSelection(items => this.proxy.$setSelection(treeViewId, items.map(({ handle }) => handle))));
        this._register(treeView.onDidChangeVisibility(isVisible => this.proxy.$setVisible(treeViewId, isVisible)));
    }

    protected _register<T extends IDisposable>(t: T): T {
        if ((t as any as Disposable) === this) {
            throw new Error('Cannot register a disposable on itself!');
        }
        return this._store.add(t);
    }

    public async $reveal(treeViewId: string, itemIn: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise<void> {
        const treeView = this.getTreeView(treeViewId);

        options = options ? options : { select: false, focus: false };
        const select = isNil(options.select) ? false : options.select;
        const focus = isNil(options.focus) ? false : options.focus;
        let expand = Math.min(isNumber(options.expand) ? options.expand : options.expand === true ? 1 : 0, 3);

        if (treeView.dataProvider.isEmpty()) {
            // Refresh if empty
            await treeView.refresh();
        }
        for (const parent of parentChain) {
            const parentItem = treeView.dataProvider.getItem(parent.handle);
            if (parentItem) {
                await treeView.expand(parentItem);
            }
        }
        const item = treeView.dataProvider.getItem(itemIn.handle);
        if (item) {
            await treeView.reveal(item);
            if (select) {
                treeView.setSelection([item]);
            }
            if (focus) {
                treeView.setFocus(item);
            }
            let itemsToExpand = [item];
            for (; itemsToExpand.length > 0 && expand > 0; expand--) {
                await treeView.expand(itemsToExpand);
                itemsToExpand = itemsToExpand.reduce((result, itemValue) => {
                    const record = treeView.dataProvider.getItem(itemValue.handle);
                    if (record && record.children && record.children.length) {
                        result.push(...record.children);
                    }
                    return result;
                }, [] as ITreeItem[]);
            }
        }
    }

    public $setMessage(treeViewId: string, message: string): void {
        const viewer = this.getTreeView(treeViewId);
        if (viewer) {
            viewer.message = message;
        }
    }
}
