// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as React from '@theia/core/shared/react';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { TreeElement, TreeElementNode } from '@theia/core/lib/browser/source-tree';
import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { PluginServer, DeployedPlugin, PluginIdentifiers, PluginDeployOptions } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-trust-service';
import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { VSXEnvironment } from '../common/vsx-environment';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { TypeBadge } from './type-badge';
import { ExtensionCard, ExtensionCardTrust } from './extension-card';
import { CommandRegistry, MenuPath, nls } from '@theia/core/lib/common';
import { codicon, ConfirmDialog, ContextMenuRenderer, HoverService, TreeWidget } from '@theia/core/lib/browser';
import { VSXExtensionNamespaceAccess, VSXUser } from '@theia/ovsx-client/lib/ovsx-types';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
import { VSXExtensionsModel } from './vsx-extensions-model';

export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu'];

export namespace VSXExtensionsContextMenu {
    export const INSTALL = [...EXTENSIONS_CONTEXT_MENU, '1_install'];
    export const DISABLE = [...EXTENSIONS_CONTEXT_MENU, '2_disable'];
    export const ENABLE = [...EXTENSIONS_CONTEXT_MENU, '2_enable'];
    export const COPY = [...EXTENSIONS_CONTEXT_MENU, '3_copy'];
    export const CONTRIBUTION = [...EXTENSIONS_CONTEXT_MENU, '4_contribution'];
}

@injectable()
export class VSXExtensionData {
    readonly version?: string;
    readonly iconUrl?: string;
    readonly publisher?: string;
    readonly name?: string;
    readonly displayName?: string;
    readonly description?: string;
    readonly averageRating?: number;
    readonly downloadCount?: number;
    readonly downloadUrl?: string;
    readonly readmeUrl?: string;
    readonly licenseUrl?: string;
    readonly repository?: string;
    readonly license?: string;
    readonly readme?: string;
    readonly preview?: boolean;
    readonly verified?: boolean;
    readonly namespaceAccess?: VSXExtensionNamespaceAccess;
    readonly publishedBy?: VSXUser;
    static KEYS: Set<(keyof VSXExtensionData)> = new Set([
        'version',
        'iconUrl',
        'publisher',
        'name',
        'displayName',
        'description',
        'averageRating',
        'downloadCount',
        'downloadUrl',
        'readmeUrl',
        'licenseUrl',
        'repository',
        'license',
        'readme',
        'preview',
        'verified',
        'namespaceAccess',
        'publishedBy'
    ]);
}

@injectable()
export class VSXExtensionOptions {
    readonly id: string;
    readonly version?: string;
    readonly model: VSXExtensionsModel;
}

export const VSXExtensionFactory = Symbol('VSXExtensionFactory');
export type VSXExtensionFactory = (options: VSXExtensionOptions) => VSXExtension;

@injectable()
export class VSXExtension implements VSXExtensionData, TreeElement {
    /**
     * Ensure the version string begins with `'v'`.
     */
    static formatVersion(version: string | undefined): string | undefined {
        if (version && !version.startsWith('v')) {
            return `v${version}`;
        }
        return version;
    }

    @inject(VSXExtensionOptions)
    protected readonly options: VSXExtensionOptions;

    @inject(OpenerService)
    protected readonly openerService: OpenerService;

    @inject(HostedPluginSupport)
    protected readonly pluginSupport: HostedPluginSupport;

    @inject(PluginServer)
    protected readonly pluginServer: PluginServer;

    @inject(ProgressService)
    protected readonly progressService: ProgressService;

    @inject(ContextMenuRenderer)
    protected readonly contextMenuRenderer: ContextMenuRenderer;

    @inject(VSXEnvironment)
    readonly environment: VSXEnvironment;

    @inject(VSXExtensionsSearchModel)
    readonly search: VSXExtensionsSearchModel;

    @inject(HoverService)
    protected readonly hoverService: HoverService;

    @inject(WindowService)
    readonly windowService: WindowService;

    @inject(CommandRegistry)
    readonly commandRegistry: CommandRegistry;

    @inject(WorkspaceTrustService)
    protected readonly workspaceTrustService: WorkspaceTrustService;

    protected readonly data: Partial<VSXExtensionData> = {};

    protected registryUri: Promise<string>;

    @postConstruct()
    protected postConstruct(): void {
        this.registryUri = this.environment.getRegistryUri();
    }

    get uri(): URI {
        return VSCodeExtensionUri.fromId(this.id);
    }

    get id(): string {
        return this.options.id;
    }

    get installedVersion(): string | undefined {
        return this.plugin?.metadata.model.version || this.options.version;
    }

    get model(): VSXExtensionsModel {
        return this.options.model;
    }

    get visible(): boolean {
        return true;
    }

    get plugin(): DeployedPlugin | undefined {
        return this.pluginSupport.getPlugin(this.id as PluginIdentifiers.UnversionedId);
    }

    get installed(): boolean {
        return this.model.isInstalled(this.id);
    }

    get uninstalled(): boolean {
        const installedVersion = this.installedVersion;
        return !!installedVersion && this.model
            .isUninstalled(PluginIdentifiers.idAndVersionToVersionedId({ id: this.id as PluginIdentifiers.UnversionedId, version: installedVersion }));
    }

    get deployed(): boolean {
        const installedVersion = this.installedVersion;
        return !!installedVersion && this.model
            .isDeployed(PluginIdentifiers.idAndVersionToVersionedId({ id: this.id as PluginIdentifiers.UnversionedId, version: installedVersion }));
    }

    get disabled(): boolean {
        return this.model.isDisabled(this.id);
    }

    get disabledByTrust(): boolean {
        return this.pluginSupport.disabledByTrust.has(this.id as PluginIdentifiers.UnversionedId);
    }

    get builtin(): boolean {
        return this.model.isBuiltIn(this.id);
    }

    update(data: Partial<VSXExtensionData>): void {
        for (const key of VSXExtensionData.KEYS) {
            if (key in data) {
                Object.assign(this.data, { [key]: data[key] });
            }
        }
    }

    reloadWindow(): void {
        this.windowService.reload();
    }

    protected getData<K extends keyof VSXExtensionData>(key: K): VSXExtensionData[K] {
        const model = this.plugin?.metadata.model;
        if (model && key in model) {
            return model[key as keyof typeof model] as VSXExtensionData[K];
        }
        return this.data[key];
    }

    get iconUrl(): string | undefined {
        const plugin = this.plugin;
        const iconUrl = plugin && plugin.metadata.model.iconUrl;
        if (iconUrl) {
            return new Endpoint({ path: iconUrl }).getRestUrl().toString();
        }
        return this.data['iconUrl'];
    }

    get publisher(): string | undefined {
        return this.getData('publisher');
    }

    get name(): string | undefined {
        return this.getData('name');
    }

    get displayName(): string | undefined {
        return this.getData('displayName') || this.name || this.id;
    }

    get description(): string | undefined {
        return this.getData('description');
    }

    get version(): string | undefined {
        return this.getData('version');
    }

    get averageRating(): number | undefined {
        return this.getData('averageRating');
    }

    get downloadCount(): number | undefined {
        return this.getData('downloadCount');
    }

    get downloadUrl(): string | undefined {
        return this.getData('downloadUrl');
    }

    get readmeUrl(): string | undefined {
        return this.localReadmeUrl ?? this.data['readmeUrl'];
    }

    /**
     * The README URL served by the local plugin host for an installed extension, or `undefined`
     * if the README is not available locally (e.g. for a non-installed extension whose README is
     * hosted by the remote registry). A local README is same-origin and can be fetched directly by
     * the frontend, whereas a remote README must be retrieved through the backend `VSXRegistryService`.
     */
    get localReadmeUrl(): string | undefined {
        const readmeUrl = this.plugin?.metadata.model.readmeUrl;
        return readmeUrl ? new Endpoint({ path: readmeUrl }).getRestUrl().toString() : undefined;
    }

    get licenseUrl(): string | undefined {
        let licenseUrl = this.data['licenseUrl'];
        if (licenseUrl) {
            return licenseUrl;
        } else {
            const plugin = this.plugin;
            licenseUrl = plugin && plugin.metadata.model.licenseUrl;
            if (licenseUrl) {
                return new Endpoint({ path: licenseUrl }).getRestUrl().toString();
            }
        }
    }

    get repository(): string | undefined {
        return this.getData('repository');
    }

    get license(): string | undefined {
        return this.getData('license');
    }

    get readme(): string | undefined {
        return this.getData('readme');
    }

    get preview(): boolean | undefined {
        return this.getData('preview');
    }

    get verified(): boolean | undefined {
        return this.getData('verified');
    }

    get namespaceAccess(): VSXExtensionNamespaceAccess | undefined {
        return this.getData('namespaceAccess');
    }

    get publishedBy(): VSXUser | undefined {
        return this.getData('publishedBy');
    }

    get tooltip(): string {
        const version = VSXExtension.formatVersion(this.version);
        let md = `__${this.displayName}__`;
        if (version) {
            md += ` ${version}`;
        }
        if (this.disabled && this.installed) {
            md += ` (${nls.localizeByDefault('Disabled')})`;
        }
        if (this.description) {
            md += `\n\n${this.description}`;
        }
        md += '\n_____\n\n';
        if (this.publisher) {
            md += nls.localizeByDefault('Publisher: {0}', this.publisher);
        }
        if (this.license) {
            md += `  \r${nls.localize('theia/vsx-registry/license', 'License: {0}', this.license)}`;
        }

        if (this.downloadCount) {
            md += `  \r${nls.localize('theia/vsx-registry/downloadCount', 'Download count: {0}', downloadCompactFormatter.format(this.downloadCount))}`;
        }

        if (this.averageRating) {
            md += `  \r${getAverageRatingTitle(this.averageRating)}`;
        }

        if (this.disabledByTrust) {
            md += `  \r${nls.localizeByDefault(
                'This extension has been disabled because the current workspace is not trusted.'
            )}`;
        }

        return md;
    }

    protected _currentTaskName: string | undefined;
    get currentTask(): string | undefined {
        return this._currentTaskName;
    }
    protected _currentTask: Promise<void> | undefined;

    protected runTask(name: string, task: () => Promise<void>): Promise<void> {
        if (this._currentTask) {
            return Promise.reject('busy');
        }
        this._currentTaskName = name;
        this._currentTask = task();
        this._currentTask.finally(() => {
            this._currentTask = undefined;
            this._currentTaskName = undefined;
        });
        return this._currentTask;
    }

    async install(options?: PluginDeployOptions): Promise<void> {
        if (!this.verified) {
            const choice = await new ConfirmDialog({
                title: nls.localize('theia/vsx-registry/confirmDialogTitle', 'Are you sure you want to proceed with the installation?'),
                msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName)
            }).open();
            if (!choice) {
                return;
            }
        }
        const isTrusted = await this.workspaceTrustService.getWorkspaceTrust();
        if (!isTrusted) {
            const choice = await new ConfirmDialog({
                title: nls.localizeByDefault('Restricted Mode'),
                msg: nls.localize(
                    'theia/vsx-registry/restrictedModeInstallWarning',
                    'The extension \'{0}\' may be disabled if it does not support running in Restricted Mode. Trust this workspace to ensure the extension works as expected.',
                    this.displayName
                )
            }).open();
            if (!choice) {
                return;
            }
        }
        await this.doInstall(options);
    }

    async uninstall(): Promise<void> {
        const { id, installedVersion } = this;
        if (id && installedVersion) {
            await this.runTask(nls.localizeByDefault('Uninstalling'),
                async () => await this.progressService.withProgress(
                    nls.localizeByDefault('Uninstalling {0}...', this.id), 'extensions',
                    () => this.pluginServer.uninstall(PluginIdentifiers.idAndVersionToVersionedId({ id: (id as PluginIdentifiers.UnversionedId), version: installedVersion }))
                )
            );
        }
    }

    async disable(): Promise<void> {
        const { id, installedVersion } = this;
        if (id && installedVersion) {
            await this.runTask(nls.localize('vsx.disabling', 'Disabling'), async () => {
                await this.progressService.withProgress(
                    nls.localize('vsx.disabling.extensions', 'Disabling {0}...', this.id), 'extensions',
                    () => this.pluginServer.disablePlugin(id as PluginIdentifiers.UnversionedId)
                );
            });
        }
    }

    async enable(): Promise<void> {
        const { id, installedVersion } = this;
        if (id && installedVersion) {
            await this.runTask(nls.localize('vsx.enabling', 'Enabling'), async () => {
                await this.progressService.withProgress(
                    nls.localize('vsx.enabling.extension', 'Enabling {0}...', this.id), 'extensions',
                    () => this.pluginServer.enablePlugin(id as PluginIdentifiers.UnversionedId)
                );
            });
        }
    }

    protected async doInstall(options?: PluginDeployOptions): Promise<void> {
        await this.runTask(nls.localizeByDefault('Installing'),
            () => this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () =>
                this.pluginServer.install(this.uri.toString(), undefined, options)
            ));
    }

    handleContextMenu(e: React.MouseEvent<HTMLElement, MouseEvent>): void {
        e.preventDefault();
        this.contextMenuRenderer.render({
            menuPath: EXTENSIONS_CONTEXT_MENU,
            anchor: {
                x: e.clientX,
                y: e.clientY,
            },
            args: [this],
            context: e.currentTarget
        });
    }

    /**
     * Get the registry link for the given extension.
     * @param path the url path.
     * @returns the registry link for the given extension at the path.
     */
    async getRegistryLink(path = ''): Promise<URI> {
        const registryUri = new URI(await this.registryUri);
        if (this.downloadUrl) {
            const downloadUri = new URI(this.downloadUrl);
            if (downloadUri.authority !== registryUri.authority) {
                throw new Error('cannot generate a valid URL');
            }
        }
        return registryUri.resolve('extension/' + this.id.replace('.', '/')).resolve(path);
    }

    async serialize(): Promise<string> {
        const serializedExtension: string[] = [];
        serializedExtension.push(`Name: ${this.displayName}`);
        serializedExtension.push(`Id: ${this.id}`);
        serializedExtension.push(`Description: ${this.description}`);
        serializedExtension.push(`Version: ${this.version}`);
        serializedExtension.push(`Publisher: ${this.publisher}`);
        if (this.downloadUrl !== undefined) {
            const registryLink = await this.getRegistryLink();
            serializedExtension.push(`Open VSX Link: ${registryLink.toString()}`);
        };
        return serializedExtension.join('\n');
    }

    async open(options: OpenerOptions = { mode: 'reveal' }): Promise<void> {
        await this.doOpen(this.uri, options);
    }

    async doOpen(uri: URI, options?: OpenerOptions): Promise<void> {
        await open(this.openerService, uri, options);
    }

    render(host: TreeWidget): React.ReactNode {
        return <VSXExtensionComponent extension={this} host={host} hoverService={this.hoverService} />;
    }
}

export abstract class AbstractVSXExtensionComponent<Props extends AbstractVSXExtensionComponent.Props = AbstractVSXExtensionComponent.Props> extends React.Component<Props> {

    readonly install = async (event?: React.MouseEvent) => {
        event?.stopPropagation();
        this.forceUpdate();
        try {
            const pending = this.props.extension.install();
            this.forceUpdate();
            await pending;
        } finally {
            this.forceUpdate();
        }
    };

    readonly uninstall = async (event?: React.MouseEvent) => {
        event?.stopPropagation();
        try {
            const pending = this.props.extension.uninstall();
            this.forceUpdate();
            await pending;
        } finally {
            this.forceUpdate();
        }
    };

    readonly reloadWindow = (event?: React.MouseEvent) => {
        event?.stopPropagation();
        this.props.extension.reloadWindow();
    };

    protected readonly manage = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
        e.stopPropagation();
        this.props.extension.handleContextMenu(e);
    };

    protected renderAction(host?: TreeWidget): React.ReactNode {
        const { builtin, currentTask, disabled, uninstalled, installed, deployed } = this.props.extension;
        const isFocused = (host?.model.getFocusedNode() as TreeElementNode)?.element === this.props.extension;
        const tabIndex = (!host || isFocused) ? 0 : undefined;
        const inactive = disabled || uninstalled || !installed;
        const outOfSync = (installed && uninstalled) || deployed === inactive;
        if (currentTask) {
            return <button className="theia-button action prominent theia-mod-disabled">{currentTask}</button>;
        }
        return <div>
            {
                outOfSync && <button className="theia-button action" onClick={this.reloadWindow}>{nls.localizeByDefault('Reload Window')}</button>
            }
            {
                !builtin && installed && !uninstalled && <button className="theia-button action" onClick={this.uninstall}>{nls.localizeByDefault('Uninstall')}</button>
            }
            {
                !builtin && !installed && <button className="theia-button prominent action" onClick={this.install}>{nls.localizeByDefault('Install')}</button>
            }
            <div className="codicon codicon-settings-gear action" tabIndex={tabIndex} onClick={this.manage}></div>
        </div>;
    }

}
export namespace AbstractVSXExtensionComponent {
    export interface Props {
        extension: VSXExtension;
    }
}

const downloadFormatter = new Intl.NumberFormat();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const downloadCompactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short' } as any);
const averageRatingFormatter = (averageRating: number): number => Math.round(averageRating * 2) / 2;
const getAverageRatingTitle = (averageRating: number): string =>
    nls.localizeByDefault('Average rating: {0} out of 5', averageRatingFormatter(averageRating));

export namespace VSXExtensionComponent {
    export interface Props extends AbstractVSXExtensionComponent.Props {
        host: TreeWidget;
        hoverService: HoverService;
    }
}

export class VSXExtensionComponent<Props extends VSXExtensionComponent.Props = VSXExtensionComponent.Props> extends AbstractVSXExtensionComponent<Props> {
    override render(): React.ReactNode {
        const {
            iconUrl, publisher, displayName, description, version, downloadCount,
            averageRating, tooltip, verified, disabled, disabledByTrust, installed
        } = this.props.extension;

        const trust: ExtensionCardTrust = verified === true ? 'verified' : verified === false ? 'unverified' : 'unknown';

        return <ExtensionCard
            title={displayName ?? ''}
            version={VSXExtension.formatVersion(version)}
            // Preserve the previous behaviour of always rendering the description row.
            description={description ?? ''}
            iconUrl={iconUrl}
            typeBadge={
                <TypeBadge
                    icon={<i className='theia-extensions-type-badge-icon theia-extensions-type-badge-icon--extension' />}
                    label={nls.localizeByDefault('Extension')}
                    variant='extension'
                />
            }
            titleLabels={
                <>
                    {disabled && installed && <span className='disabled'>&nbsp;({nls.localizeByDefault('disabled')})</span>}
                    {disabledByTrust && <span className='disabled' title={nls.localizeByDefault('Disabled in Restricted Mode')}>
                        ({nls.localizeByDefault('Restricted Mode')})
                    </span>}
                </>
            }
            stat={
                <>
                    {!!downloadCount && <span className='download-count'><i className={codicon('cloud-download')} />{downloadCompactFormatter.format(downloadCount)}</span>}
                    {!!averageRating && <span className='average-rating'><i className={codicon('star-full')} />{averageRatingFormatter(averageRating)}</span>}
                </>
            }
            publisher={publisher}
            trust={trust}
            actions={this.renderAction(this.props.host)}
            extraClassName={disabledByTrust ? 'theia-vsx-extension-disabled-by-trust' : undefined}
            hover={{ content: new MarkdownStringImpl(tooltip), hoverService: this.props.hoverService }}
            onContextMenu={event => this.manage(event)}
        />;
    }
}

export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent {
    protected header: HTMLElement | undefined;
    protected body: HTMLElement | undefined;
    protected _scrollContainer: HTMLElement | undefined;

    get scrollContainer(): HTMLElement | undefined {
        return this._scrollContainer;
    }

    override render(): React.ReactNode {
        const {
            builtin, preview, id, iconUrl, publisher, displayName, description, version,
            averageRating, downloadCount, repository, license, readme, disabled, installed, disabledByTrust
        } = this.props.extension;

        const sanitizedReadme = !!readme ? DOMPurify.sanitize(readme) : undefined;

        return <React.Fragment>
            <div className='header' ref={ref => { this.header = (ref || undefined); }}>
                {iconUrl ?
                    <img className='icon-container' src={iconUrl} /> :
                    <div className='icon-container placeholder' />}
                <div className='details'>
                    <div className='title'>
                        <span title='Extension name' className='name' onClick={this.openExtension}>{displayName}</span>
                        <span title='Extension identifier' className='identifier'>{id}</span>
                        {preview && <span className='preview'>Preview</span>}
                        {builtin && <span className='builtin'>Built-in</span>}
                        {disabled && installed && <span className='disabled'>{nls.localizeByDefault('Disabled')}</span>}
                        {disabledByTrust && <span className='restricted'>{nls.localizeByDefault('Restricted Mode')}</span>}
                    </div>
                    <div className='subtitle'>
                        <span title='Publisher name' className='publisher' onClick={this.searchPublisher}>
                            {this.renderNamespaceAccess()}
                            {publisher}
                        </span>
                        {!!downloadCount && <span className='download-count' onClick={this.openExtension}>
                            <i className={codicon('cloud-download')} />{downloadFormatter.format(downloadCount)}</span>}
                        {
                            averageRating !== undefined &&
                            <span className='average-rating' title={getAverageRatingTitle(averageRating)} onClick={this.openAverageRating}>{this.renderStars()}</span>
                        }
                        {repository && <span className='repository' onClick={this.openRepository}>Repository</span>}
                        {license && <span className='license' onClick={this.openLicense}>{license}</span>}
                        {version && <span className='version'>{VSXExtension.formatVersion(version)}</span>}
                    </div>
                    <div className='description noWrapInfo'>{description}</div>
                    {disabledByTrust && <div className='theia-vsx-extension-restricted-notice'>
                        {nls.localizeByDefault(
                            'This extension has been disabled because the current workspace is not trusted.'
                        )}
                    </div>}
                    {this.renderAction()}
                </div>
            </div>
            {
                sanitizedReadme &&
                <div className='scroll-container'
                    ref={ref => { this._scrollContainer = (ref || undefined); }}>
                    <div className='body'
                        ref={ref => { this.body = (ref || undefined); }}
                        onClick={this.openLink}
                        // eslint-disable-next-line react/no-danger
                        dangerouslySetInnerHTML={{ __html: sanitizedReadme }}
                    />
                </div>
            }
        </React.Fragment >;
    }

    protected renderNamespaceAccess(): React.ReactNode {
        const { publisher, namespaceAccess, publishedBy } = this.props.extension;
        if (namespaceAccess === undefined) {
            return undefined;
        }
        let tooltip = publishedBy ? ` Published by "${publishedBy.loginName}".` : '';
        let icon;
        if (namespaceAccess === 'public') {
            icon = 'globe';
            tooltip = `Everyone can publish to "${publisher}" namespace.` + tooltip;
        } else {
            icon = 'shield';
            tooltip = `Only verified owners can publish to "${publisher}" namespace.` + tooltip;
        }
        return <i className={`${codicon(icon)} namespace-access`} title={tooltip} onClick={this.openPublishedBy} />;
    }

    protected renderStars(): React.ReactNode {
        const rating = this.props.extension.averageRating || 0;

        const renderStarAt = (position: number) => position <= rating ?
            <i className={codicon('star-full')} /> :
            position > rating && position - rating < 1 ?
                <i className={codicon('star-half')} /> :
                <i className={codicon('star-empty')} />;
        return <React.Fragment>
            {renderStarAt(1)}{renderStarAt(2)}{renderStarAt(3)}{renderStarAt(4)}{renderStarAt(5)}
        </React.Fragment>;
    }

    // TODO replace with webview
    readonly openLink = (event: React.MouseEvent) => {
        if (!this.body) {
            return;
        }
        const target = event.nativeEvent.target;
        if (!(target instanceof HTMLElement)) {
            return;
        }
        let node = target;
        while (node.tagName.toLowerCase() !== 'a') {
            if (node === this.body) {
                return;
            }
            if (!(node.parentElement instanceof HTMLElement)) {
                return;
            }
            node = node.parentElement;
        }
        const href = node.getAttribute('href');
        if (href && !href.startsWith('#')) {
            event.preventDefault();
            this.props.extension.doOpen(new URI(href));
        }
    };

    readonly openExtension = async (e: React.MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();

        const extension = this.props.extension;
        const uri = await extension.getRegistryLink();
        extension.doOpen(uri);
    };
    readonly searchPublisher = (e: React.MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();

        const extension = this.props.extension;
        if (extension.publisher) {
            extension.search.query = extension.publisher;
        }
    };
    readonly openPublishedBy = async (e: React.MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();

        const extension = this.props.extension;
        const homepage = extension.publishedBy && extension.publishedBy.homepage;
        if (homepage) {
            extension.doOpen(new URI(homepage));
        }
    };
    readonly openAverageRating = async (e: React.MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();

        const extension = this.props.extension;
        const uri = await extension.getRegistryLink('reviews');
        extension.doOpen(uri);
    };
    readonly openRepository = (e: React.MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();

        const extension = this.props.extension;
        if (extension.repository) {
            extension.doOpen(new URI(extension.repository));
        }
    };
    readonly openLicense = (e: React.MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();

        const extension = this.props.extension;
        const licenseUrl = extension.licenseUrl;
        if (licenseUrl) {
            extension.doOpen(new URI(licenseUrl));
        }
    };
}
