import ElementCollector from 'element-collector';
import {getIdentifiedElement} from 'identify-element';

import getCurrentState from './get-current-state';
import getUniqueKeys from './get-unique-keys';

import {createTicker} from './ticker';

import {
  filterProperties,
  shouldCallbackFire,
  callback_properties
} from './utilities';


const global_ticker = createTicker();


const default_options = {
  interval: 100,
  ticker: global_ticker
};


/**
 * Class for monitoring of changes in element's existence, visibility, size or position.
 */
export class ElementMonitor {

  /**
   * Create ElementMonitor object.
   * @param {string|Array.<Element>|HTMLCollection|Function} elements
   * @param {Object} callbacks
   * @param {Object} options
   */
  constructor (elements, callbacks = {}, options) {
    this._ticker = null;
    this._memory = null;
    this.elements = new ElementCollector(elements);

    this._callbacks = {};
    for (const callback_id in callbacks) {
      this.setCallback(callback_id, callbacks[callback_id]);
    }

    this._options = {};
    this.setOptions(default_options);
    this.setOptions(options);
  }

  /**
   * Updates existing options with new ones.
   * @param {Object} options
   */
  setOptions (options = {}) {
    this._options = Object.assign(this._options, options);
  }

  /**
   * Adds new callback.
   * @param {string} id
   * @param {Function} callback
   */
  setCallback (id, callback) {
    const is_valid = typeof callback_properties[id] !== 'undefined';
    const is_function = typeof callback === 'function';
    if (is_valid && is_function) {
      this._callbacks[id] = callback;
      this._memory = this._getCurrentState();
    }
  }

  /**
   * Starts monitoring of matching elements.
   */
  start () {
    this.stop();
    this._memory = this._getCurrentState();
    this._ticker = this._options.ticker.add(
      this._options.interval,
      () => this._tick()
    );
  }

  /**
   * Stops monitoring of matching elements.
   */
  stop () {
    if (this._ticker) {
      this._ticker.remove();
    }
    this._ticker = null;
  }

  /**
   * Called periodically to check situation and react to changes.
   * @private
   */
  _tick () {
    const current_state = this._getCurrentState();
    const all_element_ids = getUniqueKeys([this._memory, current_state]);

    all_element_ids.forEach((element_id) => {
      const prev = this._memory[element_id];
      const next = current_state[element_id];
      const callbacks_to_fire = this._getCallbacksToFire(prev, next);
      this._fireCallbacks(element_id, callbacks_to_fire, prev, next);
    });

    this._memory = current_state;
  }

  /**
   * Retrieves information about current state of all matching elements.
   * @private
   */
  _getCurrentState () {
    let properties = [];

    Object.keys(this._callbacks).forEach((key) => {
      properties = properties.concat(callback_properties[key]);
    });

    return getCurrentState(this.elements.get(), properties);
  }

  /**
   * Determines which callbacks should be fired, based on which properties of matching elements changed since last check.
   * @param {Object} prev
   * @param {Object} next
   * @returns {Array}
   * @private
   */
  _getCallbacksToFire (prev, next) {
    const result = [];

    Object.keys(this._callbacks).forEach((callback_id) => {
      if (shouldCallbackFire(callback_id, prev, next)) {
        result.push(callback_id);
      }
    });

    return result;
  }

  /**
   * Fires a callback.
   * @param {string} id
   * @param {Array} callbacks
   * @param {Object} prev
   * @param {Object} next
   * @private
   */
  _fireCallbacks(id, callbacks, prev, next) {
    callbacks.forEach((callback_id) => {
      const props = callback_properties[callback_id];
      const filtered_prev = filterProperties(prev, props);
      const filtered_next = filterProperties(next, props);
      const element = getIdentifiedElement(id);
      this._callbacks[callback_id](
        element, filtered_prev, filtered_next, parseInt(id)
      );
    });
  }

}