import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Vector as VectorLayer} from 'ol/layer';
import {forkJoin} from 'rxjs';

import {HsConfig} from 'hslayers-ng';
import {HsEventBusService} from 'hslayers-ng';
//import {HsLayerManagerMetadataService} from 'hslayers-ng/components/layermanager/layermanager-metadata.service';
import {HsLayerManagerService} from 'hslayers-ng';
import {HsToastService} from 'hslayers-ng';
import {HsUtilsService} from 'hslayers-ng';

import attractivenessConfig from '../attractiveness.config.json';
import clusteringMethods from '../data/clustering_methods.json';
import {AdjusterEventService} from './adjuster-event.service';
import {AdjusterLegendService} from './adjuster-legend.service';
import {AdjusterPresetsService} from './adjuster-presets.service';
import {obce, obceIndexLayer, osmLayer} from '../app.config';

@Injectable({providedIn: 'root'})
export class AdjusterService {
  /** To be read from a config file */
  allowedClusteringMethods: Array<string>;
  /** To be read from a config file */
  initialWeights;
  /** To be read from a config file */
  serviceBaseUrl: string;
  /** Used in the UI as a selector */
  allowClusters = false //FIXME: (WIP) true;
  /** Used in the UI as a selector */
  allowIndex = true;
  factors = [];
  layersReady = new Set();
  //method: string;
  methods: Array<MethodDescription>;
  numberOfClusters: number;
  private _clusteringInProcess: boolean;
  private _clustersLoaded: boolean;
  /** Once instantiated, the load is definitely in process */
  private _loadInProcess = true;
  private _raiInProcess: boolean;

  constructor(
    public adjusterEventService: AdjusterEventService,
    public adjusterLegendService: AdjusterLegendService,
    public adjusterPresetsService: AdjusterPresetsService,
    public hsConfig: HsConfig,
    public hsEventBus: HsEventBusService,
    //public hsLayerMetadataService: HsLayerManagerMetadataService,
    public hsLayerManagerService: HsLayerManagerService,
    public hsToastService: HsToastService,
    public hsUtilsService: HsUtilsService,
    public $http: HttpClient
  ) {
    /* First safely set configurable properties */
    this.allowedClusteringMethods =
      attractivenessConfig?.allowedClusteringMethods ?? [];
    this.initialWeights = attractivenessConfig?.initialWeights ?? {};
    this.serviceBaseUrl =
      attractivenessConfig?.serviceBaseUrl ??
      'https://publish.lesprojekt.cz/nodejs/';
    // 'https://jmacura.ml/ws/' // 'http://localhost:3000/'

    this.methods = clusteringMethods.filter((m) =>
      this.allowedClusteringMethods.includes(m.codename)
    );
    this.numberOfClusters = 9;

    /* Wait for all layers to be ready */
    this.adjusterEventService.layerReady.subscribe(({name}) => {
      console.log(name + ' ready!');
      this.layersReady.add(name);
      /* Layers for each method + layer for index are ready */
      if (this.layersReady.size == this.methods.length + 1) {
        this.adjusterEventService.layerReady.complete();
      }
    });

    /* Ensure that all layers and also the loader component are ready */
    forkJoin({
      lyr: this.adjusterEventService.layerReady,
      load: this.adjusterEventService.loaderReady,
    }).subscribe(() => {
      console.log('Oll layers Korekt! Initializing adjuster...');
      //this._loadInProcess = false;
      this.init();
    });

    /* Listen to schema changes so the factors can be re-arranged in the view */
    this.adjusterPresetsService.schemaChanges.subscribe((newSchema) => {
      //TODO: i18n this!
      this.factors = newSchema.groups.map((group) => {
        return {
          id: group.id,
          name: group.labels[0]['@value'],
          //FIXME: load weights from file
          weight: 0.5,
          datasets: this.adjusterPresetsService.getGroupDatasets(group.id)
        }
      });
    });
  }

  /**
   * Sends a request to polirural-attractiveness-service
   * and applies the returned values
   */
  apply(): void {
    if (this.allowIndex) {
      this.calculateIndex();
    }
    if (this.allowClusters) {
      this.calculateClusters();
    }
  }

  calculateIndex(): void {
    this._raiInProcess = true;
    this.$http
      .post(this.serviceBaseUrl + 'cz/scores/', {
        factors: this.factors.map((f) => {
          return {
            factor: f.id,
            weight: f.weight,
            datasets: f.datasets
              .filter((ds) => ds.included)
              .map((ds) => ds.id),
          };
        }),
      })
      .toPromise()
      .then((attractivenessData: any[]) => {
        /* Spread the 'aggregate' value between 0 and 1 */
        const min = attractivenessData.reduce((a, b) =>
          a.aggregate < b.aggregate ? a : b
        ).aggregate;
        const max = attractivenessData.reduce((a, b) =>
          a.aggregate > b.aggregate ? a : b
        ).aggregate;
        const coefficient = 1 / (max - min);
        const constant = -min * coefficient;
        attractivenessData.forEach((a) => {
          a.aggregate *= coefficient;
          a.aggregate += constant;
        });
        /* Store relation between region and its data in a hash-table-like structure
         * More memory consuming, but faster then find() */
        const codeRecordRelations = {};
        attractivenessData.forEach((a) => {
          codeRecordRelations[a.code] = a;
        });
        console.time('forEach-Index');
        this.processIndex(codeRecordRelations);
        console.timeEnd('forEach-Index');
        this._raiInProcess = false;
        this.adjusterEventService.loaded.next({
          success: true,
          type: 'index',
        });
      })
      .catch((error) => {
        this.hsToastService.createToastPopupMessage('Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.`);
        console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
        console.log(error);
        this._raiInProcess = false;
        this.adjusterEventService.loaded.next({
          success: true,
          type: 'index',
          err: error,
        });
      });
  }

  calculateClusters(): void {
    this._clusteringInProcess = true;
    /* Pre-process the API params */
    const params = [];
    for (const factor of this.factors) {
      for (const dataset of factor.datasets) {
        if (!dataset.included) {
          continue;
        }
        const flattenedDataset = {
          id: dataset.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
          factor: factor.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
          weight: factor.weight
        };
        params.push(flattenedDataset)
      }
    }

    this.$http
      .post(this.serviceBaseUrl + 'cz/clusters/', {
        numberOfClusters: this.numberOfClusters,
        datasets: params
      })
      .toPromise()
      .then((data: any) => {
        const clusterData = data.response;
        /* Store relation between region and its data in a hash-table-like structure
         * more memory consuming, but much faster then find() */
        const codeRecordRelations = {};
        clusterData.forEach((c) => {
          codeRecordRelations[c.lau2] = c;
        });
        /*let sublayers = [];
          const oldSublayers = obceLayer.get('Layer');
          if (oldSublayers !== undefined && Array.isArray(oldSublayers)) {
            for (const sublyr of oldSublayers) {
              sublyr.getSource().clear();
            }
            if (oldSublayers.length > this.numberOfClusters) {
              sublayers = oldSublayers.slice(0, this.numberOfClusters);
            }
          }
          let i = sublayers.length;
          while (sublayers.length < this.numberOfClusters) {
            const sublyr = new VectorLayer({
              title: `Cluster ${i + 1}`,
              name: i + 1 + '',
              source: new VectorSource({}),
            });
            sublayers.push(sublyr);
            sublyr.Title = `Cluster ${i + 1}`;
            sublyr.Name = `c${i + 1}`;
            i++;
          }*/
        console.time('forEach-Cluster');
        for (const method of this.methods) {
          this.processClusters(method, codeRecordRelations);
          method.layer.getSource().legend_categories = this.adjusterLegendService.createClusterLegend(
            this.numberOfClusters
          );
        }
        // Another implementation of the loop above, yet too slow
        //const clusters = [];
        //const obceFeatures: Array<any> = obce.getFeatures();
        // Array of arrays
        /*const methodsFeatures = this.methods.map((m) =>
          m.layer.getSource().getFeatures()
        );
        for (const region of clusterData) {
          //if (!clusters.includes(region[this.method])) {
          //  clusters.push(region[this.method]);
          //}
          for (const features of methodsFeatures) {

            const feature = features.find(
              // NOTE: Do NOT add triple equal sign!
              (f) => f.get('nationalCode') == region['lau2']
            );
            if (!feature) {
              if (errs < 20) {
                errs++;
                console.warn(`No feature matches region ${region['lau2']}`);
                console.log(region);
              }
              continue;
            }
            Object.keys(region).forEach(function (key, index) {
              if (key !== 'lau2') {
                feature.set(key, region[key], true);
              }
            });
          }
          logs++;
          if (logs % 100 == 0) {
            console.log(`processed ${logs} items`);
          }
          //sublayers[region[this.method] - 1].getSource().addFeature(feature);
        }*/
        //obceLayer.set('Layer', sublayers);
        //this.hsLayerMetadataService.fillMetadata(obceLayer);
        //console.log(sublayers[0].getSource().getFeatures());
        console.timeEnd('forEach-Cluster');
        //this.clusters = clusters;
        this._clustersLoaded = true;
        this._clusteringInProcess = false;
        this.adjusterEventService.loaded.next({
          success: true,
          type: 'clusters',
        });
      })
      .catch((error) => {
        this.hsToastService.createToastPopupMessage('Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.`);
        console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
        console.log(error);
        this._clusteringInProcess = false;
        this.adjusterEventService.loaded.next({
          success: false,
          type: 'clusters',
          err: error,
        });
      });
  }

  init(): void {
    this._loadInProcess = true;
    this.$http
      .get(this.serviceBaseUrl + 'cz/datasets/')
      .toPromise()
      .then((data: any) => {
        //console.log(data);
        /*this.factors = data.map((dataset) => {
          return {
            name: dataset.Factor,
            weight: this.initialWeights[dataset.Factor] ?? 1,
            datasets: [],
          };
        });
        this.factors = this.hsUtilsService.removeDuplicates(
          this.factors,
          'name'
        );
        this.factors.forEach((factor) => {
          factor.datasets = data
            .filter((ds) => ds.Factor === factor.name)
            .map((ds) => {
              return {
                name: ds.Name,
                desc: ds.Description,
                included: true,
              };
            });
        });*/
        this._loadInProcess = false;
        this.apply();
        // In HSL 2.5, setting layer greyscale breaks the print() functionality
        //this.hsLayerManagerService.setGreyscale(osmLayer);
      })
      .catch((error) => {
        this.hsToastService.createToastPopupMessage('Error loading data', `Web service at ${this.serviceBaseUrl} unavailable!`);
        console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
        console.log(error);
        this._loadInProcess = false;
      });
  }

  processIndex(codeRecordRelations: Record<string, unknown>): void {
    /*if (obce.getFeatures()?.length < 1) {
      obce.once('changefeature', () => this.processIndex(codeRecordRelations));
      return;
    }*/
    let errs = 0;
    //let logs = 0;
    obce.forEachFeature((feature) => {
      // Pair each feature with its attractivity data
      const featureData = codeRecordRelations[feature.get('nationalCode')];
      if (!featureData) {
        if (errs < 20) {
          errs++;
          console.warn(`No data for feature ${feature.get('nationalCode')}`);
          console.log(feature);
        }
        return;
      }
      /*logs++;
      if (logs % 100 == 0) {
        console.log(`processed ${logs} items`);
      }*/
      Object.keys(featureData).forEach((key, index) => {
        if (key !== 'lau2') {
          feature.set(key, featureData[key], true); //true stands for "silent" - important for performance!
        }
      });
    });
    // Since we are updating the features silently, we now have to refresh manually
    obce.getFeatures()[0].dispatchEvent('change');
  }

  processClusters(
    method: MethodDescription,
    codeRecordRelations: Record<string, unknown>
  ): void {
    /*if (method.layer?.getSource().getFeatures()?.length < 1) {
      method.layer
        .getSource()
        .once('changefeature', () =>
          this.processClusters(method, codeRecordRelations)
        );
      return;
    }*/
    let errs = 0;
    //let logs = 0;
    method.layer.getSource().forEachFeature((feature) => {
      // Pair each feature with its clustering data
      const featureData = codeRecordRelations[feature.get('nationalCode')];
      /*const featureData = clusterData.find(
        // NOTE: Do NOT add triple equal sign!
        (item) => item['lau2'] == feature.get('nationalCode')
      );*/
      if (!featureData) {
        if (errs < 20) {
          errs++;
          console.warn(`No data for feature ${feature.get('nationalCode')}`);
          console.log(feature);
        }
        return;
      }
      /*logs++;
      if (logs % 100 == 0) {
        console.log(`processed ${logs} items`);
      }*/
      feature.set(method.codename, featureData[method.codename], true);
      /*Object.keys(featureData).forEach(function (key, index) {
        if (key !== 'lau2') {
          feature.set(key, featureData[key], true);
        }
      });*/
    });
    // Since we are updating the features silently, we now have to refresh manually
    method.layer.getSource().getFeatures()[0].dispatchEvent('change');
  }

  clustersLoaded(): boolean {
    return this._clustersLoaded;
  }

  /**
   * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise
   */
  isInProcess(): boolean {
    return (
      this._loadInProcess || this._clusteringInProcess || this._raiInProcess
    );
  }

  isLoading(): boolean {
    return this._loadInProcess;
  }

  isClustering(): boolean {
    return this._clusteringInProcess;
  }

  isCalculatingRAI(): boolean {
    return this._raiInProcess;
  }
}

type MethodDescription = {
  codename: string;
  layer?: VectorLayer<any>;
  name: string;
  type: string;
};
