import {Component, ElementRef, HostListener, Input, OnInit, ViewChild} from '@angular/core';
import {EditorNodeContract} from '../EditorNode.contract';
import {EditorService} from '../../../service/editor.service';
import {v4} from 'uuid';
import {EditorComponent} from 'ngx-monaco-editor';
import {KatexOptions} from 'ngx-markdown';
import {environment} from '../../../../environments/environment';
import {debug} from '../../../utility';
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
import {RemoteOperation, RemoteUser} from '../../editor/code/code.component';
import * as MonacoCollabExt from '@convergencelabs/monaco-collab-ext';

@Component({
    selector: 'editor-markdown',
    templateUrl: './markdown.component.html',
    styleUrls: ['./markdown.component.scss'],
})
export class MarkdownComponent extends EditorNodeContract implements OnInit {
    // @ViewChild('editable') editable;
    @Input() nodeId: string;
    @Input() editorUUID?: string;
    @ViewChild('editor') editor: EditorComponent;
    @ViewChild('editorContainer') editorContainer: ElementRef;

    public readonly katexOptions: KatexOptions = {
        // displayMode: true,
        // @ts-ignore
        output: 'mathml',
    };

    // public isFocused = false;
    public initialValue = 'Double-click to edit...';
    protected savedValue = 'Double-click to edit...';
    public contents = '';
    private dirtyOverride = false;
    public showEditor = false;
    protected hadOneFocusOut = false;
    public singleColumn = false;
    public containerHeight = 540;
    public editorGroupID!: string;
    protected socket?: FlitterSocketConnection;
    protected localUser!: RemoteUser;
    protected remoteUsers: RemoteUser[] = [];
    protected cursorManager: any;
    protected selectionManager: any;
    protected contentManager: any;

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

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

    constructor(
        public editorService: EditorService,
    ) {
        super();
        this.contents = this.initialValue;
        this.savedValue = this.initialValue;
    }

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

    ngOnInit() {
        this.editorService = this.editorService.getEditor(this.editorUUID);
        this.editorService.registerNodeEditor(this.nodeId, this).then(() => {
            if ( !this.node.Value ) {
                this.node.Value = {};
            }

            if ( this.node.Value.Value ) {
                this.initialValue = this.node.Value.Value;
                this.savedValue = this.node.Value.Value;
            }

            this.contents = this.initialValue;
        });

        const url = `${environment.websocketBase}/api/v1/socket/markdown/.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 markdown editor socket', socket);
                socket.asyncRequest('subscribe', { resource_id: this.node.UUID }).then(([transaction, _, data]) => {
                    debug('Subscribed to markdown group:', data);
                    if ( data.editor_group_id ) {
                        this.editorGroupID = data.editor_group_id;
                        this.socket = socket;
                        this.localUser = data.local_user;
                    }
                });
            });
        }
    }

    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.onDidContentSizeChange(updateHeight);
        updateHeight();

        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,
                            fullContents: this.contents,
                        },
                    ],
                });
            },
            onReplace: (index, length, text) => {
                if ( this.readonly ) {
                    return;
                }

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

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

    public isDirty(): boolean | Promise<boolean> {
        return this.dirtyOverride || this.contents !== this.savedValue;
    }

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

    onContentsChanged(event) {
        if ( event !== this.savedValue ) {
            this.editorService.triggerSave();
        }
    }

    onFocusIn() {
        this.showEditor = this.editorService.canEdit();

        requestAnimationFrame(() => {
            if ( this.showEditor ) {
                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);
                }
            }
        });
    }

    @HostListener('document:keyup.escape', ['$event'])
    onFocusOut(event) {
        if ( !this.hadOneFocusOut ) {
            this.hadOneFocusOut = true;
            setTimeout(() => {
                this.hadOneFocusOut = false;
            }, 500);
        } else {
            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;
            }

            this.hadOneFocusOut = false;
            this.showEditor = false;
        }
    }

    onEditorHostResize($event) {
        if ( $event.newWidth < 700 && !this.singleColumn ) {
            this.singleColumn = true;
        } else if ( $event.newWidth > 700 && this.singleColumn ) {
            this.singleColumn = false;
        }
    }

    applyOperation(op: RemoteOperation) {
        if ( !this.showEditor ) {
            this.contents = op.fullContents ?? this.contents;
            return;
        }

        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);
        }
    }
}
