import { classMixin, mergeData, findNextGreater, findNextLesser, isEven, findArrayNumberByUniqueID, trace } from "../core/Util"
import Events from "../core/Events"
import { DOMMixins } from "../dom/DOMMixins"
import { DOMEvent } from "../dom/DOMEvent"
import * as DOM from "../dom/DOM"
import { easeInOutQuint } from "../animation/Ease";
import { TimeScale } from "./TimeScale"
import { TimeGroup } from "./TimeGroup"
import { TimeEra } from "./TimeEra"
import { TimeAxis } from "./TimeAxis"
import { TimeMarker } from "./TimeMarker"
import Swipable from "../ui/Swipable"
import { Animate } from "../animation/Animate"
import { I18NMixins } from "../language/I18NMixins"



export class TimeNav {

    constructor(elem, timeline_config, options, language) {
        this.language = language
            // DOM ELEMENTS
        this._el = {
            parent: {},
            container: {},
            slider: {},
            slider_background: {},
            line: {},
            marker_container_mask: {},
            marker_container: {},
            marker_item_container: {},
            timeaxis: {},
            timeaxis_background: {}
        };

        this.collapsed = false;

        if (typeof elem === 'object') {
            this._el.container = elem;
        } else {
            this._el.container = DOM.get(elem);
        }
        this._el.container.setAttribute('tabindex', '0');

        // 'application' role supports predictable control of keyboard input in a complex component
        this._el.container.setAttribute('role', 'application');
        this._el.container.setAttribute('aria-label', this._('aria_label_timeline_navigation'));
        this._el.container.setAttribute('aria-description',
            'Navigate between markers with arrow keys. Press "Home" for the first and "End" for the last markers'
        );

        this.config = timeline_config;

        //Options
        this.options = {
            width: 600,
            height: 600,
            duration: 1000,
            ease: easeInOutQuint,
            has_groups: false,
            optimal_tick_width: 50,
            scale_factor: 2, // How many screen widths wide should the timeline be
            marker_padding: 5,
            timenav_height_min: 150, // Minimum timenav height
            marker_height_min: 30, // Minimum Marker Height
            marker_width_min: 100, // Minimum Marker Width
            zoom_sequence: [0.5, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] // Array of Fibonacci numbers for TimeNav zoom levels http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibtable.html
        };

        // Animation
        this.animator = null;

        // Ready state
        this.ready = false;

        // Markers Array
        this._markers = [];

        // Eras Array
        this._eras = [];
        this.has_eras = false;

        /**
         * @type TimeGroup
         */
        this._groups = [];

        // Row Height
        this._calculated_row_height = 100;

        // Current Marker
        this.current_id = "";

        // Current Focused Marker
        this.current_focused_id = "";

        // TimeScale
        this.timescale = {};

        // TimeAxis
        this.timeaxis = {};

        // Max Rows
        this.max_rows = 6;

        // Animate CSS
        this.animate_css = false;

        // Swipe Object
        this._swipable;

        // Merge Data and Options
        mergeData(this.options, options);

    }

    init() {
        this._initLayout();
        this._initEvents();
        this._initData();
        this.updateDisplay();

        this._onLoaded();
    }

    /*	Public
    ================================================== */
    positionMarkers(fast) {
        // POSITION X
        for (var i = 0; i < this._markers.length; i++) {
            var pos = this.timescale.getPositionInfo(i);
            if (fast) {
                this._markers[i].setClass("tl-timemarker tl-timemarker-fast");
            } else {
                this._markers[i].setClass("tl-timemarker");
            }
            this._markers[i].setPosition({ left: pos.start });
            this._markers[i].setWidth(pos.width);
        };
    }

    /*	Update Display
    ================================================== */
    updateDisplay(width, height, animate) {
        let reposition_markers = false;
        if (width) {
            if (this.options.width == 0 && width > 0) {
                reposition_markers = true;
            }
            this.options.width = width;
        }
        if (height && height != this.options.height) {
            this.options.height = height;
            this.timescale = this._getTimeScale();
        }

        // Size Markers
        this._assignRowsToMarkers();

        // Size swipable area
        this._el.slider_background.style.width = this.timescale.getPixelWidth() + this.options.width + "px";
        this._el.slider_background.style.left = -(this.options.width / 2) + "px";
        this._el.slider.style.width = this.timescale.getPixelWidth() + this.options.width + "px";

        // Update Swipable constraint
        this._swipable.updateConstraint({ top: false, bottom: false, left: (this.options.width / 2), right: -(this.timescale.getPixelWidth() - (this.options.width / 2)) });

        if (reposition_markers) {
            this._drawTimeline()
        }
        // Go to the current slide
        this.goToId(this.current_id, true);
    }


    /*	TimeScale
    ================================================== */
    _getTimeScale() {
        /* maybe the establishing config values (marker_height_min and max_rows) should be
        separated from making a TimeScale object, which happens in another spot in this file with duplicate mapping of properties of this TimeNav into the TimeScale options object? */
        // Set Max Rows
        var marker_height_min = 0;
        try {
            marker_height_min = parseInt(this.options.marker_height_min);
        } catch (e) {
            trace("Invalid value for marker_height_min option.");
            marker_height_min = 30;
        }
        if (marker_height_min == 0) {
            trace("marker_height_min option must not be zero.")
            marker_height_min = 30;
        }
        this.max_rows = Math.round((this.options.height - this._el.timeaxis_background.offsetHeight - (this.options.marker_padding)) / marker_height_min);
        if (this.max_rows < 1) {
            this.max_rows = 1;
        }
        return new TimeScale(this.config, {
            display_width: this._el.container.offsetWidth,
            screen_multiplier: this.options.scale_factor,
            max_rows: this.max_rows

        });
    }

    _updateTimeScale(new_scale) {
        this.options.scale_factor = new_scale;
        this._updateDrawTimeline();
    }

    zoomIn() { // move the the next "higher" scale factor
        var new_scale = findNextGreater(this.options.zoom_sequence, this.options.scale_factor);
        this.setZoomFactor(new_scale);
    }

    zoomOut() { // move the the next "lower" scale factor
        var new_scale = findNextLesser(this.options.zoom_sequence, this.options.scale_factor);
        this.setZoomFactor(new_scale);
    }

    setZoom(level) {
        var zoom_factor = this.options.zoom_sequence[level];
        if (typeof(zoom_factor) == 'number') {
            this.setZoomFactor(zoom_factor);
        } else {
            console.warn("Invalid zoom level. Please use an index number between 0 and " + (this.options.zoom_sequence.length - 1));
        }
    }

    setZoomFactor(factor) {
        if (factor <= this.options.zoom_sequence[0]) {
            this.fire("zoomtoggle", { zoom: "out", show: false });
        } else {
            this.fire("zoomtoggle", { zoom: "out", show: true });
        }

        if (factor >= this.options.zoom_sequence[this.options.zoom_sequence.length - 1]) {
            this.fire("zoomtoggle", { zoom: "in", show: false });
        } else {
            this.fire("zoomtoggle", { zoom: "in", show: true });
        }

        if (factor == 0) {
            console.warn("Zoom factor must be greater than zero. Using 0.1");
            factor = 0.1;
        }
        this.options.scale_factor = factor;
        //this._updateDrawTimeline(true);
        this.goToId(this.current_id, !this._updateDrawTimeline(true), true);
    }

    /*	Groups
    ================================================== */
    _createGroups() {
        this._groups = [];
        var group_labels = this.timescale.getGroupLabels();

        if (group_labels) {
            this.options.has_groups = true;
            for (var i = 0; i < group_labels.length; i++) {
                this._createGroup(group_labels[i]);
            }
        }

    }

    _createGroup(group_label) {
        var group = new TimeGroup(group_label);
        this._addGroup(group);
        this._groups.push(group);
    }

    _addGroup(group) {
        group.addTo(this._el.container);

    }

    _positionGroups() {
        if (this.options.has_groups) {
            var available_height = (this.options.height - this._el.timeaxis_background.offsetHeight),
                group_height = Math.floor((available_height / this.timescale.getNumberOfRows()) - this.options.marker_padding),
                group_labels = this.timescale.getGroupLabels();

            for (var i = 0, group_rows = 0; i < this._groups.length; i++) {
                var group_y = Math.floor(group_rows * (group_height + this.options.marker_padding));
                var group_hide = false;
                if (group_y > (available_height - this.options.marker_padding)) {
                    group_hide = true;
                }

                this._groups[i].setRowPosition(group_y, this._calculated_row_height + this.options.marker_padding / 2);
                this._groups[i].setAlternateRowColor(isEven(i), group_hide);

                group_rows += this._groups[i].data.rows; // account for groups spanning multiple rows
            }
        }
    }

    /*	Markers
    ================================================== */
    _addMarker(marker) {
        marker.addTo(this._el.marker_item_container);
        marker.on('markerclick', this._onMarkerClick, this);
        marker.on('added', this._onMarkerAdded, this);
    }

    _createMarker(data, n) {
        var marker = new TimeMarker(data, this.options);
        this._addMarker(marker);
        if (n < 0) {
            this._markers.push(marker);
        } else {
            this._markers.splice(n, 0, marker);
        }
    }

    _createMarkers(array) {
        for (var i = 0; i < array.length; i++) {
            this._createMarker(array[i], -1);
        }
    }

    _removeMarker(marker) {
        marker.removeFrom(this._el.marker_item_container);
        //marker.off('added', this._onMarkerRemoved, this);
    }

    _destroyMarker(n) {
        this._removeMarker(this._markers[n]);
        this._markers.splice(n, 1);
    }

    _calculateMarkerHeight(h) {
        return ((h / this.timescale.getNumberOfRows()) - this.options.marker_padding);
    }

    _calculateRowHeight(h) {
        return (h / this.timescale.getNumberOfRows());
    }

    _calculateAvailableHeight() {
        return (this.options.height - this._el.timeaxis_background.offsetHeight - (this.options.marker_padding));
    }

    _calculateMinimumTimeNavHeight() {
        return (this.timescale.getNumberOfRows() * this.options.marker_height_min) + this._el.timeaxis_background.offsetHeight + (this.options.marker_padding);

    }

    getMinimumHeight() {
        return this._calculateMinimumTimeNavHeight();
    }

    _assignRowsToMarkers() {
        var available_height = this._calculateAvailableHeight(),
            marker_height = this._calculateMarkerHeight(available_height);


        this._positionGroups();

        this._calculated_row_height = this._calculateRowHeight(available_height);

        for (var i = 0; i < this._markers.length; i++) {

            // Set Height
            this._markers[i].setHeight(marker_height);

            //Position by Row
            var row = this.timescale.getPositionInfo(i).row;

            var marker_y = Math.floor(row * (marker_height + this.options.marker_padding)) + this.options.marker_padding;

            var remainder_height = available_height - marker_y + this.options.marker_padding;
            this._markers[i].setRowPosition(marker_y, remainder_height);
        };

    }

    _resetMarkersActive() {
        for (var i = 0; i < this._markers.length; i++) {
            this._markers[i].setActive(false);
        }
    }

    _resetMarkersBlurListeners() {
        for (var i = 0; i < this._markers.length; i++) {
            this._markers[i].off('markerblur', this._onMarkerBlur, this);
        }
    }

    _findMarkerIndex(n) {
        var _n = -1;
        if (typeof n == 'string' || n instanceof String) {
            _n = findArrayNumberByUniqueID(n, this._markers, "unique_id", _n);
        }
        return _n;
    }

    /*	ERAS
    ================================================== */
    _createEras(array) {
        for (var i = 0; i < array.length; i++) {
            var data = array[i];
            var era = new TimeEra(data.start_date,
                data.end_date,
                data.headline,
                this.options);
            this._eras.push(era);
            era.addTo(this._el.marker_item_container);
            era.on('added', this._onEraAdded, this);
        }
    }

    _positionEras(fast) {

        var era_color = 0;
        // POSITION X
        for (var i = 0; i < this._eras.length; i++) {
            var pos = {
                start: 0,
                end: 0,
                width: 0
            };

            pos.start = this.timescale.getPosition(this._eras[i].start_date.getTime());
            pos.end = this.timescale.getPosition(this._eras[i].end_date.getTime());
            pos.width = pos.end - pos.start;

            if (fast) {
                this._eras[i].setClass("tl-timeera tl-timeera-fast");
            } else {
                this._eras[i].setClass("tl-timeera");
            }
            this._eras[i].setPosition({ left: pos.start });
            this._eras[i].setWidth(pos.width);

            era_color++;
            if (era_color > 5) {
                era_color = 0;
            }
            this._eras[i].setColor(era_color);
        };

    }

    /*	Public
    ================================================== */

    // Create a marker
    createMarker(d, n) {
        this._createMarker(d, n);
    }

    // Create many markers from an array
    createMarkers(array) {
        this._createMarkers(array);
    }

    // Destroy marker by index
    destroyMarker(n) {
        this._destroyMarker(n);
    }

    // Destroy marker by id
    destroyMarkerId(id) {
        this.destroyMarker(this._findMarkerIndex(id));
    }

    /*	Navigation
    ================================================== */
    goTo(n, fast, css_animation) {
        var self = this,
            _ease = this.options.ease,
            _duration = this.options.duration,
            _n = (n < 0) ? 0 : n;

        // Set Marker active state
        this._resetMarkersActive();
        if (n >= 0 && n < this._markers.length) {
            this._markers[n].setActive(true);
        }

        this.animateMovement(_n, fast, css_animation, _duration, _ease);

        if (n >= 0 && n < this._markers.length) {
            this.current_id = this.current_focused_id = this._markers[n].data.unique_id;
        } else {
            this.current_id = this.current_focused_id = '';
        }

        this._setLabelWithCurrentMarker();
    }

    goToId(id, fast, css_animation) {
        this.goTo(this._findMarkerIndex(id), fast, css_animation);
    }

    focusOn(n, fast, css_animation) {
        const _ease = this.options.ease,
            _duration = this.options.duration,
            _n = (n < 0) ? 0 : n;

        this.animateMovement(_n, fast, css_animation, _duration, _ease);

        this._resetMarkersBlurListeners();
        if (n >= 0 && n < this._markers.length) {
            this._markers[n].setFocus();
            this.current_focused_id = this._markers[n].data.unique_id;
            this._markers[n].on('markerblur', this._onMarkerBlur, this);
        }
    }

    focusNext() {
        const n = this._findMarkerIndex(this.current_focused_id);
        if ((n + 1) < this._markers.length) {
            this.focusOn(n + 1);
        } else {
            this.focusOn(n);
        }
    }

    focusPrevious() {
        const n = this._findMarkerIndex(this.current_focused_id);
        if (n - 1 >= 0) {
            this.focusOn(n - 1);
        } else {
            this.focusOn(n);
        }
    }

    animateMovement(n, fast, css_animation, duration, ease) {
        // Stop animation
        if (this.animator) {
            this.animator.stop();
        }

        if (fast) {
            this._el.slider.className = "tl-timenav-slider";
            this._el.slider.style.left = -this._markers[n].getLeft() +
                (this.options.width / 2) + "px";
        } else {
            if (css_animation) {
                this._el.slider.className = "tl-timenav-slider tl-timenav-slider-animate";
                this.animate_css = true;
                this._el.slider.style.left = -this._markers[n].getLeft() +
                    (this.options.width / 2) + "px";
            } else {
                this._el.slider.className = "tl-timenav-slider";
                this.animator = Animate(this._el.slider, {
                    left: -this._markers[n].getLeft() +
                        (this.options.width / 2) + "px",
                    duration: duration,
                    easing: ease
                });
            }
        }

        if (n >= 0 && n < this._markers.length) {
            this.current_id = this._markers[n].data.unique_id;
        } else {
            this.current_id = '';
        }

        this._dispatchVisibleTicksChange();
    }

    goToId(id, fast, css_animation) {
        this.goTo(this._findMarkerIndex(id), fast, css_animation);
    }

    _dispatchVisibleTicksChange() {
        /**
         * The timeout is required to wait till the end of the animation
         * and repositioning of the ticks on the screen
         */
        if (this.ticks_change_timeout) {
            clearTimeout(this.ticks_change_timeout);
            this.ticks_change_timeout = null;
        }
        this.ticks_change_timeout = setTimeout(() => {
            const visible_ticks = this.timeaxis.getVisibleTicks();
            this.fire("visible_ticks_change", { visible_ticks });
        }, this.options.duration);
    }

    /*	Events
    ================================================== */
    _onLoaded() {
        this.ready = true;
        this.fire("loaded", this.config);
    }

    _onMarkerAdded(e) {
        this.fire("dateAdded", this.config);
    }

    _onEraAdded(e) {
        this.fire("eraAdded", this.config);
    }

    _onMarkerRemoved(e) {
        this.fire("dateRemoved", this.config);
    }

    _onMarkerClick(e) {
        // Go to the clicked marker
        this.goToId(e.unique_id);
        this.fire("change", { unique_id: e.unique_id });
    }

    _onMarkerBlur(e) {
        // Reset the focused marked to the active marker after it lost the focus
        if (this.current_focused_id === this.current_id) return;
        this.focusOn(this._findMarkerIndex(this.current_id));
    }

    _onMouseScroll(e) {

        var delta = 0,
            scroll_to = 0,
            constraint = {
                right: -(this.timescale.getPixelWidth() - (this.options.width / 2)),
                left: this.options.width / 2
            };
        if (!e) {
            e = window.event;
        }
        if (e.originalEvent) {
            e = e.originalEvent;
        }

        // Webkit and browsers able to differntiate between up/down and left/right scrolling
        if (typeof e.wheelDeltaX != 'undefined') {
            delta = e.wheelDeltaY / 6;
            if (Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY)) {
                delta = e.wheelDeltaX / 6;
            } else {
                //delta = e.wheelDeltaY/6;
                delta = 0;
            }
        }
        if (delta) {
            if (e.preventDefault) {
                e.preventDefault();
            }
            e.returnValue = false;
        }
        // Stop from scrolling too far
        scroll_to = parseInt(this._el.slider.style.left.replace("px", "")) + delta;


        if (scroll_to > constraint.left) {
            scroll_to = constraint.left;
        } else if (scroll_to < constraint.right) {
            scroll_to = constraint.right;
        }

        if (this.animate_css) {
            this._el.slider.className = "tl-timenav-slider";
            this.animate_css = false;
        }

        this._el.slider.style.left = scroll_to + "px";

    }

    _onDragMove(e) {
        if (this.animate_css) {
            this._el.slider.className = "tl-timenav-slider";
            this.animate_css = false;
        }

    }

    _onKeydown(e) {
        DOMEvent.stopPropagation(e);

        switch (e.key) {
            case "ArrowUp":
            case "ArrowRight":
                {
                    this.focusNext();
                    break;
                }
            case "ArrowDown":
            case "ArrowLeft":
                {
                    this.focusPrevious();
                    break;
                }
            case "Home":
                {
                    this.focusOn(0);
                    break;
                }
            case "End":
                {
                    this.focusOn(this._markers.length - 1);
                    break;
                }
        }
    }

    /*	Private Methods
    ================================================== */

    _drawTimeline(fast) {
        this.timescale = this._getTimeScale();
        this.timeaxis.drawTicks(this.timescale, this.options.optimal_tick_width);
        this.positionMarkers(fast);
        this._assignRowsToMarkers();
        this._createGroups();
        this._positionGroups();

        if (this.has_eras) {

            this._positionEras(fast);
        }
    }

    _updateDrawTimeline(check_update) {
        var do_update = false;

        // Check to see if redraw is needed
        if (check_update) {
            /* keep this aligned with _getTimeScale or reduce code duplication */
            var temp_timescale = new TimeScale(this.config, {
                display_width: this._el.container.offsetWidth,
                screen_multiplier: this.options.scale_factor,
                max_rows: this.max_rows

            });

            if (this.timescale.getMajorScale() == temp_timescale.getMajorScale() &&
                this.timescale.getMinorScale() == temp_timescale.getMinorScale()) {
                do_update = true;
            }
        } else {
            do_update = true;
        }

        // Perform update or redraw
        if (do_update) {
            this.timescale = this._getTimeScale();
            this.timeaxis.positionTicks(this.timescale, this.options.optimal_tick_width);
            this.positionMarkers();
            this._assignRowsToMarkers();
            this._positionGroups();
            if (this.has_eras) {
                this._positionEras();
            }
            this.updateDisplay();
        } else {
            this._drawTimeline(true);
        }

        return do_update;

    }

    _setLabelWithCurrentMarker() {
        const currentMarker = this._markers[this._findMarkerIndex(this.current_focused_id)];
        const currentMarkerText = currentMarker && currentMarker.ariaLabel ?
            `, ${currentMarker.ariaLabel}, shown` :
            '';
        this._el.container.setAttribute('aria-label', `Timeline navigation ${currentMarkerText}`);
    }

    /*	Init
    ================================================== */
    _initLayout() {
        // Create Layout
        this._el.line = DOM.create('div', 'tl-timenav-line', this._el.container);
        this._el.slider = DOM.create('div', 'tl-timenav-slider', this._el.container);
        this._el.slider_background = DOM.create('div', 'tl-timenav-slider-background', this._el.slider);
        this._el.marker_container_mask = DOM.create('div', 'tl-timenav-container-mask', this._el.slider);
        this._el.marker_container = DOM.create('div', 'tl-timenav-container', this._el.marker_container_mask);
        this._el.marker_item_container = DOM.create('div', 'tl-timenav-item-container', this._el.marker_container);
        this._el.timeaxis = DOM.create('div', 'tl-timeaxis', this._el.slider);
        this._el.timeaxis_background = DOM.create('div', 'tl-timeaxis-background', this._el.container);

        // Time Axis
        this.timeaxis = new TimeAxis(this._el.timeaxis, this.options, this.language);

        // Swipable
        this._swipable = new Swipable(this._el.slider_background, this._el.slider, {
            enable: { x: true, y: false },
            constraint: { top: false, bottom: false, left: (this.options.width / 2), right: false },
            snap: false
        });
        this._swipable.enable();

    }

    _initEvents() {
        // Drag Events
        this._swipable.on('dragmove', this._onDragMove, this);

        // Scroll Events
        DOMEvent.addListener(this._el.container, 'mousewheel', this._onMouseScroll, this);
        DOMEvent.addListener(this._el.container, 'DOMMouseScroll', this._onMouseScroll, this);
        DOMEvent.addListener(this._el.container, 'keydown', this._onKeydown, this);
    }

    _initData() {
        // Create Markers and then add them
        this._createMarkers(this.config.events);

        if (this.config.eras && this.config.eras.length > 0) {
            this.has_eras = true;
            this._createEras(this.config.eras);
        }

        this._drawTimeline();

    }
}

classMixin(TimeNav, Events, DOMMixins, I18NMixins)