/* tslint:disable */
import { Emitter, Event } from '@vscode/monaco-editor/esm/vs/base/common/event';
import { IDisposable, dispose } from '@vscode/monaco-editor/esm/vs/base/common/lifecycle';
import { URI, UriComponents } from '@vscode/monaco-editor/esm/vs/base/common/uri';
import { IMainContext, MainContext, MainThreadDocumentsShape } from '@workbench-stack/extension-api/lib/protocol/server.protocol';
import { ExtHostDocumentsShape } from '@workbench-stack/extension-api/lib/protocol/common.protocol';
import { ExtHostDocumentData, setWordDefinitionFor } from './ext-host-document-data';
import { ExtHostDocumentsAndEditors } from './ext-host-documents-and-editors';
import * as TypeConverters from '@workbench-stack/extension-api/lib/core/converters/ext-host-type-converters';
import * as cdecode from 'cdecode';
import { IModelChangedEvent } from '@workbench-stack/extension-api/lib/protocol/temporary-types';
import { Subject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { getEventPayload, getEvents } from '../../util/event-pipes';

export enum DocumentUpdates {
    AddDocument = 'AddDocument',
    SaveDocument = 'SaveDocument',
    RemoveDocument = 'RemoveDocument',
    ChangeDocument = 'ChangeDocument',
}

export class ExtHostDocuments implements ExtHostDocumentsShape {
    private updates$ = new Subject();

    readonly onDidAddDocument: Observable<cdecode.TextDocument> = this.updates$.pipe(filter(getEvents(DocumentUpdates.AddDocument)), map(getEventPayload));
    readonly onDidRemoveDocument: Observable<cdecode.TextDocument> = this.updates$.pipe(filter(getEvents(DocumentUpdates.RemoveDocument)), map(getEventPayload));
    readonly onDidSaveDocument: Observable<cdecode.TextDocument> = this.updates$.pipe(filter(getEvents(DocumentUpdates.SaveDocument)), map(getEventPayload));
    readonly onDidChangeDocument: Observable<cdecode.TextDocumentChangeEvent> = this.updates$.pipe(filter(getEvents(DocumentUpdates.ChangeDocument)), map(getEventPayload));

    private _toDispose: IDisposable[];
    private _proxy: MainThreadDocumentsShape;
    private _documentsAndEditors: ExtHostDocumentsAndEditors;
    private _documentLoader = new Map<string, Promise<ExtHostDocumentData>>();

    constructor(mainContext: IMainContext, documentsAndEditors: ExtHostDocumentsAndEditors) {
        this._proxy = mainContext.getProxy(MainContext.MainThreadDocuments);
        this._documentsAndEditors = documentsAndEditors;

        this._toDispose = [
            this._documentsAndEditors.onDidRemoveDocuments(documents => {
                for (const data of documents) {
                    this.updates$.next({ type: DocumentUpdates.RemoveDocument, payload: data.document });
                }
            }),
            this._documentsAndEditors.onDidAddDocuments(documents => {
                for (const data of documents) {
                    this.updates$.next({ type: DocumentUpdates.AddDocument, payload: data.document });
                }
            })
        ];
    }

    public dispose(): void {
        dispose(this._toDispose);
    }

    public getAllDocumentData(): ExtHostDocumentData[] {
        return this._documentsAndEditors.allDocuments();
    }

    public getDocumentData(resource: cdecode.Uri): ExtHostDocumentData {
        if (!resource) {
            return undefined;
        }
        const data = this._documentsAndEditors.getDocument(resource.toString());
        if (data) {
            return data;
        }
        return undefined;
    }

    public getDocument(resource: cdecode.Uri): cdecode.TextDocument {
        const data = this.getDocumentData(resource);
        if (!data || !data.document) {
            throw new Error('Unable to retrieve document from URI');
        }
        return data.document;
    }

    public ensureDocumentData(uri: URI): Promise<ExtHostDocumentData> {

        let cached = this._documentsAndEditors.getDocument(uri.toString());
        if (cached) {
            return Promise.resolve(cached);
        }

        let promise = this._documentLoader.get(uri.toString());
        if (!promise) {
            promise = this._proxy.$tryOpenDocument(uri).then(() => {
                this._documentLoader.delete(uri.toString());
                return this._documentsAndEditors.getDocument(uri.toString());
            }, err => {
                this._documentLoader.delete(uri.toString());
                return Promise.reject(err);
            });
            this._documentLoader.set(uri.toString(), promise);
        }

        return promise;
    }

    public createDocumentData(options?: { language?: string; content?: string }): Promise<URI> {
        return this._proxy.$tryCreateDocument(options).then(data => URI.revive(data));
    }

    public $acceptModelModeChanged(uriComponents: UriComponents, oldModeId: string, newModeId: string): void {
        const uri = URI.revive(uriComponents);
        const strURL = uri.toString();
        let data = this._documentsAndEditors.getDocument(strURL);

        // Treat a mode change as a remove + add

        this.updates$.next({ type: DocumentUpdates.RemoveDocument, payload: data.document });
        data._acceptLanguageId(newModeId);
        this.updates$.next({ type: DocumentUpdates.AddDocument, payload: data.document });
    }

    public $acceptModelSaved(uriComponents: UriComponents): void {
        const uri = URI.revive(uriComponents);
        const strURL = uri.toString();
        let data = this._documentsAndEditors.getDocument(strURL);
        this.$acceptDirtyStateChanged(uriComponents, false);
        this.updates$.next({ type: DocumentUpdates.SaveDocument, payload: data.document });
    }

    public $acceptDirtyStateChanged(uriComponents: UriComponents, isDirty: boolean): void {
        const uri = URI.revive(uriComponents);
        const strURL = uri.toString();
        let data = this._documentsAndEditors.getDocument(strURL);
        data._acceptIsDirty(isDirty);
        this.updates$.next({
            type: DocumentUpdates.ChangeDocument, payload: {
                document: data.document,
                contentChanges: []
            }
        });
    }

    public $acceptModelChanged(uriComponents: UriComponents, events: IModelChangedEvent, isDirty: boolean): void {
        const uri = URI.revive(uriComponents);
        const strURL = uri.toString();
        let data = this._documentsAndEditors.getDocument(strURL);
        data._acceptIsDirty(isDirty);
        data.onEvents(events);
        this.updates$.next({
            type: DocumentUpdates.ChangeDocument, payload: {
                document: data.document,
                contentChanges: events.changes.map((change) => {
                    return {
                        range: TypeConverters.Range.to(change.range),
                        rangeOffset: change.rangeOffset,
                        rangeLength: change.rangeLength,
                        text: change.text
                    };
                })
            }
        });
    }

    public setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void {
        setWordDefinitionFor(modeId, wordDefinition);
    }
}
