import { LoadingSpinner } from '@sourcegraph/react-loading-spinner';
import { highlight } from 'highlight.js/lib/highlight';
import { castArray, isEqual } from 'lodash';
import marked from 'marked';
import * as React from 'react';
import { merge, of, Subject, Subscription } from 'rxjs';
import { bufferTime, catchError, concat, delay, distinctUntilChanged, map, scan, skip, startWith, switchMap, takeUntil, } from 'rxjs/operators';
import { MarkupContent } from 'vscode-languageserver-types';
import { getDefinition, getHover, getImplementations, getReferences, } from '../../../backend/features';
import { fetchServerCapabilities, isEmptyHover } from '../../../backend/lsp';
import { isDiscussionsEnabled } from '../../../discussions';
import { PanelItemPortal } from '../../../panel/PanelItemPortal';
import { PanelTitlePortal } from '../../../panel/PanelTitlePortal';
import { eventLogger } from '../../../tracking/eventLogger';
import { asError, isErrorLike } from '../../../util/errors';
import { RepositoryIcon } from '../../../util/icons'; // TODO: Switch to mdi icon
import { parseHash } from '../../../util/url';
import { RepoRevSidebarCommits } from '../../RepoRevSidebarCommits';
import { DiscussionsTree } from '../discussions/DiscussionsTree';
import { fetchExternalReferences } from '../references/backend';
import { FileLocations } from './FileLocations';
import { FileLocationsTree } from './FileLocationsTree';
function toSubject(props) {
    const parsedHash = parseHash(props.location.hash);
    return {
        repoPath: props.repoPath,
        commitID: props.commitID,
        filePath: props.filePath,
        mode: props.mode,
        line: parsedHash.line || 1,
        character: parsedHash.character || 1,
        extensions: props.extensions,
    };
}
function subjectIsEqual(a, b) {
    return (a &&
        b &&
        a.repoPath === b.repoPath &&
        a.commitID === b.commitID &&
        a.filePath === b.filePath &&
        a.mode === b.mode &&
        a.line === b.line &&
        a.character === b.character &&
        isEqual(a.extensions, b.extensions));
}
const LOADING = 'loading';
/**
 * A panel on the blob page that displays contextual information.
 */
export class BlobPanel extends React.PureComponent {
    constructor() {
        super(...arguments);
        this.state = {};
        this.componentUpdates = new Subject();
        this.locationsUpdates = new Subject();
        this.subscriptions = new Subscription();
        this.onSelectLocation = (tab) => eventLogger.log('BlobPanelLocationSelected', { tab });
        this.queryDefinition = () => getDefinition(this.props, this.props).pipe(map(locations => ({ loading: false, locations: locations ? castArray(locations) : [] })));
        this.queryReferencesLocal = () => getReferences(Object.assign({}, this.props, { includeDeclaration: false }), this.props).pipe(map(c => ({ loading: false, locations: c })));
        this.queryReferencesExternal = () => fetchExternalReferences(this.props).pipe(map(c => ({ loading: true, locations: c })), concat([{ loading: false, locations: [] }]), bufferTime(500), // reduce UI jitter
        scan((cur, locs) => ({
            loading: cur.loading && locs.every(({ loading }) => loading),
            locations: cur.locations.concat(...locs.map(({ locations }) => locations)),
        }), { loading: true, locations: [] }));
        this.queryImplementation = () => getImplementations(this.props, this.props).pipe(map(c => ({ loading: false, locations: c })));
    }
    componentDidMount() {
        const componentUpdates = this.componentUpdates.pipe(startWith(this.props));
        // Changes to the context subject, including upon the initial mount.
        const subjectChanges = componentUpdates.pipe(distinctUntilChanged((a, b) => subjectIsEqual(toSubject(a), toSubject(b))));
        // Update server capabilities.
        this.subscriptions.add(subjectChanges
            .pipe(
        // This remains the same for all positions/ranges in the file.
        distinctUntilChanged((a, b) => a.repoPath === b.repoPath &&
            a.commitID === b.commitID &&
            a.filePath === b.filePath &&
            a.mode === b.mode), switchMap(subject => fetchServerCapabilities({
            repoPath: subject.repoPath,
            rev: subject.rev,
            commitID: subject.commitID,
            filePath: subject.filePath,
            mode: subject.mode,
        }).pipe(catchError(error => [asError(error)]), map(c => ({ serverCapabilitiesOrError: c })), startWith({
            serverCapabilitiesOrError: undefined,
        }))))
            .subscribe(stateUpdate => this.setState(stateUpdate), error => console.error(error)));
        // Update hover.
        this.subscriptions.add(subjectChanges
            .pipe(switchMap((subject) => {
            const { position } = subject;
            if (!position ||
                position.character === 0 /* 1-indexed, so this means only line (not position) is selected */) {
                return [{ hoverOrError: undefined }];
            }
            const result = getHover(Object.assign({}, subject, { position }), { extensionsController: this.props.extensionsController }).pipe(catchError(error => [asError(error)]), map(c => ({ hoverOrError: c })));
            return merge(result, of({ hoverOrError: LOADING }).pipe(delay(150), takeUntil(result)) // delay loading spinner to reduce jitter
            ).pipe(startWith({ hoverOrError: undefined }) // clear old data immediately)
            );
        }))
            .subscribe(stateUpdate => this.setState(stateUpdate), error => console.error(error)));
        // Update references when subject changes after the initial mount.
        this.subscriptions.add(subjectChanges.pipe(skip(1)).subscribe(() => this.locationsUpdates.next()));
    }
    componentDidUpdate() {
        this.componentUpdates.next(this.props);
    }
    componentWillUnmount() {
        this.subscriptions.unsubscribe();
    }
    render() {
        let titleRendered;
        let extraRendered;
        const { hoverOrError } = this.state;
        if (hoverOrError === LOADING) {
            titleRendered = React.createElement(LoadingSpinner, { className: "icon-inline" });
        }
        else if (hoverOrError === undefined) {
            // Don't show loading indicator yet (to reduce UI jitter).
            titleRendered = undefined;
        }
        else if (hoverOrError && !isErrorLike(hoverOrError) && !isEmptyHover(hoverOrError)) {
            // Hover with one or more MarkedStrings.
            titleRendered = renderHoverContents(hoverOrError.contents[0]);
            if (Array.isArray(hoverOrError.contents) && hoverOrError.contents.length >= 2) {
                extraRendered = hoverOrError.contents.slice(1).map((s, i) => (React.createElement("div", { key: i, className: "blob-panel__extra-item px-2 pt-1" }, renderHoverContents(s))));
            }
        }
        else {
            // Error or no hover information.
            //
            // Don't bother showing the error, if any; if it occurs on the panel contents fetches, it will be
            // displayed.
        }
        const isValidToken = hoverOrError && hoverOrError !== LOADING && !isErrorLike(hoverOrError);
        const viewState = parseHash(this.props.location.hash).viewState;
        return (React.createElement(React.Fragment, null,
            titleRendered && React.createElement(PanelTitlePortal, null, titleRendered),
            extraRendered && (React.createElement(PanelItemPortal, { id: "info", label: "Info", priority: 1, element: React.createElement("div", { className: "mt-2" }, extraRendered) })),
            (isValidToken || viewState === 'def') && (React.createElement(PanelItemPortal, { id: "def", label: "Definition", priority: 0, element: React.createElement(FileLocations, { className: "panel__tabs-content", query: this.queryDefinition, updates: this.locationsUpdates, inputRepo: this.props.repoPath, inputRevision: this.props.rev, 
                    // tslint:disable-next-line:jsx-no-lambda
                    onSelect: () => this.onSelectLocation('def'), icon: RepositoryIcon, pluralNoun: "definitions", isLightTheme: this.props.isLightTheme }) })),
            (isValidToken || viewState === 'references') && (React.createElement(PanelItemPortal, { id: "references", label: "References", priority: -1, element: React.createElement(FileLocations, { className: "panel__tabs-content", query: this.queryReferencesLocal, updates: this.locationsUpdates, inputRepo: this.props.repoPath, inputRevision: this.props.rev, 
                    // tslint:disable-next-line:jsx-no-lambda
                    onSelect: () => this.onSelectLocation('references'), icon: RepositoryIcon, pluralNoun: "local references", isLightTheme: this.props.isLightTheme }) })),
            (isValidToken || viewState === 'references:external') && (React.createElement(PanelItemPortal, { id: "references:external", label: "External references", priority: -2, element: React.createElement(FileLocationsTree, { className: "panel__tabs-content", query: this.queryReferencesExternal, updates: this.locationsUpdates, 
                    // tslint:disable-next-line:jsx-no-lambda
                    onSelectLocation: () => this.onSelectLocation('references:external'), icon: RepositoryIcon, pluralNoun: "external references", isLightTheme: this.props.isLightTheme, location: this.props.location }) })),
            (isValidToken || viewState === 'impl') && (React.createElement(PanelItemPortal, { id: "impl", label: "Implementation", priority: -3, hidden: !this.state.serverCapabilitiesOrError ||
                    isErrorLike(this.state.serverCapabilitiesOrError) ||
                    !this.state.serverCapabilitiesOrError.implementationProvider, element: React.createElement(FileLocations, { className: "panel__tabs-content", query: this.queryImplementation, updates: this.locationsUpdates, inputRepo: this.props.repoPath, inputRevision: this.props.rev, 
                    // tslint:disable-next-line:jsx-no-lambda
                    onSelect: () => this.onSelectLocation('impl'), icon: RepositoryIcon, pluralNoun: "implementations", isLightTheme: this.props.isLightTheme }) })),
            React.createElement(PanelItemPortal, { id: "history", label: "File history", priority: -4, element: React.createElement(RepoRevSidebarCommits, { key: "commits", repoID: this.props.repoID, rev: this.props.rev, filePath: this.props.filePath, history: this.props.history, location: this.props.location }) }),
            isDiscussionsEnabled(this.props.configurationCascade) && (React.createElement(PanelItemPortal, { id: "discussions", label: "File discussions", priority: -5, element: React.createElement(DiscussionsTree, { repoID: this.props.repoID, repoPath: this.props.repoPath, commitID: this.props.commitID, rev: this.props.rev, filePath: this.props.filePath, history: this.props.history, location: this.props.location, user: this.props.user }) }))));
    }
}
function renderHoverContents(contents) {
    const value = typeof contents === 'string' ? contents : contents.value;
    const language = typeof contents === 'string' ? 'markdown' : MarkupContent.is(contents) ? contents.kind : contents.language;
    try {
        if (language === 'markdown') {
            return (React.createElement("div", { dangerouslySetInnerHTML: {
                    __html: marked(value, { gfm: true, breaks: true, sanitize: true }),
                } }));
        }
        return React.createElement("code", { className: "hljs", dangerouslySetInnerHTML: { __html: highlight(language, value).value } });
    }
    catch (e) {
        // Ignore rendering or syntax highlighting errors; plain text will be rendered.
    }
    return value;
}
//# sourceMappingURL=BlobPanel.js.map