import {
  Component, ViewChild, AfterContentInit, NgZone,
  ChangeDetectorRef, ChangeDetectionStrategy, ElementRef,
  OnInit,
} from '@angular/core'
import { ActivatedRoute, Params } from '@angular/router'
import { HttpClient } from '@angular/common/http'
import { Observable, Subject, forkJoin } from 'rxjs'
import { map, mergeMap } from 'rxjs/operators'

import bbox from '@turf/bbox'
import {
  MapComponent as MapLibreCompomemt,
  PopupComponent
} from '@maplibre/ngx-maplibre-gl'
import {
  MapMouseEvent, GeoJSONSource, FitBoundsOptions,
  TypedStyleLayer, MapGeoJSONFeature,
  GeoJSONSourceSpecification, MapLibreEvent
} from 'maplibre-gl'

import { DataService, NewTree } from '../data.service'
import { ActionService } from '../action.service'
import { MessageService } from '../message.service'
import { ConfigService, MapPos } from '../config.service'
import { FeatureTarget } from '../models'
import { TrailListItemComponent } from '../trail-list-item/trail-list-item.component'
import { TreePopupComponent } from '../tree-popup/tree-popup.component'
import { PoiPopupComponent } from '../poi-popup/poi-popup.component'
import { ZonePopupComponent } from '../zone-popup/zone-popup.component'
import { MapEditService } from './map-edit/map-edit.service'

const myLayers = ['tree', 'poi', 'trail', 'zone']


@Component({
  selector: 'app-map',
  templateUrl: './map.component.html',
  styleUrls: ['./map.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MapComponent implements AfterContentInit, OnInit {
  constructor(
    protected activatedRoute: ActivatedRoute,
    public configService: ConfigService,
    public dataService: DataService,
    public actionService: ActionService,
    public messageService: MessageService,
    protected ngZone: NgZone,
    private cdr: ChangeDetectorRef,
    public mapEditService: MapEditService,
    public httpClient: HttpClient,
  ) { }

  @ViewChild('map') map: MapLibreCompomemt
  @ViewChild("popup") popup: PopupComponent
  @ViewChild("treePopupDetail") treePopupDetail: TreePopupComponent
  @ViewChild("poiPopupDetail") poiPopupDetail: PoiPopupComponent
  @ViewChild("trailPopupDetail") trailPopupDetail: TrailListItemComponent
  @ViewChild("zonePopupDetail") zonePopupDetail: ZonePopupComponent
  @ViewChild('featureInfo', { static: true }) featureInfo: ElementRef

  public mapLoad = new Subject<boolean>()

  public currentPopupLayer: string = ''

  //styleUrl = 'assets/map/style.json'
  styleUrl: string // = '/tiles/style/osm'

  geolocateTrackUserLocation = true
  geolocateShowUserLocation = true
  geolocatePositionOptions = {
    "enableHighAccuracy": true
  }
  geolocateFitBoundsOptions: FitBoundsOptions = {
    "maxZoom": 18,
  }

  ngOnInit(): void {
    let conf = this.configService.conf.value
    let bms = conf.background
    if (conf.bootstrap.baseMapStyles.embedded.indexOf(bms) >= 0) {
      this.styleUrl = `/tiles/style/${bms}`
    }
    else {
      this.styleUrl = conf.bootstrap.baseMapStyles.external[bms]
    }
  }

  ngAfterContentInit(): void {
    // Hide the popups at start
    this.mapLoad.subscribe({
      complete: () => {
        this.popup.popupInstance.remove()
        // Bypass Angular's service worker
        // this.map.transformRequest = (url: String, resourceType: String) => {
        //   return {
        //     url: url.replace('http', 'https'),
        //     headers: { 'ngsw-bypass': true },
        //     credentials: 'include'  // Include cookies for cross-origin requests
        //   }
        // }
      }
    })
    forkJoin([
      this.mapLoad,
      this.getIcons(),
      this.actionService.getAllGeoJSONTrails(),
      this.actionService.getAllGeoJSONPois(),
      this.actionService.getAllGeoJSONTrees(),
      this.actionService.getAllGeoJSONZones(),
    ]).subscribe({
      complete: () => this.setup(),
      error: err => {
        this.messageService.message.next(`Network issue (${err.status})`)
        console.error(err)
      }
    })
  }

  getIcons(): Observable<any> {
    return this.httpClient.get('assets/icons/tree.png', {responseType: "blob"}).pipe(mergeMap(
      treeIconBlob => createImageBitmap(treeIconBlob).then(
          treeIcon => this.map.mapInstance.addImage("tree", treeIcon)
        )
    ))
  }

  /*
  async asyncGetIcons() {
    let treeIcon = await this.map.mapInstance.loadImage('assets/icons/tree.png')
    this.map.mapInstance.addImage("tree", treeIcon.data)
  }
    */

  setup() {
        // this.map.mapInstance.loadImage('assets/icons/tree.png').then(
        //   image => this.map.mapInstance.addImage('tree', image.data)
        // )
    this.map.mapInstance.addSource(
      'trail',
      <GeoJSONSourceSpecification>{
        'type': 'geojson',
        'data': this.dataService.trailFeatures.getValue()
      }
    )

    this.map.mapInstance.addSource(
      'poi',
      <GeoJSONSourceSpecification>{
        'type': 'geojson',
        'data': this.dataService.poisFeatures.getValue()
      }
    )

    this.map.mapInstance.addSource(
      'zone',
      <GeoJSONSourceSpecification>{
        'type': 'geojson',
        'data': this.dataService.zoneFeatures.getValue()
      }
    )

    // Add layers when the styles are available
    this.dataService.all.subscribe(
      all => {
        let styles = all.styles

        this.map.mapInstance.addLayer(
          {
            id: 'zone',
            source: 'zone',
            type: 'fill',
            paint: styles['zone'].paint,
            layout: styles['zone'].layout,
          }
        )

        this.map.mapInstance.addLayer(
          {
            id: 'trail',
            source: 'trail',
            type: 'line',
            paint: styles['trail'].paint,
            layout: styles['trail'].layout,
            /*
            paint: {
              'line-color': '#cd861a',
              'line-width': 6,
              'line-blur': 2,
              'line-opacity': 0.9
            },
            layout: {
              'line-join': 'bevel',
            }
            */
          }
        )

        this.map.mapInstance.addLayer(
          {
            id: 'poi',
            source: 'poi',
            type: 'symbol',
            paint: styles['poi'].paint,
            layout: styles['poi'].layout,
            /*
            paint: {
              'text-color': '#BB55CC',
            },
            layout: {
              //'text-field': ['match', ['get', 'plantekey_id'], '', '\ue033', '\ue034'],
              'text-field': ['get', 'symbol'],
              'text-overlap': 'always',
              'text-anchor': 'center',
              'text-font': ['TreetrailSymbols'],
              'text-size': 32,
            },
            */
          }
        )

        this.dataService.getPendingTrees().subscribe(
          pendingTrees => {
            let trees = this.dataService.treeFeatures.getValue()
            trees['features'].push(...pendingTrees.map(pt => {
              pt['feature']['properties']['pending'] = 2
              return pt['feature']
            }))
            this.map.mapInstance.addSource(
              'tree',
              <GeoJSONSourceSpecification>{
                'type': 'geojson',
                'data': trees
              }
            )

            this.map.mapInstance.addLayer(
              {
                id: 'tree',
                source: 'tree',
                type: 'symbol',
                paint: styles['tree'].paint,
                layout: styles['tree'].layout
                /*
                paint: {
                  'text-color': ['match', ['get', 'plantekey_id'], '', '#AAAA33', '#00BB00'],
                },
                layout: {
                  //'text-field': ['match', ['get', 'plantekey_id'], '', '\ue033', '\ue034'],
                  'text-field': ['get', 'symbol'],
                  'text-overlap': 'always',
                  'text-anchor': 'center',
                  'text-font': ['TreetrailSymbols'],
                  'text-size': 32,
                },
                */
              }
            )

            this.map.mapInstance.addLayer(
              {
                id: 'tree-hl',
                source: 'tree',
                type: 'symbol',
                paint: styles['tree-hl'].paint,
                layout: styles['tree-hl'].layout,
                /*
                paint: {
                  'icon-color': 'green',
                  'text-color': 'green',
                  'text-opacity': 1,
                  'text-halo-color': 'red',
                  'text-halo-width': 0.8,
                  'text-halo-blur': 0.5,
                },
                layout: {
                  'text-field': ['get', 'symbol'],
                  'text-size': 40,
                  'text-overlap': 'always',
                  'text-anchor': 'center',
                  'text-font': ['TreetrailSymbols'],
                },
                */
                filter: ['==', 'id', ''],
              }
            )

            // Track mouse overs only when the tree layer has been added
            this.map.mapInstance.on('mousemove', evt => this.onMouseMove(evt))
          }
        )

        this.map.mapInstance.on('zoomend', evt => this.onMapPosChange(evt))
        this.map.mapInstance.on('dragend', evt => this.onMapPosChange(evt))
        this.map.mapInstance.on('pitchend', evt => this.onMapPosChange(evt))

        // Add and filter zones when the data and showZones conf is available
        // TODO: Fix bootstrap
        let zl: TypedStyleLayer = <TypedStyleLayer>this.map.mapInstance.getLayer('zone')
        let visibleTypes = Object.entries(this.configService.conf.value.showZones).filter(
          ([type, visible]) => visible
        ).map(([type, visible]) => type)
        if (visibleTypes.length == 0) {
          visibleTypes = ['']
        }
        this.map.mapInstance.setFilter(
          'zone',
          ['match', ['get', 'type'], visibleTypes, true, false]
        )
        zl.setLayoutProperty('visibility', 'visible')
      }
    )

    // Handle addition of trees
    this.dataService.addTree$.subscribe(
      (newTree: NewTree) => {
        let treeFeatures = this.dataService.treeFeatures.getValue()
        treeFeatures['features'].push(newTree.getFeature())
        this.dataService.treeFeatures.next(treeFeatures)
        const mapSource = this.map.mapInstance.getSource('tree') as GeoJSONSource
        mapSource.setData(<any>treeFeatures)
      }
    )

    // Simulate a location change to init system
    this.actionService.onLocationChange()

    this.actionService.nearestTree$.subscribe(
      (tree: FeatureTarget) => {
        if (!this.map.mapInstance.getLayer
          // For unknown reason, mapInstance might not have getLayer...
          // Observed on Android Chromium
          || !this.map.mapInstance.getLayer('tree-hl')
          || tree == undefined) {
          return
        }
        this.map.mapInstance.setFilter(
          'tree-hl',
          ['==', 'id', tree.feature.properties['id']]
        )
      }
    )

    this.activatedRoute.queryParams.subscribe(
      (params: Params) => {
        if ('show' in params) {
          let featureRef: string[] = params['show'].split(':')
          let store = featureRef[0]
          let field = featureRef[1]
          let value = featureRef[2]
          // XXX: field is not used and assumed to be 'id'
          this.dataService.trailFeatures.subscribe(
            trailSource => {
              let trail = trailSource['features'].find(
                f => f['id'] == value
              )
              let bounds = bbox(trail)
              let margin = 0.00025
              this.map.mapInstance.fitBounds(
                [
                  [bounds[0] - margin, bounds[1] - margin],
                  [bounds[2] + margin, bounds[3] + margin],
                ]
              )
            }
          )
        }
      }
    )
  }

  onMove(evt: MapMouseEvent) {
    let features = this.map.mapInstance.queryRenderedFeatures(evt.point,
      { layers: ['tree', 'trail'] })
    let feature = features[0]
    if (feature) {
      if (feature.layer['id'] == 'tree') {
        this.map.mapInstance.getContainer().classList.add('on-item')
      }
      else {
        this.map.mapInstance.getContainer().classList.remove('on-item')
      }
    }
    else {
      this.map.mapInstance.getContainer().classList.remove('on-item')
    }
  }

  private getUID(layer: string, id: string): string {
    return `${layer}-${id}`
  }

  getBestFeature(evt): MapGeoJSONFeature {
    // Kiss Maplibre, which seems to return the features on different layers
    // with the same order that they were added
    let features = this.map.mapInstance.queryRenderedFeatures(evt.point, {
      layers: myLayers
    })
    return features[0]
  }

  onMouseMove(evt) {
    if (this.mapEditService.active) {
      this.map.mapInstance.getCanvas().style.cursor = 'crosshair'
      return
    }
    let features: Map<string, MapGeoJSONFeature[]> = new Map()
    let msgs: Map<string, string> = new Map()
    let found: boolean = false
    for (let layer of myLayers) {
      let features = this.map.mapInstance.queryRenderedFeatures(evt.point, {
        layers: [layer]
      })
      if (features.length > 0) {
        found = true
        if (features.length === 1) {
          msgs.set(layer, this.getFeatureMsg(layer, features[0].properties))
        }
        else {
          msgs.set(layer, `(${features.length}*)`)
        }
      }
    }
    if (found) {
      this.map.mapInstance.getCanvas().style.cursor = 'zoom-in'
      this.featureInfo.nativeElement.innerHTML = Array.from(msgs).map(
        ([key, value]) => `${key}: ${value}`
      ).join(' - ')
    } else {
      this.map.mapInstance.getCanvas().style.cursor = ''
      this.featureInfo.nativeElement.innerHTML = ''
    }
  }

  private getFeatureMsg(layer: string, f: Object): string {
    switch (layer) {
      case 'tree':
        let pekid = f['plantekey_id']
        if (pekid != '') {
          let plant = this.dataService.all.value.plants[pekid]
          if (plant) return plant.name
          else return 'unknown'
        }
        else return 'unknown'
      case 'trail':
        return f['name']
      case 'poi':
        return f['name']
      case 'zone':
        return `${f['type'] || 'Zone'}: ${f['name']}`
      default:
        return ''
    }
  }

  onClickItem(evt) {
    let feature = this.getBestFeature(evt)
    if (feature === undefined) {
      this.popup.popupInstance.remove()
      return
    }
    let fid = feature['properties']['id'] || feature['id']
    let coordinates: any
    this.ngZone.run(() => {
      switch (feature.layer.id) {
        case 'tree': {
          coordinates = (<any>feature.geometry).coordinates.slice()
          if (feature['properties']['pending']) {
            this.dataService.getPendingTree(fid).subscribe(
              tree => {
                this.treePopupDetail.tree = tree
                this.treePopupDetail.cdr.markForCheck()
              }
            )
          }
          else {
            this.treePopupDetail.tree = this.dataService.all.value.trees[fid]
          }
          this.treePopupDetail.cdr.markForCheck()
          break
        }
        case 'poi': {
          coordinates = (<any>feature.geometry).coordinates.slice()
          this.poiPopupDetail.poi = this.dataService.all.value.pois[fid]
          this.poiPopupDetail.cdr.markForCheck()
          break
        }
        case 'trail': {
          coordinates = evt.lngLat
          this.trailPopupDetail.trail = this.dataService.all.value.trails[fid]
          this.trailPopupDetail.cdr.markForCheck()
          break
        }
        case 'zone': {
          coordinates = evt.lngLat
          this.zonePopupDetail.zone = this.dataService.all.value.zones[fid]
          this.zonePopupDetail.cdr.markForCheck()
          break
        }
        default: { }
      }
      this.currentPopupLayer = feature.layer.id
    })
    this.ngZone.runOutsideAngular(() => {
      this.popup.popupInstance.setLngLat(coordinates)
      setTimeout(() => this.popup.popupInstance.addTo(this.map.mapInstance))
    })
  }

  onClick(evt: MapMouseEvent) {
    if (this.mapEditService.active) {
      this.mapEditService.addTree(evt.lngLat)
    }
    else {
      this.onClickItem(evt)
    }
  }

  onMapPosChange(evt: MapLibreEvent) {
    this.configService.setMapPos({
      center: this.map.mapInstance.getCenter(),
      zoom: this.map.mapInstance.getZoom(),
      bearing: this.map.mapInstance.getBearing(),
      pitch: this.map.mapInstance.getPitch(),
    }).subscribe()
  }
}
