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

/**
 * Event fired before a request is executed.
 * 
 * @event module:meteoJS/repetitiveRequests#before:request
 */

/**
 * Event fired on a successful request.
 * 
 * @event module:meteoJS/repetitiveRequests#success:request
 * @property {external:XMLHttpRequest} request - XMLHttpRequest of the successful request.
 */

/**
 * Event fired if a request failed.
 * 
 * @event module:meteoJS/repetitiveRequests#error:request
 * @property {external:XMLHttpRequest} request - XMLHttpRequest of the failed request.
 */

/**
 * Options for constructor.
 * 
 * @typedef {Object} module:meteoJS/repetitiveRequests~options
 * @property {undefined|string} [url=undefined] - URL to make repetitive
 *   requests to. If undefined, no request will be done.
 * @property {string} [user=''] - User to send with request.
 * @property {string} [password=''] - Password to send with request.
 * @property {boolean} [start=true] - Start repetetive requests on construction.
 * @property {undefined|string} [defaultTimeout=undefined]
 *   Default timeout until next request, if response has no Cache-Control
 *   HTTP-Header. In milliseconds. If undefined, a further request will only be
 *   done, if the reponse returned a valid Cache-Control header.
 * @property {undefined|string} [timeoutOnError=undefined]
 *   Timeout until next request after a error response. In milliseconds. If
 *   undefined, no further request will be done after an error.
 * @property {boolean} [pauseOnHiddenDocument=false] - Pause making repetitive
 *   requests when document is hidden.
 * @property {''|'arraybuffer'|'blob'|'document'|'json'|'text'} [responseType='']
 *   Specifies the content type of the response.
 *   See {@link https://developer.mozilla.org/de/docs/Web/API/XMLHttpRequest/responseType}.
 */

/**
 * Makes requests again and again. Useful to stay up to date with
 *   the data available on the server. If the response returns a Cache-Control
 *   HTTP-Header, then the next request will be done per default after this
 *   time.
 * 
 * @fires module:meteoJS/repetitiveRequests#before:request
 * @fires module:meteoJS/repetitiveRequests#success:request
 * @fires module:meteoJS/repetitiveRequests#error:request
 */
export class RepetitiveRequests {
  
  /**
   * @param {module:meteoJS/repetitiveRequests~options} [options] - Options.
   */
  constructor({
    url = undefined,
    user = '',
    password = '',
    start = true,
    defaultTimeout = undefined,
    timeoutOnError = undefined,
    pauseOnHiddenDocument = false,
    responseType = ''
  } = {}) {
    
    /**
     * @type undefined|string
     * @private
     */
    this._url = url;
    
    /**
     * @type string
     * @private
     */
    this._user = user;
    
    /**
     * @type string
     * @private
     */
    this._password = password;
    
    /**
     * @type boolean
     * @private
     */
    this._isStarted = start;
    
    /**
     * @type undefined|integer
     * @private
     */
    this._defaultTimeout = defaultTimeout;
    
    /**
     * @type undefined|integer
     * @private
     */
    this._timeoutOnError = timeoutOnError;
    
    /**
     * @type boolean
     * @private
     */
    this._pauseOnHiddenDocument = pauseOnHiddenDocument;
    this._initPauseOnHiddenDocument();

    /**
     * @type boolean
     * @private
     */
    this._isSuppressedByHiddenDocument = false;
    
    /**
     * @type string
     * @private
     */
    this._responseType = responseType;
    
    /**
     * @type mixed
     * @private
     */
    this._timeoutID = undefined;
    
    /**
     * @type boolean
     * @private
     */
    this._loading = false;
    
    if (this._isStarted)
      this.start();
  }
  
  /**
   * Current URL to make requests to.
   * 
   * @type undefined|string
   */
  get url() {
    return this._url;
  }
  set url(url) {
    this._url = url;
  }
  
  /**
   * User to send with request.
   * 
   * @type string
   */
  get user() {
    return this._user;
  }
  set user(user) {
    this._user = user;
  }
  
  /**
   * Password to send with request.
   * 
   * @type string
   */
  get password() {
    return this._password;
  }
  set password(password) {
    this._password = password;
  }
  
  /**
   * Content type of the response.
   * 
   * @type string
   */
  get responseType() {
    return this._responseType;
  }
  set responseType(responseType) {
    this._responseType = responseType;
  }
  
  /**
   * Start repetitive requests. Makes immediatly the first request.
   */
  start() {
    this._isStarted = true;
    this._startRequest();
  }
  
  /**
   * Stops repetitive requests. Events aren't triggered anymore. Even if a
   * former request creates a response.
   */
  stop() {
    this._isStarted = false;
    if (this._timeoutID !== undefined) {
      clearTimeout(this._timeoutID);
      this._timeoutID = undefined;
    }
  }
  
  /**
   * Executes next request after the passed delay. If already another request
   * is planned, nothing is done.
   * 
   * @private
   * @param {integer} delay - Delay in milliseconds.
   */
  _planRequest({
    delay
  }) {
    if (this._timeoutID !== undefined)
      return;
    
    this._timeoutID = setTimeout(() => {
      if (this._pauseOnHiddenDocument
        && ('hidden' in document)
        && document.hidden) {
        this._isSuppressedByHiddenDocument = true;
        return;
      }

      this._startRequest();
    }, delay);
  }
  
  /**
   * Makes a new request and triggeres events.
   * 
   * @private
   */
  _startRequest() {
    if (this._timeoutID !== undefined) {
      clearTimeout(this._timeoutID);
      this._timeoutID = undefined;
    }
    
    this._makeRequest()
      .then(({ request }) => {
        if (!this._isStarted)
          return;
      
        let delay = this._defaultTimeout;
      
        // Read ResponseHeader
        let cacheControl = request.getResponseHeader('Cache-Control');
        if (cacheControl !== null) {
          let maxAges = /(^|,\s*)max-age=([0-9]+)($|\s*,)/.exec(cacheControl);
          if (maxAges !== null &&
            maxAges[2] > 0)
            delay = Math.round(maxAges[2]*1000);
        }
      
        this.trigger('success:request', { request });
      
        if (delay !== undefined)
          this._planRequest({ delay });
      }, ({ request } = {}) => {
        if (!this._isStarted)
          return;
      
        if (request === undefined)
          return;
      
        this.trigger('error:request', { request });
      
        if (this._timeoutOnError !== undefined)
          this._planRequest({ delay: this._timeoutOnError });
      }, ({ request = undefined }) => {
        /* Promise() returned by _makeRequest() also rejects, when URL isn't
         * defined or is actually loading. In these cases don't throw an
         * error event. */
        if (request !== undefined)
          this.trigger('error:request', { request });
      });
  }
  
  /**
   * Makes a new request immediatly, except another request is already loading.
   * 
   * @private
   * @returns {Promise}
   */
  async _makeRequest() {
    this.trigger('before:request');
    return new Promise((resolve, reject) => {
      if (this._url === undefined) {
        reject();
        return;
      }
      
      if (this._loading) {
        reject();
        return;
      }
      this._loading = true;
      
      let request = new XMLHttpRequest();
      if (this.responseType !== undefined)
        request.responseType = this.responseType;
      request.addEventListener('load', () => {
        this._loading = false;
        
        if (request.status == 200)
          resolve({ request });
        else
          reject({ request });
      });
      request.addEventListener('error', () => {
        this._loading = false;
        reject({ request });
      });
      
      request.open('GET', this._url, true, this._user, this._password);
      request.send();
    });
  }
  
  /**
   * @private
   */
  _initPauseOnHiddenDocument() {
    if (!this._pauseOnHiddenDocument)
      return;
    
    document.addEventListener('visibilitychange', () => {
      if (('hidden' in document)
        && !document.hidden
        && this._isSuppressedByHiddenDocument
        && this._isStarted) {
        this._isSuppressedByHiddenDocument = false;
        this.start();
      }
    });
  }
}
addEventFunctions(RepetitiveRequests.prototype);
export default RepetitiveRequests;