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

/**
 * Returns a textual representation of a time according to a passed format.
 * 
 * This function could be used to convert Date-objects to a readable time with
 * external libraries. One such implementation is given by
 * {@link module:meteoJS/timeline/visualisation.makeTimeTextCallbackFunction}.
 * 
 * @typedef {Function}
 *   module:meteoJS/timeline/visualisation~timeTextCallbackFunction
 * @param {Date} time - A valid datetime.
 * @param {string} format - Format string.
 * @returns {string} Textual representation.
 */

/**
 * Options for Visualisation.
 * 
 * @typedef {Object} module:meteoJS/timeline/visualisation~options
 * @param {module:meteoJS/timeline.Timeline} [timeline]
 *   Timeline object.
 * @param {external:jQuery} [node] - Node.
 * @param {module:meteoJS/timeline/animation.Animation} [animation]
 *   Animation object. If specified, the animation will be stopped on user
 *   interaction with the visualisation object.
 * @param {boolean} [enabledStepsOnly=true] - Use only enabled times.
 * @param {boolean} [allEnabledStepsOnly=false]
 *   Use only times that are enabled by all sets of time.
 * @param {string} [textInvalid='-']
 *   Output string, if time of timeline is invalid.
 * @param {module:meteoJS/timeline/visualisation~timeTextCallbackFunction}
 *   [getTimeText]
 *   Returns a textual representation of a time according to a passed format.
 * @param {string} [outputTimezone]
 *   'local' for browser local timezone.
 */

/**
 * Object to visualise {@link module:meteoJS/timeline.Timeline}.
 * 
 * How to create your own visualisation object:
 * * Inherit this object
 * * Inherit method onChangeTime()
 * * Inherit method onChangeTimes()
 * * Inherit method emptyNode()
 * * Inherit method onInitNode()
 * * To get all times to display use getTimelineTimes()
 * * Use method timeToText() to convert a Date to String.
 * * Use method attachEventListener() to attach event listeners on a object.
 *   The event listener will be deleted automatically if the output is muted.
 * * At the end of the constructor call "this.setNode(this.options.node);".
 * 
 * <pre><code>import Visualisation from 'meteojs/timeline/Visualisation';</code></pre>
 * 
 * @abstract
 * @listens module:meteoJS/timeline#change:time
 * @listens module:meteoJS/timeline#change:times
 * @listens module:meteoJS/timeline#change:enabledTimes
 */
export class Visualisation {
  
  /**
   * @param {module:meteoJS/timeline/visualisation~options} options - Options.
   */
  constructor({
    timeline = undefined,
    node = undefined,
    animation = undefined,
    enabledStepsOnly = true,
    allEnabledStepsOnly = false,
    textInvalid = '-',
    getTimeText = undefined,
    outputTimezone = undefined
  } = {}) {
    /**
     * @type Object
     * @private
     */
    this.options = {
      timeline,
      node,
      animation,
      enabledStepsOnly,
      allEnabledStepsOnly,
      textInvalid,
      getTimeText,
      outputTimezone
    };
    
    // Normalize options
    if (this.options.timeline === undefined)
      this.options.timeline = new Timeline();
    
    /**
     * @member {Array[]}
     * @private
     */
    this.listeners = [];
    
    /**
     * @member {undefined|mixed}
     * @private
     */
    this.inputListener = undefined;
  }
  
  /**
   * Sets jQuery-Node for output.
   * 
   * @public
   * @param {external:jQuery|undefined} node Node, undefined to mute the output.
   * @returns {module:meteoJS/timeline/visualisation.Visualisation} This.
   */
  setNode(node) {
    if (this.options.node !== undefined)
      this.emptyNode();
  
    if (node === undefined) {
      this.detachEventListeners();
      this.options.node = node;
    }
    else {
      this.options.node = node;
      var isListenersDefined = this.listeners.length > 0;
      if (!isListenersDefined) {
        this.attachEventListener(this.options.timeline, 'change:time', function () {
          this.onChangeTime();
        }, this);
        this.attachEventListener(this.options.timeline, 'change:times', function () {
          this.onChangeTimes();
        }, this);
        this.attachEventListener(this.options.timeline, 'change:enabledTimes', function () {
          this.onChangeTimes();
        }, this);
      }
      this.onInitNode(isListenersDefined);
      this.onChangeTimes();
      this.onChangeTime();
    }
  
    if (this.inputListener === undefined)
      this.inputListener = this.on('input', function () {
        if (this.options.animation !== undefined)
          this.options.animation.stop();
      }, this);
  
    return this;
  }
  
  /**
   * Gets current value of output timezone.
   * 
   * @public
   * @returns {string|undefined} Output timezone.
   */
  getOutputTimezone() {
    return this.options.outputTimezone;
  }
  
  /**
   * Sets output timezone, undefined for UTC.
   * 
   * @public
   * @param {string|undefined} outputTimezone Timezone for datetime output.
   * @returns {module:meteoJS/timeline/visualisation.Visualisation} This.
   */
  setOutputTimezone(outputTimezone) {
    var updateOutput = (this.options.outputTimezone != outputTimezone);
    this.options.outputTimezone = outputTimezone;
    if (updateOutput &&
        this.options.node !== undefined) {
      this.onChangeTimes();
      this.onChangeTime();
    }
    return this;
  }
  
  /**
   * Called if the timeline triggers the
   * {@link module:meteoJS/timeline#change:time} event.
   * Prerequisite: this.options.node must be defined.
   * 
   * @abstract
   * @protected
   */
  onChangeTime() {}
  
  /**
   * Called if the timeline triggers the {@link module:meteoJS/timeline#change:times}
   * or {@link module:meteoJS/timeline#change:enabledTimes} event.
   * Prerequisite: this.options.node must be defined.
   * 
   * @abstract
   * @protected
   */
  onChangeTimes() {}
  
  /**
   * Called to empty the output node. Mainly if the output is muted.
   * Prerequisite: this.options.node must be defined.
   * 
   * @abstract
   * @protected
   */
  emptyNode() {}
  
  /**
   * Called once an output node is set.
   * Prerequisite: this.options.node must be defined.
   * 
   * @abstract
   * @protected
   * @param {boolean} isListenersDefined
   *   True if the event listeners are already set.
   */
  onInitNode() {}
  
  /**
   * Returns the times to display. This could be either all times in the timeline
   * or only the enabled times or the all enabled times. The user of the
   * visualisation object select this by the options.
   * 
   * @protected
   * @returns {Date[]} Times.
   */
  getTimelineTimes() {
    var methodName = this.options.allEnabledStepsOnly ?
      'getAllEnabledTimes' :
      this.options.enabledStepsOnly ? 'getEnabledTimes' : 'getTimes';
    return this.options.timeline[methodName]();
  }
  
  /**
   * Converts a Date-object to a string.
   * 
   * @protected
   * @param {Date} time - Time.
   * @param {string} format - Format string, passed to the .
   * @returns {string} String.
   */
  timeToText(time, format) {
    if (isNaN(time))
      return this.options.textInvalid;
    if (this.options.getTimeText !== undefined)
      return this.options.getTimeText.call(this, time, format);
    return time.toISOString();
  }
  
  /**
   * Attach an event listener on an object. Object could be a jQuery-object or
   * an object using {@link module:meteoJS/events}.
   * 
   * @protected
   * @param {Object} obj - Object to put the event listener on.
   * @param {mixed} listener - Event listener key.
   * @param {Function} func - Function to be executed when event is triggered.
   * @param {Object} [thisArg] - This in the function func when event triggered.
   */
  attachEventListener(obj, listener, func, thisArg) {
    this.listeners.push([obj, listener]);
    obj.on(listener, func, thisArg);
  }
  
  /**
   * Detaches all event listeners.
   * 
   * @private
   */
  detachEventListeners() {
    this.listeners.forEach(function (listenerArr) {
      if ('un' in listenerArr[0])
        listenerArr[0].un(listenerArr[1]);
      else if ('off' in listenerArr[0])
        listenerArr[0].off(listenerArr[1]);
    });
    this.listeners = [];
  }
  
}
addEventFunctions(Visualisation.prototype);
export default Visualisation;

/**
 * moment.js object.
 * 
 * @external momentjs
 * @see {@link https://momentjs.com}
 */

/**
 * Format a Date-object via the {@link https://momentjs.com|Moment.js} library.
 * 
 * <pre><code>import { makeTimeTextCallbackFunction } from 'meteojs/timeline/Visualisation';</code></pre>
 * 
 * @param {external:momentjs} moment - Moment.js object.
 * @returns {module:meteoJS/timeline/visualisation~timeTextCallbackFunction}
 *   Callback.
 */
export function makeTimeTextCallbackFunction(moment) {
  return function (time, format) {
    const m = moment.utc(time);
    if (this.options.outputTimezone !== undefined)
      (this.options.outputTimezone == 'local')
        ? m.local()
        : m.tz(this.options.outputTimezone);
    return m.format(format);
  };
}