/**
 * @module meteoJS/synview/type
 */
import LayerGroup from 'ol/layer/Group';
import addEventFunctions from '../Events.js';
import ResourceCollection from './ResourceCollection.js';
import Resource from './Resource.js';

/**
 * Preload options.
 * 
 * @typedef {Object} module:meteoJS/synview/type~preloadOptions
 * @property {boolean} [enabled=false] - Enable preload of the resources.
 */

/**
 * Options for the constructor.
 * 
 * @typedef {Object} module:meteoJS/synview/type~options
 * @param {string|undefined} id ID.
 * @param {boolean} [visible] Visibility.
 * @param {undefined|number} [zIndex] zIndex on map.
 * @param {'nearest'|'floor'} [displayMethod]
 *   Method to determine the displayed resource.
 * @param {number} [displayMaxResourceAge]
 *   Maximum time space between display and resource time (in seconds).
 * @param {number} [displayFadeStart]
 *   Fade resource from this age to the display time (in seconds).
 * @param {number} [displayFadeStartOpacity]
 *   Opacity (between 0 and 1) at displayFadingTime.
 * @param {undefined|String} className - Classname.
 * @param {undefined|boolean} [imageSmoothingEnabled=undefined]
 *   Value of
 *   {@link https://developer.mozilla.org/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled|imageSmoothingEnabled}
 *   when drawing the layers of this type to canvas.
 *   Undefined uses the default (true). When a
 *   {@link module:meteoJS/synview/resource.Resource}
 *   has explicitly set an own value 
 *   ({@link module:meteoJS/synview/resource.Resource#imageSmoothingEnabled}),
 *   this will be ignored.
 * @param {module:meteoJS/synview/tooltip~contentFunction|undefined} [tooltip]
 *   Tooltip function. If color detection will be used with this type, you must
 *   set an unique className.
 * @param {module:meteoJS/synview/type~preloadOptions} [preload]
 *   Preload options.
 */

/**
 * Triggered on change of visibilty.
 * 
 * @event module:meteoJS/synview/type#change:visible
 */

/**
 * Triggered, if the set of timestamps changes (due to resource changes).
 * 
 * @event module:meteoJS/synview/type#change:resources
 */

/**
 * @classdesc Type to display by synview, like a serie of radar images.
 * 
 * @fires module:meteoJS/synview/type#change:visible
 */
export class Type {
  
  /**
   * @param {module:meteoJS/synview/type~options} options Options.
   */
  constructor({
    id = undefined,
    visible = true,
    zIndex = undefined,
    displayMethod = 'floor',
    displayMaxResourceAge = 3*3600,
    displayFadeStart = 15*60,
    displayFadeStartOpacity = 0.95,
    resources = undefined,
    className = undefined,
    imageSmoothingEnabled = undefined,
    tooltip = undefined,
    preload = {}
  } = {}) {
    /**
     * @type Object
     * @private
     */
    this.options = {
      id,
      visible,
      zIndex,
      displayMethod,
      displayMaxResourceAge,
      displayFadeStart,
      displayFadeStartOpacity,
      resources,
      className,
      imageSmoothingEnabled,
      tooltip
    };
    
    /**
     * The mapping group to display all the resources. (openlayers specific)
     * @member {undefined|external:ol/layer/Group~LayerGroup}
     * @default
     * @private
     */
    this.layerGroup = undefined;
    
    /**
     * Collection of resources.
     * @member {module:meteoJS/synview/resourceCollection.ResourceCollection}
     * @private
     */
    this.collection = new ResourceCollection();
    
    /**
     * Time of displayed resource.
     * @member {Date}
     * @private
     */
    this.displayedResourceTime = new Date('invalid');
    
    // Collection initialisieren
    this.collection.on('add:item', function (resource) {
      resource.className = this.className;
      this._addOLLayer(resource);
      if (this._preload.enabled)
        resource.preload();
    }, this);
    this.collection.on('remove:item', function (resource) {
      this._removeOLLayer(resource);
    }, this);
    this.collection.on('replace:item', function (newResource, oldResource) {
      if (newResource !== oldResource) {
        newResource.className = this.className;
        this._replaceOLLayer(newResource, oldResource);
        if (this._preload.enabled)
          newResource.preload();
      }
    }, this);
    
    /**
     * Preload options.
     * @type module:meteoJS/synview/type~preloadOptions
     * @private
     */
    this._preload = preload;
    
    if (this.options.resources !== undefined)
      this.collection.setResources(this.options.resources);
    delete this.options.resources;
  }
  
  /**
   * Returns ID of type.
   * 
   * @return {string|undefined}
   */
  getId() {
    return this.options.id;
  }
  
  /**
   * Sets ID of type.
   * 
   * @param {string|undefined} id ID.
   * @return {module:meteoJS/synview/type.Type} This.
   */
  setId(id) {
    this.options.id = id;
    return this;
  }
  
  /**
   * Returns visibility.
   * 
   * @return {boolean} Visibility.
   */
  getVisible() {
    return this.options.visible;
  }
  
  /**
   * Sets visibility.
   * 
   * @param {boolean} visible Visibility.
   * @return {module:meteoJS/synview/type.Type} This.
   * @fires module:meteoJS/synview/type#change:visible
   */
  setVisible(visible) {
    // Nur etwas unternehmen, falls Visible ändert
    if (this.options.visible ? !visible : visible) {
      this.options.visible = visible ? true : false;
      if (this.layerGroup !== undefined)
        this.layerGroup.setVisible(this.options.visible);
      this.getResourceCollection().getItems().forEach(function (resource) {
        if (isNaN(resource.getDatetime()))
          resource.setVisible(this.options.visible);
        resource.setLayerGroup(this.options.visible ? this.layerGroup : undefined);
      }, this);
      this.trigger('change:visible');
    }
    return this;
  }
  
  /**
   * Returns the z Index.
   * 
   * @return {undefined|number}
   */
  getZIndex() {
    return this.options.zIndex;
  }
  
  /**
   * Sets the z Index.
   * 
   * @param {undefined|number} zIndex z-Index.
   * @return {module:meteoJS/synview/type.Type} This.
   */
  setZIndex(zIndex) {
    this.options.zIndex = zIndex;
    if (this.layerGroup !== undefined)
      this.layerGroup.setZIndex(zIndex);
    this.getResourceCollection().getItems().forEach(function (resource) {
      resource.setZIndex(zIndex);
    });
    return this;
  }
  
  /**
   * Classname.
   * 
   * @type undefined|String
   */
  get className() {
    return this.options.className;
  }
  set className(className) {
    this.options.className = className;
  }
  
  /**
   * Returns layer-group of this type on the map.
   * 
   * return {external:ol/layer/Group~LayerGroup} Layer-group.
   */
  getLayerGroup() {
    return (this.layerGroup === undefined) ? new LayerGroup() : this.layerGroup;
  }
  
  /**
   * Sets map layer-group for this type.
   * 
   * @param {external:ol/layer/Group~LayerGroup} group layer-group.
   * @return {module:meteoJS/synview/type.Type} This.
   */
  setLayerGroup(group) {
    this.layerGroup = group;
    if (this.layerGroup !== undefined) {
      if ('setVisible' in this.layerGroup) // Leaflet doesn't know visibility
        this.layerGroup.setVisible(this.options.visible);
      this.layerGroup.setZIndex(this.options.zIndex);
    }
    this.getResourceCollection().getItems().forEach(function (resource) {
      resource.setLayerGroup(this.options.visible ? group : undefined);
    }, this);
    return this;
  }
  
  /**
   * Returns collection of the resources.
   * Note: If you directly append resources to the collection, no
   * {@link module:meteoJS/synview/type#change:resources} event will be fired.
   * 
   * @return {module:meteoJS/synview/resourceCollection.ResourceCollection} resourceCollection.
   */
  getResourceCollection() {
    return this.collection;
  }
  
  /**
   * Append a resource to the collection.
   * If type is visible, this might also change the resources on the map.
   * 
   * @param {module:meteoJS/synview/resource.Resource} resource Resource object.
   * @return {module:meteoJS/synview/type.Type} This.
   * @fires module:meteoJS/synview/type#change:resources
   */
  appendResource(resource) {
    this.collection.append(resource);
    
    // show current layer again
    this.setDisplayTime(this.displayedResourceTime);
    
    /* Trigger event after setDisplayTime, therewith the synview object can
     * set the desired time in the timeline object. */
    this.trigger('change:resources');
    return this;
  }
  
  /**
   * Removes a resource from the collection.
   * If type is visible, this might also change the resources on the map.
   * 
   * @param {module:meteoJS/synview/resource.Resource} resource Resource object.
   * @return {module:meteoJS/synview/type.Type} This.
   * @fires module:meteoJS/synview/type#change:resources
   */
  removeResource(resource) {
    // hide current layer
    this._hideVisibleResource();
    
    this.collection.remove(resource.getDatetime());
    
    // show current layer again
    this.setDisplayTime(this.displayedResourceTime);
    
    /* Trigger event after setDisplayTime, therewith the synview object can
     * set the desired time in the timeline object. */
    this.trigger('change:resources');
    return this;
  }
  
  /**
   * Sets resources in the collection (and replaces previous ones).
   * If type is visible, this might also change the resources on the map.
   * 
   * @param {module:meteoJS/synview/resource.Resource[]} resources List of resource objects.
   * @return {module:meteoJS/synview/type.Type} This.
   * @fires module:meteoJS/synview/type#change:resources
   */
  setResources(resources) {
    // hide current layer
    this._hideVisibleResource();
    
    this.collection.setResources(resources);
    
    // show current layer again
    this.setDisplayTime(this.displayedResourceTime);
    
    /* Trigger event after setDisplayTime, therewith the synview object can
     * set the desired time in the timeline object. */
    this.trigger('change:resources');
    return this;
  }
  
  /**
   * Returns resource of the displayed resource. If type contains resources
   * with timestamps as well as a static resource, only a resource with timestamp
   * will be returned. If type is invisible or no layer group is set, no resource
   * is display, therefore an empty resource will be returned.
   * 
   * @return {module:meteoJS/synview/resource.Resource} Resource.
   */
  getDisplayedResource() {
    if (this.getVisible() &&
        this.layerGroup !== undefined) {
      if (isNaN(this.displayedResourceTime))
        return (this.collection.getTimes().length > 0) ?
          new Resource() :
          this.collection.getResourceByTime(this.displayedResourceTime);
      else
        return this.collection.getResourceByTime(this.displayedResourceTime);
    }
    else
      return new Resource();
  }
  
  /**
   * Sets time to display. Corresponding to the options an adequate resource will
   * be searched and displayed. (accessible via getDisplayedResource())
   * 
   * @param {Date} time Display time.
   * @return {module:meteoJS/synview/type.Type} This.
   */
  setDisplayTime(time) {
    if (!this.getVisible())
      return this;
    var time_to_show = this._getResourceTimeByDisplayTime(time);
    if (time_to_show === undefined ||
        time_to_show !== undefined &&
        !isNaN(this.displayedResourceTime) &&
        this.displayedResourceTime.valueOf() != time_to_show.valueOf())
      this._hideVisibleResource();
    if (time_to_show !== undefined) {
      this.displayedResourceTime = time_to_show;
      var resource = this.getResourceCollection().getItemById(time_to_show.valueOf());
      if (resource.getId()) {
        resource.setVisible(true);
        var opacity = 1.0;
        if (Math.abs(time.valueOf() - time_to_show.valueOf()) > this.options.displayMaxResourceAge*1000) // 3h
          opacity = 0.0;
        else if (Math.abs(time.valueOf() - time_to_show.valueOf()) > this.options.displayFadeStart*1000) // 15min
          opacity = this.options.displayFadeStartOpacity *
            (Math.abs(time.valueOf() - time_to_show.valueOf()) -
             this.options.displayMaxResourceAge * 1000) /
            (1000 *
             (this.options.displayFadeStart - this.options.displayMaxResourceAge));
        resource.setOpacity(opacity);
      }
    }
    else
      this.displayedResourceTime = new Date('invalid');
    return this;
  }
  
  /**
   * Returns the current tooltip function, undefined for no tooltip.
   * 
   * @return {module:meteoJS/synview/tooltip~contentFunction|undefined} Tooltip function.
   */
  getTooltip() {
    return this.options.tooltip;
  }
  
  /**
   * Sets the tooltip function. Undefined for no tooltip.
   * 
   * @param {module:meteoJS/synview/tooltip~contentFunction|undefined} tooltip Tooltip function.
   * @return {module:meteoJS/synview/type.Type} This.
   */
  setTooltip(tooltip) {
    this.options.tooltip = tooltip;
    return this;
  }
  
  /**
   * Sets style of all resources (if resource has 'setOLStyle' method).
   * If argument 'style' isn't declared, the style will be updated.
   * Convenience method, you could also loop over all resources.
   * 
   * @param {externalol/style/Style~Style} [style] OpenLayers style.
   * @returns {module:meteoJS/synview/type.Type} This.
   */
  setResourcesOLStyle() {
    var styleArguments = arguments;
    this.getResourceCollection().getItems().forEach(function (resource) {
      if ('setOLStyle' in resource)
        resource.setOLStyle.apply(resource, styleArguments);
    });
    return this;
  }
  
  /**
   * Blendet aktuell dargestellten OL-Layer aus.
   * @private
   */
  _hideVisibleResource() {
    if (!isNaN(this.displayedResourceTime))
      this.getResourceCollection()
        .getItemById(this.displayedResourceTime.valueOf())
        .setVisible(false);
  }
  
  /**
   * Füge dem layers-Objekt einen neuen OL-Layer hinzu
   * @private
   * @param {module:meteoJS/synview/resource.Resource} resource Entsprechende Resource zum Hinzufügen
   */
  _addOLLayer(resource) {
    // Show static resources if visible
    if (isNaN(resource.getDatetime()))
      resource.setVisible(this.getVisible());
    if (this.options.imageSmoothingEnabled !== undefined &&
        resource.imageSmoothingEnabled === undefined)
      resource.imageSmoothingEnabled = this.options.imageSmoothingEnabled;
    resource.setLayerGroup(this.getLayerGroup());
    resource.setZIndex(this.options.zIndex);
  }
  
  /**
   * Löscht aus layers-Objekt einen OL-Layer
   * @private
   * @param {module:meteoJS/synview/resource.Resource} resource Entsprechende Resource zum Hinzufügen
   */
  _removeOLLayer(resource) {
    resource.setLayerGroup(undefined);
  }
  
  /**
   * Ersetzt im layers-Objekt einen OL-Layer
   * @private
   * @param {module:meteoJS/synview/resource.Resource} newResource Resource zum Hinzufügen
   * @param {module:meteoJS/synview/resource.Resource} oldResource Resource zum Ersetzen
   */
  _replaceOLLayer(newResource, oldResource) {
    this._removeOLLayer(oldResource);
    this._addOLLayer(newResource);
  }
  
  /**
   * Gibt eine Zeit mit vorhandener Resource zu einer darzustellenden Zeit zurück.
   * Es gibt dazu verschiedene Optionen (this.options.displayMethod):
   * 'nearest': Wähle die zeitlich nächstgelegene Resource aus
   * 'floor':   Wähle die Resource direkt zum Zeitpunkt oder zeitlich direkt vor
   *            dem Termin.
   * @private
   * @return {undefined|Date} Resource time or undefined if not existing.
   */
  _getResourceTimeByDisplayTime(time) {
    if (isNaN(time))
      return undefined;
    var resultTime = undefined;
    this.collection.getTimes().forEach(function (resourceTime) {
      /*if (resultTime === undefined)
        resultTime = resourceTime;
      else {*/
      switch (this.options.displayMethod) {
      case 'exact':
        if (time.valueOf() == resourceTime.valueOf())
          resultTime = resourceTime;
        break;
      case 'nearest':
        if (resultTime === undefined ||
                Math.abs(time.valueOf() - resourceTime.valueOf()) <
                  Math.abs(time.valueOf() - resultTime.valueOf()))
          resultTime = resourceTime;
        break;
      case 'floor':
      default:
        if (resultTime === undefined ||
                resourceTime.valueOf() <= time.valueOf() &&
                (time.valueOf() - resourceTime.valueOf() <
                 time.valueOf() - resultTime.valueOf()))
          resultTime = resourceTime;
      }
      //}
    }, this);
    return resultTime;
  }
  
}
addEventFunctions(Type.prototype);
export default Type;