/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the Source EULA. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as Platform from 'vs/base/common/platform';
import * as Browser from 'vs/base/browser/browser';
import * as Lifecycle from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import * as Diff from 'vs/base/common/diff/diff';
import * as Touch from 'vs/base/browser/touch';
import * as Mouse from 'vs/base/browser/mouseEvent';
import * as Keyboard from 'vs/base/browser/keyboardEvent';
import * as Model from 'sql/base/parts/tree/browser/treeModel';
import * as dnd from './treeDnd';
import { ArrayNavigator } from 'vs/base/common/navigator';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { HeightMap, IViewItem } from 'sql/base/parts/tree/browser/treeViewModel';
import * as _ from 'sql/base/parts/tree/browser/tree';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Event, Emitter } from 'vs/base/common/event';
import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd';
import { DefaultTreestyler } from './treeDefaults';
import { Delayer, timeout } from 'vs/base/common/async';
import { MappedNavigator } from 'sql/base/common/navigator';
import { onUnexpectedError } from 'vs/base/common/errors';
import { StaticDND } from 'vs/base/browser/ui/list/listView';

export interface IRow {
	element: HTMLElement | null;
	templateId: string;
	templateData: any;
}

function removeFromParent(element: HTMLElement): void {
	try {
		element.parentElement!.removeChild(element);
	} catch (e) {
		// this will throw if this happens due to a blur event, nasty business
	}
}

export class RowCache implements Lifecycle.IDisposable {

	private _cache: { [templateId: string]: IRow[]; } | null;

	constructor(private context: _.ITreeContext) {
		this._cache = { '': [] };
	}

	public alloc(templateId: string): IRow {
		let result = this.cache(templateId).pop();

		if (!result) {
			let content = document.createElement('div');
			content.className = 'content';

			let row = document.createElement('div');
			row.appendChild(content);

			let templateData: any = null;

			try {
				templateData = this.context.renderer!.renderTemplate(this.context.tree, templateId, content);
			} catch (err) {
				onUnexpectedError(`Tree usage error: exception while rendering template ${err}`);
			}

			result = {
				element: row,
				templateId: templateId,
				templateData
			};
		}

		return result;
	}

	public release(templateId: string, row: IRow): void {
		removeFromParent(row.element!);
		this.cache(templateId).push(row);
	}

	private cache(templateId: string): IRow[] {
		return this._cache![templateId] || (this._cache![templateId] = []);
	}

	public garbageCollect(): void {
		if (this._cache) {
			Object.keys(this._cache).forEach(templateId => {
				this._cache![templateId].forEach(cachedRow => {
					this.context.renderer!.disposeTemplate(this.context.tree, templateId, cachedRow.templateData);
					cachedRow.element = null;
					cachedRow.templateData = null;
				});

				delete this._cache![templateId];
			});
		}
	}

	public dispose(): void {
		this.garbageCollect();
		this._cache = null;
	}
}

export interface IViewContext extends _.ITreeContext {
	cache: RowCache;
	horizontalScrolling: boolean;
}

export class ViewItem implements IViewItem {

	private context: IViewContext;

	public model: Model.Item;
	public id: string;
	protected row: IRow | null;

	public top: number;
	public height: number;
	public width: number = 0;
	public onDragStart!: (e: DragEvent) => void;

	public needsRender: boolean = false;
	public uri: string | null = null;
	public unbindDragStart: Lifecycle.IDisposable = Lifecycle.Disposable.None;
	public loadingTimer: any;

	public _styles: any;
	private _draggable: boolean = false;

	constructor(context: IViewContext, model: Model.Item) {
		this.context = context;
		this.model = model;

		this.id = this.model.id;
		this.row = null;

		this.top = 0;
		this.height = model.getHeight();

		this._styles = {};
		model.getAllTraits().forEach(t => this._styles[t] = true);

		if (model.isExpanded()) {
			this.addClass('expanded');
		}
	}

	set expanded(value: boolean) {
		value ? this.addClass('expanded') : this.removeClass('expanded');
	}

	set loading(value: boolean) {
		value ? this.addClass('loading') : this.removeClass('loading');
	}

	set draggable(value: boolean) {
		this._draggable = value;
		this.render(true);
	}

	get draggable() {
		return this._draggable;
	}

	set dropTarget(value: boolean) {
		value ? this.addClass('drop-target') : this.removeClass('drop-target');
	}

	public get element(): HTMLElement {
		return (this.row && this.row.element)!;
	}

	private _templateId: string | undefined;
	private get templateId(): string {
		return this._templateId || (this._templateId = (this.context.renderer!.getTemplateId && this.context.renderer!.getTemplateId(this.context.tree, this.model.getElement())));
	}

	public addClass(name: string): void {
		this._styles[name] = true;
		this.render(true);
	}

	public removeClass(name: string): void {
		delete this._styles[name]; // is this slow?
		this.render(true);
	}

	public render(skipUserRender = false): void {
		if (!this.model || !this.element) {
			return;
		}

		let classes = ['monaco-tree-row'];
		classes.push.apply(classes, Object.keys(this._styles));

		if (this.model.hasChildren()) {
			classes.push('has-children');
		}

		this.element.className = classes.join(' ');
		this.element.draggable = this.draggable;
		this.element.style.height = this.height + 'px';

		// ARIA
		this.element.setAttribute('role', 'treeitem');
		const accessibility = this.context.accessibilityProvider!;
		const ariaLabel = accessibility.getAriaLabel(this.context.tree, this.model.getElement());
		if (ariaLabel) {
			this.element.setAttribute('aria-label', ariaLabel);
		}
		if (accessibility.getPosInSet && accessibility.getSetSize) {
			this.element.setAttribute('aria-setsize', accessibility.getSetSize());
			this.element.setAttribute('aria-posinset', accessibility.getPosInSet(this.context.tree, this.model.getElement()));
		}
		if (this.model.hasTrait('focused')) {
			const base64Id = btoa(encodeURIComponent(this.model.id));
			this.element.setAttribute('id', base64Id);
			this.element.setAttribute('aria-selected', 'true');
		} else {
			this.element.removeAttribute('id');
			this.element.setAttribute('aria-selected', 'false');
		}
		if (this.model.hasChildren()) {
			this.element.setAttribute('aria-expanded', String(!!this._styles['expanded']));
		} else {
			this.element.removeAttribute('aria-expanded');
		}
		this.element.setAttribute('aria-level', String(this.model.getDepth()));

		if (this.context.options.paddingOnRow) {
			this.element.style.paddingLeft = this.context.options.twistiePixels! + ((this.model.getDepth() - 1) * this.context.options.indentPixels!) + 'px';
		} else {
			this.element.style.paddingLeft = ((this.model.getDepth() - 1) * this.context.options.indentPixels!) + 'px';
			(<HTMLElement>this.row!.element!.firstElementChild).style.paddingLeft = this.context.options.twistiePixels + 'px';
		}

		let uri = this.context.dnd!.getDragURI(this.context.tree, this.model.getElement());

		if (uri !== this.uri) {
			if (this.unbindDragStart) {
				this.unbindDragStart.dispose();
			}

			if (uri) {
				this.uri = uri;
				this.draggable = true;
				this.unbindDragStart = DOM.addDisposableListener(this.element, 'dragstart', (e) => {
					this.onDragStart(e);
				});
			} else {
				this.uri = null;
			}
		}

		if (!skipUserRender && this.element) {
			let paddingLeft: number = 0;
			if (this.context.horizontalScrolling) {
				const style = window.getComputedStyle(this.element);
				paddingLeft = parseFloat(style.paddingLeft!);
			}

			if (this.context.horizontalScrolling) {
				this.element.style.width = Browser.isFirefox ? '-moz-fit-content' : 'fit-content';
			}

			try {
				this.context.renderer!.renderElement(this.context.tree, this.model.getElement(), this.templateId, this.row!.templateData);
			} catch (err) {
				onUnexpectedError(`Tree usage error: exception while rendering element. ${err}`);
			}

			if (this.context.horizontalScrolling) {
				this.width = DOM.getContentWidth(this.element) + paddingLeft;
				this.element.style.width = '';
			}
		}
	}

	updateWidth(): any {
		if (!this.context.horizontalScrolling || !this.element) {
			return;
		}

		const style = window.getComputedStyle(this.element);
		const paddingLeft = parseFloat(style.paddingLeft!);
		this.element.style.width = Browser.isFirefox ? '-moz-fit-content' : 'fit-content';
		this.width = DOM.getContentWidth(this.element) + paddingLeft;
		this.element.style.width = '';
	}

	public insertInDOM(container: HTMLElement, afterElement: HTMLElement | null): void {
		if (!this.row) {
			this.row = this.context.cache.alloc(this.templateId);

			// used in reverse lookup from HTMLElement to Item
			(<any>this.element)[TreeView.BINDING] = this;
		}

		if (this.element.parentElement) {
			return;
		}

		if (afterElement === null) {
			container.appendChild(this.element);
		} else {
			try {
				container.insertBefore(this.element, afterElement);
			} catch (e) {
				onUnexpectedError('Failed to locate previous tree element');
				container.appendChild(this.element);
			}
		}

		this.render();
	}

	public removeFromDOM(): void {
		if (!this.row) {
			return;
		}

		this.unbindDragStart.dispose();

		this.uri = null;

		(<any>this.element)[TreeView.BINDING] = null;
		this.context.cache.release(this.templateId, this.row);
		this.row = null;
	}

	public dispose(): void {
		this.row = null;
	}
}

class RootViewItem extends ViewItem {

	constructor(context: IViewContext, model: Model.Item, wrapper: HTMLElement) {
		super(context, model);

		this.row = {
			element: wrapper,
			templateData: null,
			templateId: null!
		};
	}

	public override render(): void {
		if (!this.model || !this.element) {
			return;
		}

		let classes = ['monaco-tree-wrapper'];
		classes.push.apply(classes, Object.keys(this._styles));

		if (this.model.hasChildren()) {
			classes.push('has-children');
		}

		this.element.className = classes.join(' ');
	}

	public override insertInDOM(container: HTMLElement, afterElement: HTMLElement): void {
		// noop
	}

	public override removeFromDOM(): void {
		// noop
	}
}

function reactionEquals(one: _.IDragOverReaction, other: _.IDragOverReaction | null): boolean {
	if (!one && !other) {
		return true;
	} else if (!one || !other) {
		return false;
	} else if (one.accept !== other.accept) {
		return false;
	} else if (one.bubble !== other.bubble) {
		return false;
	} else if (one.effect !== other.effect) {
		return false;
	} else {
		return true;
	}
}

export class TreeView extends HeightMap {

	static readonly BINDING = 'monaco-tree-row';

	/**
	 * The delay here is used here to display the loading status after a certain number of milliseconds.
	 * It was changed from 800 as it turns out that refreshes happen instantly before the status
	 * can be notified to the user. With 100 ms, the refresh indicator will now be visible in most instances.
	 * */
	static readonly LOADING_DECORATION_DELAY = 100;

	private static counter: number = 0;
	private instance: number;

	private context: IViewContext;
	private modelListeners: Lifecycle.IDisposable[];
	private model: Model.TreeModel | null = null;

	private viewListeners: Lifecycle.IDisposable[];
	private domNode: HTMLElement;
	private wrapper: HTMLElement;
	private styleElement: HTMLStyleElement;
	private treeStyler: _.ITreeStyler;
	private rowsContainer: HTMLElement;
	private scrollableElement: ScrollableElement;
	// @ts-expect-error This always errored - ignoring for now unless we find it impacting something
	private msGesture: MSGesture | undefined;
	private lastPointerType: string = '';

	private horizontalScrolling: boolean;
	private contentWidthUpdateDelayer = new Delayer<void>(50);

	private lastRenderTop: number;
	private lastRenderHeight: number;

	private inputItem!: ViewItem;
	private items: { [id: string]: ViewItem; };

	private isRefreshing = false;
	private refreshingPreviousChildrenIds: { [id: string]: string[] } = {};
	private currentDragAndDropData: IDragAndDropData | null = null;
	private currentDropElement: any;
	private currentDropElementReaction!: _.IDragOverReaction;
	private currentDropTarget: ViewItem | null = null;
	private shouldInvalidateDropReaction: boolean;
	private currentDropTargets: ViewItem[] | null = null;
	private currentDropDisposable: Lifecycle.IDisposable = Lifecycle.Disposable.None;
	private gestureDisposable: Lifecycle.IDisposable = Lifecycle.Disposable.None;
	private dragAndDropScrollInterval: number | null = null;
	private dragAndDropScrollTimeout: number | null = null;
	private dragAndDropMouseY: number | null = null;

	private didJustPressContextMenuKey: boolean;

	private highlightedItemWasDraggable: boolean = false;
	private onHiddenScrollTop: number | null = null;

	private readonly _onDOMFocus = new Emitter<void>();
	readonly onDOMFocus: Event<void> = this._onDOMFocus.event;

	private readonly _onDOMBlur = new Emitter<void>();
	readonly onDOMBlur: Event<void> = this._onDOMBlur.event;

	private readonly _onDidScroll = new Emitter<void>();
	readonly onDidScroll: Event<void> = this._onDidScroll.event;

	constructor(context: _.ITreeContext, container: HTMLElement) {
		super();

		TreeView.counter++;
		this.instance = TreeView.counter;

		const horizontalScrollMode = typeof context.options.horizontalScrollMode === 'undefined' ? ScrollbarVisibility.Hidden : context.options.horizontalScrollMode;
		this.horizontalScrolling = horizontalScrollMode !== ScrollbarVisibility.Hidden;

		this.context = {
			dataSource: context.dataSource,
			renderer: context.renderer,
			controller: context.controller,
			dnd: context.dnd,
			filter: context.filter,
			sorter: context.sorter,
			tree: context.tree,
			accessibilityProvider: context.accessibilityProvider,
			options: context.options,
			cache: new RowCache(context),
			horizontalScrolling: this.horizontalScrolling
		};

		this.modelListeners = [];
		this.viewListeners = [];

		this.items = {};

		this.domNode = document.createElement('div');
		this.domNode.className = `monaco-tree no-focused-item monaco-tree-instance-${this.instance}`;
		// to allow direct tabbing into the tree instead of first focusing the tree
		this.domNode.tabIndex = context.options.preventRootFocus ? -1 : 0;

		this.styleElement = DOM.createStyleSheet(this.domNode);

		this.treeStyler = context.styler || new DefaultTreestyler(this.styleElement, `monaco-tree-instance-${this.instance}`);

		// ARIA
		this.domNode.setAttribute('role', 'tree');
		if (this.context.options.ariaLabel) {
			this.domNode.setAttribute('aria-label', this.context.options.ariaLabel);
		}

		if (this.context.options.alwaysFocused) {
			this.domNode.classList.add('focused');
		}

		if (!this.context.options.paddingOnRow) {
			this.domNode.classList.add('no-row-padding');
		}

		this.wrapper = document.createElement('div');
		this.wrapper.className = 'monaco-tree-wrapper';
		this.scrollableElement = new ScrollableElement(this.wrapper, {
			alwaysConsumeMouseWheel: true,
			horizontal: horizontalScrollMode,
			vertical: (typeof context.options.verticalScrollMode !== 'undefined' ? context.options.verticalScrollMode : ScrollbarVisibility.Auto),
			useShadows: context.options.useShadows
		});
		this.scrollableElement.onScroll((e) => {
			this.render(e.scrollTop, e.height, e.scrollLeft, e.width, e.scrollWidth);
			this._onDidScroll.fire();
		});

		this.gestureDisposable = Touch.Gesture.addTarget(this.wrapper);

		this.rowsContainer = document.createElement('div');
		this.rowsContainer.className = 'monaco-tree-rows';
		if (context.options.showTwistie) {
			this.rowsContainer.className += ' show-twisties';
		}

		let focusTracker = DOM.trackFocus(this.domNode);
		this.viewListeners.push(focusTracker.onDidFocus(() => this.onFocus()));
		this.viewListeners.push(focusTracker.onDidBlur(() => this.onBlur()));
		this.viewListeners.push(focusTracker);

		this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'keydown', (e) => this.onKeyDown(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'keyup', (e) => this.onKeyUp(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'mousedown', (e) => this.onMouseDown(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'mouseup', (e) => this.onMouseUp(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'auxclick', (e: MouseEvent) => {
			if (e && e.button === 1) {
				this.onMouseMiddleClick(e);
			}
		}));
		this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'click', (e) => this.onClick(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'contextmenu', (e) => this.onContextMenu(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Tap, (e) => this.onTap(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Change, (e) => this.onTouchChange(e)));

		this.viewListeners.push(DOM.addDisposableListener(window, 'dragover', (e) => this.onDragOver(e)));
		this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'drop', (e) => this.onDrop(e)));
		this.viewListeners.push(DOM.addDisposableListener(window, 'dragend', (e) => this.onDragEnd(e)));
		this.viewListeners.push(DOM.addDisposableListener(window, 'dragleave', (e) => this.onDragOver(e)));

		this.wrapper.appendChild(this.rowsContainer);
		this.domNode.appendChild(this.scrollableElement.getDomNode());
		container.appendChild(this.domNode);

		this.lastRenderTop = 0;
		this.lastRenderHeight = 0;

		this.didJustPressContextMenuKey = false;

		this.currentDropTarget = null;
		this.currentDropTargets = [];
		this.shouldInvalidateDropReaction = false;

		this.dragAndDropScrollInterval = null;
		this.dragAndDropScrollTimeout = null;

		this.onRowsChanged();
		this.layout();

		this.setupMSGesture();

		this.applyStyles(context.options);
	}

	public applyStyles(styles: _.ITreeStyles): void {
		this.treeStyler.style(styles);
	}

	protected override createViewItem(item: Model.Item): IViewItem {
		return new ViewItem(this.context, item);
	}

	public getHTMLElement(): HTMLElement {
		return this.domNode;
	}

	public focus(): void {
		this.domNode.focus();
	}

	public isFocused(): boolean {
		return document.activeElement === this.domNode;
	}

	public blur(): void {
		this.domNode.blur();
	}

	public onVisible(): void {
		this.scrollTop = this.onHiddenScrollTop!;
		this.onHiddenScrollTop = null;
		this.setupMSGesture();
	}

	private setupMSGesture(): void {
		if ((<any>window).MSGesture) {
			// @ts-expect-error This always errored - ignoring for now unless we find it impacting something
			this.msGesture = new MSGesture();
			setTimeout(() => this.msGesture!.target = this.wrapper, 100); // TODO@joh, TODO@IETeam
		}
	}

	public onHidden(): void {
		this.onHiddenScrollTop = this.scrollTop;
	}

	private isTreeVisible(): boolean {
		return this.onHiddenScrollTop === null;
	}

	public layout(height?: number, width?: number): void {
		if (!this.isTreeVisible()) {
			return;
		}

		this.viewHeight = height || DOM.getContentHeight(this.wrapper); // render
		this.scrollHeight = this.getContentHeight();

		if (this.horizontalScrolling) {
			this.viewWidth = width || DOM.getContentWidth(this.wrapper);
		}
	}

	private render(scrollTop: number, viewHeight: number, scrollLeft: number, viewWidth: number, scrollWidth: number): void {
		let i: number;
		let stop: number;

		let renderTop = scrollTop;
		let renderBottom = scrollTop + viewHeight;
		let thisRenderBottom = this.lastRenderTop + this.lastRenderHeight;

		// when view scrolls down, start rendering from the renderBottom
		for (i = this.indexAfter(renderBottom) - 1, stop = this.indexAt(Math.max(thisRenderBottom, renderTop)); i >= stop; i--) {
			this.insertItemInDOM(<ViewItem>this.itemAtIndex(i));
		}

		// when view scrolls up, start rendering from either this.renderTop or renderBottom
		for (i = Math.min(this.indexAt(this.lastRenderTop), this.indexAfter(renderBottom)) - 1, stop = this.indexAt(renderTop); i >= stop; i--) {
			this.insertItemInDOM(<ViewItem>this.itemAtIndex(i));
		}

		// when view scrolls down, start unrendering from renderTop
		for (i = this.indexAt(this.lastRenderTop), stop = Math.min(this.indexAt(renderTop), this.indexAfter(thisRenderBottom)); i < stop; i++) {
			this.removeItemFromDOM(<ViewItem>this.itemAtIndex(i));
		}

		// when view scrolls up, start unrendering from either renderBottom this.renderTop
		for (i = Math.max(this.indexAfter(renderBottom), this.indexAt(this.lastRenderTop)), stop = this.indexAfter(thisRenderBottom); i < stop; i++) {
			this.removeItemFromDOM(<ViewItem>this.itemAtIndex(i));
		}

		let topItem = this.itemAtIndex(this.indexAt(renderTop));

		if (topItem) {
			this.rowsContainer.style.top = (topItem.top - renderTop) + 'px';
		}

		if (this.horizontalScrolling) {
			this.rowsContainer.style.left = -scrollLeft + 'px';
			this.rowsContainer.style.width = `${Math.max(scrollWidth, viewWidth)}px`;
		}

		this.lastRenderTop = renderTop;
		this.lastRenderHeight = renderBottom - renderTop;
	}

	public setModel(newModel: Model.TreeModel): void {
		this.releaseModel();
		this.model = newModel;

		this.model.onRefresh(this.onRefreshing, this, this.modelListeners);
		this.model.onDidRefresh(this.onRefreshed, this, this.modelListeners);
		this.model.onSetInput(this.onClearingInput, this, this.modelListeners);
		this.model.onDidSetInput(this.onSetInput, this, this.modelListeners);
		this.model.onDidFocus(this.onModelFocusChange, this, this.modelListeners);

		this.model.onRefreshItemChildren(this.onItemChildrenRefreshing, this, this.modelListeners);
		this.model.onDidRefreshItemChildren(this.onItemChildrenRefreshed, this, this.modelListeners);
		this.model.onDidRefreshItem(this.onItemRefresh, this, this.modelListeners);
		this.model.onExpandItem(this.onItemExpanding, this, this.modelListeners);
		this.model.onDidExpandItem(this.onItemExpanded, this, this.modelListeners);
		this.model.onCollapseItem(this.onItemCollapsing, this, this.modelListeners);
		this.model.onDidRevealItem(this.onItemReveal, this, this.modelListeners);
		this.model.onDidAddTraitItem(this.onItemAddTrait, this, this.modelListeners);
		this.model.onDidRemoveTraitItem(this.onItemRemoveTrait, this, this.modelListeners);
	}

	private onRefreshing(): void {
		this.isRefreshing = true;
	}

	private onRefreshed(): void {
		this.isRefreshing = false;
		this.onRowsChanged();
	}

	private onRowsChanged(scrollTop: number = this.scrollTop): void {
		if (this.isRefreshing) {
			return;
		}

		this.scrollTop = scrollTop;
		this.updateScrollWidth();
	}

	private updateScrollWidth(): void {
		if (!this.horizontalScrolling) {
			return;
		}

		this.contentWidthUpdateDelayer.trigger(() => {
			const keys = Object.keys(this.items);
			let scrollWidth = 0;

			for (const key of keys) {
				scrollWidth = Math.max(scrollWidth, this.items[key].width);
			}

			this.scrollWidth = scrollWidth + 10 /* scrollbar */;
		});
	}

	public focusNextPage(eventPayload?: any): void {
		let lastPageIndex = this.indexAt(this.scrollTop + this.viewHeight);
		lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
		let lastPageElement = this.itemAtIndex(lastPageIndex).model.getElement();
		let currentlyFocusedElement = this.model!.getFocus();

		if (currentlyFocusedElement !== lastPageElement) {
			this.model!.setFocus(lastPageElement, eventPayload);
		} else {
			let previousScrollTop = this.scrollTop;
			this.scrollTop += this.viewHeight;

			if (this.scrollTop !== previousScrollTop) {

				// Let the scroll event listener run
				setTimeout(() => {
					this.focusNextPage(eventPayload);
				}, 0);
			}
		}
	}

	public focusPreviousPage(eventPayload?: any): void {
		let firstPageIndex: number;

		if (this.scrollTop === 0) {
			firstPageIndex = this.indexAt(this.scrollTop);
		} else {
			firstPageIndex = this.indexAfter(this.scrollTop - 1);
		}

		let firstPageElement = this.itemAtIndex(firstPageIndex).model.getElement();
		let currentlyFocusedElement = this.model!.getFocus();

		if (currentlyFocusedElement !== firstPageElement) {
			this.model!.setFocus(firstPageElement, eventPayload);
		} else {
			let previousScrollTop = this.scrollTop;
			this.scrollTop -= this.viewHeight;

			if (this.scrollTop !== previousScrollTop) {

				// Let the scroll event listener run
				setTimeout(() => {
					this.focusPreviousPage(eventPayload);
				}, 0);
			}
		}
	}

	public get viewHeight() {
		const scrollDimensions = this.scrollableElement.getScrollDimensions();
		return scrollDimensions.height;
	}

	public set viewHeight(height: number) {
		this.scrollableElement.setScrollDimensions({ height });
	}

	private set scrollHeight(scrollHeight: number) {
		scrollHeight = scrollHeight + (this.horizontalScrolling ? 10 : 0);
		this.scrollableElement.setScrollDimensions({ scrollHeight });
	}

	public get viewWidth(): number {
		const scrollDimensions = this.scrollableElement.getScrollDimensions();
		return scrollDimensions.width;
	}

	public set viewWidth(viewWidth: number) {
		this.scrollableElement.setScrollDimensions({ width: viewWidth });
	}

	private set scrollWidth(scrollWidth: number) {
		this.scrollableElement.setScrollDimensions({ scrollWidth });
	}

	public get scrollTop(): number {
		const scrollPosition = this.scrollableElement.getScrollPosition();
		return scrollPosition.scrollTop;
	}

	public set scrollTop(scrollTop: number) {
		const scrollHeight = this.getContentHeight() + (this.horizontalScrolling ? 10 : 0);
		this.scrollableElement.setScrollDimensions({ scrollHeight });
		this.scrollableElement.setScrollPosition({ scrollTop });
	}

	public getScrollPosition(): number {
		const height = this.getContentHeight() - this.viewHeight;
		return height <= 0 ? 1 : this.scrollTop / height;
	}

	public setScrollPosition(pos: number): void {
		const height = this.getContentHeight() - this.viewHeight;
		this.scrollTop = height * pos;
	}

	// Events

	private onClearingInput(e: Model.IInputEvent): void {
		let item = <Model.Item>e.item;
		if (item) {
			this.onRemoveItems(new MappedNavigator(item.getNavigator(), item => item && item.id));
			this.onRowsChanged();
		}
	}

	private onSetInput(e: Model.IInputEvent): void {
		this.context.cache.garbageCollect();
		this.inputItem = new RootViewItem(this.context, <Model.Item>e.item, this.wrapper);
	}

	private onItemChildrenRefreshing(e: Model.IItemChildrenRefreshEvent): void {
		let item = <Model.Item>e.item;
		let viewItem = this.items[item.id];

		if (viewItem && this.context.options.showLoading) {
			viewItem.loadingTimer = setTimeout(() => {
				viewItem.loadingTimer = 0;
				viewItem.loading = true;
			}, TreeView.LOADING_DECORATION_DELAY);
		}

		if (!e.isNested) {
			let childrenIds: string[] = [];
			let navigator = item.getNavigator();
			let childItem: Model.Item | null;

			// eslint-disable-next-line no-cond-assign
			while (childItem = navigator.next()) {
				childrenIds.push(childItem.id);
			}

			this.refreshingPreviousChildrenIds[item.id] = childrenIds;
		}
	}

	private onItemChildrenRefreshed(e: Model.IItemChildrenRefreshEvent): void {
		let item = <Model.Item>e.item;
		let viewItem = this.items[item.id];

		if (viewItem) {
			if (viewItem.loadingTimer) {
				clearTimeout(viewItem.loadingTimer);
				viewItem.loadingTimer = 0;
			}

			viewItem.loading = false;
		}

		if (!e.isNested) {
			let previousChildrenIds = this.refreshingPreviousChildrenIds[item.id];
			let afterModelItems: Model.Item[] = [];
			let navigator = item.getNavigator();
			let childItem: Model.Item | null;

			// eslint-disable-next-line no-cond-assign
			while (childItem = navigator.next()) {
				afterModelItems.push(childItem);
			}

			let skipDiff = Math.abs(previousChildrenIds.length - afterModelItems.length) > 1000;
			let diff: Diff.IDiffChange[] = [];
			let doToInsertItemsAlreadyExist: boolean = false;

			if (!skipDiff) {
				const lcs = new Diff.LcsDiff(
					{
						getElements: () => previousChildrenIds
					},
					{
						getElements: () => afterModelItems.map(item => item.id)
					},
					null
				);

				diff = lcs.ComputeDiff(false).changes;

				// this means that the result of the diff algorithm would result
				// in inserting items that were already registered. this can only
				// happen if the data provider returns bad ids OR if the sorting
				// of the elements has changed
				doToInsertItemsAlreadyExist = diff.some(d => {
					if (d.modifiedLength > 0) {
						for (let i = d.modifiedStart, len = d.modifiedStart + d.modifiedLength; i < len; i++) {
							if (this.items.hasOwnProperty(afterModelItems[i].id)) {
								return true;
							}
						}
					}
					return false;
				});
			}

			// 50 is an optimization number, at some point we're better off
			// just replacing everything
			if (!skipDiff && !doToInsertItemsAlreadyExist && diff.length < 50) {
				for (const diffChange of diff) {

					if (diffChange.originalLength > 0) {
						this.onRemoveItems(new ArrayNavigator(previousChildrenIds, diffChange.originalStart, diffChange.originalStart + diffChange.originalLength));
					}

					if (diffChange.modifiedLength > 0) {
						let beforeItem: Model.Item | null = afterModelItems[diffChange.modifiedStart - 1] || item;
						beforeItem = beforeItem.getDepth() > 0 ? beforeItem : null;

						this.onInsertItems(new ArrayNavigator(afterModelItems, diffChange.modifiedStart, diffChange.modifiedStart + diffChange.modifiedLength), beforeItem ? beforeItem.id : null);
					}
				}

			} else if (skipDiff || diff.length) {
				this.onRemoveItems(new ArrayNavigator(previousChildrenIds));
				this.onInsertItems(new ArrayNavigator(afterModelItems), item.getDepth() > 0 ? item.id : null);
			}

			if (skipDiff || diff.length) {
				this.onRowsChanged();
			}
		}
	}

	private onItemRefresh(item: Model.Item): void {
		this.onItemsRefresh([item]);
	}

	private onItemsRefresh(items: Model.Item[]): void {
		this.onRefreshItemSet(items.filter(item => this.items.hasOwnProperty(item.id)));
		this.onRowsChanged();
	}

	private onItemExpanding(e: Model.IItemExpandEvent): void {
		let viewItem = this.items[e.item.id];
		if (viewItem) {
			viewItem.expanded = true;
		}
	}

	private onItemExpanded(e: Model.IItemExpandEvent): void {
		let item = <Model.Item>e.item;
		let viewItem = this.items[item.id];
		if (viewItem) {
			viewItem.expanded = true;

			let height = this.onInsertItems(item.getNavigator(), item.id) || 0;
			let scrollTop = this.scrollTop;

			if (viewItem.top + viewItem.height <= this.scrollTop) {
				scrollTop += height;
			}

			this.onRowsChanged(scrollTop);
		}
	}

	private onItemCollapsing(e: Model.IItemCollapseEvent): void {
		let item = <Model.Item>e.item;
		let viewItem = this.items[item.id];
		if (viewItem) {
			viewItem.expanded = false;
			this.onRemoveItems(new MappedNavigator(item.getNavigator(), item => item && item.id));
			this.onRowsChanged();
		}
	}

	private onItemReveal(e: Model.IItemRevealEvent): void {
		let item = <Model.Item>e.item;
		let relativeTop = <number>e.relativeTop;
		let viewItem = this.items[item.id];
		if (viewItem) {
			if (relativeTop !== null) {
				relativeTop = relativeTop < 0 ? 0 : relativeTop;
				relativeTop = relativeTop > 1 ? 1 : relativeTop;

				// y = mx + b
				let m = viewItem.height - this.viewHeight;
				this.scrollTop = m * relativeTop + viewItem.top;
			} else {
				let viewItemBottom = viewItem.top + viewItem.height;
				let wrapperBottom = this.scrollTop + this.viewHeight;

				if (viewItem.top < this.scrollTop) {
					this.scrollTop = viewItem.top;
				} else if (viewItemBottom >= wrapperBottom) {
					this.scrollTop = viewItemBottom - this.viewHeight;
				}
			}
		}
	}

	private onItemAddTrait(e: Model.IItemTraitEvent): void {
		let item = <Model.Item>e.item;
		let trait = <string>e.trait;
		let viewItem = this.items[item.id];
		if (viewItem) {
			viewItem.addClass(trait);
		}
		if (trait === 'highlighted') {
			this.domNode.classList.add(trait);

			// Ugly Firefox fix: input fields can't be selected if parent nodes are draggable
			if (viewItem) {
				this.highlightedItemWasDraggable = !!viewItem.draggable;
				if (viewItem.draggable) {
					viewItem.draggable = false;
				}
			}
		}
	}

	private onItemRemoveTrait(e: Model.IItemTraitEvent): void {
		let item = <Model.Item>e.item;
		let trait = <string>e.trait;
		let viewItem = this.items[item.id];
		if (viewItem) {
			viewItem.removeClass(trait);
		}
		if (trait === 'highlighted') {
			this.domNode.classList.remove(trait);

			// Ugly Firefox fix: input fields can't be selected if parent nodes are draggable
			if (this.highlightedItemWasDraggable) {
				viewItem.draggable = true;
			}
			this.highlightedItemWasDraggable = false;
		}
	}

	private onModelFocusChange(): void {
		const focus = this.model && this.model.getFocus();

		this.domNode.classList.toggle('no-focused-item', !focus);

		// ARIA
		if (focus) {
			this.domNode.setAttribute('aria-activedescendant', btoa(encodeURIComponent(this.context.dataSource.getId(this.context.tree, focus))));
		} else {
			this.domNode.removeAttribute('aria-activedescendant');
		}
	}

	// HeightMap "events"

	public override onInsertItem(item: ViewItem): void {
		item.onDragStart = (e) => { this.onDragStart(item, e); };
		item.needsRender = true;
		this.refreshViewItem(item);
		this.items[item.id] = item;
	}

	public override onRefreshItem(item: ViewItem, needsRender = false): void {
		item.needsRender = item.needsRender || needsRender;
		this.refreshViewItem(item);
	}

	public override onRemoveItem(item: ViewItem): void {
		this.removeItemFromDOM(item);
		item.dispose();
		delete this.items[item.id];
	}

	// ViewItem refresh

	private refreshViewItem(item: ViewItem): void {
		item.render();

		if (this.shouldBeRendered(item)) {
			this.insertItemInDOM(item);
		} else {
			this.removeItemFromDOM(item);
		}
	}

	// DOM Events

	private onClick(e: MouseEvent): void {
		if (this.lastPointerType && this.lastPointerType !== 'mouse') {
			return;
		}

		let event = new Mouse.StandardMouseEvent(e);
		let item = this.getItemAround(event.target);

		if (!item) {
			return;
		}

		this.context.controller!.onClick(this.context.tree, item.model.getElement(), event);
	}

	private onMouseMiddleClick(e: MouseEvent): void {
		if (!this.context.controller!.onMouseMiddleClick!) {
			return;
		}

		let event = new Mouse.StandardMouseEvent(e);
		let item = this.getItemAround(event.target);

		if (!item) {
			return;
		}
		this.context.controller!.onMouseMiddleClick!(this.context.tree, item.model.getElement(), event);
	}

	private onMouseDown(e: MouseEvent): void {
		this.didJustPressContextMenuKey = false;

		if (!this.context.controller!.onMouseDown!) {
			return;
		}

		if (this.lastPointerType && this.lastPointerType !== 'mouse') {
			return;
		}

		let event = new Mouse.StandardMouseEvent(e);

		if (event.ctrlKey && Platform.isNative && Platform.isMacintosh) {
			return;
		}

		let item = this.getItemAround(event.target);

		if (!item) {
			return;
		}

		this.context.controller!.onMouseDown!(this.context.tree, item.model.getElement(), event);
	}

	private onMouseUp(e: MouseEvent): void {
		if (!this.context.controller!.onMouseUp!) {
			return;
		}

		if (this.lastPointerType && this.lastPointerType !== 'mouse') {
			return;
		}

		let event = new Mouse.StandardMouseEvent(e);

		if (event.ctrlKey && Platform.isNative && Platform.isMacintosh) {
			return;
		}

		let item = this.getItemAround(event.target);

		if (!item) {
			return;
		}

		this.context.controller!.onMouseUp!(this.context.tree, item.model.getElement(), event);
	}

	private onTap(e: Touch.GestureEvent): void {
		let item = this.getItemAround(<HTMLElement>e.initialTarget);

		if (!item) {
			return;
		}

		this.context.controller!.onTap(this.context.tree, item.model.getElement(), e);
	}

	private onTouchChange(event: Touch.GestureEvent): void {
		event.preventDefault();
		event.stopPropagation();

		this.scrollTop -= event.translationY;
	}

	private onContextMenu(keyboardEvent: KeyboardEvent): void;
	private onContextMenu(mouseEvent: MouseEvent): void;
	private onContextMenu(event: KeyboardEvent | MouseEvent): void {
		let resultEvent: _.ContextMenuEvent;
		let element: any;

		if (event instanceof KeyboardEvent || this.didJustPressContextMenuKey) {
			this.didJustPressContextMenuKey = false;

			let keyboardEvent = new Keyboard.StandardKeyboardEvent(<KeyboardEvent>event);
			element = this.model!.getFocus();

			let position: DOM.IDomNodePagePosition;

			if (!element) {
				element = this.model!.getInput();
				position = DOM.getDomNodePagePosition(this.inputItem.element);
			} else {
				const id = this.context.dataSource.getId(this.context.tree, element);
				const viewItem = this.items[id!];
				position = DOM.getDomNodePagePosition(viewItem.element);
			}

			resultEvent = new _.KeyboardContextMenuEvent(position.left + position.width, position.top, keyboardEvent);

		} else {
			let mouseEvent = new Mouse.StandardMouseEvent(<MouseEvent>event);
			let item = this.getItemAround(mouseEvent.target);

			if (!item) {
				return;
			}

			element = item.model.getElement();
			resultEvent = new _.MouseContextMenuEvent(mouseEvent);
		}

		this.context.controller!.onContextMenu(this.context.tree, element, resultEvent);
	}

	private onKeyDown(e: KeyboardEvent): void {
		let event = new Keyboard.StandardKeyboardEvent(e);

		this.didJustPressContextMenuKey = event.keyCode === KeyCode.ContextMenu || (event.shiftKey && event.keyCode === KeyCode.F10);

		if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
			return; // Ignore event if target is a form input field (avoids browser specific issues)
		}

		if (this.didJustPressContextMenuKey) {
			event.preventDefault();
			event.stopPropagation();
		}

		this.context.controller!.onKeyDown(this.context.tree, event);
	}

	private onKeyUp(e: KeyboardEvent): void {
		if (this.didJustPressContextMenuKey) {
			this.onContextMenu(e);
		}

		this.didJustPressContextMenuKey = false;
		this.context.controller!.onKeyUp(this.context.tree, new Keyboard.StandardKeyboardEvent(e));
	}

	private onDragStart(item: ViewItem, e: any): void {
		if (this.model!.getHighlight()) {
			return;
		}

		let element = item.model.getElement();
		let selection = this.model!.getSelection();
		let elements: any[];

		if (selection.indexOf(element) > -1) {
			elements = selection;
		} else {
			elements = [element];
		}

		e.dataTransfer.effectAllowed = 'copyMove';
		e.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([item.uri]));
		if (e.dataTransfer.setDragImage) {
			let label: string;

			if (this.context.dnd!.getDragLabel) {
				label = this.context.dnd!.getDragLabel!(this.context.tree, elements);
			} else {
				label = String(elements.length);
			}

			const dragImage = document.createElement('div');
			dragImage.className = 'monaco-tree-drag-image';
			dragImage.textContent = label;
			document.body.appendChild(dragImage);
			e.dataTransfer.setDragImage(dragImage, -10, -10);
			setTimeout(() => document.body.removeChild(dragImage), 0);
		}

		this.currentDragAndDropData = new dnd.ElementsDragAndDropData(elements);
		StaticDND.CurrentDragAndDropData = new dnd.ExternalElementsDragAndDropData(elements);

		this.context.dnd!.onDragStart(this.context.tree, this.currentDragAndDropData, new Mouse.DragMouseEvent(e));
	}

	private setupDragAndDropScrollInterval(): void {
		let viewTop = DOM.getTopLeftOffset(this.wrapper).top;

		if (!this.dragAndDropScrollInterval) {
			this.dragAndDropScrollInterval = window.setInterval(() => {
				if (this.dragAndDropMouseY === null) {
					return;
				}

				let diff = this.dragAndDropMouseY - viewTop;
				let scrollDiff = 0;
				let upperLimit = this.viewHeight - 35;

				if (diff < 35) {
					scrollDiff = Math.max(-14, 0.2 * (diff - 35));
				} else if (diff > upperLimit) {
					scrollDiff = Math.min(14, 0.2 * (diff - upperLimit));
				}

				this.scrollTop += scrollDiff;
			}, 10);

			this.cancelDragAndDropScrollTimeout();

			this.dragAndDropScrollTimeout = window.setTimeout(() => {
				this.cancelDragAndDropScrollInterval();
				this.dragAndDropScrollTimeout = null;
			}, 1000);
		}
	}

	private cancelDragAndDropScrollInterval(): void {
		if (this.dragAndDropScrollInterval) {
			window.clearInterval(this.dragAndDropScrollInterval);
			this.dragAndDropScrollInterval = null;
		}

		this.cancelDragAndDropScrollTimeout();
	}

	private cancelDragAndDropScrollTimeout(): void {
		if (this.dragAndDropScrollTimeout) {
			window.clearTimeout(this.dragAndDropScrollTimeout);
			this.dragAndDropScrollTimeout = null;
		}
	}

	private onDragOver(e: DragEvent): boolean {
		e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)

		let event = new Mouse.DragMouseEvent(e);

		let viewItem = this.getItemAround(event.target);

		if (!viewItem || (event.posx === 0 && event.posy === 0 && event.browserEvent.type === DOM.EventType.DRAG_LEAVE)) {
			// dragging outside of tree

			if (this.currentDropTarget) {
				// clear previously hovered element feedback

				this.currentDropTargets!.forEach(i => i.dropTarget = false);
				this.currentDropTargets = [];
				this.currentDropDisposable.dispose();
			}

			this.cancelDragAndDropScrollInterval();
			this.currentDropTarget = null;
			this.currentDropElement = null;
			this.dragAndDropMouseY = null;

			return false;
		}

		// dragging inside the tree
		this.setupDragAndDropScrollInterval();
		this.dragAndDropMouseY = event.posy;

		if (!this.currentDragAndDropData) {
			// just started dragging

			if (StaticDND.CurrentDragAndDropData) {
				this.currentDragAndDropData = StaticDND.CurrentDragAndDropData;
			} else {
				if (!event.dataTransfer.types) {
					return false;
				}

				this.currentDragAndDropData = new dnd.DesktopDragAndDropData();
			}
		}

		this.currentDragAndDropData.update((event.browserEvent as DragEvent).dataTransfer!);

		let element: any;
		let item: Model.Item | null = viewItem.model;
		let reaction: _.IDragOverReaction | null;

		// check the bubble up behavior
		do {
			element = item ? item.getElement() : this.model!.getInput();
			reaction = this.context.dnd!.onDragOver(this.context.tree, this.currentDragAndDropData, element, event);

			if (!reaction || reaction.bubble !== _.DragOverBubble.BUBBLE_UP) {
				break;
			}

			item = item && item.parent;
		} while (item);

		if (!item) {
			this.currentDropElement = null;
			return false;
		}

		let canDrop = reaction && reaction.accept;

		if (canDrop) {
			this.currentDropElement = item.getElement();
			event.preventDefault();
			event.dataTransfer.dropEffect = reaction!.effect === _.DragOverEffect.COPY ? 'copy' : 'move';
		} else {
			this.currentDropElement = null;
		}

		// item is the model item where drop() should be called

		// can be null
		let currentDropTarget = item.id === this.inputItem.id ? this.inputItem : this.items[item.id];

		if (this.shouldInvalidateDropReaction || this.currentDropTarget !== currentDropTarget || !reactionEquals(this.currentDropElementReaction, reaction)) {
			this.shouldInvalidateDropReaction = false;

			if (this.currentDropTarget) {
				this.currentDropTargets!.forEach(i => i.dropTarget = false);
				this.currentDropTargets = [];
				this.currentDropDisposable.dispose();
			}

			this.currentDropTarget = currentDropTarget;
			this.currentDropElementReaction = reaction!;

			if (canDrop) {
				// setup hover feedback for drop target

				if (this.currentDropTarget) {
					this.currentDropTarget.dropTarget = true;
					this.currentDropTargets!.push(this.currentDropTarget);
				}

				if (reaction!.bubble === _.DragOverBubble.BUBBLE_DOWN) {
					let nav = item.getNavigator();
					let child: Model.Item | null;
					// eslint-disable-next-line no-cond-assign
					while (child = nav.next()) {
						viewItem = this.items[child.id];
						if (viewItem) {
							viewItem.dropTarget = true;
							this.currentDropTargets!.push(viewItem);
						}
					}
				}

				if (reaction!.autoExpand) {
					const timeoutPromise = timeout(500);
					this.currentDropDisposable = Lifecycle.toDisposable(() => timeoutPromise.cancel());

					timeoutPromise
						.then(() => this.context.tree.expand(this.currentDropElement))
						.then(() => this.shouldInvalidateDropReaction = true);
				}
			}
		}

		return true;
	}

	private onDrop(e: DragEvent): void {
		if (this.currentDropElement) {
			let event = new Mouse.DragMouseEvent(e);
			event.preventDefault();
			this.currentDragAndDropData!.update((event.browserEvent as DragEvent).dataTransfer!);
			this.context.dnd!.drop(this.context.tree, this.currentDragAndDropData!, this.currentDropElement, event);
			this.onDragEnd(e);
		} else {
			this.context.dnd!.dropAbort(this.context.tree, this.currentDragAndDropData!);
		}
		this.cancelDragAndDropScrollInterval();
	}

	private onDragEnd(e: DragEvent): void {
		if (this.currentDropTarget) {
			this.currentDropTargets!.forEach(i => i.dropTarget = false);
			this.currentDropTargets = [];
		} else {
			this.context.dnd!.dropAbort(this.context.tree, this.currentDragAndDropData!);
		}

		this.currentDropDisposable.dispose();

		this.cancelDragAndDropScrollInterval();
		this.currentDragAndDropData = null;
		StaticDND.CurrentDragAndDropData = undefined;
		this.currentDropElement = null;
		this.currentDropTarget = null;
		this.dragAndDropMouseY = null;
	}

	private onFocus(): void {
		if (!this.context.options.alwaysFocused) {
			this.domNode.classList.add('focused');
		}

		this._onDOMFocus.fire();
	}

	private onBlur(): void {
		if (!this.context.options.alwaysFocused) {
			this.domNode.classList.remove('focused');
		}

		this.domNode.removeAttribute('aria-activedescendant'); // ARIA

		this._onDOMBlur.fire();
	}

	// DOM changes

	private insertItemInDOM(item: ViewItem): void {
		let elementAfter: HTMLElement | null = null;
		let itemAfter = <ViewItem>this.itemAfter(item);

		if (itemAfter && itemAfter.element) {
			elementAfter = itemAfter.element;
		}

		item.insertInDOM(this.rowsContainer, elementAfter);
	}

	private removeItemFromDOM(item: ViewItem): void {
		if (!item) {
			return;
		}

		item.removeFromDOM();
	}

	// Helpers

	private shouldBeRendered(item: ViewItem): boolean {
		return item.top < this.lastRenderTop + this.lastRenderHeight && item.top + item.height > this.lastRenderTop;
	}

	private getItemAround(element: HTMLElement): ViewItem | undefined {
		let candidate: ViewItem = this.inputItem;
		let el: HTMLElement | null = element;

		do {
			if ((<any>el)[TreeView.BINDING]) {
				candidate = (<any>el)[TreeView.BINDING];
			}

			if (el === this.wrapper || el === this.domNode) {
				return candidate;
			}

			if (el === this.scrollableElement.getDomNode() || el === document.body) {
				return undefined;
			}
		} while (el = el.parentElement); // eslint-disable-line no-cond-assign

		return undefined;
	}

	// Cleanup

	private releaseModel(): void {
		if (this.model) {
			this.modelListeners = Lifecycle.dispose(this.modelListeners);
			this.model = null;
		}
	}

	public override dispose(): void {
		// TODO@joao: improve
		this.scrollableElement.dispose();

		this.releaseModel();

		this.viewListeners = Lifecycle.dispose(this.viewListeners);

		this._onDOMFocus.dispose();
		this._onDOMBlur.dispose();

		if (this.domNode.parentNode) {
			this.domNode.parentNode.removeChild(this.domNode);
		}

		if (this.items) {
			Object.keys(this.items).forEach(key => this.items[key].removeFromDOM());
		}

		if (this.context.cache) {
			this.context.cache.dispose();
		}
		this.gestureDisposable.dispose();

		super.dispose();
	}
}
