'use strict';
import { Functions } from './system';
import { DecorationRenderOptions, Disposable, Event, EventEmitter, ExtensionContext, OverviewRulerLane, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, TextEditorViewColumnChangeEvent, window, workspace } from 'vscode';
import { BlameAnnotationProvider } from './blameAnnotationProvider';
import { TextDocumentComparer, TextEditorComparer } from './comparers';
import { IBlameConfig } from './configuration';
import { ExtensionKey } from './constants';
import { BlameabilityChangeEvent, GitContextTracker, GitService, GitUri } from './gitService';
import { Logger } from './logger';
import { WhitespaceController } from './whitespaceController';

export const BlameDecorations = {
    annotation: window.createTextEditorDecorationType({
        before: {
            margin: '0 1.75em 0 0'
        },
        after: {
            margin: '0 0 0 4em'
        }
    } as DecorationRenderOptions),
    highlight: undefined as TextEditorDecorationType | undefined
};

export class BlameAnnotationController extends Disposable {

    private _onDidToggleBlameAnnotations = new EventEmitter<void>();
    get onDidToggleBlameAnnotations(): Event<void> {
        return this._onDidToggleBlameAnnotations.event;
    }

    private _annotationProviders: Map<number, BlameAnnotationProvider> = new Map();
    private _blameAnnotationsDisposable: Disposable | undefined;
    private _config: IBlameConfig;
    private _disposable: Disposable;
    private _whitespaceController: WhitespaceController | undefined;

    constructor(private context: ExtensionContext, private git: GitService, private gitContextTracker: GitContextTracker) {
        super(() => this.dispose());

        this._onConfigurationChanged();

        const subscriptions: Disposable[] = [];

        subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));

        this._disposable = Disposable.from(...subscriptions);
    }

    dispose() {
        this._annotationProviders.forEach(async (p, i) => await this.clear(i));

        BlameDecorations.annotation && BlameDecorations.annotation.dispose();
        BlameDecorations.highlight && BlameDecorations.highlight.dispose();

        this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
        this._whitespaceController && this._whitespaceController.dispose();
        this._disposable && this._disposable.dispose();
    }

    private _onConfigurationChanged() {
        let toggleWhitespace = workspace.getConfiguration(`${ExtensionKey}.advanced.toggleWhitespace`).get<boolean>('enabled');
        if (!toggleWhitespace) {
            // Until https://github.com/Microsoft/vscode/issues/11485 is fixed we need to toggle whitespace for non-monospace fonts and ligatures
            // TODO: detect monospace font
            toggleWhitespace = workspace.getConfiguration('editor').get<boolean>('fontLigatures');
        }

        if (toggleWhitespace && !this._whitespaceController) {
            this._whitespaceController = new WhitespaceController();
        }
        else if (!toggleWhitespace && this._whitespaceController) {
            this._whitespaceController.dispose();
            this._whitespaceController = undefined;
        }

        const cfg = workspace.getConfiguration(ExtensionKey).get<IBlameConfig>('blame')!;

        if (cfg.annotation.highlight !== (this._config && this._config.annotation.highlight)) {
            BlameDecorations.highlight && BlameDecorations.highlight.dispose();

            switch (cfg.annotation.highlight) {
                case 'gutter':
                    BlameDecorations.highlight = window.createTextEditorDecorationType({
                        dark: {
                            gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
                            overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
                        },
                        light: {
                            gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'),
                            overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
                        },
                        gutterIconSize: 'contain',
                        overviewRulerLane: OverviewRulerLane.Right
                    });
                    break;

                case 'line':
                    BlameDecorations.highlight = window.createTextEditorDecorationType({
                        dark: {
                            backgroundColor: 'rgba(255, 255, 255, 0.15)',
                            overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
                        },
                        light: {
                            backgroundColor: 'rgba(0, 0, 0, 0.15)',
                            overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
                        },
                        overviewRulerLane: OverviewRulerLane.Right,
                        isWholeLine: true
                    });
                    break;

                case 'both':
                    BlameDecorations.highlight = window.createTextEditorDecorationType({
                        dark: {
                            backgroundColor: 'rgba(255, 255, 255, 0.15)',
                            gutterIconPath: this.context.asAbsolutePath('images/blame-dark.svg'),
                            overviewRulerColor: 'rgba(255, 255, 255, 0.75)'
                        },
                        light: {
                            backgroundColor: 'rgba(0, 0, 0, 0.15)',
                            gutterIconPath: this.context.asAbsolutePath('images/blame-light.svg'),
                            overviewRulerColor: 'rgba(0, 0, 0, 0.75)'
                        },
                        gutterIconSize: 'contain',
                        overviewRulerLane: OverviewRulerLane.Right,
                        isWholeLine: true
                    });
                    break;

                default:
                    BlameDecorations.highlight = undefined;
                    break;
            }
        }

        this._config = cfg;
    }

    async clear(column: number) {
        const provider = this._annotationProviders.get(column);
        if (!provider) return;

        this._annotationProviders.delete(column);
        await provider.dispose();

        if (this._annotationProviders.size === 0) {
            Logger.log(`Remove listener registrations for blame annotations`);
            this._blameAnnotationsDisposable && this._blameAnnotationsDisposable.dispose();
            this._blameAnnotationsDisposable = undefined;
        }

        this._onDidToggleBlameAnnotations.fire();
    }

    async showBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
        if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;

        const currentProvider = this._annotationProviders.get(editor.viewColumn || -1);
        if (currentProvider && TextEditorComparer.equals(currentProvider.editor, editor)) {
            await currentProvider.setSelection(shaOrLine);
            return true;
        }

        const gitUri = await GitUri.fromUri(editor.document.uri, this.git);
        const provider = new BlameAnnotationProvider(this.context, this.git, this._whitespaceController, editor, gitUri);
        if (!await provider.supportsBlame()) return false;

        if (currentProvider) {
            await this.clear(currentProvider.editor.viewColumn || -1);
        }

        if (!this._blameAnnotationsDisposable && this._annotationProviders.size === 0) {
            Logger.log(`Add listener registrations for blame annotations`);

            const subscriptions: Disposable[] = [];

            subscriptions.push(window.onDidChangeVisibleTextEditors(Functions.debounce(this._onVisibleTextEditorsChanged, 100), this));
            subscriptions.push(window.onDidChangeTextEditorViewColumn(this._onTextEditorViewColumnChanged, this));
            subscriptions.push(workspace.onDidChangeTextDocument(this._onTextDocumentChanged, this));
            subscriptions.push(workspace.onDidCloseTextDocument(this._onTextDocumentClosed, this));
            subscriptions.push(this.gitContextTracker.onDidBlameabilityChange(this._onBlameabilityChanged, this));

            this._blameAnnotationsDisposable = Disposable.from(...subscriptions);
        }

        this._annotationProviders.set(editor.viewColumn || -1, provider);
        if (await provider.provideBlameAnnotation(shaOrLine)) {
            this._onDidToggleBlameAnnotations.fire();
            return true;
        }
        return false;
    }

    isAnnotating(editor: TextEditor): boolean {
        if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;

        return !!this._annotationProviders.get(editor.viewColumn || -1);
    }

    async toggleBlameAnnotation(editor: TextEditor, shaOrLine?: string | number): Promise<boolean> {
        if (!editor || !editor.document || !this.git.isEditorBlameable(editor)) return false;

        const provider = this._annotationProviders.get(editor.viewColumn || -1);
        if (!provider) return this.showBlameAnnotation(editor, shaOrLine);

        await this.clear(provider.editor.viewColumn || -1);
        return false;
    }

    private _onBlameabilityChanged(e: BlameabilityChangeEvent) {
        if (e.blameable || !e.editor) return;

        for (const [key, p] of this._annotationProviders) {
            if (!TextDocumentComparer.equals(p.document, e.editor.document)) continue;

            Logger.log('BlameabilityChanged:', `Clear blame annotations for column ${key}`);
            this.clear(key);
        }
    }

    private _onTextDocumentChanged(e: TextDocumentChangeEvent) {
        for (const [key, p] of this._annotationProviders) {
            if (!TextDocumentComparer.equals(p.document, e.document)) continue;

            // We have to defer because isDirty is not reliable inside this event
            setTimeout(() => {
                // If the document is dirty all is fine, just kick out since the GitContextTracker will handle it
                if (e.document.isDirty) return;

                // If the document isn't dirty, it is very likely this event was triggered by an outside edit of this document
                // Which means the document has been reloaded and the blame annotations have been removed, so we need to update (clear) our state tracking
                Logger.log('TextDocumentChanged:', `Clear blame annotations for column ${key}`);
                this.clear(key);
            }, 1);
        }
    }

    private _onTextDocumentClosed(e: TextDocument) {
        for (const [key, p] of this._annotationProviders) {
            if (!TextDocumentComparer.equals(p.document, e)) continue;

            Logger.log('TextDocumentClosed:', `Clear blame annotations for column ${key}`);
            this.clear(key);
        }
    }

    private async _onTextEditorViewColumnChanged(e: TextEditorViewColumnChangeEvent) {
        const viewColumn = e.viewColumn || -1;

        Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${viewColumn}`);
        await this.clear(viewColumn);

        for (const [key, p] of this._annotationProviders) {
            if (!TextEditorComparer.equals(p.editor, e.textEditor)) continue;

            Logger.log('TextEditorViewColumnChanged:', `Clear blame annotations for column ${key}`);
            await this.clear(key);
        }
    }

    private async _onVisibleTextEditorsChanged(e: TextEditor[]) {
        if (e.every(_ => _.document.uri.scheme === 'inmemory')) return;

        for (const [key, p] of this._annotationProviders) {
            if (e.some(_ => TextEditorComparer.equals(p.editor, _))) continue;

            Logger.log('VisibleTextEditorsChanged:', `Clear blame annotations for column ${key}`);
            this.clear(key);
        }
    }
}