/*global define*/
define([
        '../../Core/ClockRange',
        '../../Core/defined',
        '../../Core/destroyObject',
        '../../Core/DeveloperError',
        '../../Core/JulianDate',
        '../getElement',
        './TimelineHighlightRange',
        './TimelineTrack'
    ], function(
        ClockRange,
        defined,
        destroyObject,
        DeveloperError,
        JulianDate,
        getElement,
        TimelineHighlightRange,
        TimelineTrack) {
    "use strict";

    var timelineWheelDelta = 1e12;

    var timelineMouseMode = {
        none : 0,
        scrub : 1,
        slide : 2,
        zoom : 3,
        touchOnly : 4
    };
    var timelineTouchMode = {
        none : 0,
        scrub : 1,
        slideZoom : 2,
        singleTap : 3,
        ignore : 4
    };

    var timelineTicScales = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 15.0, 30.0, 60.0, // 1min
    120.0, // 2min
    300.0, // 5min
    600.0, // 10min
    900.0, // 15min
    1800.0, // 30min
    3600.0, // 1hr
    7200.0, // 2hr
    14400.0, // 4hr
    21600.0, // 6hr
    43200.0, // 12hr
    86400.0, // 24hr
    172800.0, // 2days
    345600.0, // 4days
    604800.0, // 7days
    1296000.0, // 15days
    2592000.0, // 30days
    5184000.0, // 60days
    7776000.0, // 90days
    15552000.0, // 180days
    31536000.0, // 365days
    63072000.0, // 2years
    126144000.0, // 4years
    157680000.0, // 5years
    315360000.0, // 10years
    630720000.0, // 20years
    1261440000.0, // 40years
    1576800000.0, // 50years
    3153600000.0, // 100years
    6307200000.0, // 200years
    12614400000.0, // 400years
    15768000000.0, // 500years
    31536000000.0 // 1000years
    ];

    var timelineMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];


    /**
     * The Timeline is a widget for displaying and controlling the current scene time.
     * @alias Timeline
     * @constructor
     *
     * @param {Element} container The parent HTML container node for this widget.
     * @param {Clock} clock The clock to use.
     */
    function Timeline(container, clock) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(container)) {
            throw new DeveloperError('container is required.');
        }
        if (!defined(clock)) {
            throw new DeveloperError('clock is required.');
        }
        //>>includeEnd('debug');

        container = getElement(container);

        /**
         * Gets the parent container.
         * @type {Element}
         */
        this.container = container;

        var topDiv = document.createElement('div');
        topDiv.className = 'cesium-timeline-main';
        container.appendChild(topDiv);
        this._topDiv = topDiv;

        this._endJulian = undefined;
        this._epochJulian = undefined;
        this._lastXPos = undefined;
        this._scrubElement = undefined;
        this._startJulian = undefined;
        this._timeBarSecondsSpan = undefined;
        this._clock = clock;
        this._scrubJulian = clock.currentTime;
        this._mainTicSpan = -1;
        this._mouseMode = timelineMouseMode.none;
        this._touchMode = timelineTouchMode.none;
        this._touchState = {
            centerX : 0,
            spanX : 0
        };
        this._mouseX = 0;
        this._timelineDrag = 0;
        this._timelineDragLocation = undefined;
        this._lastHeight = undefined;
        this._lastWidth = undefined;

        this._topDiv.innerHTML = '<div class="cesium-timeline-bar"></div><div class="cesium-timeline-trackContainer">' +
                                     '<canvas class="cesium-timeline-tracks" width="10" height="1">' +
                                     '</canvas></div><div class="cesium-timeline-needle"></div><span class="cesium-timeline-ruler"></span>';
        this._timeBarEle = this._topDiv.childNodes[0];
        this._trackContainer = this._topDiv.childNodes[1];
        this._trackListEle = this._topDiv.childNodes[1].childNodes[0];
        this._needleEle = this._topDiv.childNodes[2];
        this._rulerEle = this._topDiv.childNodes[3];
        this._context = this._trackListEle.getContext('2d');

        this._trackList = [];
        this._highlightRanges = [];

        this.zoomTo(clock.startTime, clock.stopTime);

        this._onMouseDown = createMouseDownCallback(this);
        this._onMouseUp = createMouseUpCallback(this);
        this._onMouseMove = createMouseMoveCallback(this);
        this._onMouseWheel = createMouseWheelCallback(this);
        this._onTouchStart = createTouchStartCallback(this);
        this._onTouchMove = createTouchMoveCallback(this);
        this._onTouchEnd = createTouchEndCallback(this);

        var timeBarEle = this._timeBarEle;
        document.addEventListener('mouseup', this._onMouseUp, false);
        document.addEventListener('mousemove', this._onMouseMove, false);
        timeBarEle.addEventListener('mousedown', this._onMouseDown, false);
        timeBarEle.addEventListener('DOMMouseScroll', this._onMouseWheel, false); // Mozilla mouse wheel
        timeBarEle.addEventListener('mousewheel', this._onMouseWheel, false);
        timeBarEle.addEventListener('touchstart', this._onTouchStart, false);
        timeBarEle.addEventListener('touchmove', this._onTouchMove, false);
        timeBarEle.addEventListener('touchend', this._onTouchEnd, false);

        this._topDiv.oncontextmenu = function() {
            return false;
        };

        clock.onTick.addEventListener(this.updateFromClock, this);
        this.updateFromClock();
    }

    /**
     * @private
     */
    Timeline.prototype.addEventListener = function(type, listener, useCapture) {
        this._topDiv.addEventListener(type, listener, useCapture);
    };

    /**
     * @private
     */
    Timeline.prototype.removeEventListener = function(type, listener, useCapture) {
        this._topDiv.removeEventListener(type, listener, useCapture);
    };

    /**
     * @returns {Boolean} true if the object has been destroyed, false otherwise.
     */
    Timeline.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Destroys the widget.  Should be called if permanently
     * removing the widget from layout.
     */
    Timeline.prototype.destroy = function() {
        this._clock.onTick.removeEventListener(this.updateFromClock, this);

        document.removeEventListener('mouseup', this._onMouseUp, false);
        document.removeEventListener('mousemove', this._onMouseMove, false);

        var timeBarEle = this._timeBarEle;
        timeBarEle.removeEventListener('mousedown', this._onMouseDown, false);
        timeBarEle.removeEventListener('DOMMouseScroll', this._onMouseWheel, false); // Mozilla mouse wheel
        timeBarEle.removeEventListener('mousewheel', this._onMouseWheel, false);
        timeBarEle.removeEventListener('touchstart', this._onTouchStart, false);
        timeBarEle.removeEventListener('touchmove', this._onTouchMove, false);
        timeBarEle.removeEventListener('touchend', this._onTouchEnd, false);
        this.container.removeChild(this._topDiv);
        destroyObject(this);
    };

    /**
     * @private
     */
    Timeline.prototype.addHighlightRange = function(color, heightInPx, base) {
        var newHighlightRange = new TimelineHighlightRange(color, heightInPx, base);
        this._highlightRanges.push(newHighlightRange);
        this.resize();
        return newHighlightRange;
    };

    /**
     * @private
     */
    Timeline.prototype.addTrack = function(interval, heightInPx, color, backgroundColor) {
        var newTrack = new TimelineTrack(interval, heightInPx, color, backgroundColor);
        this._trackList.push(newTrack);
        this._lastHeight = undefined;
        this.resize();
        return newTrack;
    };

    /**
     * Sets the view to the provided times.
     *
     * @param {JulianDate} startTime The start time.
     * @param {JulianDate} stopTime The stop time.
     */
    Timeline.prototype.zoomTo = function(startTime, stopTime) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(startTime)) {
            throw new DeveloperError('startTime is required.');
        }
        if (!defined(stopTime)) {
            throw new DeveloperError('stopTime is required');
        }
        if (JulianDate.lessThanOrEquals(stopTime, startTime)) {
            throw new DeveloperError('Start time must come before end time.');
        }
        //>>includeEnd('debug');

        this._startJulian = startTime;
        this._endJulian = stopTime;
        this._timeBarSecondsSpan = JulianDate.secondsDifference(stopTime, startTime);

        // If clock is not unbounded, clamp timeline range to clock.
        if (this._clock && (this._clock.clockRange !== ClockRange.UNBOUNDED)) {
            var clockStart = this._clock.startTime;
            var clockEnd = this._clock.stopTime;
            var clockSpan = JulianDate.secondsDifference(clockEnd, clockStart);
            var startOffset = JulianDate.secondsDifference(clockStart, this._startJulian);
            var endOffset = JulianDate.secondsDifference(clockEnd, this._endJulian);

            if (this._timeBarSecondsSpan >= clockSpan) {
                // if new duration longer than clock range duration, clamp to full range.
                this._timeBarSecondsSpan = clockSpan;
                this._startJulian = this._clock.startTime;
                this._endJulian = this._clock.stopTime;
            } else if (startOffset > 0) {
                // if timeline start is before clock start, shift right
                this._endJulian = JulianDate.addSeconds(this._endJulian, startOffset, new JulianDate());
                this._startJulian = clockStart;
                this._timeBarSecondsSpan = JulianDate.secondsDifference(this._endJulian, this._startJulian);
            } else if (endOffset < 0) {
                // if timeline end is after clock end, shift left
                this._startJulian = JulianDate.addSeconds(this._startJulian, endOffset, new JulianDate());
                this._endJulian = clockEnd;
                this._timeBarSecondsSpan = JulianDate.secondsDifference(this._endJulian, this._startJulian);
            }
        }

        this._makeTics();

        var evt = document.createEvent('Event');
        evt.initEvent('setzoom', true, true);
        evt.startJulian = this._startJulian;
        evt.endJulian = this._endJulian;
        evt.epochJulian = this._epochJulian;
        evt.totalSpan = this._timeBarSecondsSpan;
        evt.mainTicSpan = this._mainTicSpan;
        this._topDiv.dispatchEvent(evt);
    };

    /**
     * @private
     */
    Timeline.prototype.zoomFrom = function(amount) {
        var centerSec = JulianDate.secondsDifference(this._scrubJulian, this._startJulian);
        if ((amount > 1) || (centerSec < 0) || (centerSec > this._timeBarSecondsSpan)) {
            centerSec = this._timeBarSecondsSpan * 0.5;
        } else {
            centerSec += (centerSec - this._timeBarSecondsSpan * 0.5);
        }
        var centerSecFlip = this._timeBarSecondsSpan - centerSec;
        this.zoomTo(JulianDate.addSeconds(this._startJulian, centerSec - (centerSec * amount), new JulianDate()), JulianDate.addSeconds(this._endJulian, (centerSecFlip * amount) - centerSecFlip, new JulianDate()));
    };

    function twoDigits(num) {
        return ((num < 10) ? ('0' + num.toString()) : num.toString());
    }

    /**
     * @private
     */
    Timeline.prototype.makeLabel = function(time) {
        var gregorian = JulianDate.toGregorianDate(time);
        var millisecond = gregorian.millisecond, millisecondString = ' UTC';
        if ((millisecond > 0) && (this._timeBarSecondsSpan < 3600)) {
            millisecondString = Math.floor(millisecond).toString();
            while (millisecondString.length < 3) {
                millisecondString = '0' + millisecondString;
            }
            millisecondString = '.' + millisecondString;
        }

        return timelineMonthNames[gregorian.month - 1] + ' ' + gregorian.day + ' ' + gregorian.year + ' ' + twoDigits(gregorian.hour) +
            ':' + twoDigits(gregorian.minute) + ':' + twoDigits(gregorian.second) + millisecondString;
    };

    /**
     * @private
     */
    Timeline.prototype.smallestTicInPixels = 7.0;

    /**
     * @private
     */
    Timeline.prototype._makeTics = function() {
        var timeBar = this._timeBarEle;

        var seconds = JulianDate.secondsDifference(this._scrubJulian, this._startJulian);
        var xPos = Math.round(seconds * this._topDiv.clientWidth / this._timeBarSecondsSpan);
        var scrubX = xPos - 8, tic;
        var widget = this;

        this._needleEle.style.left = xPos.toString() + 'px';

        var tics = '';

        var minimumDuration = 0.01;
        var maximumDuration = 31536000000.0; // ~1000 years
        var epsilon = 1e-10;

        // If time step size is known, enter it here...
        var minSize = 0;

        var duration = this._timeBarSecondsSpan;
        if (duration < minimumDuration) {
            duration = minimumDuration;
            this._timeBarSecondsSpan = minimumDuration;
            this._endJulian = JulianDate.addSeconds(this._startJulian, minimumDuration, new JulianDate());
        } else if (duration > maximumDuration) {
            duration = maximumDuration;
            this._timeBarSecondsSpan = maximumDuration;
            this._endJulian = JulianDate.addSeconds(this._startJulian, maximumDuration, new JulianDate());
        }

        var timeBarWidth = this._timeBarEle.clientWidth;
        if (timeBarWidth < 10) {
            timeBarWidth = 10;
        }
        var startJulian = this._startJulian;

        // epsilonTime: a small fraction of one pixel width of the timeline, measured in seconds.
        var epsilonTime = Math.min((duration / timeBarWidth) * 1e-5, 0.4);

        // epochJulian: a nearby time to be considered "zero seconds", should be a round-ish number by human standards.
        var epochJulian;
        if (duration > 315360000) { // 3650+ days visible, epoch is start of the first visible century.
            epochJulian = JulianDate.fromIso8601(JulianDate.toDate(startJulian).toISOString().substring(0, 2) + '00-01-01T00:00:00Z');
        } else if (duration > 31536000) { // 365+ days visible, epoch is start of the first visible decade.
            epochJulian = JulianDate.fromIso8601(JulianDate.toDate(startJulian).toISOString().substring(0, 3) + '0-01-01T00:00:00Z');
        } else if (duration > 86400) { // 1+ day(s) visible, epoch is start of the year.
            epochJulian = JulianDate.fromIso8601(JulianDate.toDate(startJulian).toISOString().substring(0, 4) + '-01-01T00:00:00Z');
        } else { // Less than a day on timeline, epoch is midnight of the visible day.
            epochJulian = JulianDate.fromIso8601(JulianDate.toDate(startJulian).toISOString().substring(0, 10) + 'T00:00:00Z');
        }
        // startTime: Seconds offset of the left side of the timeline from epochJulian.
        var startTime = JulianDate.secondsDifference(this._startJulian, JulianDate.addSeconds(epochJulian, epsilonTime, new JulianDate()));
        // endTime: Seconds offset of the right side of the timeline from epochJulian.
        var endTime = startTime + duration;
        this._epochJulian = epochJulian;

        function getStartTic(ticScale) {
            return Math.floor(startTime / ticScale) * ticScale;
        }

        function getNextTic(tic, ticScale) {
            return Math.ceil((tic / ticScale) + 0.5) * ticScale;
        }

        function getAlpha(time) {
            return (time - startTime) / duration;
        }

        function remainder(x, y) {
            //return x % y;
            return x - (y * Math.round(x / y));
        }

        // Width in pixels of a typical label, plus padding
        this._rulerEle.innerHTML = this.makeLabel(JulianDate.addSeconds(this._endJulian, -minimumDuration, new JulianDate()));
        var sampleWidth = this._rulerEle.offsetWidth + 20;
        if (sampleWidth < 30) {
            // Workaround an apparent IE bug with measuring the width after going full-screen from inside an iframe.
            sampleWidth = 180;
        }

        var origMinSize = minSize;
        minSize -= epsilon;

        var renderState = {
            startTime : startTime,
            startJulian : startJulian,
            epochJulian : epochJulian,
            duration : duration,
            timeBarWidth : timeBarWidth,
            getAlpha : getAlpha
        };
        this._highlightRanges.forEach(function(highlightRange) {
            tics += highlightRange.render(renderState);
        });

        // Calculate tic mark label spacing in the TimeBar.
        var mainTic = 0.0, subTic = 0.0, tinyTic = 0.0;
        // Ideal labeled tic as percentage of zoom interval
        var idealTic = sampleWidth / timeBarWidth;
        if (idealTic > 1.0) {
            // Clamp to width of window, for thin windows.
            idealTic = 1.0;
        }
        // Ideal labeled tic size in seconds
        idealTic *= this._timeBarSecondsSpan;
        var ticIndex = -1, smallestIndex = -1;

        var i, ticScaleLen = timelineTicScales.length;
        for (i = 0; i < ticScaleLen; ++i) {
            var sc = timelineTicScales[i];
            ++ticIndex;
            mainTic = sc;
            // Find acceptable main tic size not smaller than ideal size.
            if ((sc > idealTic) && (sc > minSize)) {
                break;
            }
            if ((smallestIndex < 0) && ((timeBarWidth * (sc / this._timeBarSecondsSpan)) >= this.smallestTicInPixels)) {
                smallestIndex = ticIndex;
            }
        }
        if (ticIndex > 0) {
            while (ticIndex > 0) // Compute sub-tic size that evenly divides main tic.
            {
                --ticIndex;
                if (Math.abs(remainder(mainTic, timelineTicScales[ticIndex])) < 0.00001) {
                    if (timelineTicScales[ticIndex] >= minSize) {
                        subTic = timelineTicScales[ticIndex];
                    }
                    break;
                }
            }

            if (smallestIndex >= 0) {
                while (smallestIndex < ticIndex) // Compute tiny tic size that evenly divides sub-tic.
                {
                    if ((Math.abs(remainder(subTic, timelineTicScales[smallestIndex])) < 0.00001) && (timelineTicScales[smallestIndex] >= minSize)) {
                        tinyTic = timelineTicScales[smallestIndex];
                        break;
                    }
                    ++smallestIndex;
                }
            }
        }

        minSize = origMinSize;
        if ((minSize > epsilon) && (tinyTic < 0.00001) && (Math.abs(minSize - mainTic) > epsilon)) {
            tinyTic = minSize;
            if (minSize <= (mainTic + epsilon)) {
                subTic = 0.0;
            }
        }

        var lastTextLeft = -999999, textWidth;
        if ((timeBarWidth * (tinyTic / this._timeBarSecondsSpan)) >= 3.0) {
            for (tic = getStartTic(tinyTic); tic <= endTime; tic = getNextTic(tic, tinyTic)) {
                tics += '<span class="cesium-timeline-ticTiny" style="left: ' + Math.round(timeBarWidth * getAlpha(tic)).toString() + 'px;"></span>';
            }
        }
        if ((timeBarWidth * (subTic / this._timeBarSecondsSpan)) >= 3.0) {
            for (tic = getStartTic(subTic); tic <= endTime; tic = getNextTic(tic, subTic)) {
                tics += '<span class="cesium-timeline-ticSub" style="left: ' + Math.round(timeBarWidth * getAlpha(tic)).toString() + 'px;"></span>';
            }
        }
        if ((timeBarWidth * (mainTic / this._timeBarSecondsSpan)) >= 2.0) {
            this._mainTicSpan = mainTic;
            endTime += mainTic;
            tic = getStartTic(mainTic);
            var leapSecond = JulianDate.computeTaiMinusUtc(epochJulian);
            while (tic <= endTime) {
                var ticTime = JulianDate.addSeconds(startJulian, tic - startTime, new JulianDate());
                if (mainTic > 2.1) {
                    var ticLeap = JulianDate.computeTaiMinusUtc(ticTime);
                    if (Math.abs(ticLeap - leapSecond) > 0.1) {
                        tic += (ticLeap - leapSecond);
                        ticTime = JulianDate.addSeconds(startJulian, tic - startTime, new JulianDate());
                    }
                }
                var ticLeft = Math.round(timeBarWidth * getAlpha(tic));
                var ticLabel = this.makeLabel(ticTime);
                this._rulerEle.innerHTML = ticLabel;
                textWidth = this._rulerEle.offsetWidth;
                if (textWidth < 10) {
                    // IE iframe fullscreen sampleWidth workaround, continued.
                    textWidth = sampleWidth;
                }
                var labelLeft = ticLeft - ((textWidth / 2) - 1);
                if (labelLeft > lastTextLeft) {
                    lastTextLeft = labelLeft + textWidth + 5;
                    tics += '<span class="cesium-timeline-ticMain" style="left: ' + ticLeft.toString() + 'px;"></span>' + '<span class="cesium-timeline-ticLabel" style="left: ' + labelLeft.toString() +
                            'px;">' + ticLabel + '</span>';
                } else {
                    tics += '<span class="cesium-timeline-ticSub" style="left: ' + ticLeft.toString() + 'px;"></span>';
                }
                tic = getNextTic(tic, mainTic);
            }
        } else {
            this._mainTicSpan = -1;
        }

        tics += '<span class="cesium-timeline-icon16" style="left:' + scrubX + 'px;bottom:0;background-position: 0px 0px;"></span>';
        timeBar.innerHTML = tics;
        this._scrubElement = timeBar.lastChild;

        // Clear track canvas.
        this._context.clearRect(0, 0, this._trackListEle.width, this._trackListEle.height);

        renderState.y = 0;
        this._trackList.forEach(function(track) {
            track.render(widget._context, renderState);
            renderState.y += track.height;
        });
    };

    /**
     * @private
     */
    Timeline.prototype.updateFromClock = function() {
        this._scrubJulian = this._clock.currentTime;
        var scrubElement = this._scrubElement;
        if (defined(this._scrubElement)) {
            var seconds = JulianDate.secondsDifference(this._scrubJulian, this._startJulian);
            var xPos = Math.round(seconds * this._topDiv.clientWidth / this._timeBarSecondsSpan);

            if (this._lastXPos !== xPos) {
                this._lastXPos = xPos;

                scrubElement.style.left = (xPos - 8) + 'px';
                this._needleEle.style.left = xPos + 'px';
            }
        }
        if (defined(this._timelineDragLocation)) {
            this._setTimeBarTime(this._timelineDragLocation, this._timelineDragLocation * this._timeBarSecondsSpan / this._topDiv.clientWidth);
            this.zoomTo(JulianDate.addSeconds(this._startJulian, this._timelineDrag, new JulianDate()), JulianDate.addSeconds(this._endJulian, this._timelineDrag, new JulianDate()));
        }
    };

    /**
     * @private
     */
    Timeline.prototype._setTimeBarTime = function(xPos, seconds) {
        xPos = Math.round(xPos);
        this._scrubJulian = JulianDate.addSeconds(this._startJulian, seconds, new JulianDate());
        if (this._scrubElement) {
            var scrubX = xPos - 8;
            this._scrubElement.style.left = scrubX.toString() + 'px';
            this._needleEle.style.left = xPos.toString() + 'px';
        }

        var evt = document.createEvent('Event');
        evt.initEvent('settime', true, true);
        evt.clientX = xPos;
        evt.timeSeconds = seconds;
        evt.timeJulian = this._scrubJulian;
        evt.clock = this._clock;
        this._topDiv.dispatchEvent(evt);
    };

    function createMouseDownCallback(timeline) {
        return function(e) {
            if (timeline._mouseMode !== timelineMouseMode.touchOnly) {
                if (e.button === 0) {
                    timeline._mouseMode = timelineMouseMode.scrub;
                    if (timeline._scrubElement) {
                        timeline._scrubElement.style.backgroundPosition = '-16px 0';
                    }
                    timeline._onMouseMove(e);
                } else {
                    timeline._mouseX = e.clientX;
                    if (e.button === 2) {
                        timeline._mouseMode = timelineMouseMode.zoom;
                    } else {
                        timeline._mouseMode = timelineMouseMode.slide;
                    }
                }
            }
            e.preventDefault();
        };
    }

    function createMouseUpCallback(timeline) {
        return function(e) {
            timeline._mouseMode = timelineMouseMode.none;
            if (timeline._scrubElement) {
                timeline._scrubElement.style.backgroundPosition = '0px 0px';
            }
            timeline._timelineDrag = 0;
            timeline._timelineDragLocation = undefined;
        };
    }

    function createMouseMoveCallback(timeline) {
        return function(e) {
            var dx;
            if (timeline._mouseMode === timelineMouseMode.scrub) {
                e.preventDefault();
                var x = e.clientX - timeline._topDiv.getBoundingClientRect().left;

                if (x < 0) {
                    timeline._timelineDragLocation = 0;
                    timeline._timelineDrag = -0.01 * timeline._timeBarSecondsSpan;
                } else if (x > timeline._topDiv.clientWidth) {
                    timeline._timelineDragLocation = timeline._topDiv.clientWidth;
                    timeline._timelineDrag = 0.01 * timeline._timeBarSecondsSpan;
                } else {
                    timeline._timelineDragLocation = undefined;
                    timeline._setTimeBarTime(x, x * timeline._timeBarSecondsSpan / timeline._topDiv.clientWidth);
                }

            } else if (timeline._mouseMode === timelineMouseMode.slide) {
                dx = timeline._mouseX - e.clientX;
                timeline._mouseX = e.clientX;
                if (dx !== 0) {
                    var dsec = dx * timeline._timeBarSecondsSpan / timeline._topDiv.clientWidth;
                    timeline.zoomTo(JulianDate.addSeconds(timeline._startJulian, dsec, new JulianDate()), JulianDate.addSeconds(timeline._endJulian, dsec, new JulianDate()));
                }
            } else if (timeline._mouseMode === timelineMouseMode.zoom) {
                dx = timeline._mouseX - e.clientX;
                timeline._mouseX = e.clientX;
                if (dx !== 0) {
                    timeline.zoomFrom(Math.pow(1.01, dx));
                }
            }
        };
    }

    function createMouseWheelCallback(timeline) {
        return function(e) {
            var dy = e.wheelDeltaY || e.wheelDelta || (-e.detail);
            timelineWheelDelta = Math.max(Math.min(Math.abs(dy), timelineWheelDelta), 1);
            dy /= timelineWheelDelta;
            timeline.zoomFrom(Math.pow(1.05, -dy));
        };
    }

    function createTouchStartCallback(timeline) {
        return function(e) {
            var len = e.touches.length, seconds, xPos, leftX = timeline._topDiv.getBoundingClientRect().left;
            e.preventDefault();
            timeline._mouseMode = timelineMouseMode.touchOnly;
            if (len === 1) {
                seconds = JulianDate.secondsDifference(timeline._scrubJulian, timeline._startJulian);
                xPos = Math.round(seconds * timeline._topDiv.clientWidth / timeline._timeBarSecondsSpan + leftX);
                if (Math.abs(e.touches[0].clientX - xPos) < 50) {
                    timeline._touchMode = timelineTouchMode.scrub;
                    if (timeline._scrubElement) {
                        timeline._scrubElement.style.backgroundPosition = (len === 1) ? '-16px 0' : '0 0';
                    }
                } else {
                    timeline._touchMode = timelineTouchMode.singleTap;
                    timeline._touchState.centerX = e.touches[0].clientX - leftX;
                }
            } else if (len === 2) {
                timeline._touchMode = timelineTouchMode.slideZoom;
                timeline._touchState.centerX = (e.touches[0].clientX + e.touches[1].clientX) * 0.5 - leftX;
                timeline._touchState.spanX = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
            } else {
                timeline._touchMode = timelineTouchMode.ignore;
            }
        };
    }

    function createTouchEndCallback(timeline) {
        return function(e) {
            var len = e.touches.length, leftX = timeline._topDiv.getBoundingClientRect().left;
            if (timeline._touchMode === timelineTouchMode.singleTap) {
                timeline._touchMode = timelineTouchMode.scrub;
                timeline._handleTouchMove(e);
            } else if (timeline._touchMode === timelineTouchMode.scrub) {
                timeline._handleTouchMove(e);
            }
            timeline._mouseMode = timelineMouseMode.touchOnly;
            if (len !== 1) {
                timeline._touchMode = (len > 0) ? timelineTouchMode.ignore : timelineTouchMode.none;
            } else if (timeline._touchMode === timelineTouchMode.slideZoom) {
                timeline._touchState.centerX = e.touches[0].clientX - leftX;
            }
            if (timeline._scrubElement) {
                timeline._scrubElement.style.backgroundPosition = '0 0';
            }
        };
    }

    function createTouchMoveCallback(timeline) {
        return function(e) {
            var dx, x, len, newCenter, newSpan, newStartTime, zoom = 1, leftX = timeline._topDiv.getBoundingClientRect().left;
            if (timeline._touchMode === timelineTouchMode.singleTap) {
                timeline._touchMode = timelineTouchMode.slideZoom;
            }
            timeline._mouseMode = timelineMouseMode.touchOnly;
            if (timeline._touchMode === timelineTouchMode.scrub) {
                e.preventDefault();
                if (e.changedTouches.length === 1) {
                    x = e.changedTouches[0].clientX - leftX;
                    if ((x >= 0) && (x <= timeline._topDiv.clientWidth)) {
                        timeline._setTimeBarTime(x, x * timeline._timeBarSecondsSpan / timeline._topDiv.clientWidth);
                    }
                }
            } else if (timeline._touchMode === timelineTouchMode.slideZoom) {
                len = e.touches.length;
                if (len === 2) {
                    newCenter = (e.touches[0].clientX + e.touches[1].clientX) * 0.5 - leftX;
                    newSpan = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
                } else if (len === 1) {
                    newCenter = e.touches[0].clientX - leftX;
                    newSpan = 0;
                }

                if (defined(newCenter)) {
                    if ((newSpan > 0) && (timeline._touchState.spanX > 0)) {
                        // Zoom and slide
                        zoom = (timeline._touchState.spanX / newSpan);
                        newStartTime = JulianDate.addSeconds(timeline._startJulian, ((timeline._touchState.centerX * timeline._timeBarSecondsSpan) - (newCenter * timeline._timeBarSecondsSpan * zoom)) / timeline._topDiv.clientWidth, new JulianDate());
                    } else {
                        // Slide to newCenter
                        dx = timeline._touchState.centerX - newCenter;
                        newStartTime = JulianDate.addSeconds(timeline._startJulian, dx * timeline._timeBarSecondsSpan / timeline._topDiv.clientWidth, new JulianDate());
                    }

                    timeline.zoomTo(newStartTime, JulianDate.addSeconds(newStartTime, timeline._timeBarSecondsSpan * zoom, new JulianDate()));
                    timeline._touchState.centerX = newCenter;
                    timeline._touchState.spanX = newSpan;
                }
            }
        };
    }

    /**
     * Resizes the widget to match the container size.
     */
    Timeline.prototype.resize = function() {
        var width = this.container.clientWidth;
        var height = this.container.clientHeight;

        if (width === this._lastWidth && height === this._lastHeight) {
            return;
        }

        this._trackContainer.style.height = height + 'px';

        var trackListHeight = 1;
        this._trackList.forEach(function(track) {
            trackListHeight += track.height;
        });
        this._trackListEle.style.height = trackListHeight.toString() + 'px';
        this._trackListEle.width = this._trackListEle.clientWidth;
        this._trackListEle.height = trackListHeight;
        this._makeTics();

        this._lastWidth = width;
        this._lastHeight = height;
    };

    return Timeline;
});
