/* eslint-disable */
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define([], factory) :
            (global.MomentumSlider = factory());
}(this, (function () {
    'use strict';
    /* eslint-enable */

    // fixes weird safari 10 bug where preventDefault is prevented
    // @see https://github.com/metafizzy/flickity/issues/457#issuecomment-254501356
    window.addEventListener('touchmove', function () {});

    function MomentumSlider(options) {
        this.o = extend({}, this.defaults, options);
        this.initHtml();
        this.initValues();
        this.initEvents();
        this.updateClassnames();
    }

    MomentumSlider.prototype = {
        defaults: {
            el: '.ms-container',
            cssClass: '',
            vertical: false,
            multiplier: 1,
            bounceCoefficient: 0.3,
            bounceMax: 100,
            loop: 0,
            interactive: true,
            reverse: false,
            currentIndex: 0
        },
        initHtml: function () {
            this.msContainer = is.str(this.o.el) ? document.querySelector(this.o.el) : this.o.el;
            if (this.o.range) {
                var html = '<div class="ms-container ' + this.o.cssClass + '"><ul class="ms-track">';
                for (var i = this.o.range[0]; i <= this.o.range[1]; i++) {
                    html += is.fnc(this.o.rangeContent) ? buildSlide(this.o.rangeContent(i)) : buildSlide(i);
                }
                html += '</ul></div>';
                var msHtml = document.createElement('div');
                msHtml.innerHTML = html;
                this.msContainer.appendChild(msHtml.firstChild);
                this.msContainer = this.msContainer.lastChild;
            }
            this.msContainer.classList.add('ms-container--' + (this.o.vertical ? 'vertical' : 'horizontal'));
            if (this.o.reverse) {
                this.msContainer.classList.add('ms-container--reverse');
            }
            this.msTrack = this.msContainer.children[0];
            this.msSlides = this.msTrack.children;
            this.step = this.o.vertical ? this.msSlides[0].scrollHeight : this.msSlides[0].scrollWidth;
            this.sliderLength = this.msSlides.length;
            if (this.o.loop) {
                var loopLength, slideIndex, fragment;
                // begin
                fragment = document.createDocumentFragment();
                loopLength = this.o.loop;
                slideIndex = this.sliderLength - loopLength;
                while (loopLength--) {
                    fragment.appendChild(this.msSlides[slideIndex++].cloneNode(true));
                }
                this.msTrack.insertBefore(fragment, this.msTrack.firstChild);
                // end
                fragment = document.createDocumentFragment();
                slideIndex = loopLength = this.o.loop;
                while (loopLength--) {
                    fragment.appendChild(this.msSlides[slideIndex++].cloneNode(true));
                }
                this.msTrack.appendChild(fragment);
                // update
                this.sliderLength += this.o.loop * 2;
            }
            this.sliderWidth = this.sliderLength * this.step;
        },
        initValues: function () {
            this.boundMin = this.o.reverse ? 0 : -this.step * (this.sliderLength - 1);
            this.boundMax = this.o.reverse ? this.step * (this.sliderLength - 1) : 0;
            this.targetPosition = this.targetPosition || 0;
            this.ticking = false;
            this.enabled = true;
            this.pointerActive = false;
            this.pointerMoved = false;
            this.trackingPoints = [];
            this.msTrack.style[this.o.vertical ? 'height' : 'width'] = this.sliderWidth + 'px';
            this.currentIndex = (this.currentIndex || this.o.currentIndex) + this.o.loop;
            this.updateSlider(undefined, true);
            this.renderTarget();
            var index = this.sliderLength;
            while (index--) {
                this.setStyle(index, this.currentIndex == index ? 0 : -1);
            }
        },
        initEvents: function () {
            if (this.o.interactive) {
                this.msContainer.addEventListener('touchstart', this.onDown.bind(this));
                this.msContainer.addEventListener('mousedown', this.onDown.bind(this));
                document.addEventListener('touchmove', this.onMove.bind(this), getPassiveSupported() ? {
                    passive: false
                } : false);
                document.addEventListener('touchend', this.onUp.bind(this));
                document.addEventListener('touchcancel', this.stopTracking.bind(this));
                document.addEventListener('mousemove', this.onMove.bind(this), getPassiveSupported() ? {
                    passive: false
                } : false);
                document.addEventListener('mouseup', this.onUp.bind(this));
                if (this.o.prevEl) {
                    this.prevEl = is.str(this.o.prevEl) ? document.querySelector(this.o.prevEl) : this.o.prevEl;
                    this.prevEl.addEventListener('click', this.prev.bind(this));
                }
                if (this.o.nextEl) {
                    this.nextEl = is.str(this.o.nextEl) ? document.querySelector(this.o.nextEl) : this.o.nextEl;
                    this.nextEl.addEventListener('click', this.next.bind(this));
                }
            }
            window.addEventListener('resize', this.onResize.bind(this));
        },
        prev: function () {
            if (this.enabled) {
                this.updateSlider(Math.round(this.targetPosition / this.step) * this.step + (this.o.reverse ? -this.step : this.step));
            }
        },
        next: function () {
            if (this.enabled) {
                this.updateSlider(Math.round(this.targetPosition / this.step) * this.step + (this.o.reverse ? this.step : -this.step));
            }
        },
        select: function (index) {
            if (this.enabled) {
                // this.currentIndex = index;
                // this.updateSlider();
                this.updateSlider((index + this.o.loop) * (this.o.reverse ? this.step : -this.step));
            }
        },
        setStyleToNode: function (node, style, diff, lower) {
            if (style) {
                var value = '';
                for (var property in style) {
                    if (property[0] == '.') {
                        this.setStyleToNode(node.querySelector(property), style[property], diff, lower);
                    } else if (property == 'transform') {
                        style[property].forEach(function(transform) {
                            for (var t in transform) {
                                value += t + '(' + getCurrentValue(transform[t], diff, lower);
                                if (t == 'rotate') {
                                    value += 'deg';
                                } else if (t == 'translateX' || t == 'translateY' || t == 'translateZ') {
                                    value += 'px';
                                }
                                value += ') ';
                            }
                        });
                    } else {
                        value = getCurrentValue(style[property], diff, lower);
                    }
                    node.style[property] = value;
                }
            }
        },
        setStyle: function (index, diff, lower) {
            this.setStyleToNode(this.msSlides[index], this.o.style, diff, lower);
            if (is.fnc(this.o.customStyles)) {
                this.o.customStyles(index, diff, lower);
            }
        },
        renderTarget: function () {
            if (this.o.sync) {
                var syncIndex = this.o.sync.length;
                var syncSlider;
                while (syncIndex--) {
                    syncSlider = this.o.sync[syncIndex];
                    syncSlider.targetPosition = (syncSlider.o.reverse ? -1 : 1) * this.targetPosition / this.sliderWidth * syncSlider.sliderWidth;
                    syncSlider.renderTarget();
                }
            }

            var paddingLength = this.o.loop * this.step;
            var contentLength = this.sliderWidth - (paddingLength * 2);
            if (this.o.loop) {
                if (-this.targetPosition < paddingLength) {
                    while (-this.targetPosition < paddingLength) {
                        this.targetPosition -= contentLength;
                    }
                } else if (-this.targetPosition >= paddingLength + contentLength) {
                    while (-this.targetPosition >= paddingLength + contentLength) {
                        this.targetPosition += contentLength;
                    }
                }
            }

            // var actualIndex = -this.targetPosition / this.step;
            var actualIndex = (this.o.reverse ? 1 : -1) * this.targetPosition / this.step;
            this.onChangeCurrentIndex(Math.round(actualIndex));
            var lowerIndex = Math.floor(actualIndex);
            var higherIndex = Math.ceil(actualIndex);
            var lowerDiff = actualIndex - lowerIndex;
            var higherDiff = actualIndex - higherIndex;

            if (!is.und(this.lowerIndex) && this.lowerIndex != lowerIndex && this.lowerIndex != higherIndex) {
                this.setStyle(this.lowerIndex, 1, true);
            }
            if (!is.und(this.higherIndex) && this.higherIndex != lowerIndex && this.higherIndex != higherIndex) {
                this.setStyle(this.higherIndex, -1);
            }

            if (lowerIndex >= 0 && lowerIndex < this.sliderLength) {
                this.setStyle(lowerIndex, lowerDiff, true);
                this.lowerIndex = lowerIndex;
            }
            if (higherIndex >= 0 && higherIndex < this.sliderLength) {
                this.setStyle(higherIndex, higherDiff);
                this.higherIndex = higherIndex;
            }

            var transformValue = 'translate' + (this.o.vertical ? 'Y' : 'X') + '(' + this.targetPosition + 'px)';
            this.msTrack.style[transformProperty] = transformValue;
        },
        onDown: function (ev) {
            if (this.enabled && !this.pointerActive) {
                var event = normalizeEvent(ev);
                this.pointerActive = true;
                this.pointerId = event.id;

                this.pointerLastX = this.pointerCurrentX = event.x;
                this.pointerLastY = this.pointerCurrentY = event.y;
                this.trackingPoints = [];
                this.addTrackingPoint(this.pointerLastX, this.pointerLastY);

                if (this.animateInstance) this.animateInstance.stop();
            }
        },
        onMove: function (ev) {
            if (this.enabled && this.pointerActive) {
                var event = normalizeEvent(ev);

                if (event.id === this.pointerId) {
                    this.pointerCurrentX = event.x;
                    this.pointerCurrentY = event.y;
                    
                    var shouldMoveSlider = this.pointerMoved
                    if (!this.pointerMoved) {
                        var movingVertically =
                            Math.abs(Math.abs(this.pointerCurrentX) - Math.abs(this.pointerLastX)) <
                            Math.abs(Math.abs(this.pointerCurrentY) - Math.abs(this.pointerLastY))
                        if (
                            (this.o.vertical && movingVertically) ||
                            (!this.o.vertical && !movingVertically)
                        ) {
                            shouldMoveSlider = true
                        }
                    }

                    if (shouldMoveSlider) {
                        ev.preventDefault();
                        this.pointerMoved = true;
                        this.addTrackingPoint(this.pointerLastX, this.pointerLastY);
                        this.requestTick();
                    } else {
                        this.stopTracking(-1);
                    }
                }
            }
        },
        onUp: function (ev) {
            if (this.enabled && this.pointerActive) {
                var event = normalizeEvent(ev);

                if (event.id === this.pointerId) {
                    var slide = ev.target;
                    if (this.msTrack.contains(slide)) {
                        while (!slide.matches('.ms-slide, .ms-track')) {
                            slide = slide.parentNode;
                        }
                    }
                    var index = Array.prototype.indexOf.call(this.msSlides, slide);
                    if (!this.pointerMoved) {
                        if (index !== -1) {
                            this.currentIndex = index;
                            this.updateSlider();
                        }
                    }
                    this.stopTracking(index);
                }
            }
        },
        onResize: function () {
            // this.initValues();
        },
        stopTracking: function (index) {
            this.pointerActive = false;
            if (this.pointerMoved || index === -1) {
                this.pointerMoved = false;
                this.addTrackingPoint(this.pointerLastX, this.pointerLastY);
                this.startDecelAnim();
            }
        },
        addTrackingPoint: function (x, y) {
            var time = Date.now();
            while (this.trackingPoints.length > 0) {
                if (time - this.trackingPoints[0].time <= 100) {
                    break;
                }
                this.trackingPoints.shift();
            }

            this.trackingPoints.push({
                x: x,
                y: y,
                time: time
            });
        },
        updateAndRender: function () {
            var pointerChange = this.o.vertical ? this.pointerCurrentY - this.pointerLastY : this.pointerCurrentX - this.pointerLastX;
            this.targetPosition += pointerChange * this.o.multiplier;

            if (this.o.bounceCoefficient) {
                var diff = this.checkBounds();
                if (diff !== 0) {
                    this.targetPosition -= pointerChange * dragOutOfBoundsMultiplier(diff) * this.o.multiplier;
                }
            } else {
                this.checkBounds(true);
            }

            this.renderTarget();

            this.pointerLastX = this.pointerCurrentX;
            this.pointerLastY = this.pointerCurrentY;
            this.ticking = false;
        },
        requestTick: function () {
            if (!this.ticking) {
                requestAnimationFrame(this.updateAndRender.bind(this));
            }
            this.ticking = true;
        },
        checkBounds: function (restrict) {
            var diff = 0;

            if (this.boundMin !== undefined && this.targetPosition < this.boundMin) {
                diff = this.boundMin - this.targetPosition;
            } else if (this.boundMax !== undefined && this.targetPosition > this.boundMax) {
                diff = this.boundMax - this.targetPosition;
            }

            if (restrict) {
                if (diff !== 0) {
                    this.targetPosition = diff > 0 ? this.boundMin : this.boundMax;
                }
            }

            return diff;
        },
        startDecelAnim: function () {
            var firstPoint = this.trackingPoints[0];
            var lastPoint = this.trackingPoints[this.trackingPoints.length - 1];

            var positionOffset = this.o.vertical ? lastPoint.y - firstPoint.y : lastPoint.x - firstPoint.x;
            var timeOffset = lastPoint.time - firstPoint.time;

            var D = timeOffset / 15 / this.o.multiplier;
            this.decVel = positionOffset / D || 0;

            var newTargetPosition = this.targetPosition + (this.decVel * 12);
            var newTargetPositionOffset = newTargetPosition % this.step;
            newTargetPosition = newTargetPosition - newTargetPositionOffset;
            if (Math.abs(newTargetPositionOffset) > this.step / 2) {
                newTargetPosition += (newTargetPositionOffset > 0 ? 1 : -1) * this.step;
            }

            this.updateSlider(newTargetPosition);
        },
        fixCurrentIndex: function () {
            if (this.o.loop) {
                if (this.currentIndex < this.o.loop) {
                    this.currentIndex = this.sliderLength - this.o.loop + (this.currentIndex - this.o.loop);
                } else if (this.currentIndex > this.sliderLength - this.o.loop - 1) {
                    this.currentIndex = this.currentIndex + this.o.loop * 2 - this.sliderLength;
                }
            }
        },
        updateSlider: function (newTargetPosition, initial) {
            if (is.und(newTargetPosition)) {
                newTargetPosition = (this.o.reverse ? 1 : -1) * this.currentIndex * this.step;
            } else {
                this.currentIndex = (this.o.reverse ? 1 : -1) * newTargetPosition / this.step;
            }
            this.fixCurrentIndex();
            if (newTargetPosition !== this.targetPosition) {
                this.updateClassnames();
                this.animateTarget(newTargetPosition, initial);
            }
        },
        updateClassnames: function () {
            if (this.prevEl) {
                if (this.currentIndex === 0) {
                    this.prevEl.classList.add('ms-first');
                } else {
                    this.prevEl.classList.remove('ms-first');
                }
            }
            if (this.nextEl) {
                if (this.currentIndex === this.sliderLength - 1) {
                    this.nextEl.classList.add('ms-last');
                } else {
                    this.nextEl.classList.remove('ms-last');
                }
            }
        },
        animateTarget: function (newTargetPosition, initial, back) {
            if (this.animateInstance) this.animateInstance.stop();
            var _ = this;
            var from = this.targetPosition;
            var to = newTargetPosition;
            this.animateInstance = animate(function(progress) {
                _.targetPosition = to > from ? from + ((to - from) * progress) : from - ((from - to) * progress); // 0 - ((0 - -2100) * progress)
                var sliderMin = _.o.reverse ? 0 : -(_.sliderLength - 1) * _.step;
                var sliderMax = _.o.reverse ? (_.sliderLength - 1) * _.step : 0;
                if (!back &&
                    !_.o.loop &&
                    _.o.bounceCoefficient &&
                    (
                        (
                            _.targetPosition > sliderMax &&
                            _.targetPosition > sliderMax + Math.min((to - sliderMax) * _.o.bounceCoefficient, _.o.bounceMax)
                        ) ||
                        (
                            _.targetPosition < sliderMin &&
                            _.targetPosition < sliderMin - Math.min(-(to - sliderMin) * _.o.bounceCoefficient, _.o.bounceMax)
                        )
                    )
                ) {
                    _.animateInstance.stop();
                    _.animateTarget(_.targetPosition < sliderMin ? sliderMin : sliderMax, false, true);
                    _.currentIndex = _.targetPosition < sliderMin ? 0 : _.sliderLength - 1;
                } else {
                    _.renderTarget();
                }
            }, initial ? 0 : 500, function(t) { return t * (2 - t); });
        },
        onChangeCurrentIndex: function (index) {
            var currentIndex = this.o.loop ? index - this.o.loop : index;
            currentIndex = currentIndex === this.sliderLength - this.o.loop * 2 ? 0 : currentIndex;
            if (is.fnc(this.o.change) && currentIndex !== this.lastCurrentIndex) {
                this.o.change(currentIndex, this.lastCurrentIndex);
                this.lastCurrentIndex = currentIndex;
            }
        },
        getCurrentIndex: function () {
            return this.o.loop ? this.currentIndex - this.o.loop : this.currentIndex;
        },
        enable: function () {
            this.enabled = true;
        },
        disable: function () {
            this.enabled = false;
        }
    };


    // Utils

    var is = {
        arr: function (a) { return Array.isArray(a); },
        str: function (a) { return typeof a === 'string'; },
        und: function (a) { return typeof a === 'undefined'; },
        fnc: function(a) { return typeof a === 'function' }
    };

    function stringToHyphens(str) {
        return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
    }

    function getCSSValue(el, prop) {
        if (prop in el.style) {
            return getComputedStyle(el).getPropertyValue(stringToHyphens(prop)) || '0';
        }
    }

    var t = 'transform';
    var transformProperty = (getCSSValue(document.body, t) ? t : '-webkit-' + t);

    function buildSlide(value) {
        return '<li class="ms-slide">' + value + '</li>';
    }

    function extendSingle(target, source) {
        for (var key in source)
            target[key] = is.arr(source[key]) ? source[key].slice(0) : source[key];
        return target;
    }

    function extend(target) {
        if (!target) target = {};
        for (var i = 1; i < arguments.length; i++)
            extendSingle(target, arguments[i]);
        return target;
    }

    function animate(step, duration, easing) {
        if (duration) {
            var start = performance.now();
            var timer = null;
            var stopped = false;
            var animation = function (t) {
                var progress = (t - start) / duration;
                if (progress < 0) progress = 0;
                if (progress > 1) progress = 1;
                if (is.fnc(easing)) progress = easing(progress);
                step(progress);
                if (progress !== 1 && !stopped) timer = requestAnimationFrame(animation);
            };
            timer = requestAnimationFrame(animation);
            return new function() {
                this.stop = function() {
                    if (timer) cancelAnimationFrame(timer);
                    stopped = true;
                };
            };
        } else {
            step(1);
        }
    }

    /* eslint-disable */
    function getPassiveSupported() {
        var passiveSupported = false;
        try {
            var options = Object.defineProperty({}, 'passive', {
                get: function get() {
                    passiveSupported = true;
                }
            });

            window.addEventListener('test', null, options);
        } catch (err) {}
        getPassiveSupported = function () {
            return passiveSupported;
        };
        return passiveSupported;
    }
    /* eslint-enable */

    function normalizeEvent(ev) {
        if (ev.type === 'touchmove' || ev.type === 'touchstart' || ev.type === 'touchend') {
            var touch = ev.targetTouches[0] || ev.changedTouches[0];
            return {
                x: touch.clientX,
                y: touch.clientY,
                id: touch.identifier
            };
        } else {
            return {
                x: ev.clientX,
                y: ev.clientY,
                id: null
            };
        }
    }

    function dragOutOfBoundsMultiplier(val) {
        return 0.000005 * Math.pow(val, 2) + 0.0001 * val + 0.55;
    }

    function getCurrentValue(values, diff, lower) {
        var lowerValue = values[0];
        var centerValue = values[1];
        var higherValue = values[2] || lowerValue;
        var diffValue = lower ? centerValue - lowerValue : centerValue - higherValue;
        return lower ? centerValue - diffValue * diff : centerValue + diffValue * diff;
    }

    return extend(MomentumSlider, {
        extend: extend,
        transformProperty: transformProperty,
        getCurrentValue: getCurrentValue
    });

})));