import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {v4} from 'uuid';
import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service';
import {EditorNodeContract} from '../../nodes/EditorNode.contract';
import {EditorService} from '../../../service/editor.service';
import {EditorComponent} from 'ngx-monaco-editor';
import * as MonacoCollabExt from '@convergencelabs/monaco-collab-ext';
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
import {debug} from '../../../utility';
import {environment} from '../../../../environments/environment';

export interface RemoteUser {
    uuid: string;
    uid: string;
    display: string;
    color: string;
    cursor?: any;
    selection?: any;
}

export interface RemoteInsertOperation {
    type: 'insert';
    index: number;
    text: string;
    fullContents?: string;
}

export interface RemoteReplaceOperation {
    type: 'replace';
    index: number;
    text: string;
    length: number;
    fullContents?: string;
}

export interface RemoteDeleteOperation {
    type: 'delete';
    index: number;
    length: number;
    fullContents?: string;
}

export type RemoteOperation = RemoteInsertOperation | RemoteReplaceOperation | RemoteDeleteOperation;

@Component({
    selector: 'editor-code',
    templateUrl: './code.component.html',
    styleUrls: ['./code.component.scss'],
})
export class CodeComponent extends EditorNodeContract implements OnInit, OnDestroy {
    @Input() nodeId: string;
    @Input() editorUUID?: string;
    @ViewChild('theEditor') theEditor: EditorComponent;
    @ViewChild('editorContainer') editorContainer: ElementRef;
    public dirty = false;
    protected dbRecord: any = {};
    protected codeRefId!: string;
    public notAvailableOffline = false;
    public containerHeight = 540;

    protected cursorManager?: MonacoCollabExt.RemoteCursorManager;
    protected selectionManager?: MonacoCollabExt.RemoteSelectionManager;
    protected contentManager?: MonacoCollabExt.EditorContentManager;

    protected remoteUsers: RemoteUser[] = [];
    protected localUser?: RemoteUser;
    protected socket?: FlitterSocketConnection;
    protected editorGroupID!: string;

    public editorOptions = {
        theme: this.isDark() ? 'vs-dark' : 'vs',
        language: 'javascript',
        uri: v4(),
        readOnly: false,
        automaticLayout: true,
        scrollBeyondLastLine: false,
        scrollbar: {
            alwaysConsumeMouseWheel: false,
        },
    };

    public editorValue = '';
    public get readonly() {
        return !this.node || !this.editorService.canEdit();
    }

    public languageOptions: Array<string> = [
        'ABAP',
        'AES',
        'Apex',
        'AZCLI',
        'Bat',
        'C',
        'Cameligo',
        'Clojure',
        'CoffeeScript',
        'Cpp',
        'Csharp',
        'CSP',
        'CSS',
        'Dockerfile',
        'Fsharp',
        'Go',
        'GraphQL',
        'Handlebars',
        'HTML',
        'INI',
        'Java',
        'JavaScript',
        'JSON',
        'Kotlin',
        'LeSS',
        'Lua',
        'Markdown',
        'MiPS',
        'MSDAX',
        'MySQL',
        'Objective-C',
        'Pascal',
        'Pascaligo',
        'Perl',
        'pgSQL',
        'PHP',
        'Plaintext',
        'Postiats',
        'PowerQuery',
        'PowerShell',
        'Pug',
        'Python',
        'R',
        'Razor',
        'Redis',
        'RedShift',
        'RestructuredText',
        'Ruby',
        'Rust',
        'SB',
        'Scheme',
        'SCSS',
        'Shell',
        'SOL',
        'SQL',
        'St',
        'Swift',
        'TCL',
        'Twig',
        'TypeScript',
        'VB',
        'XML',
        'YAML',
    ];
    protected hadLoad = false;

    constructor(
        public editorService: EditorService,
        public readonly api: ApiService,
    ) { super(); }

    public isDark() {
        return document.body.classList.contains('dark');
    }

    public isDirty(): boolean | Promise<boolean> {
        return this.dirty;
    }

    public needsSave(): boolean | Promise<boolean> {
        return this.dirty;
    }

    public writeChangesToNode(): void | Promise<void> {
        this.node.Value.Mode = 'code';
        this.node.Value.Value = this.codeRefId;
        this.node.value = this.codeRefId;
    }

    public needsLoad(): boolean | Promise<boolean> {
        return this.node && !this.hadLoad;
    }

    public performLoad(): void | Promise<void> {
        return new Promise((res, rej) => {
            if ( !this.node.Value ) {
                this.node.Value = {};
            }

            if ( !this.node.Value.Value && this.editorService.canEdit() ) {
                this.api.createCodium(this.page.UUID, this.node.UUID).then(data => {
                    this.dbRecord = data;
                    this.node.Value.Mode = 'code';
                    this.node.Value.Value = data.UUID;
                    this.node.value = data.UUID;
                    this.codeRefId = data.UUID;
                    this.editorOptions.readOnly = this.readonly;
                    this.onSelectChange(false);
                    this.hadLoad = true;
                    this.notAvailableOffline = false;
                    res();
                }).catch(rej);
            } else {
                this.api.getCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.node.associatedTypeVersionNum).then(data => {
                    this.dbRecord = data;
                    this.initialValue = this.dbRecord.code;
                    this.editorValue = this.dbRecord.code;
                    this.editorOptions.language = this.dbRecord.Language;
                    this.codeRefId = this.node.Value.Value;
                    this.editorOptions.readOnly = this.readonly;
                    this.onSelectChange(false);
                    this.hadLoad = true;
                    this.notAvailableOffline = false;
                    res();
                }).catch(e => {
                    if ( e instanceof ResourceNotAvailableOfflineError ) {
                        this.notAvailableOffline = true;
                    } else {
                        rej(e);
                    }
                });
            }
        });
    }

    public performSave(): void | Promise<void> {
        if ( !this.editorService.canEdit() ) {
            return;
        }

        return new Promise((res, rej) => {
            this.dbRecord.code = this.editorValue;
            this.dbRecord.Language = this.editorOptions.language;

            this.api.saveCodium(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbRecord).then(data => {
                this.dbRecord = data;
                this.editorOptions.language = this.dbRecord.Language;
                this.editorValue = this.dbRecord.code;
                this.dirty = false;
                res();
            }).catch(rej);
        });
    }

    public performDelete(): void | Promise<void> {
        return this.api.deleteCodium(this.page.UUID, this.node.UUID, this.node.Value.Value);
    }

    ngOnInit() {
        this.editorService = this.editorService.getEditor(this.editorUUID);

        this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
            this.editorOptions.readOnly = !this.editorService.canEdit();
        });

        const url = `${environment.websocketBase}/api/v1/socket/code/.websocket`;
        debug(`Editor socket URL: ${url}`);

        if ( !this.editorService.isVersion() ) {
            const socket = new FlitterSocketConnection(url);
            socket.controller(this);

            socket.on_open().then(() => {
                debug('Connected to code editor socket', socket);
                socket.asyncRequest('subscribe', { resource_id: this.node.Value.Value }).then(([transaction, _, data]) => {
                    debug('Subscribed to editor group:', data);
                    if ( data.editor_group_id ) {
                        this.editorGroupID = data.editor_group_id;
                        this.socket = socket;
                        this.localUser = data.local_user;
                    }
                });
            });
        }
    }

    ngOnDestroy() {
        if ( this.socket ) {
            this.socket.socket.close();
        }
    }

    onMonacoEditorInit(editor) {
        let ignoreEvent = false;
        const updateHeight = () => {
            const contentHeight = Math.max(540, editor.getContentHeight());
            this.containerHeight = contentHeight;

            try {
                ignoreEvent = true;
                editor.layout({ width: this.editorContainer.nativeElement.offsetWidth, height: contentHeight });
            } finally {
                ignoreEvent = false;
            }
        };

        editor.onDidChangeCursorPosition(event => {
            this.socket?.asyncRequest('update_cursor', {
                position: event.position,
                uuid: this.localUser?.uuid,
                editor_group_id: this.editorGroupID,
            });
        });

        editor.onDidChangeCursorSelection(event => {
            this.socket?.asyncRequest('update_selection', {
                startPosition: {
                    lineNumber: event.selection.startLineNumber,
                    column: event.selection.startColumn,
                },
                endPosition: {
                    lineNumber: event.selection.endLineNumber,
                    column: event.selection.endColumn,
                },
                uuid: this.localUser?.uuid,
                editor_group_id: this.editorGroupID,
            });
        });

        editor.onDidContentSizeChange(updateHeight);
        updateHeight();

        this.cursorManager = new MonacoCollabExt.RemoteCursorManager({
            editor,
            tooltips: true,
            tooltipDuration: 2,
        });

        this.selectionManager = new MonacoCollabExt.RemoteSelectionManager({ editor });

        this.contentManager = new MonacoCollabExt.EditorContentManager({
            editor,
            onInsert: (index, text) => {
                if ( this.readonly ) {
                    return;
                }

                this.socket?.asyncRequest('apply', {
                    editor_group_id: this.editorGroupID,
                    operations: [
                        {
                            type: 'insert',
                            index,
                            text,
                        },
                    ],
                });
            },
            onReplace: (index, length, text) => {
                if ( this.readonly ) {
                    return;
                }

                this.socket?.asyncRequest('apply', {
                    editor_group_id: this.editorGroupID,
                    operations: [
                        {
                            type: 'replace',
                            index,
                            text,
                            length,
                        },
                    ],
                });
            },
            onDelete: (index, length) => {
                if ( this.readonly ) {
                    return;
                }

                this.socket?.asyncRequest('apply', {
                    editor_group_id: this.editorGroupID,
                    operations: [
                        {
                            type: 'delete',
                            index,
                            length,
                        },
                    ],
                });
            },
        });
    }

    applyOperation(op: RemoteOperation) {
        if ( op.type === 'insert' ) {
            this.contentManager?.insert(op.index, op.text);
        } else if ( op.type === 'replace' ) {
            this.contentManager?.replace(op.index, op.length, op.text);
        } else if ( op.type === 'delete' ) {
            this.contentManager?.delete(op.index, op.length);
        }
    }

    async applyRemoteOperation(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
        const ops: RemoteOperation[] = transaction.incoming.operations || [];
        for ( const op of ops ) {
            this.applyOperation(op);
        }
    }

    async updateCursorPosition(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
        const position = transaction.incoming.position;
        const uuid = transaction.incoming.uuid;

        for ( const user of this.remoteUsers ) {
            if ( user.uuid === uuid ) {
                user.cursor?.setPosition(position);
            }
        }
    }

    async updateSelection(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
        const startPosition = transaction.incoming.startPosition;
        const endPosition = transaction.incoming.endPosition;
        const uuid = transaction.incoming.uuid;

        for ( const user of this.remoteUsers ) {
            if ( user.uuid === uuid ) {
                if ( startPosition && endPosition ) {
                    user.selection?.setPositions(startPosition, endPosition);
                    user.selection?.show();
                } else {
                    user.selection?.hide();
                }
            }
        }
    }

    async setEditorGroupUsers(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) {
        const remoteUsers: RemoteUser[] = Array.isArray(transaction.incoming?.users) ? transaction.incoming.users : [];
        console.log('set editor group users', remoteUsers, transaction);
        for ( const user of this.remoteUsers ) {
            this.cursorManager?.removeCursor(user.uuid);
            user.cursor?.dispose();
            delete user.cursor;

            this.selectionManager?.removeSelection(user.uuid);
            user.selection?.dispose();
            delete user.selection;
        }

        while ( !this.cursorManager ) {
            await new Promise(r => setTimeout(r, 500));
        }

        this.remoteUsers = remoteUsers;
        for ( const user of this.remoteUsers ) {
            user.cursor = this.cursorManager?.addCursor(user.uuid, user.color, user.display);
            user.cursor?.setOffset(0);
            user.cursor?.show();

            user.selection = this.selectionManager?.addSelection(user.uuid, user.color);
        }
    }

    public onEditorModelChange($event) {
        if ( this.editorValue !== this.dbRecord.code ) {
            this.dirty = true;
            this.editorService.triggerSave();
        }
    }

    public onSelectChange(updateDbRecord = true) {
        if ( updateDbRecord ) {
            this.dbRecord.Language = this.editorOptions.language;
            this.editorService.triggerSave();
            this.dirty = true;
        }

        this.editorOptions = {...this.editorOptions};
    }
}
