import {Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild} from '@angular/core';
import {debug} from '../../utility';
import {DomSanitizer} from '@angular/platform-browser';

export interface EditingUserSelect {
    path: string;
    offset: string;
    user_data: {
        uuid: string,
        uid: string,
        display: string,
        color: string
    };
}

export interface MutationBroadcast {
    path: string;
    type: 'characterData' | 'attributes' | 'childList';
    data: string;
    fullContents: string;
    addedNodes?: Array<{previousSiblingPath: string, type: string, data: string}>;
}

@Component({
    selector: 'wysiwyg-editor',
    templateUrl: './wysiwyg.component.html',
    styleUrls: ['./wysiwyg.component.scss'],
})
export class WysiwygComponent implements OnInit {
    @ViewChild('editable') editable;
    @Input() readonly = false;
    @Input() requestSelectionRefresh?: () => any;
    @Input() set contents(val: string) {
        if ( this.isFocused ) {
            if ( this.editingContents !== val ) {
                this.editingContents = val;
            }
        } else {
            if ( this.currentContents !== val ) {
                this.currentContents = val;
            }
        }
    }

    get contents(): string {
        return this.currentContents;
    }

    @Input() editingUsers: Array<{uuid: string, uid: string, display: string, color: string}> = [];

    protected privEditingUserSelections: Array<any> = [];
    @Input() set editingUserSelections(sels: Array<any>) {
        this.privEditingUserSelections = sels;
        this.processSelections();
    }

    get editingUserSelections() {
        return this.privEditingUserSelections;
    }

    @Output() contentsChanged: EventEmitter<string> = new EventEmitter<string>();
    @Output() selectionChanged: EventEmitter<{path: string, offset: number}> = new EventEmitter<{path: string; offset: number}>();
    @Output() contentsMutated: EventEmitter<MutationBroadcast> = new EventEmitter<MutationBroadcast>();

    public currentContents = '';
    protected editingContents = '';

    public isFocused = false;
    protected hadOneFocusOut = false;
    protected isEditOnly = false;
    protected applyingRemoteMutation = false;
    protected ignoreNextMutation = false;

    public get editonly() {
        return this.isEditOnly;
    }

    @Input()
    public set editonly(val: boolean) {
        this.isEditOnly = val;
        if ( this.isEditOnly && !this.readonly ) {
            this.isFocused = true;
        }
    }

    public get displayContents() {
        return this.replaceLinks((this.contents || 'Double-click to edit...').replace(/</g, '\n<'));
    }

    private replaceLinks(text) {
        const exp = /((href|src)=["']|)(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
        return text.replace(exp, (...args) => {
            return  args[2] ?
                args[0] :
                '<a href="' + args[3] + '" target="_blank">' + args[3] + '</a>';
        });
    }

    constructor(
        protected sanitizer: DomSanitizer,
    ) { }

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

    ngOnInit() {
        debug('Initializing WYSIWYG', this);

        document.addEventListener('selectionchange', event => {
            this.selectionChanged.emit(this.getSerializedSelection());
        });
    }

    onFocusIn(event: MouseEvent) {
        this.isFocused = !this.readonly;
        this.editingContents = this.currentContents;
        if ( this.requestSelectionRefresh ) {
            this.requestSelectionRefresh();
        }
    }

    getSerializedSelection() {
        const selection = window.getSelection();
        if ( this.editable && this.editable.nativeElement.contains(selection.focusNode) ) {
            return {
                path: this.getPathToElement(selection.focusNode),
                offset: selection.focusOffset,
            };
        }
    }

    getElementFromPath(path: string) {
        if ( !this.editable ) {
            return;
        }

        const parts = path.split('.')
            .map(x => x.split('@'))
            .map(([tagName, idxString]) => [tagName, Number(idxString)]);

        let currentElem = this.editable.nativeElement;
        for ( const part of parts ) {
            if ( !currentElem ) {
                return;
            }

            const [tagName, idx] = part;
            const children = [...(tagName === '#text' ? currentElem.childNodes : currentElem.getElementsByTagName(tagName))];
            currentElem = children[idx];
        }

        return currentElem;
    }

    getPathToElement(elem: any) {
        if ( !this.editable ) {
            throw new Error('Cannot get path to element unless editable.');
        }

        debug('getPathToElement', elem);

        const maxNest = 5000;
        let currentNest = 0;
        const parents = [elem];
        let currentParent = elem;

        do {
            currentNest += 1;
            if ( currentNest > maxNest ) {
                throw new Error('Reached max nesting limit.');
            }

            currentParent = currentParent.parentElement;
            if ( currentParent ) {
                parents.push(currentParent);
            }
        } while ( currentParent && currentParent !== this.editable.nativeElement );

        return parents.reverse()
            .slice(1)
            .map((element, idx) => {
                const siblings = element.tagName ? element.parentElement.getElementsByTagName(element.tagName)
                    : element.parentElement.childNodes;

                let siblingIdx = -1;
                [...siblings].some((sibling, potentialIdx) => {
                    if ( sibling === element ) {
                        siblingIdx = potentialIdx;
                        return true;
                    }
                });

                return `${element.tagName || '#text'}@${siblingIdx}`;
            })
            .join('.');
    }

    processSelections() {
        if ( !this.editable ) {
            return;
        }

        const editableBase = this.editable.nativeElement;

        for ( const sel of this.privEditingUserSelections ) {
            if ( !sel.path ) {
                continue;
            }

            sel.element = this.getElementFromPath(sel.path);
            if ( sel.element ) {
                sel.top = sel.element.offsetTop;
                sel.left = sel.element.offsetLeft;

                const range = document.createRange();
                range.collapse(true);
                range.setStart(sel.element, sel.offset);
                range.setEnd(sel.element, sel.offset);

                const rect = range.getBoundingClientRect();

                sel.top = rect.top;

                // FIXME I'm not sure why the 45px offset is needed, but it is...
                sel.left = (rect.left - editableBase.getBoundingClientRect().left) + 45;
            }
        }
    }

    @HostListener('document:keyup.escape', ['$event'])
    onFocusOut(event) {
        if ( this.isEditOnly || !this.isFocused ) {
            return;
        }

        if ( !this.hadOneFocusOut ) {
            this.hadOneFocusOut = true;
            setTimeout(() => {
                this.hadOneFocusOut = false;
            }, 500);
        } else {
            this.isFocused = false;
            this.hadOneFocusOut = false;
            this.currentContents = this.editingContents;
        }
    }

    documentCommand(cmd: string) {
        // Yes, technically this is deprecated, but it'll be poly-filled if necessary. Bite me.
        document.execCommand(cmd, false, '');
    }

    onContentsChanged(mutation: MutationRecord) {
        if ( this.applyingRemoteMutation ) {
            return;
        }

        if ( this.ignoreNextMutation ) {
            this.ignoreNextMutation = false;
            return;
        }

        const target = this.getPathToElement(mutation.target);
        console.log({mutation, target, type: mutation.type, data: mutation.target.textContent});

        const innerHTML = this.editable.nativeElement.innerHTML;

        if ( mutation.type === 'characterData' ) {
            this.contentsMutated.emit({
                path: target,
                type: mutation.type,
                data: mutation.target.textContent,
                fullContents: innerHTML,
            });
        } else if ( mutation.type === 'childList' ) {
            const addedNodes: Array<{previousSiblingPath: string, type: string, data: string}> = [];

            mutation.addedNodes.forEach(value => {
                const previousSiblingPath = this.getPathToElement(value.previousSibling);
                if ( previousSiblingPath ) {
                    addedNodes.push({
                        previousSiblingPath,
                        type: value.nodeName,
                        data: value.textContent,
                    });
                }
            });

            this.contentsMutated.emit({
                path: target,
                type: mutation.type,
                data: mutation.target.textContent,
                fullContents: innerHTML,
                addedNodes,
            });
        }

        if ( this.contents !== innerHTML ) {
            this.contents = innerHTML;
            this.contentsChanged.emit(innerHTML);
        }
    }

    applyRemoteContentMutation(mutation: MutationBroadcast) {
        this.applyingRemoteMutation = true;
        console.log('got remote content mutation', mutation);

        if ( this.editable ) {
            const target = this.getElementFromPath(mutation.path);
            console.log(target);
            if ( target ) {
                if ( mutation.type === 'characterData' ) {
                    this.ignoreNextMutation = true;
                    target.nodeValue = mutation.data;
                } else if ( mutation.type === 'childList' ) {
                    if ( Array.isArray(mutation.addedNodes) ) {
                        for ( const addedNode of mutation.addedNodes ) {
                            const previousSibling = this.getElementFromPath(addedNode.previousSiblingPath);
                            const elem = document.createElement(addedNode.type);
                            elem.nodeValue = addedNode.data;

                            this.ignoreNextMutation = true;
                            previousSibling.parentElement.insertBefore(elem, previousSibling.nextSibling);
                        }
                    }
                }
            }
        } else {
            if ( this.contents !== mutation.fullContents ) {
                this.ignoreNextMutation = true;
                this.contents = mutation.fullContents;
            }
        }

        this.applyingRemoteMutation = false;
    }

    @HostListener('document:keydown.tab', ['$event'])
    onIndent(event) {
        event.preventDefault();
        this.documentCommand('indent');
    }

    @HostListener('document:keydown.shift.tab', ['$event'])
    onOutdent(event) {
        event.preventDefault();
        this.documentCommand('outdent');
    }

    @HostListener('document:keydown.control.b', ['$event'])
    onBold(event) {
        event.preventDefault();
        this.documentCommand('bold');
    }

    @HostListener('document:keydown.control.i', ['$event'])
    onItalic(event) {
        event.preventDefault();
        this.documentCommand('italic');
    }

    @HostListener('document:keydown.control.u', ['$event'])
    onUnderline(event) {
        event.preventDefault();
        this.documentCommand('underline');
    }

    @HostListener('document:keydown.control.z', ['$event'])
    onUndo(event) {
        event.preventDefault();
        this.documentCommand('undo');
    }

    @HostListener('document:keydown.control.shift.z', ['$event'])
    onRedo(event) {
        event.preventDefault();
        this.documentCommand('redo');
    }

    getContrastYIQ(hexcolor) {
        hexcolor = hexcolor.replace('#', '');
        const r = parseInt(hexcolor.substr(0, 2), 16);
        const g = parseInt(hexcolor.substr(2, 2), 16);
        const b = parseInt(hexcolor.substr(4, 2), 16);
        const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
        return (yiq >= 128) ? 'black' : 'white';
    }
}
