// *****************************************************************************
// Copyright (C) 2017-2018 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 'react';
import { injectable, inject } from 'inversify';
import debounce = require('lodash.debounce');
import { CancellationTokenSource, CommandService, nls } from '../../common';
import { ReactWidget } from '../widgets/react-widget';
import { FrontendApplicationStateService } from '../frontend-application-state';
import { LabelParser, LabelIcon } from '../label-parser';
import { StatusBar, StatusBarEntry, StatusBarAlignment, StatusBarViewEntry } from './status-bar-types';
import { StatusBarViewModel } from './status-bar-view-model';
import { HoverService } from '../hover-service';
import { codicon } from '../widgets';
import { PreferenceService } from '../../common/preferences';
import { MarkdownString } from '../../common/markdown-rendering';
export { StatusBar, StatusBarAlignment, StatusBarEntry };

@injectable()
export class StatusBarImpl extends ReactWidget implements StatusBar {

    protected backgroundColor: string | undefined;
    protected color: string | undefined;

    protected preferencesReady = false;

    constructor(
        @inject(CommandService) protected readonly commands: CommandService,
        @inject(LabelParser) protected readonly entryService: LabelParser,
        @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService,
        @inject(PreferenceService) protected readonly preferences: PreferenceService,
        @inject(StatusBarViewModel) protected readonly viewModel: StatusBarViewModel,
        @inject(HoverService) protected readonly hoverService: HoverService,
    ) {
        super();
        delete this.scrollOptions;
        this.id = 'theia-statusBar';
        this.addClass('noselect');
        // Hide the status bar until the `workbench.statusBar.visible` preference returns with a `true` value.
        this.hide();
        this.preferences.ready.then(() => {
            this.preferencesReady = true;
            this.updateVisibility();
        });
        this.toDispose.push(
            this.preferences.onPreferenceChanged(preference => {
                if (preference.preferenceName === 'workbench.statusBar.visible') {
                    this.updateVisibility();
                }
            })
        );
        this.toDispose.push(this.viewModel.onDidChange(() => this.debouncedUpdate()));
    }

    protected updateVisibility(): void {
        if (!this.preferencesReady) {
            return; // Don't change visibility until preferences have loaded
        }
        const prefHides = !this.preferences.get<boolean>('workbench.statusBar.visible', true);
        this.setHidden(prefHides);
    }

    protected debouncedUpdate = debounce(() => this.update(), 50);

    protected get ready(): Promise<void> {
        return this.applicationStateService.reachedAnyState('initialized_layout', 'ready');
    }

    async setElement(id: string, entry: StatusBarEntry): Promise<void> {
        await this.ready;
        this.viewModel.set(id, entry);
    }

    async removeElement(id: string): Promise<void> {
        await this.ready;
        this.viewModel.remove(id);
    }

    async setBackgroundColor(color?: string): Promise<void> {
        await this.ready;
        this.internalSetBackgroundColor(color);
        this.update();
    }

    protected internalSetBackgroundColor(color?: string): void {
        this.backgroundColor = color;
        this.node.style.backgroundColor = this.backgroundColor || '';
    }

    async setColor(color?: string): Promise<void> {
        await this.ready;
        this.internalSetColor(color);
        this.update();
    }

    protected internalSetColor(color?: string): void {
        this.color = color;
    }
    protected render(): React.JSX.Element {
        const leftEntries = Array.from(this.viewModel.getLeft(), entry => this.renderElement(entry));
        const rightEntries = Array.from(this.viewModel.getRight(), entry => this.renderElement(entry));

        return <React.Fragment>
            <div className='area left'>{leftEntries}</div>
            <div className='area right'>{rightEntries}</div>
        </React.Fragment>;
    }

    protected triggerCommand(entry: StatusBarEntry): () => void {
        return () => {
            if (entry.command) {
                const args = entry.arguments || [];
                this.commands.executeCommand(entry.command, ...args);
            }
        };
    }

    /**
     * Request a hover to be displayed for a status bar entry.
     * @param e The mouse event that triggered the hover request
     * @param entry The status bar entry to display hover for
     * @param skipDelay When true, requests the hover immediately without delay (for click-triggered hovers)
     */
    protected requestHover(e: React.MouseEvent<HTMLElement, MouseEvent>, entry: StatusBarEntry, skipDelay = false): void {
        const target = e.currentTarget;
        if (typeof entry.tooltip === 'function') {
            const cancellationSource = new CancellationTokenSource();
            this.doRequestHover(target, nls.localizeByDefault('Loading...'), skipDelay, () => cancellationSource.dispose());
            Promise.resolve(entry.tooltip(cancellationSource.token))
                .catch(() => undefined)
                .then(res => res && !cancellationSource.token.isCancellationRequested && this.doRequestHover(target, res, skipDelay));
        } else {
            this.doRequestHover(target, entry.tooltip!, skipDelay);
        }
    }

    protected doRequestHover(target: HTMLElement, content: string | HTMLElement | MarkdownString, skipDelay = false, onHide?: () => void): void {
        this.hoverService.requestHover({
            content,
            target,
            position: 'top',
            interactive: content instanceof HTMLElement || MarkdownString.is(content),
            onHide,
            skipHoverDelay: skipDelay
        });
    }

    protected createAttributes(viewEntry: StatusBarViewEntry): React.Attributes & React.HTMLAttributes<HTMLElement> {
        const attrs: React.Attributes & React.HTMLAttributes<HTMLElement> = {};
        const entry = viewEntry.entry;
        attrs.id = 'status-bar-' + viewEntry.id;
        attrs.className = 'element';
        if (entry.command || entry.onclick || entry.tooltip) {
            attrs.className += ' hasCommand';
        }
        if (entry.command) {
            attrs.onClick = this.triggerCommand(entry);
        } else if (entry.onclick) {
            attrs.onClick = e => entry.onclick?.(e.nativeEvent);
        } else {
            attrs.onClick = e => this.requestHover(e, entry, true);
        }

        if (viewEntry.compact && viewEntry.alignment !== undefined) {
            attrs.className += viewEntry.alignment === StatusBarAlignment.RIGHT ? ' compact-right' : ' compact-left';
        }

        if (entry.tooltip) {
            attrs.onMouseEnter = e => this.requestHover(e, entry);
        }
        if (entry.className) {
            attrs.className += ' ' + entry.className;
        }
        if (entry.accessibilityInformation) {
            attrs['aria-label'] = entry.accessibilityInformation.label;
            attrs.role = entry.accessibilityInformation.role;
        } else {
            attrs['aria-label'] = [entry.text, entry.tooltip].join(', ');
        }
        if (entry.backgroundColor) {
            attrs.className += ' has-background';
        }

        attrs.style = {
            color: entry.color || this.color,
            backgroundColor: entry.backgroundColor
        };

        return attrs;
    }

    protected renderElement(entry: StatusBarViewEntry): React.JSX.Element {
        const childStrings = this.entryService.parse(entry.entry.text);
        const children: React.JSX.Element[] = [];

        childStrings.forEach((val, key) => {
            if (LabelIcon.is(val)) {
                const animation = val.animation ? ` fa-${val.animation}` : '';
                if (val.name.startsWith('codicon-')) {
                    children.push(<span key={key} className={`codicon ${val.name}${animation}`}></span>);
                } else if (val.name.startsWith('fa-')) {
                    children.push(<span key={key} className={`fa ${val.name}${animation}`}></span>);
                } else {
                    children.push(<span key={key} className={`${codicon(val.name)}${animation}`}></span>);
                }
            } else {
                children.push(<span key={key}>{val}</span>);
            }
        });
        return <div key={entry.id} {...this.createAttributes(entry)}>
            {children}
        </div>;
    }

}
