text: import ClusterItem from './item/ClusterItem';
  
  import { ReservedGroupIds } from './ItemSet';
  
  export default class ClusterGenerator {
      /**
       * @param {ItemSet} itemSet itemsSet instance
       * @constructor ClusterGenerator
      */
      constructor(itemSet) {
          this.itemSet = itemSet;
          this.groups = {};
          this.cache = {};
          this.cache[-1] = [];
      }
      
      /**
       * 
       * @param {Object} data             Object containing parameters start content, className.
       * @param {{toScreen: function, toTime: function}} conversion
       *                                  Conversion functions from time to screen and vice versa
       * @param {Object} [options]        Configuration options
      */
      createClusterItem(itemData, conversion, options) {
          const newItem = new ClusterItem(itemData, conversion, options);
          return newItem;
      }
  
      /**
       * Set the items to be clustered.
       * This will clear cached clusters.
       * @param {Item[]} items
       * @param {Object} [options]  Available options:
       *                            {boolean} applyOnChangedLevel
       *                                If true (default), the changed data is applied
       *                                as soon the cluster level changes. If false,
       *                                The changed data is applied immediately
       */
      setItems(items, options) {
          this.items = items || [];
          this.dataChanged = true;
          this.applyOnChangedLevel = false;
  
          if (options && options.applyOnChangedLevel) {
              this.applyOnChangedLevel = options.applyOnChangedLevel;
          }
      }
  
      /**
       * Update the current data set: clear cache, and recalculate the clustering for
       * the current level
       */
      updateData() {
          this.dataChanged = true;
          this.applyOnChangedLevel = false;
      }
  
      /**
       * Cluster the events which are too close together
       * @param {{maxItems: number, clusterCriteria: function, titleTemplate: string}} options 
       * @param {scale: number} scale      The scale of the current window : (windowWidth / (endDate - startDate)) 
       * @return {Item[]} clusters
      */
      getClusters(oldClusters, scale, options) {
          let { maxItems, clusterCriteria } = typeof options === "boolean" ? {} : options;
          
          if (!clusterCriteria) {
              clusterCriteria = () => true;
          }
  
          maxItems = maxItems || 1;
  
          let level = -1;
          let granularity = 2;
          let timeWindow = 0;
  
          if (scale > 0) {
              if (scale >= 1) {
                  return [];
              }
              
              level = Math.abs(Math.round(Math.log(100 / scale) / Math.log(granularity)));
              timeWindow = Math.abs(Math.pow(granularity, level));
          }
  
          // clear the cache when and re-generate groups the data when needed.
          if (this.dataChanged) {
              const levelChanged = (level != this.cacheLevel);
              const applyDataNow = this.applyOnChangedLevel ? levelChanged : true;
              if (applyDataNow) {
                  this._dropLevelsCache();
                  this._filterData();
              }
          }
  
          this.cacheLevel = level;
          let clusters = this.cache[level];
          if (!clusters) {
              clusters = [];
              for (let groupName in this.groups) {
                  if (this.groups.hasOwnProperty(groupName)) {
                      const items = this.groups[groupName];
                      const iMax = items.length;
                      let i = 0;
                      while (i < iMax) {
                          // find all items around current item, within the timeWindow
                          let item = items[i];
                          let neighbors = 1; // start at 1, to include itself)
  
                          // loop through items left from the current item
                          let j = i - 1;
                          while (j >= 0 && (item.center - items[j].center) < timeWindow / 2) {
                              if (!items[j].cluster && clusterCriteria(item.data, items[j].data)) {
                                  neighbors++;
                              }
                              j--;
                          }
  
                          // loop through items right from the current item
                          let k = i + 1;
                          while (k < items.length && (items[k].center - item.center) < timeWindow / 2) {
                              if (clusterCriteria(item.data, items[k].data)) {
                                  neighbors++;
                              }
                              k++;
                          }
  
                          // loop through the created clusters
                          let l = clusters.length - 1;
                          while (l >= 0 && (item.center - clusters[l].center) < timeWindow) {
                              if (item.group == clusters[l].group && clusterCriteria(item.data, clusters[l].data)) {
                                  neighbors++;
                              }
                              l--;
                          }
  
                          // aggregate until the number of items is within maxItems
                          if (neighbors > maxItems) {
                              // too busy in this window.
                              const num = neighbors - maxItems + 1;
                              const clusterItems = [];
  
                              // append the items to the cluster,
                              // and calculate the average start for the cluster
                              let m = i;
                              while (clusterItems.length < num && m < items.length) {
                                  if (clusterCriteria(items[m].data, items[m].data)) {
                                      clusterItems.push(items[m]);
                                  }
                                  m++;
                              }
  
                              const groupId = this.itemSet.getGroupId(item.data);
                              const group = this.itemSet.groups[groupId] || this.itemSet.groups[ReservedGroupIds.UNGROUPED];
                              let cluster = this._getClusterForItems(clusterItems, group, oldClusters, options);
                              clusters.push(cluster);
  
                              i += num;
                          } else {
                              delete item.cluster;
                              i += 1;
                          }
                      }
                  }
              }
  
              this.cache[level] = clusters;
          }
  
          return clusters;
      }
  
      /**
       * Filter the items per group.
       * @private
       */
      _filterData() {
          // filter per group
          const groups = {};
          this.groups = groups;
  
          // split the items per group
          for (const item of Object.values(this.items)) {
              // put the item in the correct group
              const groupName = item.parent ? item.parent.groupId : '';
              let group = groups[groupName];
              if (!group) {
                  group = [];
                  groups[groupName] = group;
              }
              group.push(item);
  
              // calculate the center of the item
              if (item.data.start) {
                  if (item.data.end) {
                      // range
                      item.center = (item.data.start.valueOf() + item.data.end.valueOf()) / 2;
                  } else {
                      // box, dot
                      item.center = item.data.start.valueOf();
                  }
              }
          }
  
          // sort the items per group
          for (let currentGroupName in groups) {
              if (groups.hasOwnProperty(currentGroupName)) {
                  groups[currentGroupName].sort((a, b) => a.center - b.center);
              }
          }
  
          this.dataChanged = false;
      }
  
      /**
       * Create new cluster or return existing
       * @private
       */
      _getClusterForItems(clusterItems, group, oldClusters, options) {
          const oldClustersLookup = (oldClusters || []).map(cluster => ({
              cluster,
              itemsIds: new Set(cluster.data.uiItems.map(item => item.id))
          }));
          let cluster;
          if (oldClustersLookup.length) {
              for (let oldClusterData of oldClustersLookup) {
                  if (oldClusterData.itemsIds.size === clusterItems.length 
                      && clusterItems.every(clusterItem => oldClusterData.itemsIds.has(clusterItem.id))) {
                      cluster = oldClusterData.cluster;
                      break;
                  }
              }
          }
          
          if (cluster) {
              cluster.setUiItems(clusterItems);
              if (cluster.group !== group) {
                  if (cluster.group) {
                      cluster.group.remove(cluster);    
                  }
  
                  if (group) {
                      group.add(cluster, false);    
                      cluster.group = group;
                  }
              }
              return cluster;
          }
  
          let titleTemplate = options.titleTemplate || '';
          const conversion = {
              toScreen: this.itemSet.body.util.toScreen,
              toTime: this.itemSet.body.util.toTime
          };
  
          const clusterContent = '<div title="' + title + '">' + clusterItems.length + '</div>';
          const title = titleTemplate.replace(/{count}/, clusterItems.length);
          const clusterOptions = Object.assign({}, options, this.itemSet.options);
          const data = {
              'content': clusterContent,
              'title': title,
              'group': group,
              'uiItems': clusterItems,
              'eventEmitter': this.itemSet.body.emitter,
              'range': this.itemSet.body.range
          };
          cluster = this.createClusterItem(data,
          conversion,
          clusterOptions);
  
          if (group) {
              group.add(cluster, false);
              cluster.group = group;
          }
  
          cluster.attach();
  
          return cluster;
      }
  
      /**
       * Drop cache
       * @private
       */
      _dropLevelsCache() {
          this.cache = {};
          this.cacheLevel = -1;
          this.cache[this.cacheLevel] = [];
      }
  }
  