import { Component, Host, Input, Output, EventEmitter,OnDestroy, OnInit, OnChanges, SimpleChanges, forwardRef } from '@angular/core';
import { LayerGroupComponent, MapComponent } from '@farmmaps/ng-openlayers';
import { ItemService,IItem,AppConfig } from '@farmmaps/common';
import { IItemLayer, ITemporalItemLayer} from '../../../models/item.layer';
import { ILayerData} from '../../../models/layer.data';
import { IRenderoutputTiles,IRenderoutputImage,IGradientstop,ILayer,IHistogram,IColor} from '../../../models/color.map';
import {Extent} from 'ol/extent';
import Projection from 'ol/proj/Projection';
import * as proj from 'ol/proj';
import * as loadingstrategy from 'ol/loadingstrategy';
import * as style from 'ol/style';
import {Tile,Layer,Image} from 'ol/layer';
import {XYZ,ImageStatic,OSM,BingMaps,TileWMS,TileArcGISRest,TileJSON,Source} from 'ol/source';
import {Vector as VectorSource} from 'ol/source';
import { Vector as VectorLayer } from 'ol/layer';
import { VectorImage as VectorImageLayer } from 'ol/layer';
import VectorTileSource from 'ol/source/VectorTile';
import VectorTileLayer from 'ol/layer/VectorTile';
import {GeoJSON,MVT} from 'ol/format';
import { Geometry } from 'ol/geom';
import BaseLayer from 'ol/layer/Base';
import Feature from 'ol/Feature';

@Component({
    selector: 'fm-map-item-layers',
    template: `<ng-content></ng-content>`,
    providers: [
        { provide: LayerGroupComponent, useExisting: forwardRef(() => ItemLayersComponent) }
    ],
    standalone: false
})

export class ItemLayersComponent extends LayerGroupComponent implements OnChanges, OnInit,OnDestroy {
  @Input() itemLayers: IItemLayer[];
  @Input() itemLayer: IItemLayer;
  @Output() onFeatureSelected: EventEmitter<any> = new EventEmitter<any>();
  @Output() onFeatureHover: EventEmitter<any> = new EventEmitter<any>();
  @Output() onPrerender: EventEmitter<any> = new EventEmitter<any>();
  private _apiEndPoint: string;
  private initialized = false;
  private mapEventHandlerInstalled = false;
  private topLayerPrerenderEventhandlerInstalled = false;
  private selectedFeatures = {};
  private selectionLayer:Layer<Source> = null;
  
  constructor(private itemService: ItemService, private map: MapComponent, public appConfig: AppConfig) {
    super(map);
    this._apiEndPoint = appConfig.getConfig("apiEndPoint");
  }

  private styleCache = {}

  componentToHex(c) {
    const hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
  }

   rgbaToHex(r, g, b,a) {
     return "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b) + this.componentToHex(a);
  }

  getColorFromGradient(layer: ILayer, value: number): IColor {
    const gradient: IGradientstop[] = layer.renderer.colorMap.gradient;
    const histogram: IHistogram = layer.renderer.band.histogram;
    const index = (value - histogram.min) / histogram.max;
    let min = gradient[0];
    let max = gradient[gradient.length - 1];
    for (let n = 0; n < gradient.length; n++) {
      const s = gradient[n];
      if (s.relativestop <= index && min.relativestop < s.relativestop && n < gradient.length - 1) min = s;
      if (s.relativestop >= index && max.relativestop > s.relativestop && n > 0) max = s;
    }
    const i = index - min.relativestop;
    const size = max.relativestop - min.relativestop;
    const alpha = Math.round(min.color.alpha + ((max.color.alpha - min.color.alpha) * i / size));
    const red = Math.round(min.color.red + ((max.color.red - min.color.red) * i / size));
    const green = Math.round(min.color.green + ((max.color.green - min.color.green) * i / size));
    const blue = Math.round(min.color.blue + ((max.color.blue - min.color.blue) * i / size));

    return { alpha: alpha, red: red, green: green, blue: blue };
  }

  getColorForValue(layer: ILayer, value: number): IColor {
    let color: IColor =  { alpha:0,red:0,green:0,blue:0};
    if(layer.renderer.colorMap.entries.length>0) {
      color=layer.renderer.colorMap.noValue;
    }
    layer.renderer.colorMap.entries.forEach((entry) => {
        if(entry.value==value) {
          color =entry.color;
          return;
        }
    });

    return color;
  }

  getColor(item: IItem, layer: ILayer, feature): style.Style {
    const value = layer.indexKey ? feature.get(layer.indexKey) : feature.get(layer.name);
    const key = item.code + "_" + value;
    if (!this.styleCache[key]) {
      let color: IColor;
      if(layer.renderer.colorMap.colormapType == "manual") {
        color = this.getColorForValue(layer, value);
      } else {
        color = this.getColorFromGradient(layer, value);
      }

      this.styleCache[key] = new style.Style(
        {
          image: new style.Circle({
            fill: new style.Fill({
              color: this.rgbaToHex(color.red, color.green, color.blue, color.alpha)
            }),
            radius: 3
          }),
          fill: new style.Fill({
            color: this.rgbaToHex(color.red, color.green, color.blue, color.alpha)
          }),
          stroke: new style.Stroke({
            color: this.rgbaToHex(color.red, color.green, color.blue, 255),
            width: 1.25
          }),
        });
    }
    return this.styleCache[key];
  }

  createGeotiffLayer(item:IItem,itemLayer:IItemLayer):Layer<Source> {
    let layerIndex = -1;
    let layer: Layer<Source> = null;  
    layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : item.data.layers[0].index;
    const source = new XYZ({ maxZoom: 19, minZoom: 1, url: `${this._apiEndPoint}/api/v1/items/${item.code}/tiles/${layerIndex}/{z}/{x}/{y}.png?v=${Date.parse(item.updated)}` });
    layer = new Tile({ source: source });
    const data = item.data;
    const l = (data && data.layers && data.layers.length > 0) ? data.layers[0] : null;
    if (l && l.rendering && l.rendering.renderoutputType == "Tiles") {
      const rt = l.rendering as IRenderoutputTiles;
      const source = new XYZ({crossOrigin: 'use-credentials', maxZoom: rt.maxzoom, minZoom: rt.minzoom, url: `${this._apiEndPoint}/api/v1/items/${item.code}/tiles/${layerIndex}/{z}/{x}/{y}.png?v=${Date.parse(item.updated)}` });
      layer = new Tile({ source: source });
    }
    if (l && l.rendering && l.rendering.renderoutputType == "Image") {
      const ri = l.rendering as IRenderoutputImage;  
      const source = new ImageStatic({ imageExtent:ri.extent,projection:'EPSG:3857',  crossOrigin: 'use-credentials', url: `${this._apiEndPoint}/api/v1/items/${item.code}/mapimage/${layerIndex}?v=${Date.parse(item.updated)}` });
      layer = new Image({ source: source });
    }
    return layer;
  }

  createShapeLayer(item:IItem,itemLayer:IItemLayer):Layer<Source> {
    let layerIndex = -1;
    let layer: Layer<Source> = null; 
    layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : item.data.layers[0].index;
    const data = item.data;
    const l:ILayer = (data && data.layers && data.layers.length > 0) ? data.layers[layerIndex] : null;
    if (l && l.rendering && l.rendering.renderoutputType == "VectorTiles") {
      var rt = l.rendering as IRenderoutputTiles;
      layer = new VectorTileLayer({
        declutter: true,
        source: new VectorTileSource({
          maxZoom: rt.maxzoom,
          minZoom: rt.minzoom,
          format: new MVT(),
          url: `${this._apiEndPoint}/api/v1/items/${item.code}/vectortiles/{z}/{x}/{y}.pbf?v=${Date.parse(item.updated)}`
        }),
        style: (feature) => {
          return this.getColor(item,l, feature);
        }
      })
    } else if (l && l.rendering && l.rendering.renderoutputType == "Tiles") {
      var rt = l.rendering as IRenderoutputTiles;
      layer = new Tile({
        source: new XYZ({
          maxZoom: rt.maxzoom,
          minZoom: rt.minzoom,
          url: `${this._apiEndPoint}/api/v1/items/${item.code}/vectortiles/image_tiles/${layerIndex}/{z}/{x}/{y}.png?v=${Date.parse(item.updated)}`
        })
      });
    } else {
      const __this = this;
      const format = new GeoJSON();
      const source = new VectorSource({
        strategy: loadingstrategy.bbox,
        loader: function (extent: Extent, resolution: number, projection: Projection) {
          const source = this as VectorSource<Feature<Geometry>>;
          __this.itemService.getItemFeatures(item.code, extent, projection.getCode(), layerIndex).subscribe(function (data) {
            const features = format.readFeatures(data).filter(feature => feature instanceof Feature) as Feature[];
            for (const f of features) {
              if (f.get("code")) {
                f.setId(f.get("code"));
              }
            }
            source.addFeatures(features);
          });
        }
      });
      layer = new VectorImageLayer({
        declutter: true,
        source: source,
        style: (feature) => {
          const key =feature.get("code") + "_" + feature.get("color");
          if (!this.styleCache[key]) {
            const color = feature.get("color");
            this.styleCache[key] = new style.Style(
              {
                fill: new style.Fill({
                  color: color
                }),
                stroke: new style.Stroke({
                  color: color,
                  width: 1.25
                }),
                image: new style.Circle({
                  fill: new style.Fill({
                    color: color
                  }),
                  stroke: new style.Stroke({
                    color: color,
                    width: 1.25
                  }),
                  radius: 5
                }),
              }
            )
          }
          return this.styleCache[key];
        }
      });
    }
    if(l.minzoom) {
      layer.setMinZoom(l.minzoom);
    }
    if(l.maxzoom) {
      layer.setMaxZoom(l.maxzoom);
    }
    return layer;
  }

  createSelectionLayer(itemLayer:IItemLayer):Layer<Source> {
    let layerIndex = -1;
    const layer: Layer<Source> = null; 
    layerIndex = itemLayer.layerIndex != -1 ? itemLayer.layerIndex : itemLayer.item.data.layers[0].index;
    const data = itemLayer.item.data;
    const l:ILayer = (data && data.layers && data.layers.length > 0) ? data.layers[layerIndex] : null;
    if (l && l.rendering && l.rendering.renderoutputType == "VectorTiles") {
      return new VectorTileLayer({
        renderMode: 'vector',
        source: (itemLayer.layer as VectorTileLayer).getSource(),
        style: (feature) => {
          if (feature.getId() in this.selectedFeatures) {

            return new style.Style(
              {               
                stroke: new style.Stroke({
                  color: '#0d6efd',
                  width: 3
                })
              }
            );
          }
        },
        minZoom: itemLayer.layer.getMinZoom(),
        maxZoom: itemLayer.layer.getMaxZoom()        
      });
    }
    return null;
  }

  createExternalLayer(item:IItem,itemLayer:IItemLayer):Layer<Source> {
    const data = item.data as ILayerData;
    let layer: Layer<Source> = null; 
    switch (data.interfaceType) {
      case 'OSM': {
        const source = new OSM();
        layer = new Tile({ source: source });
        break;
      }
      case 'BingMaps': {
        const source = new BingMaps(data.options);
        layer = new Tile({ source: source });
        break;
      }
      case 'TileWMS': {
        const source = new TileWMS(data.options);
        layer = new Tile({ source: source });
        break;
      }
      case 'TileJSON': {
        const source = new TileJSON(data.options);
        layer = new Tile({ source: source });
        break;
      }
      case 'TileArcGISRest': {
        const source = new TileArcGISRest(data.options);
        layer = new Tile({ source: source });
        break;
      }
      case 'VectorWFSJson': {
        const source = new VectorSource({
          format: new GeoJSON(),
          url: function (extent) {
            return (
              data.options.url + '&srsname=' + data.projection +
              '&bbox=' +
              extent.join(',') +
              ',EPSG:3857'
            );
          },
          strategy: loadingstrategy.bbox,
        });
        layer = new VectorLayer({ source: source });
        break;
      }
      default: {
        break;
      }
    }
    return layer;
  }

  createLayer(itemLayer: IItemLayer): Layer<Source> {
    let layer: Layer<Source> = null;   
    const layerIndex = -1;
    if (itemLayer.item.itemType == 'vnd.farmmaps.itemtype.geotiff.processed') {
      layer = this.createGeotiffLayer(itemLayer.item,itemLayer);
    } else if (itemLayer.item.itemType == 'vnd.farmmaps.itemtype.shape.processed') {
      layer = this.createShapeLayer(itemLayer.item,itemLayer);
    } else if (itemLayer.item.itemType == 'vnd.farmmaps.itemtype.layer') {
      layer = this.createExternalLayer(itemLayer.item,itemLayer);
    }
    if (layer) {
      const geometry = new GeoJSON().readGeometry(itemLayer.item.geometry);
      const extent = geometry ? proj.transformExtent(geometry.getExtent(), 'EPSG:4326', 'EPSG:3857') : null;
      if (extent) layer.setExtent(extent);
    }

    return layer;
  }

  ngOnInit() {
    super.ngOnInit();
    if(this.itemLayers) {
      this.updateLayers(this.itemLayers);
    } else if(this.itemLayer) {
      if(this.getItemlayer(this.itemLayer).item.itemType == 'vnd.farmmaps.itemtype.shape.processed') {
        this.installMapEventHandler();
      }
      this.updateLayers([this.itemLayer])
    } else {
      this.updateLayers([]);
    }
    this.initialized=true;
  }

  installMapEventHandler() {
    if(!this.mapEventHandlerInstalled) {
      this.map.instance.on(['click', 'pointermove'],this.mapEventHandler);
      this.mapEventHandlerInstalled=true;
    }
  }

  unInstallMapEventHandler() {
    if(this.mapEventHandlerInstalled) {
      this.map.instance.un(['click', 'pointermove'],this.mapEventHandler);
      this.mapEventHandlerInstalled=false;
    }
  }

  installTopLayerPrerenderEventhandler(olLayer : Layer<Source>) {
    if(!this.topLayerPrerenderEventhandlerInstalled  && this.onPrerender.observers.length > 0  && olLayer) {
      if(this.instance.getVisible()) {
        olLayer.on('prerender',this.topLayerPrerenderEventhandler);
        olLayer.on('postrender',this.topLayerPostrenderEventhandler);
        this.topLayerPrerenderEventhandlerInstalled = true;
      }
    }
  }

  unInstallTopLayerPrerenderEventhandler() {
    if(this.topLayerPrerenderEventhandlerInstalled && this.onPrerender.observers.length > 0 )
    {
      if(this.instance.getVisible()) {
        const olLayers = this.instance.getLayers().getArray().forEach((l:any) => {
          l.un('prerender',this.topLayerPrerenderEventhandler);
          l.un('postrender',this.topLayerPostrenderEventhandler);
        });
        this.topLayerPrerenderEventhandlerInstalled = false;
      }
    }
  }


  addOrUpdateOlLayer(itemLayer:IItemLayer,index:number):Layer<Source> {
    if(!itemLayer) return null;
    const olLayers = this.instance.getLayers();
    let layer = itemLayer.layer;
    const olIndex = olLayers.getArray().indexOf(layer);
    if (olIndex < 0) {
      // New layer: we add it to the map
      layer = this.createLayer(itemLayer);
      if (layer) {
        olLayers.insertAt(index, layer);
      }
    } else if (index !== olIndex) {
      // layer has moved inside the layers list
      olLayers.removeAt(olIndex);
      olLayers.insertAt(index, layer);
    }
    if(layer) {
      itemLayer.layer = layer;
      layer.setOpacity(itemLayer.opacity);
      layer.setVisible(itemLayer.visible); 
    }
    return layer;
  }

  updateLayers(itemLayers: IItemLayer[] | IItemLayer) {
    this.unInstallTopLayerPrerenderEventhandler();
    let dataLayer = false;
    let ils:IItemLayer[] = [];
    if(Array.isArray(itemLayers)) {
       ils = itemLayers;
    } else {
      dataLayer=true;
       ils=[itemLayers];
    }
    const newLayers: Layer<Source>[] = [];
    if (ils) {
      ils.forEach((itemLayer, index) => {
        if(itemLayer.item.itemType ==  'vnd.farmmaps.itemtype.temporal') {
          const il = itemLayer as ITemporalItemLayer;
          const previousLayer = this.addOrUpdateOlLayer(il.previousItemLayer,newLayers.length);          
          if(previousLayer) newLayers.push(previousLayer);
          const selectedLayer = this.addOrUpdateOlLayer(il.selectedItemLayer,newLayers.length);
          if(selectedLayer) newLayers.push(selectedLayer);
          const nextLayer = this.addOrUpdateOlLayer(il.nextItemLayer,newLayers.length);
          if(nextLayer) newLayers.push(nextLayer);
          this.installTopLayerPrerenderEventhandler(selectedLayer);
        } else {
          const layer = this.addOrUpdateOlLayer(itemLayer,newLayers.length);
          if(layer) newLayers.push(layer);    
          this.installTopLayerPrerenderEventhandler(layer);       
        }
      });
      // Remove the layers that have disapeared from childrenLayers
      const olLayers = this.instance.getLayers();
      while(olLayers.getLength() > newLayers.length) {
        olLayers.removeAt(newLayers.length);
      }
      this.selectionLayer=null;
      if(this.mapEventHandlerInstalled && ils.length==1 && this.getItemlayer(itemLayers[0]).item.itemType == 'vnd.farmmaps.itemtype.shape.processed') {
        this.selectionLayer = this.createSelectionLayer(this.getItemlayer(itemLayers[0]));
        if(this.selectionLayer) olLayers.push(this.selectionLayer)
      }
    }
  }

  topLayerPrerenderEventhandler = (event) => {
     this.onPrerender.emit(event);
  }

  topLayerPostrenderEventhandler = (event) => {
    const ctx = event.context;
    ctx.restore();
  }


  mapEventHandler = (event) => {
    // select only when having observers
    if(event.type === 'click' && !this.onFeatureSelected.observers.length) return;
    if(event.type === 'pointermove' && !this.onFeatureHover.observers.length) return;
    const itemLayer= this.getItemlayer(this.itemLayer);
    if(itemLayer && itemLayer.layer) {
       this.selectedFeatures = {};
       if(itemLayer.layer ) {
        const minZoom = itemLayer.layer.getMinZoom();
        const currentZoom = this.map.instance.getView().getZoom();
        if(currentZoom>minZoom) {
            itemLayer.layer.getFeatures(event.pixel).then((features) => {      
            if(!features.length) {
              this.onFeatureHover.emit(null);
              return;
            }
           const fid = features[0].getId();
           const feature = features[0];
           if(event.type === 'pointermove') {
             this.selectedFeatures[fid] = features[0];
             this.onFeatureHover.emit({ "feature": feature,"itemCode":itemLayer.item.code });
           } else {
             this.onFeatureSelected.emit({ "feature": feature,"itemCode":itemLayer.item.code });
           }
          })
          if(this.selectionLayer) this.selectionLayer.changed(); 
        }      
       }
    }    
  }

  getItemlayer(itemLayer:IItemLayer):IItemLayer {     
     if((itemLayer as ITemporalItemLayer).selectedItemLayer) return (itemLayer as ITemporalItemLayer).selectedItemLayer;
     return itemLayer;
  }

  ngOnChanges(changes: SimpleChanges) {
    if (this.instance && this.initialized) {
      if (changes['itemLayers']) {
        const itemLayers = changes['itemLayers'].currentValue as IItemLayer[];
        this.updateLayers(itemLayers);
      }
      if (changes['itemLayer']) {      
        const itemLayer = changes['itemLayer'].currentValue as IItemLayer;
        this.itemLayer = itemLayer
        if(itemLayer) {
          if(this.getItemlayer(this.itemLayer).item.itemType == 'vnd.farmmaps.itemtype.shape.processed') {
            this.installMapEventHandler();
          }
          this.updateLayers([itemLayer]);
        } else {
          this.unInstallMapEventHandler();
          this.updateLayers([]);          
        }
      }
    }
  }

  ngOnDestroy() {
    this.unInstallMapEventHandler();
    super.ngOnDestroy();
  }
}
