import Component from '@ember/component';
import {computed} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {run} from '@ember/runloop';
import {task, timeout} from 'ember-concurrency';

// initially rendered offscreen with opacity 0 so that sizing is available
// shown when passed in an uncollapsed selected range
// display is delayed until the mouse button is lifted
// positioned so that it's always fully within the editor container
// animation occurs via CSS transitions
// position is kept after hiding, it's made inoperable by CSS pointer-events

// pixels that should be added to separate toolbar from positioning rect
export const TOOLBAR_MARGIN = 15;

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

export default Component.extend({
    attributeBindings: ['style'],
    classNames: ['absolute', 'z-999'],

    // public attrs
    basicOnly: false,
    editor: null,
    editorRange: null,
    activeMarkupTagNames: null,
    activeSectionTagNames: null,

    // internal properties
    showToolbar: false,
    top: null,
    left: -1000,
    right: null,

    // private properties
    _isMouseDown: false,
    _hasSelectedRange: false,
    _onMousedownHandler: null,
    _onMousemoveHandler: null,
    _onMouseupHandler: null,
    _onResizeHandler: null,

    // closure actions
    toggleMarkup() {},
    toggleSection() {},
    toggleHeaderSection() {},
    editLink() {},

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

    style: computed('showToolbar', '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`;
            }
        });

        // ensure hidden toolbar is non-interactive
        if (this.showToolbar) {
            styles.push('pointer-events: auto !important');
        } else {
            styles.push('pointer-events: none !important');
        }

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

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

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

        // track mousedown/mouseup on the window so that we're sure to get the
        // events even when they start outside of this component or end outside
        // the window
        this._onMousedownHandler = run.bind(this, this._handleMousedown);
        window.addEventListener('mousedown', this._onMousedownHandler);
        this._onMouseupHandler = run.bind(this, this._handleMouseup);
        window.addEventListener('mouseup', this._onMouseupHandler);
        this._onResizeHandler = run.bind(this, this._handleResize);
        window.addEventListener('resize', this._onResizeHandler);
    },

    didReceiveAttrs() {
        this._super(...arguments);
        let range = this.editorRange;

        if (range && !range.isCollapsed) {
            this._hasSelectedRange = true;
        } else {
            this._hasSelectedRange = false;
        }

        if ((this._hasSelectedRange && !this.showToolbar) || (!this._hasSelectedRange && this.showToolbar)) {
            this._toggleVisibility.perform();
        }
    },

    willDestroyElement() {
        this._super(...arguments);
        this._removeStyleElement();
        run.cancel(this._throttleResize);
        window.removeEventListener('mousedown', this._onMousedownHandler);
        window.removeEventListener('mousemove', this._onMousemoveHandler);
        window.removeEventListener('mouseup', this._onMouseupHandler);
        window.removeEventListener('resize', this._onResizeHandler);
    },

    actions: {
        toggleMarkup(markupName) {
            if (markupName === 'em' && this.activeMarkupTagNames.isI) {
                markupName = 'i';
            }

            this.toggleMarkup(markupName);
        },

        toggleSection(sectionName) {
            let range = this.editorRange;
            this.editor.run((postEditor) => {
                this.toggleSection(sectionName, postEditor);
                postEditor.setRange(range);
            });
        },

        toggleHeaderSection(headingTagName) {
            let range = this.editorRange;
            this.editor.run((postEditor) => {
                this.toggleHeaderSection(headingTagName, postEditor, {force: true});
                postEditor.setRange(range);
            });
        },

        editLink() {
            this.editLink(this.editorRange);
        }
    },

    /* private methods ------------------------------------------------------ */

    _toggleVisibility: task(function* (skipMousemove = false) {
        // double-taps will often trigger before the selection change event so
        // we want to keep the truthy mousemove skip around so that re-triggers
        // within the 50ms timeout do not reset it
        if (skipMousemove) {
            this._skipMousemove = true;
        }

        // debounce for 50ms to account for "click to deselect" otherwise we
        // run twice and the fade out animation jumps position
        yield timeout(50);

        // return early if the editorRange hasn't changed, this prevents
        // re-rendering unnecessarily which can cause minor position jumps when
        // styles are toggled because getBoundingClientRect on getSelection
        // changes slightly depending on the style of selected text
        if (this._hasSelectedRange && this.editorRange === this._lastRange) {
            return;
        }

        // if we have a range, show the toolbnar once the mouse is lifted
        if (this._hasSelectedRange && !this._isMouseDown) {
            this._showToolbar(this._skipMousemove);
        } else {
            this._hideToolbar();
        }

        this._skipMousemove = false;
    }).restartable(),

    _handleMousedown(event) {
        // we only care about the left mouse button
        if (event.which === 1) {
            this._isMouseDown = true;
            // prevent mousedown on toolbar buttons losing editor focus before the following click event can trigger the buttons behaviour
            if (this.element.contains(event.target)) {
                event.preventDefault();
            }
        }
    },

    _handleMousemove() {
        if (this._hasSelectedRange && !this.showToolbar) {
            this.set('showToolbar', true);
        }

        this._removeMousemoveHandler();
    },

    _removeMousemoveHandler() {
        window.removeEventListener('mousemove', this._onMousemoveHandler);
        this._onMousemoveHandler = null;
    },

    _handleMouseup(event) {
        if (event.which === 1) {
            this._isMouseDown = false;
            // we want to skip the mousemove handler here because we know the
            // selection (if there was one) was via the mouse and we don't want
            // to wait for another mousemove before showing the toolbar
            this._toggleVisibility.perform(true);
        }
    },

    _handleResize() {
        if (this.showToolbar) {
            this._throttleResize = run.throttle(this, this._positionToolbar, 100);
        }
    },

    _showToolbar(skipMousemove) {
        this._positionToolbar();

        if (skipMousemove) {
            this.set('showToolbar', true);
        }

        if (!this.showToolbar && !this._onMousemoveHandler) {
            this._onMousemoveHandler = run.bind(this, this._handleMousemove);
            window.addEventListener('mousemove', this._onMousemoveHandler);
        }

        // track displayed range so that we don't re-position unnecessarily
        this._lastRange = this.editorRange;
    },

    _hideToolbar() {
        if (!this.isDestroyed || !this.isDestroying) {
            this.set('showToolbar', false);
        }
        this._lastRange = null;
        this._removeMousemoveHandler();
    },

    _positionToolbar() {
        let containerRect = this.element.offsetParent.getBoundingClientRect();
        let range = window.getSelection().getRangeAt(0);
        let rangeRect = range.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(tickPosition);
        }

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

    _addStyleElement(tickPosition) {
        let beforeStyle = `left: calc(${tickPosition}% - ${TICK_ADJUSTMENT + 2}px);`;
        let afterStyle = `left: calc(${tickPosition}% - ${TICK_ADJUSTMENT}px);`;
        let styleElement = document.createElement('style');
        styleElement.id = `${this.elementId}-style`;
        styleElement.innerHTML = `
            #${this.elementId} > ul:before { ${beforeStyle} }
            #${this.elementId} > ul:after { ${afterStyle} }
        `;
        document.head.appendChild(styleElement);
    },

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