/**
 * @module meteoJS/timeline/animation
 */
import $ from 'jquery';
import addEventFunctions from '../Events.js';
import Timeline from '../Timeline.js';

/**
 * Options for animation constructor.
 * 
 * @typedef {Object} module:meteoJS/timeline/animation~options
 * @param {module:meteoJS/timeline.Timeline} timeline - Timeline to animate.
 * @param {number} [restartPause=1.8]
 *   Time in seconds to pause before the animation restart.
 * @param {number} [imagePeriod=0.2]
 *   Time in seconds between animation of two images.
 *   Ignored, if imageFrequency is specified.
 * @param {number|undefined} [imageFrequency]
 *   Time of images during one second.
 * @param {boolean} [enabledStepsOnly=true] - Use only enabled times.
 * @param {boolean} [allEnabledStepsOnly=false]
 *   Use only times that are enabled by all sets of time.
 */

/**
 * Event on animation start.
 * 
 * @event module:meteoJS/timeline/animation#start:animation
 */

/**
 * Event on animation stop.
 * 
 * @event module:meteoJS/timeline/animation#stop:animation
 */

/**
 * Event on reaching last timestamp.
 * 
 * @event module:meteoJS/timeline/animation#end:animation
 */

/**
 * Event triggered immediatly before restart of animation.
 * 
 * @event module:meteoJS/timeline/animation#restart:animation
 */

/**
 * Event triggered when imageFrequency/imagePeriod is changed.
 * 
 * @event module:meteoJS/timeline/animation#change:imageFrequency
 */

/**
 * Event triggered when restartPause is changed.
 * 
 * @event module:meteoJS/timeline/animation#change:restartPause
 */

/**
 * Object to animate {@link module:meteoJS/timeline.Timeline}.
 * 
 * <pre><code>import Animation from 'meteojs/timeline/Animation';</code></pre>
 */
export class Animation {
  
  /**
   * @param {module:meteoJS/timeline/animation~options} options - Options.
   */
  constructor({ timeline,
    restartPause = 1.8,
    imagePeriod = 0.2,
    imageFrequency = undefined,
    enabledStepsOnly = true,
    allEnabledStepsOnly = false } = {}) {
    /**
     * @type module:meteoJS/timeline/animation~options
     * @private
     */
    this.options = {
      timeline,
      restartPause,
      imagePeriod,
      imageFrequency,
      enabledStepsOnly,
      allEnabledStepsOnly
    };
    // Normalize options
    if (this.options.timeline === undefined)
      this.options.timeline = new Timeline();
    if (this.options.imageFrequency !== undefined &&
        this.options.imageFrequency != 0)
      this.options.imagePeriod = 1/this.options.imageFrequency;
    /**
     * ID to window.setInterval() of the animation.
     * If undefined, there is no started animation.
     * @type undefined|number
     * @private
     */
    this.animationIntervalID = undefined;
    
    /**
     * ID to window.setTimeout() ot the animation (used for restart-pause).
     * If undefined, there is no started setTimeout (i.e. no restart-pause).
     * @type undefined|number
     * @private
     */
    this.animationTimeoutID = undefined;
    
    /**
     * Current position in this.times in the animation.
     * @type integer
     * @private
     */
    this.animationStep = 0;
    
    /**
     * Hash with timestamps-valueOf's as keys and index in this.times as values.
     * @type Object
     * @private
     */
    this.timesHash = {};
    
    /**
     * List of timestamps. Current list of times of the timeline to animate over.
     * @type Date[]
     * @private
     */
    this.times = [];
    
    // Timeline initialisieren
    let onChangeTimes = () => {
      this.times = this.options.timeline[this._getTimelineTimesMethod()]();
      this.timesHash = {};
      this.times.forEach((time, i) => this.timesHash[time.valueOf()] = i);
    };
    this.options.timeline.on(this._getTimelineChangeTimesEvent(), onChangeTimes);
    onChangeTimes();
  }
  
  /**
   * Returns time period between two animation steps (in s).
   * 
   * @returns {number} Time period.
   */
  getImagePeriod() {
    return this.options.imagePeriod;
  }
  
  /**
   * Sets time period between to animation steps (in s)
   * 
   * @param {number} imagePeriod - Time period.
   * @returns {module:meteoJS/timeline/animation.Animation} This.
   */
  setImagePeriod(imagePeriod) {
    this.options.imagePeriod = imagePeriod;
    if (this.isStarted())
      this._updateAnimation();
    this.trigger('change:imageFrequency');
    return this;
  }
  
  /**
   * Returns time frequency of animation steps (in 1/s).
   * 
   * @returns {number} Time frequency.
   */
  getImageFrequency() {
    return 1/this.options.imagePeriod;
  }
  
  /**
   * Sets time frequency of animation steps (in 1/s).
   * 
   * @param {number} imageFrequency - Time frequency.
   * @returns {module:meteoJS/timeline/animation.Animation} This.
   */
  setImageFrequency(imageFrequency) {
    if (imageFrequency != 0)
      this.setImagePeriod(1/imageFrequency);
    return this;
  }
  
  /**
   * Returns time duration before a restart (jump from end to beginning, in s).
   * 
   * @returns {number} Time duration.
   */
  getRestartPause() {
    return this.options.restartPause;
  }
  
  /**
   * Sets time duration before a restart (in s).
   * 
   * @param {number} restartPause - Time duration.
   * @returns {module:meteoJS/timeline/animation.Animation} This.
   */
  setRestartPause(restartPause) {
    this.options.restartPause = Number(restartPause); // Convert string to number
    this.trigger('change:restartPause');
    return this;
  }
  
  /**
   * Is animation started.
   * 
   * @returns {boolean}
   */
  isStarted() {
    return this.animationIntervalID !== undefined ||
           this.animationTimeoutID !== undefined;
  }
  
  /**
   * Starts the animation.
   * 
   * @returns {module:meteoJS/timeline/animation.Animation} This.
   * @fires module:meteoJS/timeline/animation#start:animation
   */
  start() {
    if (this.options.timeline.getSelectedTime().valueOf() in this.timesHash)
      this._setStep(this.timesHash[this.options.timeline.getSelectedTime().valueOf()]);
    if (!this.isStarted())
      this._updateAnimation();
    this.trigger('start:animation');
  }
  
  /**
   * Stops the animation.
   * 
   * @returns {module:meteoJS/timeline/animation.Animation} This.
   * @fires module:meteoJS/timeline/animation#stop:animation
   */
  stop() {
    this._clearAnimation();
    this.trigger('stop:animation');
  }
  
  /**
   * Toggles the animation.
   * 
   * @returns {module:meteoJS/timeline/animation.Animation} This.
   */
  toggle() {
    if (this.isStarted())
      this.stop();
    else
      this.start();
  }
  
  /**
   * Setzt Schritt der Animation
   * @private
   * @param {number} step
   */
  _setStep(step) {
    if (0 <= step && step < this._getCount())
      this.animationStep = step;
  }
  
  /**
   * Gibt timeline-Event Name zum abhören von Änderungen der Zeitschritte zurück.
   * @private
   * @returns {string}
   */
  _getTimelineChangeTimesEvent() {
    return (this.options.enabledStepsOnly || this.options.allEnabledStepsOnly)
      ? 'change:enabledTimes' : 'change:times';
  }
  
  /**
   * Gibt timeline-Methode aller Zeitschritte zurück.
   * @private
   * @returns {string}
   */
  _getTimelineTimesMethod() {
    return this.options.allEnabledStepsOnly ? 'getAllEnabledTimes' :
      this.options.enabledStepsOnly ? 'getEnabledTimes' : 'getTimes';
  }
  
  /**
   * Gibt Anzahl Animationsschritte zurück
   * @private
   * @returns {number}
   */
  _getCount() {
    return this.options.timeline[this._getTimelineTimesMethod()]().length;
  }
  
  /**
   * Handelt die Animation
   * @private
   * @fires module:meteoJS/timeline/animation#end:animation
   * @fires module:meteoJS/timeline/animation#restart:animation
   */
  _updateAnimation() {
    this._clearAnimation();
    if (this.animationStep < this._getCount()-1)
      this._initAnimation();
    else
      this._initRestartPause();
  }
  
  /**
   * Startet Animation
   * @private
   */
  _initAnimation() {
    if (this.animationIntervalID === undefined)
      this.animationIntervalID = window.setInterval(() => {
        this.animationStep++;
        if (this.animationStep < this.times.length)
          this.options.timeline.setSelectedTime(this.times[this.animationStep]);
        if (this.animationStep >= this._getCount()-1) {
          this.trigger('end:animation');
          this._clearAnimation();
          this._initRestartPause();
        }
      }, this.options.imagePeriod * 1000);
  }
  
  /**
   * Startet den Timer für die Restart-Pause
   * Verwende als Zeitspanne imagePeriod+restartPause. Sonst wird bei restartPause
   * 0s der letzte Zeitschritt gar nie angezeigt.
   * @private
   */
  _initRestartPause() {
    if (this.animationTimeoutID === undefined)
      this.animationTimeoutID = window.setTimeout(() => {
        this.animationStep = 0;
        this.trigger('restart:animation');
        if (this.animationStep < this.times.length)
          this.options.timeline.setSelectedTime(this.times[this.animationStep]);
        this._clearAnimation();
        this._initAnimation();
      }, (this.options.imagePeriod + this.options.restartPause) * 1000);
  }
  
  /**
   * Löscht window.interval, falls vorhanden
   * @private
   */
  _clearAnimation() {
    if (this.animationIntervalID !== undefined) {
      window.clearInterval(this.animationIntervalID);
      this.animationIntervalID = undefined;
    }
    if (this.animationTimeoutID !== undefined) {
      window.clearTimeout(this.animationTimeoutID);
      this.animationTimeoutID = undefined;
    }
  }
  
}
addEventFunctions(Animation.prototype);
export default Animation;

/**
 * Insert an input-group to change frequency.
 * 
 * <pre><code>import { insertFrequencyInput } from 'meteojs/timeline/Animation';</code></pre>
 * 
 * @param {external:jQuery} node - Node to insert input-group.
 * @param {Object} options - Options for input-group.
 * @param {module:meteoJS/timeline/animation.Animation} options.animation
 *   Animation object.
 * @param {string} [options.suffix='fps'] - Suffix text for input-group.
 * @returns {external:jQuery} Input-group node.
 */
export function insertFrequencyInput(node, { animation, suffix = 'fps' }) {
  const number = $('<input>')
    .addClass('form-control')
    .attr('type', 'number')
    .attr('min', 1)
    .attr('step', 1);
  const inputGroupNumber = $('<div>')
    .addClass('input-group')
    .append(number)
    .append($('<span>').addClass('input-group-text').text(suffix));
  number.on('change', () => animation.setImageFrequency(number.val()));
  const onChangeImageFrequency = () => number.val(animation.getImageFrequency());
  animation.on('change:imageFrequency', onChangeImageFrequency);
  onChangeImageFrequency();
  node.append(inputGroupNumber);
  return inputGroupNumber;
}

/**
 * Insert an input-range to change frequency.
 * 
 * <pre><code>import { insertFrequencyRange } from 'meteojs/timeline/Animation';</code></pre>
 * 
 * @param {external:jQuery} node - Node to insert input-range.
 * @param {Object} options - Options for input-range.
 * @param {module:meteoJS/timeline/animation.Animation} options.animation
 *   Animation object.
 * @param {number[]} options.frequencies - Frequencies to select.
 * @returns {external:jQuery} Input-range node.
 */
export function insertFrequencyRange(node, { animation, frequencies }) {
  frequencies = frequencies ? frequencies : [1];
  let range = $('<input>')
    .addClass('form-range')
    .attr('type', 'range')
    .attr('min', 0)
    .attr('max', frequencies.length-1);
  range.on('change input', () => {
    let i = range.val();
    if (i < frequencies.length)
      animation.setImageFrequency(frequencies[i]);
  });
  let onChangeImageFrequency = () => {
    let i = frequencies.indexOf(animation.getImageFrequency());
    if (i > -1)
      range.val(i);
  };
  animation.on('change:imageFrequency', onChangeImageFrequency);
  onChangeImageFrequency();
  node.append(range);
  return range;
}

/**
 * Insert an button-group to change frequency.
 * 
 * <pre><code>import { insertFrequencyButtonGroup } from 'meteojs/timeline/Animation';</code></pre>
 * 
 * @param {external:jQuery} node - Node to insert the button-group.
 * @param {Object} options - Options for the button-group.
 * @param {module:meteoJS/timeline/animation.Animation} options.animation
 *   Animation object.
 * @param {number[]} options.frequencies - Frequencies to select.
 * @param {string|undefined} [options.btnGroupClass='btn-group']
 *   Class added to the button-group node.
 * @param {string|undefined} [options.btnClass='btn btn-primary']
 *   Class added to each button.
 * @param {string} [options.suffix='fps']
 *   Suffix text for each button after frequency.
 * @returns {external:jQuery} Button-group node.
 */
export function insertFrequencyButtonGroup(node, {
  animation,
  frequencies,
  btnGroupClass = 'btn-group',
  btnClass = 'btn btn-primary',
  suffix = 'fps'
}) {
  let btnGroup = $('<div>').addClass(btnGroupClass);
  frequencies = frequencies ? frequencies : [];
  frequencies.forEach(freq => {
    btnGroup.append($('<button>')
      .addClass(btnClass)
      .data('frequency', freq)
      .text(freq + ' ' + suffix)
      .click(() => animation.setImageFrequency(freq)));
  });
  let onChange = () => {
    btnGroup.children('button').removeClass('active').each(function () {
      if ($(this).data('frequency') == animation.getImageFrequency())
        $(this).addClass('active');
    });
  };
  animation.on('change:imageFrequency', onChange);
  onChange();
  node.append(btnGroup);
  return btnGroup;
}

/**
 * Insert an input-group to change restart pause.
 * 
 * <pre><code>import { insertRestartPauseInput } from 'meteojs/timeline/Animation';</code></pre>
 * 
 * @param {external:jQuery} node - Node to insert input-group.
 * @param {Object} options - Options for input-group.
 * @param {module:meteoJS/timeline/animation.Animation} options.animation
 *   Animation object.
 * @param {string} [options.suffix='s'] - Suffix text for input-group.
 * @returns {external:jQuery} Input-group node.
 */
export function insertRestartPauseInput(node, { animation, suffix = 's' }) {
  const input = $('<input>')
    .addClass('form-control')
    .attr('type', 'number')
    .attr('min', 0)
    .attr('step', 0.1);
  const inputGroupNumber = $('<div>')
    .addClass('input-group')
    .append(input)
    .append($('<span>').addClass('input-group-text').text(suffix));
  input.on('change', () => animation.setRestartPause(input.val()));
  const onChange = () => input.val(animation.getRestartPause());
  animation.on('change:restartPause', onChange);
  onChange();
  node.append(inputGroupNumber);
  return inputGroupNumber;
}

/**
 * Insert an input-range to change restart pause.
 * 
 * <pre><code>import { insertRestartPauseRange } from 'meteojs/timeline/Animation';</code></pre>
 * 
 * @param {external:jQuery} node - Node to insert input-range.
 * @param {Object} options - Options for input-range.
 * @param {module:meteoJS/timeline/animation.Animation} options.animation
 *   Animation object.
 * @param {number[]} options.pauses - Restart pauses to select.
 * @returns {external:jQuery} Input-range node.
 */
export function insertRestartPauseRange(node, { animation, pauses }) {
  pauses = pauses ? pauses : [1];
  pauses = pauses.map(p => Math.round(p * 1000));
  let range = $('<input>')
    .addClass('form-range')
    .attr('type', 'range')
    .attr('min', 0)
    .attr('max', pauses.length-1);
  range.on('change input', () => {
    let i = range.val();
    if (i < pauses.length)
      animation.setRestartPause(pauses[i] / 1000);
  });
  let onChangeImageFrequency = () => {
    let i =
      pauses.indexOf(Math.round(animation.getRestartPause() * 1000));
    if (i > -1)
      range.val(i);
  };
  animation.on('change:imageFrequency', onChangeImageFrequency);
  onChangeImageFrequency();
  node.append(range);
  return range;
}

/**
 * Insert an button-group to change restart pause.
 * 
 * <pre><code>import { insertRestartPauseButtonGroup } from 'meteojs/timeline/Animation';</code></pre>
 * 
 * @param {external:jQuery} node - Node to insert the button-group.
 * @param {Object} options - Options for the button-group.
 * @param {module:meteoJS/timeline/animation.Animation} options.animation
 *   Animation object.
 * @param {number[]} options.pauses - Restart pauses to select.
 * @param {string|undefined} [options.btnGroupClass='btn-group']
 *   Class added to the button-group node.
 * @param {string|undefined} [options.btnClass='btn btn-primary']
 *   Class added to each button.
 * @param {string} [options.suffix='s']
 *   Suffix in each button after duration text.
 * @returns {external:jQuery} Button-group node.
 */
export function insertRestartPauseButtonGroup(node, {
  animation,
  pauses,
  btnGroupClass = 'btn-group',
  btnClass = 'btn btn-primary',
  suffix = 's'
}) {
  let btnGroup = $('<div>').addClass(btnGroupClass);
  pauses = pauses ? pauses : [];
  pauses.forEach(pause => {
    btnGroup.append($('<button>')
      .addClass(btnClass)
      .data('pause', pause)
      .text(pause + ' ' + suffix)
      .click(() => animation.setRestartPause(pause)));
  });
  let onChange = () => {
    btnGroup.children('button').removeClass('active').each(function () {
      if ($(this).data('pause') == animation.getRestartPause())
        $(this).addClass('active');
    });
  };
  animation.on('change:restartPause', onChange);
  onChange();
  node.append(btnGroup);
  return btnGroup;
}