import * as tslib_1 from "tslib";
import { createHoverifier, findPositionsFromEvents, HoverOverlay, } from '@sourcegraph/codeintellify';
import { getTokenAtPosition } from '@sourcegraph/codeintellify/lib/token_position';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Subject, Subscription } from 'rxjs';
import { catchError, filter, map, withLatestFrom } from 'rxjs/operators';
import { getHover, getJumpURL } from '../../backend/features';
import { fetchBlob } from '../../repo/blob/BlobPage';
import { getModeFromPath } from '../../util';
import { isErrorLike } from '../../util/errors';
import { isDefined, propertyIsDefined } from '../../util/types';
const LinkComponent = (props) => React.createElement(Link, Object.assign({}, props));
const domFunctions = {
    getCodeElementFromTarget: (target) => {
        // If the target is part of the blame annotation (a.blame or span.blame__contents), return null.
        if (target.classList.contains('blame') ||
            target.classList.contains('blame__contents') ||
            target.classList.contains('line-decoration-attachment') ||
            target.classList.contains('line-decoration-attachment__contents')) {
            return null;
        }
        const row = target.closest('tr');
        if (!row) {
            return null;
        }
        return row.cells[1];
    },
    getCodeElementFromLineNumber: (codeView, line) => {
        const lineNumberElement = codeView.querySelector(`td[data-line="${line}"]`);
        if (!lineNumberElement) {
            return null;
        }
        return lineNumberElement.nextElementSibling;
    },
    getLineNumberFromCodeElement: (codeCell) => {
        const row = codeCell.closest('tr');
        if (!row) {
            throw new Error('Could not find closest row for codeCell');
        }
        const numberCell = row.cells[0];
        if (!numberCell || !numberCell.dataset.line) {
            throw new Error('Could not find line number');
        }
        return parseInt(numberCell.dataset.line, 10);
    },
};
const REPO_PATH = 'github.com/gorilla/mux';
const COMMIT_ID = '9e1f5955c0d22b55d9e20d6faa28589f83b2faca';
const REV = undefined;
const FILE_PATH = 'mux.go';
export class CodeIntellifyBlob extends React.Component {
    constructor(props) {
        super(props);
        /** Emits whenever the ref callback for the code element is called */
        this.codeViewElements = new Subject();
        this.nextCodeViewElement = (element) => this.codeViewElements.next(element);
        /** Emits whenever the ref callback for the hover element is called */
        this.hoverOverlayElements = new Subject();
        this.nextOverlayElement = (element) => this.hoverOverlayElements.next(element);
        /** Emits whenever the ref callback for the demo file element is called */
        this.codeIntellifyBlobElements = new Subject();
        this.nextCodeIntellifyBlobElements = (element) => this.codeIntellifyBlobElements.next(element);
        /** Emits when the go to definition button was clicked */
        this.goToDefinitionClicks = new Subject();
        this.nextGoToDefinitionClick = (event) => this.goToDefinitionClicks.next(event);
        /** Emits when the close button was clicked */
        this.closeButtonClicks = new Subject();
        this.nextCloseButtonClick = (event) => this.closeButtonClicks.next(event);
        this.subscriptions = new Subscription();
        this.componentUpdates = new Subject();
        this.target = null;
        this.state = {};
        const hoverifier = createHoverifier({
            closeButtonClicks: this.closeButtonClicks,
            goToDefinitionClicks: this.goToDefinitionClicks,
            hoverOverlayElements: this.hoverOverlayElements,
            pushHistory: path => this.props.history.push(path),
            hoverOverlayRerenders: this.componentUpdates.pipe(withLatestFrom(this.hoverOverlayElements, this.codeIntellifyBlobElements), map(([, hoverOverlayElement, codeIntellifyBlobElement]) => ({
                hoverOverlayElement,
                codeIntellifyBlobElement,
            })), filter(propertyIsDefined('codeIntellifyBlobElement')), map(({ hoverOverlayElement, codeIntellifyBlobElement }) => ({
                hoverOverlayElement,
                relativeElement: codeIntellifyBlobElement.closest(this.props.parentElement),
            })), 
            // Can't reposition HoverOverlay or file weren't rendered
            filter(propertyIsDefined('relativeElement')), filter(propertyIsDefined('hoverOverlayElement'))),
            fetchHover: hoveredToken => getHover(this.getLSPTextDocumentPositionParams(hoveredToken), this.props),
            fetchJumpURL: hoveredToken => getJumpURL(this.getLSPTextDocumentPositionParams(hoveredToken), this.props),
        });
        this.subscriptions.add(hoverifier);
        const positionEvents = this.codeViewElements.pipe(filter(isDefined), findPositionsFromEvents(domFunctions));
        const targets = positionEvents.pipe(map(({ event: { target } }) => target));
        targets.subscribe(target => (this.target = target));
        this.subscriptions.add(hoverifier.hoverify({
            positionEvents,
            resolveContext: () => ({
                repoPath: REPO_PATH,
                commitID: COMMIT_ID,
                rev: REV || '',
                filePath: FILE_PATH,
            }),
            dom: domFunctions,
        }));
        this.subscriptions.add(hoverifier.hoverStateUpdates.subscribe(update => this.setState(update)));
        this.subscriptions.add(this.codeViewElements
            .pipe(filter(isDefined), map(codeView => getTokenAtPosition(codeView, props.defaultHoverPosition, domFunctions)), filter(isDefined))
            .subscribe(token => {
            const showOnHomepage = props.className === 'code-intellify-container' && window.innerWidth >= 1393;
            const showOnModal = props.className === 'code-intellify-container-modal' && window.innerWidth >= 1275;
            if (showOnHomepage || showOnModal) {
                token.click();
            }
        }));
    }
    componentDidMount() {
        // Fetch repository revision.
        fetchBlob({
            repoPath: REPO_PATH,
            commitID: COMMIT_ID,
            filePath: FILE_PATH,
            isLightTheme: false,
            disableTimeout: false,
        })
            .pipe(catchError(error => {
            console.error(error);
            return [error];
        }))
            .subscribe(blobOrError => this.setState({ blobOrError }), err => console.error(err));
        this.componentUpdates.next();
        this.subscriptions.add(this.props.extensionsOnVisibleTextDocumentsChange([
            {
                uri: `git://github.com/gorilla/mux?9e1f5955c0d22b55d9e20d6faa28589f83b2faca#mux.go`,
                languageId: 'go',
                text: '',
            },
        ]));
    }
    componentDidUpdate() {
        this.componentUpdates.next();
    }
    getLSPTextDocumentPositionParams(position) {
        return {
            repoPath: position.repoPath,
            filePath: position.filePath,
            commitID: position.commitID,
            rev: position.rev,
            mode: getModeFromPath(FILE_PATH),
            position,
        };
    }
    render() {
        if (!this.state.blobOrError) {
            // Render placeholder for layout before content is fetched.
            return React.createElement("div", { className: "blob-page__placeholder" }, "Loading...");
        }
        const hoverOverlayProps = this.adjustHoverOverlayPosition(this.target);
        return (React.createElement("div", { className: this.props.className, ref: this.nextCodeIntellifyBlobElements },
            React.createElement("div", { className: "code-header" },
                React.createElement("span", { className: "code-header__title" }, "github.com/gorilla/mux/mux.go"),
                React.createElement("span", { className: "code-header__link" },
                    React.createElement("a", { href: "https://sourcegraph.com/github.com/gorilla/mux/-/blob/mux.go" }, "View full file"))),
            !isErrorLike(this.state.blobOrError) && (React.createElement("code", { className: `blob__code blob__code--wrapped e2e-blob`, ref: this.nextCodeViewElement, dangerouslySetInnerHTML: {
                    __html: trimHTMLString(this.state.blobOrError.highlight.html, this.props.startLine - 1, this.props.endLine + 1),
                } })),
            this.state.hoverOverlayProps && (React.createElement(HoverOverlay, Object.assign({}, hoverOverlayProps, { linkComponent: LinkComponent, hoverRef: this.nextOverlayElement, onGoToDefinitionClick: this.nextGoToDefinitionClick, onCloseButtonClick: this.nextCloseButtonClick, showCloseButton: false, className: this.props.tooltipClass })))));
    }
    /**
     * This function adjusts the position of the hoverOverlay so that it does not overflow on the right side
     * of the viewport. If a hoverOverlay will exceed the viewport, this function will adjust the position
     * so that it aligns the right side of the hover overlay with the right side of the target element.
     *
     */
    adjustHoverOverlayPosition(target) {
        const viewPortEdge = window.innerWidth;
        if (!this.state.hoverOverlayProps) {
            return undefined;
        }
        if (!target) {
            return this.state.hoverOverlayProps;
        }
        const _a = this.state.hoverOverlayProps, { overlayPosition } = _a, rest = tslib_1.__rest(_a, ["overlayPosition"]);
        const targetBounds = target.getBoundingClientRect();
        let newOverlayPosition = overlayPosition;
        if (overlayPosition && viewPortEdge < targetBounds.left + 512 && targetBounds.right - 512 >= 0) {
            const containerWidth = document.querySelector(this.props.parentElement).parentElement.getBoundingClientRect().width;
            const parentWidth = document.querySelector(this.props.parentElement).getBoundingClientRect().width;
            // One side of the total horizontal margin.
            const halfMarginWidth = (viewPortEdge - containerWidth) / 2;
            // The difference between the viewport width and parent width. We need to subtract this because
            // `left` is relative to the parent, whereas targetBounds.right is relative to the viewport.
            const relativeElementDifference = viewPortEdge - parentWidth;
            newOverlayPosition = {
                top: overlayPosition.top,
                // 512 is the width of a hoverOverlay.
                left: targetBounds.right - 512 - relativeElementDifference + halfMarginWidth,
            };
        }
        return Object.assign({}, rest, { overlayPosition: newOverlayPosition });
    }
}
/**
 * We can only fetch blobs as an entire file. For demo purposes, we only want to show part of the file.
 * This function trims the HTML string of the file that will be code-intellfied on the homepage to only show
 * the lines that we specify. It makes some assumptions for this specific use case, such as the presence
 * of a single table and tbody element in the html, so be careful when changing.
 */
function trimHTMLString(html, startLine, endLine) {
    const domParser = new DOMParser();
    const doc = domParser.parseFromString(html, 'text/html');
    const startToRemove = doc.querySelectorAll(`tr:nth-child(n + 0):nth-child(-n + ${startLine})`);
    const endToRemove = doc.querySelectorAll(`tr:nth-child(n + ${endLine})`);
    const elementsToRemove = [...startToRemove, ...endToRemove];
    const tableEl = doc.querySelector('tbody'); // assume a single tbody element will exist in blob HTML
    for (const el of elementsToRemove) {
        tableEl.removeChild(el);
    }
    const xmlSerializer = new XMLSerializer();
    const tbl = doc.querySelector('table'); // assume a single table element will exist in blob HTML
    const trimmedHTMLString = xmlSerializer.serializeToString(tbl);
    return trimmedHTMLString;
}
//# sourceMappingURL=CodeIntellifyBlob.js.map