import * as React from 'react';
import { merge, of, Subject, Subscription } from 'rxjs';
import { catchError, debounceTime, delay, distinctUntilChanged, filter, mergeMap, share, switchMap, takeUntil, } from 'rxjs/operators';
import { fetchTreeEntries } from '../repo/backend';
import { asError, isErrorLike } from '../util/errors';
import { ChildTreeLayer } from './ChildTreeLayer';
import { Directory } from './Directory';
import { File } from './File';
import { hasSingleChild, maxEntries, singleChildEntriesToGitTree, treePadding, } from './util';
const LOADING = 'loading';
export class TreeLayer extends React.Component {
    constructor(props) {
        super(props);
        this.subscriptions = new Subscription();
        this.componentUpdates = new Subject();
        this.rowHovers = new Subject();
        /**
         * Non-root tree layers call this to activate a prefetch request in the root tree layer
         */
        this.invokeOnHover = (e) => {
            if (this.props.onHover) {
                e.stopPropagation();
                this.props.onHover(this.node.path);
            }
        };
        this.handleTreeClick = () => {
            this.props.onSelect(this.node);
            const path = this.props.entryInfo ? this.props.entryInfo.path : '';
            this.props.onToggleExpand(path, !this.props.isExpanded, this.node);
        };
        /**
         * noopRowClick is the click handler for <a> rows of the tree element
         * that shouldn't update URL on click w/o modifier key (but should retain
         * anchor element properties, like right click "Copy link address").
         */
        this.noopRowClick = (e) => {
            if (!e.altKey && !e.metaKey && !e.shiftKey && !e.ctrlKey) {
                e.preventDefault();
                e.stopPropagation();
            }
            this.handleTreeClick();
        };
        /**
         * linkRowClick is the click handler for <Link>
         */
        this.linkRowClick = (e) => {
            this.props.setActiveNode(this.node);
            this.props.onSelect(this.node);
        };
        this.setChildNode = (node, index) => {
            this.node.childNodes[index] = node;
        };
        this.node = {
            index: this.props.index,
            parent: this.props.parent,
            childNodes: [],
            path: this.props.entryInfo ? this.props.entryInfo.path : '',
            url: this.props.entryInfo ? this.props.entryInfo.url : '',
        };
        this.state = {};
    }
    componentDidMount() {
        // Set this row as a childNode of its TreeLayer parent
        this.props.setChildNodes(this.node, this.node.index);
        this.subscriptions.add(this.componentUpdates
            .pipe(distinctUntilChanged((x, y) => x.repoPath === y.repoPath &&
            x.rev === y.rev &&
            x.commitID === y.commitID &&
            x.parentPath === y.parentPath &&
            x.isExpanded === y.isExpanded), filter(props => props.isExpanded), switchMap(props => {
            const treeFetch = fetchTreeEntries({
                repoPath: props.repoPath,
                rev: props.rev,
                commitID: props.commitID,
                filePath: props.parentPath || '',
                first: maxEntries,
            }).pipe(catchError(err => [asError(err)]), share());
            return merge(treeFetch, of(LOADING).pipe(delay(300), takeUntil(treeFetch)));
        }))
            .subscribe(treeOrError => this.setState({ treeOrError }), err => console.error(err)));
        // If the layer is already expanded, fetch contents.
        if (this.props.isExpanded) {
            this.componentUpdates.next(this.props);
        }
        // If navigating directly to an entry, set the correct active node.
        if (this.props.activePath === this.node.path) {
            this.props.setActiveNode(this.node);
        }
        // This handles pre-fetching when a user
        // hovers over a directory. The `subscribe` is empty because
        // we simply want to cache the network request.
        this.subscriptions.add(this.rowHovers
            .pipe(debounceTime(100), mergeMap(path => fetchTreeEntries({
            repoPath: this.props.repoPath,
            rev: this.props.rev,
            commitID: this.props.commitID,
            filePath: path,
            first: maxEntries,
        }).pipe(catchError(err => [asError(err)]))))
            .subscribe());
    }
    shouldComponentUpdate(nextProps) {
        if (nextProps.activeNode !== this.props.activeNode) {
            if (nextProps.activeNode === this.node) {
                return true;
            }
            // Update if currently active node
            if (this.props.activeNode === this.node) {
                return true;
            }
            // Update if parent of currently active node
            let currentParent = this.props.activeNode.parent;
            while (currentParent) {
                if (currentParent === this.node) {
                    return true;
                }
                currentParent = currentParent.parent;
            }
        }
        if (nextProps.selectedNode !== this.props.selectedNode) {
            // Update if this row will be the selected node.
            if (nextProps.selectedNode === this.node) {
                return true;
            }
            // Update if a parent of the next selected row.
            let parent = nextProps.selectedNode.parent;
            while (parent) {
                if (parent === this.node) {
                    return true;
                }
                parent = parent && parent.parent;
            }
            // Update if currently selected node.
            if (this.props.selectedNode === this.node) {
                return true;
            }
            // Update if parent of currently selected node.
            let currentParent = this.props.selectedNode.parent;
            while (currentParent) {
                if (currentParent === this.node) {
                    return true;
                }
                currentParent = currentParent && currentParent.parent;
            }
            // If none of the above conditions are met, there's no need to update.
            return false;
        }
        return true;
    }
    componentDidUpdate(prevProps) {
        // Reset the childNodes of TreeLayer to none if the parent path changes, so we don't have children of past visited layers in the childNodes.
        if (prevProps.parentPath !== this.props.parentPath) {
            this.node.childNodes = [];
        }
        // If the entry being viewed changes, set the new active node.
        if (prevProps.activePath !== this.props.activePath && this.node.path === this.props.activePath) {
            this.props.setActiveNode(this.node);
        }
        this.componentUpdates.next(this.props);
        const isDir = this.props.entryInfo && this.props.entryInfo.isDirectory;
        // When scrolling through the tree with the keyboard, if we hover a child tree node, prefetch its children.
        if (this.node === this.props.selectedNode && isDir && this.props.onHover) {
            this.props.onHover(this.node.path);
        }
        // Call onToggleExpand if activePath changes.
    }
    componentWillUnmount() {
        this.subscriptions.unsubscribe();
    }
    render() {
        const entryInfo = this.props.entryInfo;
        const className = [
            'tree__row',
            this.props.isExpanded && 'tree__row--expanded',
            this.node === this.props.activeNode && 'tree__row--active',
            this.node === this.props.selectedNode && 'tree__row--selected',
        ]
            .filter(c => !!c)
            .join(' ');
        const { treeOrError } = this.state;
        // If this layer has a single child directory, we have to parse treeOrError.entries
        // and convert it from a non-hierarchical flatlist to a singleChildGitTree so SingleChildTreeLayers know
        // which entries to render, and which entries to pass to its children.
        let singleChildTreeEntry = {};
        if (treeOrError &&
            treeOrError !== LOADING &&
            !isErrorLike(treeOrError) &&
            hasSingleChild(treeOrError.entries)) {
            singleChildTreeEntry = singleChildEntriesToGitTree(treeOrError.entries);
        }
        // Every other layer is a row in the file tree, and will fetch and render its children (if any) when expanded.
        return (React.createElement("div", null,
            React.createElement("table", { className: "tree-layer", onMouseOver: entryInfo.isDirectory ? this.invokeOnHover : undefined },
                React.createElement("tbody", null, entryInfo.isDirectory ? (React.createElement(React.Fragment, null,
                    React.createElement(Directory, Object.assign({}, this.props, { className: className, maxEntries: maxEntries, loading: treeOrError === LOADING, handleTreeClick: this.handleTreeClick, noopRowClick: this.noopRowClick, linkRowClick: this.linkRowClick })),
                    this.props.isExpanded &&
                        treeOrError !== LOADING && (React.createElement("tr", null,
                        React.createElement("td", { className: "tree__cell" }, isErrorLike(treeOrError) ? (React.createElement("div", { className: "tree__row-alert alert alert-danger", 
                            // tslint:disable-next-line:jsx-ban-props (needed because of dynamic styling)
                            style: treePadding(this.props.depth, true) },
                            "Error loading file tree: ",
                            treeOrError.message)) : (treeOrError && (React.createElement(ChildTreeLayer, Object.assign({}, this.props, { parent: this.node, key: singleChildTreeEntry.path, entries: treeOrError.entries, singleChildTreeEntry: singleChildTreeEntry, childrenEntries: singleChildTreeEntry.children, setChildNodes: this.setChildNode }))))))))) : (React.createElement(File, Object.assign({}, this.props, { maxEntries: maxEntries, className: className, handleTreeClick: this.handleTreeClick, noopRowClick: this.noopRowClick, linkRowClick: this.linkRowClick })))))));
    }
}
//# sourceMappingURL=TreeLayer.js.map