import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { LoadingService } from '@iqser/common-ui';
import { EditorThemeService } from '@services/editor-theme.service';
import { Subject } from 'rxjs';
import { debounceTime, filter, tap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { List, OnChange } from '@iqser/common-ui/lib/utils';
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
import ICodeEditor = monaco.editor.ICodeEditor;
import IDiffEditor = monaco.editor.IDiffEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import ILineChange = monaco.editor.ILineChange;
import IEditorMouseEvent = monaco.editor.IEditorMouseEvent;

const MIN_WORD_LENGTH = 2;
const lineChangeToDecoration = ({ originalEndLineNumber, originalStartLineNumber }: ILineChange) =>
    ({
        range: new monaco.Range(originalStartLineNumber, 1, originalEndLineNumber, 1),
        options: {
            glyphMarginClassName: 'arrow-left',
            isWholeLine: true,
        },
    } as IModelDeltaDecoration);
const isPositive = (lineChange: ILineChange) => lineChange.originalEndLineNumber - lineChange.originalStartLineNumber >= 0;
const notZero = (lineChange: ILineChange) => lineChange.originalEndLineNumber !== 0 && lineChange.originalStartLineNumber !== 0;

@Component({
    selector: 'redaction-editor',
    templateUrl: './editor.component.html',
    styleUrls: ['./editor.component.scss'],
})
export class EditorComponent implements OnInit, OnChanges {
    #initialEntriesSet = new Set<string>();
    private _diffEditor: IDiffEditor;
    private _decorations: string[] = [];
    protected readonly _editorTextChanged$ = new Subject<string>();
    @Input() showDiffEditor = false;
    @Input() diffEditorText: string;
    @Input() @OnChange<List, EditorComponent>('revert') initialEntries: List;
    @Input() canEdit = false;
    /**
     * Used as [modified] input on diff editor
     * Shouldn't be updated when editing in diff editor.
     */
    diffValue: string;
    editorOptions: IStandaloneEditorConstructionOptions = {};
    codeEditor: ICodeEditor;
    value: string;

    constructor(private readonly _loadingService: LoadingService, private readonly _editorThemeService: EditorThemeService) {
        const textChanged$ = this._editorTextChanged$.pipe(
            debounceTime(300), // prevent race condition with onPaste event
            takeUntilDestroyed(),
            filter(text => text.length > 0),
            tap(newText => {
                const newDecorations = this.#getDecorations(newText);
                this._decorations = this.codeEditor.deltaDecorations(this._decorations, newDecorations);
                this.diffValue = this.value;
                this._loadingService.stop();
            }),
        );
        // eslint-disable-next-line rxjs/no-ignored-subscription
        textChanged$.subscribe();
    }

    get currentEntries(): string[] {
        return this.value.split('\n');
    }

    // TODO: this is called way too often, we need a better option
    get hasChanges(): boolean {
        return this.value.trim() !== this.initialEntries.join('\n');
    }

    get #currentEntriesCount(): number {
        return this.currentEntries.length;
    }

    onPaste(event: ClipboardEvent) {
        if ((event.target as HTMLTextAreaElement).ariaRoleDescription === 'editor') {
            this._loadingService.start();
        }
    }

    ngOnChanges(changes: SimpleChanges) {
        if (changes.diffEditorText) {
            this.revert();
            this._diffEditor?.getOriginalEditor().setValue(this.diffEditorText);
        }
    }

    ngOnInit(): void {
        this.revert();

        this.editorOptions = {
            theme: 'vs',
            language: 'text/plain',
            glyphMargin: true,
            automaticLayout: true,
            readOnly: !this.canEdit,
            extraEditorClassName: this.canEdit ? '' : 'disabled',
        };
    }

    onDiffEditorInit(editor: IDiffEditor): void {
        this._diffEditor = editor;
        this.codeEditor = editor.getModifiedEditor();
        this.#addMarginButtons();
        this._diffEditor?.getOriginalEditor().setValue(this.diffEditorText);
        this._diffEditor?.getModifiedEditor().setValue(this.diffValue);
        this._diffEditor.getOriginalEditor().onMouseDown(event => this.#handleMarginButtonClick(event));
        this._diffEditor.getModifiedEditor().onDidChangeModelContent(() => {
            this.value = this._diffEditor.getModel().modified.getValue();
        });
        this.#setTheme();
    }

    onCodeEditorInit(editor: ICodeEditor): void {
        this.codeEditor = editor;
        this.#setTheme();
    }

    revert() {
        this.value = this.initialEntries.join('\n');
        this.#initialEntriesSet = new Set<string>(this.initialEntries);
        this.diffValue = this.value;
        this._diffEditor?.getModifiedEditor().setValue(this.diffValue);
        this._editorTextChanged$.next(this.value);
    }

    #getDecorations(newText: string) {
        const currentEntries = newText.split('\n');
        const newDecorations: IModelDeltaDecoration[] = [];

        for (let index = 0; index < currentEntries.length; index++) {
            const entry = currentEntries.at(index)?.trim();
            if (!entry || entry.length === 0) {
                continue;
            }

            if (this.#initialEntriesSet.has(entry)) {
                continue;
            }

            const line = index + 1;
            newDecorations.push(this.#getDecoration(entry, line));
        }

        return newDecorations;
    }

    #defineThemes(): void {
        for (const theme of this._editorThemeService.themes) {
            (window as any).monaco.editor.defineTheme(theme, this._editorThemeService.configurations[theme]);
        }
    }

    #setTheme(): void {
        this.#defineThemes();
        (window as any).monaco.editor.setTheme(this._editorThemeService.getTheme(this.canEdit));
    }

    #handleMarginButtonClick(event: IEditorMouseEvent) {
        const isMarginButtonClick = event.target.detail.glyphMarginWidth && event.target.detail.glyphMarginWidth !== 0;
        if (isMarginButtonClick) {
            this.#addContentFromOriginalAtLine(event.target.position.lineNumber);
        }
    }

    #addContentFromOriginalAtLine(lineNumber: number) {
        const lineContent = this._diffEditor.getOriginalEditor().getModel().getLineContent(lineNumber) + '\n';
        const lineChanges = this._diffEditor.getLineChanges();
        let targetLineNumber = lineNumber;
        for (const lineChange of lineChanges) {
            const { originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber } = lineChange;
            if (originalStartLineNumber <= lineNumber && originalEndLineNumber >= lineNumber) {
                targetLineNumber = modifiedStartLineNumber + 1;
                break;
            }
        }
        const currentEntriesCount = this.#currentEntriesCount;
        const prefix = targetLineNumber > currentEntriesCount && this.currentEntries[currentEntriesCount - 1] !== '' ? '\n' : '';
        const range = new monaco.Range(targetLineNumber, 1, targetLineNumber, 1);
        this._diffEditor.getModifiedEditor().executeEdits(null, [{ range, text: prefix + lineContent, forceMoveMarkers: true }]);
    }

    #addMarginButtons() {
        let glyphDecorations = [];
        this._diffEditor.onDidUpdateDiff(() => {
            /**
             * Sometimes diff editor thinks that there's a change that starts on a line greater than 0 and ends on line 0.
             * We must get all change that started on a line and ended on the same or greater line
             * Otherwise margin buttons will have an odd behaviour
             */
            const originalText = this.diffEditorText.split('\n');
            const isNotEmpty = (change: ILineChange) => originalText[change.originalStartLineNumber - 1].length;
            const isTruthy = change => isPositive(change) && notZero(change) && isNotEmpty(change);
            const lineChanges = this._diffEditor.getLineChanges().filter(isTruthy);
            const decorations = lineChanges.map(lineChangeToDecoration);
            glyphDecorations = this._diffEditor.getOriginalEditor().deltaDecorations(glyphDecorations, decorations);
        });
    }

    #getDecoration(entry: string, line: number): IModelDeltaDecoration {
        const cssClass = entry.length < MIN_WORD_LENGTH ? 'too-short-marker' : 'changed-row-marker';
        const range = new monaco.Range(line, 1, line, 1);

        return { range: range, options: { isWholeLine: true, className: cssClass } };
    }
}
