/**
 * @module meteoJS/timeline
 */
import addEventFunctions from './Events.js';

/**
 * Special key identifier.
 * 
 * @typedef {"ctrl"|"alt"|"shift"|"meta"|Number}
 *   module:meteoJS/timeline~specialKeyIdentifier
 */

/**
 * Definition of pressed keys with optional special keys.
 * 
 * @typedef {module:meteoJS/timeline~specialKeyIdentifier|
             module:meteoJS/timeline~specialKeyIdentifier[]}
 *   module:meteoJS/timeline~optionPressedKeys
 */

/**
 * Keyboard navigation options.
 * 
 * @typedef {Object} module:meteoJS/timeline~optionKeyboardNavigation
 * @param {boolean} [enabled=false] - Enable Keyboard Navigation.
 * @param {module:meteoJS/timeline~optionPressedKeys} [first=36]
 *   Keyboard event to execute
 *   {@link module:meteoJS/timeline.Timeline#first|first()}.
 *   Default: Home.
 * @param {module:meteoJS/timeline~optionPressedKeys} [last=35]
 *   Keyboard event to execute
 *   {@link module:meteoJS/timeline.Timeline#last|last()}.
 *   Default: End.
 * @param {module:meteoJS/timeline~optionPressedKeys} [prev=37]
 *   Keyboard event to execute
 *   {@link module:meteoJS/timeline.Timeline#prev|prev()}.
 *   Default: Arrow left.
 * @param {module:meteoJS/timeline~optionPressedKeys} [next=39]
 *   Keyboard event to execute
 *   {@link module:meteoJS/timeline.Timeline#next|next()}.
 *   Default: Arrow right.
 * @param {module:meteoJS/timeline~optionPressedKeys} [prevAllEnabledTime=[37, 'ctrl']]
 *   Keyboard event to execute
 *   {@link module:meteoJS/timeline.Timeline#prevAllEnabledTime|prevAllEnabledTime()}.
 *   Default: Arrow left + Ctrl.
 * @param {module:meteoJS/timeline~optionPressedKeys} [nextAllEnabledTime=[39, 'ctrl']]
 *   Keyboard event to execute
 *   {@link module:meteoJS/timeline.Timeline#nextAllEnabledTime|nextAllEnabledTime()}.
 *   Default: Arrow right + Ctrl.
 * @param {Object.<string,module:meteoJS/timeline~optionPressedKeys>} [add]
 *   Keyboard event to execute {@link module:meteoJS/timeline.Timeline#add|add()}.
 *   The keys are combined with an amount integer and a timeKey
 *   (definition analog to the add() function).
 *   Defaults: ArrowRight plus 3h:Ctrl+Shift, 6h:Shift, 12h:Alt+Shift, 24h:Alt
 * @param {Object.<string,module:meteoJS/timeline~optionPressedKeys>} [sub]
 *   Keyboard event to execute {@link module:meteoJS/timeline.Timeline#sub|sub()}.
 *   The keys are combined with an amount integer and a timeKey
 *   (definition analog to the add() function).
 *   Defaults: ArrowLeft plus 3h:Ctrl+Shift, 6h:Shift, 12h:Alt+Shift, 24h:Alt
 */

/**
 * Options for timeline constructor.
 * 
 * @typedef {Object} module:meteoJS/timeline~options
 * @param {number|undefined} [maxTimeGap]
 *   Maximum of time period (in seconds) between two timestamps. If this option
 *   is specified, than e.g. the method getTimes() could return more timestamps
 *   than defined by setTimesBySetID.
 * @param {module:meteoJS/timeline~optionKeyboardNavigation}
 *   [keyboardNavigation] - Keyboard navigation options.
 */

/**
 * @event module:meteoJS/timeline#change:time
 * @property {Date} oldDate - Time before change.
 */

/**
 * @event module:meteoJS/timeline#change:times
 */

/**
 * @event module:meteoJS/timeline#change:enabledTimes
 */

/**
 * @classdesc
 * Class represents a timeline.
 * On this timeline, you could define different set of times. This is useful for
 * the usecase 1: You have different data types for different times (like radar
 * and satellite pictures). Then, the timeline provides a list of all available
 * times. Each time in each set of times could be enabled or disabled. This
 * yields to the usecase 2: In a viewer of model charts, you probably want to
 * show all the times with charts. (Global models normally have a time interval
 * of 3 hours between charts) But for different parameters, you only provide
 * charts at a greater interval. E.g. you calculate 24h-precipiation sums only
 * for 00 UTC. So you can set the times of the 3-hour-interval and only set
 * the 00 UTC timestamps as enabled. To visualise the timeline use some
 * child class of the
 * {@link module:meteoJS/timeline/visualisation.Visualisation} class. To animate
 * through time steps use the {@link module:meteoJS/timeline/animation.Animation}
 * class.
 * 
 * <pre><code>import Timeline from 'meteojs/Timeline';</code></pre>
 */
export class Timeline {
  
  /**
   * @param {module:meteoJS/timeline~options} [options] - Options.
   */
  constructor({ maxTimeGap = undefined,
    keyboardNavigation = {} } = {}) {
    /**
     * @type undefined|number
     * @private
     */
    this.maxTimeGap = maxTimeGap;
  
    /**
     * Date object with current selected time. Maybe invalid.
     * @member {Date}
     * @private
     */
    this.selectedTime = new Date('invalid');
  
    /**
     * Times of this timeline. Sorted upwardly.
     * @member {Date[]}
     * @private
     */
    this.times = [];
  
    /**
     * Times of this timeline, that are enabled at least in one set of times.
     * Sorted upwardly.
     * @member {Date[]}
     * @private
     */
    this.enabledTimes = [];
  
    /**
     * Times of this timeline, that are enabled through all set of times.
     * Sorted upwardly.
     * @member {Date[]}
     * @private
     */
    this.allEnabledTimes = [];
  
    /**
     * Objekt mit keys und Date-Arrays (zeitlich sortiert)
     * @type Object.<mixed,Object>
     * @private
     */
    this.timesByKey = {};
    
    /**
     * @type {module:meteoJS/timeline~keyboardNavigationOptions}
     * @private
     */
    this._keyboardNavigation = {};
    this._initKeyboardNavigation(keyboardNavigation);
  }
  
  /**
   * Current selected time.
   * 
   * @returns {Date} Selected time, could be invalid.
   */
  getSelectedTime() {
    return this.selectedTime;
  }
  
  /**
   * Sets current selected time. You can select a time returned by getTimes only.
   * If this is not the case, an invalid timestamp will be set.
   * 
   * @param {Date} time - Time to select.
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   * @fires module:meteoJS/timeline#change:time
   */
  setSelectedTime(time) {
    this._setSelectedTime(
      (_indexOfTimeInTimesArray(time, this.times) > -1) ?
        time : new Date('invalid'));
    return this;
  }
  
  /**
   * Returns a list of all timestamps represented by this timeline.
   * This includes on the one hand all timestamps defined by setTimesBySetID, on
   * the other hand there could exists additional timestamps (e.g. through the
   * maxTimeGap option).
   * 
   * @returns {Date[]} All defined times, sorted upwardly.
   */
  getTimes() {
    return this.times;
  }
  
  /**
   * Returns a list of all enabled timestamps of this timeline.
   * 
   * @returns {Date[]} All enabled times, sorted upwardly.
   */
  getEnabledTimes() {
    return this.enabledTimes;
  }
  
  /**
   * Returns a list of times. These times are enabled throug every set of times.
   * 
   * @returns {Date[]} Enabled times, sorted upwardly.
   */
  getAllEnabledTimes() {
    return this.allEnabledTimes;
  }
  
  /**
   * Defines a set of times. Set is identified by an ID.
   * If the set was already defined, the set of times will be overwritten.
   * 
   * @param {mixed} id - ID of the set of times.
   * @param {Date[]} times - Times (must be sorted upwardly).
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   * @fires module:meteoJS/timeline#change:times
   * @fires module:meteoJS/timeline#change:enabledTimes
   */
  setTimesBySetID(id, times) {
    this.timesByKey[id] = {
      times: times,
      enabled: times
    };
    this._updateTimes();
    this._updateEnabledTimes();
    return this;
  }
  
  /**
   * Defines the enbaled times of a set of times. The passed times must be
   * contained in the times of the set (defined earlier by setTimesBySetID).
   * 
   * @param {mixed} id - ID of the set of times.
   * @param {Date[]} times - Times to set enabled (must be sorted upwardly).
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   * @fires module:meteoJS/timeline#change:enabledTimes
   */
  setEnabledTimesBySetID(id, times) {
    if (id in this.timesByKey) {
      this.timesByKey[id].enabled = times;
      this._updateEnabledTimes();
    }
    return this;
  }
  
  /**
   * Returns IDs of all defined sets.
   * 
   * @return {mixed[]} IDs.
   */
  getSetIDs() {
    return Object.keys(this.timesByKey);
  }
  
  /**
   * Deletes a set of times.
   * 
   * @param {mixed} id - ID of the set of times.
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   * @fires module:meteoJS/timeline#change:times
   * @fires module:meteoJS/timeline#change:enabledTimes
   */
  deleteSetID(id) {
    if (id in this.timesByKey) {
      delete this.timesByKey[id];
      this._updateTimes();
      this._updateEnabledTimes();
    }
    return this;
  }
  
  /**
   * Set selected time to the first time, which is enabled.
   * 
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   */
  first() {
    this._setSelectedTime(this.getFirstEnabledTime());
    return this;
  }
  
  /**
   * Set selected time to the last time, which is enabled.
   * 
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   */
  last() {
    this._setSelectedTime(this.getLastEnabledTime());
    return this;
  }
  
  /**
   * Changes selected time to the next enabled time.
   * 
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   */
  next() {
    this._setSelectedTime(this.getNextEnabledTime());
    return this;
  }
  
  /**
   * Changes selected time to the previous enabled time.
   * 
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   */
  prev() {
    this._setSelectedTime(this.getPrevEnabledTime());
    return this;
  }
  
  /**
   * Changes selected time to the next time, which is enabled by all sets.
   * 
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   */
  nextAllEnabledTime() {
    this._setSelectedTime(this.getNextAllEnabledTime());
    return this;
  }
  
  /**
   * Changes selected time to the previous time, which is enabled by all sets.
   * 
   * @returns {module:meteoJS/timeline.Timeline} Returns this.
   */
  prevAllEnabledTime() {
    this._setSelectedTime(this.getPrevAllEnabledTime());
    return this;
  }
  
  /**
   * Changes the selected time width adding an amount of "time".
   * If the "new" timestamp is not available, the selected time is not changed.
   * 
   * @param {number} amount - "Time"-Amount.
   * @param {'years'|'y'|'months'|'M'|'days'|'d'|'hours'|'h'|'minutes'|'m'|'seconds'|'s'|'milliseconds'|'ms'}
   *   timeKey - Period, nomenclature analogue to momentjs.
   * @returns {module:meteoJS/timeline.Timeline} - Returns this.
   */
  add(amount, timeKey) {
    let d = this.getSelectedTime();
    let delta = 0;
    switch (timeKey) {
    case 'hours':
    case 'h':
      delta = amount * 3600 * 1000;
      break;
    case 'minutes':
    case 'm':
      delta = amount * 60 * 1000;
      break;
    case 'seconds':
    case 's':
      delta = amount * 1000;
      break;
    case 'milliseconds':
    case 'ms':
      delta = amount;
      break;
    }
    if (delta != 0)
      d = new Date(d.valueOf() + delta);
    
    switch (timeKey) {
    case 'years':
    case 'y':
      d.setUTCFullYear(d.getUTCFullYear() + amount);
      break;
    case 'months':
    case 'M':
      d.setUTCMonth(d.getUTCMonth() + amount);
      break;
    case 'days':
    case 'd':
      d.setUTCDate(d.getUTCDate() + amount);
      break;
    }
    
    if (_indexOfTimeInTimesArray(d, this.times) > -1)
      this._setSelectedTime(d);
    return this;
  }
  
  /**
   * Changes the selected time width subracting an amount of "time".
   * If the "new" timestamp is not available, the selected time is not changed.
   * 
   * @param {number} amount - "Time"-Amount.
   * @param {'years'|'y'|'months'|'M'|'days'|'d'|'hours'|'h'|'minutes'|'m'|'seconds'|'s'|'milliseconds'|'ms'}
   *   timeKey - Period, nomenclature analogue to momentjs.
   * @returns {module:meteoJS/timeline.Timeline} - Returns this.
   */
  sub(amount, timeKey) {
    let d = this.getSelectedTime();
    let delta = 0;
    switch (timeKey) {
    case 'hours':
    case 'h':
      delta = amount * 3600 * 1000;
      break;
    case 'minutes':
    case 'm':
      delta = amount * 60 * 1000;
      break;
    case 'seconds':
    case 's':
      delta = amount * 1000;
      break;
    case 'milliseconds':
    case 'ms':
      delta = amount;
      break;
    }
    if (delta != 0)
      d = new Date(d.valueOf() - delta);
    
    switch (timeKey) {
    case 'years':
    case 'y':
      d.setUTCFullYear(d.getUTCFullYear() - amount);
      break;
    case 'months':
    case 'M':
      d.setUTCMonth(d.getUTCMonth() - amount);
      break;
    case 'days':
    case 'd':
      d.setUTCDate(d.getUTCDate() - amount);
      break;
    }
    
    if (_indexOfTimeInTimesArray(d, this.times) > -1)
      this._setSelectedTime(d);
    return this;
  }
  
  /**
   * Returns first time in this timeline, which is enabled by at least one set.
   * 
   * @returns {Date} First enabled time, could be invalid.
   */
  getFirstEnabledTime() {
    return (this.enabledTimes.length > 0) ?
      this.enabledTimes[0] : new Date('invalid');
  }
  
  /**
   * Returns last time in this timeline, which is enabled by at least one set.
   * 
   * @returns {Date} Last enabled time, could be invalid.
   */
  getLastEnabledTime() {
    return (this.enabledTimes.length > 0) ?
      this.enabledTimes[this.enabledTimes.length-1] : new Date('invalid');
  }
  
  /**
   * Returns next time after the selected time, which is enabled by at least
   * one set. If selected time is invalid, the first enabled time is returned.
   * 
   * @returns {Date} Next enabled time.
   */
  getNextEnabledTime() {
    if (this.enabledTimes.length < 1)
      return new Date('invalid');
    var index = _indexOfTimeInTimesArray(this.getSelectedTime(), this.enabledTimes);
    if (index > -1) {
      index++;
      return (index < this.enabledTimes.length) ?
        this.enabledTimes[index] :
        this.enabledTimes[this.enabledTimes.length-1];
    }
    else if (isNaN(this.getSelectedTime()))
      return this.enabledTimes[0];
    else {
      // Es war kein Zeitpunkt aus enabledTimes
      var result = new Date('invalid');
      for (var i=0; i<this.enabledTimes.length; i++)
        if (this.getSelectedTime().valueOf() < this.enabledTimes[i].valueOf()) {
          result = this.enabledTimes[i];
          break;
        }
      return result;
    }
  }
  
  /**
   * Returns previous time before the selected time, which is enabled by at least
   * one set. If selected time is invalid, the last enabled time is returned.
   * 
   * @returns {Date} Previous enabled time.
   */
  getPrevEnabledTime() {
    if (this.enabledTimes.length < 1)
      return new Date('invalid');
    var index = _indexOfTimeInTimesArray(this.getSelectedTime(), this.enabledTimes);
    if (index > -1) {
      index--;
      return (-1 < index) ? this.enabledTimes[index] : this.enabledTimes[0];
    }
    else if (isNaN(this.getSelectedTime()))
      return this.enabledTimes[0];
    else {
      // Es war kein Zeitpunkt aus enabledTimes
      var result = new Date('invalid');
      for (var i=this.enabledTimes.length-1; i>=0; i--)
        if (this.getSelectedTime().valueOf() > this.enabledTimes[i].valueOf()) {
          result = this.enabledTimes[i];
          break;
        }
      return result;
    }
  }
  
  /**
   * Returns first time in this timeline, which is enabled by at all sets.
   * 
   * @returns {Date} First time, which is enabled by all sets.
   */
  getFirstAllEnabledTime() {
    return (this.allEnabledTimes.length > 0) ?
      this.allEnabledTimes[0] : new Date('invalid');
  }
  
  /**
   * Returns last time in this timeline, which is enabled by at all sets.
   * 
   * @returns {Date} Last time, which is enabled by all sets.
   */
  getLastAllEnabledTime() {
    return (this.allEnabledTimes.length > 0) ?
      this.allEnabledTimes[this.allEnabledTimes.length-1] : new Date('invalid');
  }
  
  /**
   * Returns next time after the selected time, which is enabled by
   * all sets. If selected time is invalid, the last all enabled time is returned.
   * 
   * @returns {Date} Next time, which is enabled by all sets.
   */
  getNextAllEnabledTime() {
    if (this.allEnabledTimes.length < 1)
      return new Date('invalid');
    var index = _indexOfTimeInTimesArray(this.getSelectedTime(), this.allEnabledTimes);
    if (index > -1) {
      index++;
      return (index < this.allEnabledTimes.length) ?
        this.allEnabledTimes[index] :
        this.allEnabledTimes[this.allEnabledTimes.length-1];
    }
    else if (isNaN(this.getSelectedTime()))
      return this.allEnabledTimes[0];
    else {
      // Es war kein Zeitpunkt aus allEnabledTimes
      var result = new Date('invalid');
      for (var i=0; i<this.allEnabledTimes.length; i++)
        if (this.getSelectedTime().valueOf() < this.allEnabledTimes[i].valueOf()) {
          result = this.allEnabledTimes[i];
          break;
        }
      return result;
    }
  }
  
  /**
   * Returns previous time before the selected time, which is enabled by
   * all sets. If selected time is invalid, the first all enabled time is returned.
   * 
   * @returns {Date} Previous time, which is enabled by all sets.
   */
  getPrevAllEnabledTime() {
    if (this.allEnabledTimes.length < 1)
      return new Date('invalid');
    var index = _indexOfTimeInTimesArray(this.getSelectedTime(), this.allEnabledTimes);
    if (index > -1) {
      index--;
      return (-1 < index) ? this.allEnabledTimes[index] : this.allEnabledTimes[0];
    }
    else if (isNaN(this.getSelectedTime()))
      return this.allEnabledTimes[0];
    else {
      // Es war kein Zeitpunkt aus allEnabledTimes
      var result = new Date('invalid');
      for (var i=this.allEnabledTimes.length-1; i>=0; i--)
        if (this.getSelectedTime().valueOf() > this.allEnabledTimes[i].valueOf()) {
          result = this.allEnabledTimes[i];
          break;
        }
      return result;
    }
  }
  
  /**
   * Returns if the passed time is an enabled time.
   * 
   * @returns {boolean}
   */
  isTimeEnabled(time) {
    return this.enabledTimes.reduce(function (acc, t) {
      return (t.valueOf() == time.valueOf()) ? true : acc;
    }, false);
  }
  
  /**
   * Returns if the passed time is an enabled time.
   * 
   * @returns {boolean}
   */
  isTimeAllEnabled(time) {
    return this.allEnabledTimes.reduce(function (acc, t) {
      return (t.valueOf() == time.valueOf()) ? true : acc;
    }, false);
  }
  
  /**
   * Is the selected time the first enabled time.
   * 
   * @returns {boolean}
   */
  isFirstEnabledTime() {
    return this.getFirstEnabledTime().valueOf() == this.getSelectedTime().valueOf();
  }
  
  /**
   * Is the selected time the last enabled time.
   * 
   * @returns {boolean}
   */
  isLastEnabledTime() {
    return this.getLastEnabledTime().valueOf() == this.getSelectedTime().valueOf();
  }
  
  /**
   * Internal setter of the selected time. Caller must guarantee, that either
   * the passed timestamp exists in this.times or is invalid.
   * @param {Date} selectedTime - Selected time.
   * @fires module:meteoJS/timeline#change:time
   * @private
   */
  _setSelectedTime(selectedTime) {
    var oldTime = this.selectedTime;
    this.selectedTime = selectedTime;
    this.trigger('change:time', oldTime);
    return this.selectedTime;
  }
  
  /**
   * Bringt den Inhalt des Arrays this.times in
   * Übereinstimmung mit dem Inhalt von this.timesByKey
   * @private
   * @fires module:meteoJS/timeline#change:times
   */
  _updateTimes() {
    this.times = [];
    var timesArr = [];
    var times = {};
    for (var key in this.timesByKey)
      this.timesByKey[key].times.forEach(function (t) {
        if (!(t.valueOf() in times)) {
          timesArr.push(t);
          times[t.valueOf()] = t;
        }
      });
    _sortTimesArray(timesArr);
    timesArr.forEach(function (time) {
      if (this.times.length < 1) {
        this.times.push(time);
        return;
      }
      var lastTime = this.times[this.times.length-1];
      if (this.maxTimeGap !== undefined &&
          (time.valueOf()-lastTime.valueOf()) > 1000*this.maxTimeGap) {
        var t = lastTime;
        do {
          t = new Date(t.getTime() + this.maxTimeGap*1000);
          this.times.push(t);
        } while ((time.valueOf()-t.valueOf()) > 1000*this.maxTimeGap);
      }
      this.times.push(time);
    }, this);
    _sortTimesArray(this.times);
    this.trigger('change:times');
  }
  
  /**
   * Bringt den Inhalt der Arrays this.enabledTimes und this.allEnabledTimes in
   * Übereinstimmung mit dem Inhalt von this.timesByKey
   * @private
   * @fires module:meteoJS/timeline#change:enabledTimes
   */
  _updateEnabledTimes() {
    this.enabledTimes = [];
    this.allEnabledTimes = [];
    var enabledTimes = {};
    var allEnabledTimes = {};
    for (var key in this.timesByKey) {
      this.timesByKey[key].enabled.forEach(function (t) {
        if (!(t.valueOf() in enabledTimes)) {
          this.enabledTimes.push(t);
          enabledTimes[t.valueOf()] = t;
        }
        if (!(t.valueOf() in allEnabledTimes))
          allEnabledTimes[t.valueOf()] = 1;
        else
          allEnabledTimes[t.valueOf()]++;
      }, this);
    }
    _sortTimesArray(this.enabledTimes);
    for (var value in allEnabledTimes)
      if (allEnabledTimes[value] == Object.keys(this.timesByKey).length)
        this.allEnabledTimes.push(enabledTimes[value]);
    _sortTimesArray(this.allEnabledTimes);
    this.trigger('change:enabledTimes');
  }
  
  /**
   * Intialize property "_keyboardNavigation".
   * 
   * @param {module:meteoJS/timeline~optionKeyboardNavigation}
   *   [keyboardNavigation] - Keyboard navigation options.
   * @private
   */
  _initKeyboardNavigation({
    enabled = false,
    first = 36,
    last = 35,
    prev = 37,
    next = 39,
    prevAllEnabledTime = [37, 'ctrl'],
    nextAllEnabledTime = [39, 'ctrl'],
    add = undefined,
    sub = undefined
  } = {}) {
    if (add === undefined)
      add = {
        '3h': [39, 'ctrl', 'shift'],
        '6h': [39, 'shift'],
        '12h': [39, 'alt', 'shift'],
        '24h': [39, 'alt']
      };
    if (sub === undefined)
      sub = {
        '3h': [37, 'ctrl', 'shift'],
        '6h': [37, 'shift'],
        '12h': [37, 'alt', 'shift'],
        '24h': [37, 'alt']
      };
    this._keyboardNavigation = {
      enabled,
      first,
      last,
      prev,
      next,
      prevAllEnabledTime,
      nextAllEnabledTime,
      add,
      sub
    };
    if (document && this._keyboardNavigation.enabled)
      document.addEventListener('keydown', event => {
        Object.keys(this._keyboardNavigation).forEach(method => {
          if (method == 'enabled')
            return;
          if (/^(add|sub)$/.test(method)) {
            Object.keys(this._keyboardNavigation[method]).forEach(time => {
              const matches = time.match(/^([0-9]+)\s*([a-zA-Z]+)$/);
              if (matches === null)
                return;
              if (_isEventMatchPressedKeys(event, this._keyboardNavigation[method][time])) {
                this[method](+matches[1], matches[2]);
                event.preventDefault();
                event.stopPropagation();
              }
            });
          }
          else if (method in this
            && _isEventMatchPressedKeys(event, this._keyboardNavigation[method])) {
            this[method]();
            event.preventDefault();
            event.stopPropagation();
          }
        });
      });
  }
}
addEventFunctions(Timeline.prototype);
export default Timeline;

/**
 * Gibt den Index eines Zeitpunktes in einem Array aus Zeitpunkten zurück.
 * @param {Date} time Zeitpunkt
 * @param {Date[]} times Array aus Zeitpunkten
 * @returns {number} -1 für "nicht gefunden
 * @static
 * @private
 */
export let _indexOfTimeInTimesArray = (time, times) => {
  return times.findIndex(function (t) {
    return t.valueOf() == time.valueOf();
  });
};

/**
 * Sortiert einen Array aus Zeitpunkten zeitlich aufwärts
 * @param {Date[]} times Array aus Zeitpunkten
 * @static
 * @private
 */
function _sortTimesArray(times) {
  times.sort(function (a,b) { return a.valueOf()-b.valueOf(); });
}

/**
 * Returns if an event represents a certain key pressed with (optional)
 * additional special keys.
 * 
 * @param {KeyboardEvent} keyboardEvent - Keyboard event.
 * @param {module:meteoJS/timeline~optionPressedKeys} pressedKeys
 *   Checks if this keys are pressed.
 * @private
 */
export function _isEventMatchPressedKeys(keyboardEvent, pressedKeys) {
  if (typeof pressedKeys != 'object' ||
      !('forEach' in pressedKeys))
    pressedKeys = [pressedKeys];
  if (pressedKeys.length == 0)
    return false;
  let result =
    [['ctrl', 'ctrlKey'],
      ['alt', 'altKey'],
      ['shift', 'shiftKey'],
      ['meta', 'metaKey']]
      .reduce((acc, cur) => acc && (((pressedKeys.indexOf(cur[0]) > -1))
        ? keyboardEvent[cur[1]]
        : !keyboardEvent[cur[1]]),
      true);
  pressedKeys.forEach(o => {
    switch (o) {
    case 'ctrl':
    case 'alt':
    case 'shift':
    case 'meta':  break;
    default:      if (o != keyboardEvent.keyCode) result = false;
    }
  });
  return result;
}