import Component from '@ember/component';
import layout from '../templates/components/koenig-link-input';
import {TOOLBAR_MARGIN} from './koenig-toolbar';
import {computed} from '@ember/object';
import {getLinkMarkupFromRange} from '../utils/markup-utils';
import {htmlSafe} from '@ember/string';
import {run} from '@ember/runloop';

// pixels that should be added to the `left` property of the tick adjustment styles
// TODO: handle via CSS?
const TICK_ADJUSTMENT = 8;

// TODO: move to a util
function getScrollParent(node) {
    const isElement = node instanceof HTMLElement;
    const overflowY = isElement && window.getComputedStyle(node).overflowY;
    const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';

    if (!node) {
        return null;
    } else if (isScrollable && node.scrollHeight >= node.clientHeight) {
        return node;
    }

    return getScrollParent(node.parentNode) || document.body;
}

export default Component.extend({
    layout,

    attributeBindings: ['style'],
    classNames: ['kg-input-bar', 'absolute', 'z-999'],

    // public attrs
    editor: null,
    linkRange: null,
    selectedRange: null,

    // internal properties
    top: null,
    left: null,
    right: null,
    href: '',

    // private properties
    _selectedRange: null,
    _windowRange: null,
    _onMousedownHandler: null,
    _onMouseupHandler: null,

    // closure actions
    cancel() {},

    /* computed properties -------------------------------------------------- */

    style: computed('top', 'left', 'right', function () {
        let position = this.getProperties('top', 'left', 'right');
        let styles = Object.keys(position).map((style) => {
            if (position[style] !== null) {
                return `${style}: ${position[style]}px`;
            }
        });

        return htmlSafe(styles.compact().join('; '));
    }),

    /* lifecycle hooks ------------------------------------------------------ */

    init() {
        this._super(...arguments);

        // record the range now because the property is bound and will update
        // as we make changes whilst calculating the link position
        this._selectedRange = this.selectedRange;
        this._linkRange = this.linkRange;

        // grab a window range so that we can use getBoundingClientRect. Using
        // document.createRange is more efficient than doing editor.setRange
        // because it doesn't trigger all of the selection changing side-effects
        // TODO: extract MobiledocRange->NativeRange into a util
        let editor = this.editor;
        let cursor = editor.cursor;
        let {head, tail} = this._linkRange;
        let {node: headNode, offset: headOffset} = cursor._findNodeForPosition(head);
        let {node: tailNode, offset: tailOffset} = cursor._findNodeForPosition(tail);
        let range = document.createRange();
        range.setStart(headNode, headOffset);
        range.setEnd(tailNode, tailOffset);
        this._windowRange = range;

        // wait until rendered to position so that we have access to this.element
        run.schedule('afterRender', this, function () {
            this._positionToolbar();
            this._focusInput();
        });

        // grab an existing href value if there is one
        this._getHrefFromMarkup();

        // watch the window for mousedown events so that we can close the menu
        // when we detect a click outside
        this._onMousedownHandler = run.bind(this, this._handleMousedown);
        window.addEventListener('mousedown', this._onMousedownHandler);

        // watch for keydown events so that we can close the menu on Escape
        this._onKeydownHandler = run.bind(this, this._handleKeydown);
        window.addEventListener('keydown', this._onKeydownHandler);
    },

    willDestroyElement() {
        this._super(...arguments);
        this._removeStyleElement();
        window.removeEventListener('mousedown', this._onMousedownHandler);
        window.removeEventListener('keydown', this._onKeydownHandler);
    },

    actions: {
        inputKeydown(event) {
            if (event.key === 'Enter') {
                // prevent Enter from triggering in the editor and removing text
                event.preventDefault();

                let href = this.href;

                // create a single editor runloop here so that we don't get
                // separate remove and replace ops pushed onto the undo stack
                this.editor.run((postEditor) => {
                    if (href) {
                        this._replaceLink(href, postEditor);
                    } else {
                        this._removeLinks(postEditor);
                    }
                });

                this._cancelAndReselect();
            }
        },

        clear() {
            this.set('href', '');
            this._focusInput();
        }
    },

    // if we have a single link or a slice of a single link selected, grab the
    // href and adjust our linkRange to encompass the whole link
    _getHrefFromMarkup() {
        let linkMarkup = getLinkMarkupFromRange(this._linkRange);
        if (linkMarkup) {
            this.set('href', linkMarkup.attributes.href);
            this._linkRange = this._linkRange.expandByMarker(marker => !!marker.markups.includes(linkMarkup));
        }
    },

    _replaceLink(href, postEditor) {
        this._removeLinks(postEditor);
        let linkMarkup = postEditor.builder.createMarkup('a', {href});
        postEditor.toggleMarkup(linkMarkup, this._linkRange);
    },

    // loop over all markers that are touched by linkRange, removing any 'a'
    // markups on them to clear all links
    _removeLinks(postEditor) {
        let {headMarker, tailMarker} = this.linkRange;
        let curMarker = headMarker;

        while (curMarker && curMarker !== tailMarker.next) {
            curMarker.markups.filterBy('tagName', 'a').forEach((markup) => {
                curMarker.removeMarkup(markup);
                postEditor._markDirty(curMarker);
            });
            curMarker = curMarker.next;
        }
    },

    _cancelAndReselect() {
        this.cancel();
        if (this._selectedRange) {
            this.editor.selectRange(this._selectedRange);
        }
    },

    _focusInput() {
        let scrollParent = getScrollParent(this.element);
        let scrollTop = scrollParent.scrollTop;

        this.element.querySelector('input').focus();

        // reset the scroll position to avoid jumps
        // TODO: why does the input focus cause a scroll to the bottom of the doc?
        scrollParent.scrollTop = scrollTop;
    },

    // TODO: largely shared with {{koenig-toolbar}} code - extract to a shared util?
    _positionToolbar() {
        let containerRect = this.element.offsetParent.getBoundingClientRect();
        let rangeRect = this._windowRange.getBoundingClientRect();
        let {width, height} = this.element.getBoundingClientRect();
        let newPosition = {};

        // rangeRect is relative to the viewport so we need to subtract the
        // container measurements to get a position relative to the container
        newPosition = {
            top: rangeRect.top - containerRect.top - height - TOOLBAR_MARGIN,
            left: rangeRect.left - containerRect.left + rangeRect.width / 2 - width / 2,
            right: null
        };

        let tickPosition = 50;
        // don't overflow left boundary
        if (newPosition.left < 0) {
            newPosition.left = 0;

            // calculate the tick percentage position
            let absTickPosition = rangeRect.left - containerRect.left + rangeRect.width / 2;
            tickPosition = absTickPosition / width * 100;
            if (tickPosition < 5) {
                tickPosition = 5;
            }
        }
        // same for right boundary
        if (newPosition.left + width > containerRect.width) {
            newPosition.left = null;
            newPosition.right = 0;

            // calculate the tick percentage position
            let absTickPosition = rangeRect.right - containerRect.right - rangeRect.width / 2;
            tickPosition = 100 + absTickPosition / width * 100;
            if (tickPosition > 95) {
                tickPosition = 95;
            }
        }

        // the tick is a pseudo-element so we the only way we can affect it's
        // style is by adding a style element to the head
        this._removeStyleElement(); // reset to base styles
        if (tickPosition !== 50) {
            this._addStyleElement(`left: calc(${tickPosition}% - ${TICK_ADJUSTMENT}px)`);
        }

        // update the toolbar position
        this.setProperties(newPosition);
    },

    _addStyleElement(styles) {
        let styleElement = document.createElement('style');
        styleElement.id = `${this.elementId}-style`;
        styleElement.innerHTML = `#${this.elementId}:before, #${this.elementId}:after { ${styles} }`;
        document.head.appendChild(styleElement);
    },

    _removeStyleElement() {
        let styleElement = document.querySelector(`#${this.elementId}-style`);
        if (styleElement) {
            styleElement.remove();
        }
    },

    _handleMousedown(event) {
        if (!event.target.closest(`#${this.elementId}`)) {
            // no need to re-select for mouse clicks
            this.cancel();
        }
    },

    _handleKeydown(event) {
        if (event.key === 'Escape') {
            this._cancelAndReselect();
        }
    }
});
