import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { ActivatedRoute, Router } from '@angular/router'
import { Location } from "@angular/common"

import { Observable, BehaviorSubject, forkJoin, of, from } from 'rxjs'
import { first, map } from 'rxjs/operators'

import { GeoJSONSource } from 'maplibre-gl'

import { GeolocationService } from '@ng-web-apis/geolocation'
import { NgxImageCompressService } from 'ngx-image-compress'

import * as untar from 'js-untar'

import { ConfigService } from './config.service'
import { DataService, pendingTreeDbName } from './data.service'
import { FeatureFinderService } from './feature-finder.service'
import {
  FeatureTarget, TreeTrail, Tree, Trees,
  Trail, Trails, All, Pois, Poi, Zones, Zone,
  Styles, Style
} from './models'
import { PlantekeyService } from './plantekey.service'
import { MessageService } from './message.service'
import { FeatureCollection } from 'geojson'

// CACHE_NAME is not exported
//import { CACHE_NAME } from 'maplibre-gl/src/util/tile_request_cache'
const CACHE_NAME = 'mapbox-tiles'

@Injectable()
export class ActionService {

  // Some cross application observables
  //public settings = new BehaviorSubject<Settings>(new Settings({}))
  //settings$ = this.settings.asObservable()

  private _isOnline = new BehaviorSubject<boolean>(navigator.onLine)
  isOnline$ = this._isOnline.asObservable()

  private _nearestTree = new BehaviorSubject<FeatureTarget>(undefined)
  nearestTree$ = this._nearestTree.asObservable()

  location: GeolocationPosition | undefined = undefined

  constructor(
    public readonly geolocation$: GeolocationService,
    public httpClient: HttpClient,
    public configService: ConfigService,
    public dataService: DataService,
    public featureFinderService: FeatureFinderService,
    private router: Router,
    private route: ActivatedRoute,
    public browserLocation: Location,
    public plantekeyService: PlantekeyService,
    public messageService: MessageService,
    public imageCompressService: NgxImageCompressService,
  ) {
    window.addEventListener("online",
      () => {
        this._isOnline.next(true)
      }
    )
    window.addEventListener("offline",
      () => {
        this._isOnline.next(false)
      }
    )

    this.isOnline$.subscribe(
      isOnline => {
        if (isOnline) {
          this.dataService.syncPendingTrees()
        }
      }
    )

    this.geolocation$.subscribe({
      next: (location: GeolocationPosition) => {
        this.location = location
        this.onLocationChange()
      },
      error: (err) => {
        console.log(err)
        this.location = undefined
      }
    })
  }

  logout() {
    localStorage.removeItem('token')
    this.configService.setUserPref('userName', undefined)
    this.browserLocation.back()
  }

  onLocationChange() {
    let newFeature: FeatureTarget = this.featureFinderService.findNewFeature(this.location)
    if (newFeature) {
      this.vibrate()
      this._nearestTree.next(newFeature)
    }
  }

  getAllGeoJSONPois(): Observable<FeatureCollection> {
    return this.httpClient.get<FeatureCollection>('v1/poi').pipe(
      map(data => {
        this.dataService.poisFeatures.next(data)
        return data
      })
    )
  }

  getAllGeoJSONTrees(): Observable<FeatureCollection> {
    return this.httpClient.get<FeatureCollection>('v1/tree').pipe(
      map(data => {
        this.dataService.treeFeatures.next(data)
        return data
      })
    )
  }

  getAllGeoJSONZones(): Observable<FeatureCollection> {
    return this.httpClient.get<FeatureCollection>('v1/zone').pipe(
      map(data => {
        this.dataService.zoneFeatures.next(data)
        return data
      })
    )
  }

  getAllGeoJSONTrails(): Observable<FeatureCollection> {
    return this.httpClient.get<FeatureCollection>('v1/trail').pipe(
      map(data => {
        this.dataService.trailFeatures.next(data)
        return data
      })
    )
  }

  getAllTrees(): Observable<Trees> {
    return this.getAllGeoJSONTrees().pipe(map(
      source => Object.fromEntries(
        source['features'].map(
          feature => new Tree(
            <string>feature['id'],
            feature,
            feature['properties']['plantekey_id'],
            feature['properties']['photo'],
            feature['properties']['data'],
            feature['properties']['height'],
            feature['properties']['comments'],
          )
        ).map((t: Tree) => [t.id, t]))
    ))
  }

  getAllPois(): Observable<Pois> {
    return this.getAllGeoJSONPois().pipe(map(
      source => Object.fromEntries(
        source['features'].map(
          feature => new Poi(
            +feature['id'],
            feature,
            feature['properties']['name'],
            feature['properties']['type'],
            feature['properties']['description'],
            feature['properties']['photo'],
            feature['properties']['data'],
          )
        ).map((t: Poi) => [t.id, t]))
    ))
  }

  getAllTrails(): Observable<Trails> {
    return this.getAllGeoJSONTrails().pipe(map(
      trailsSource => {
        let tl: Trail[] = trailsSource['features'].map(
          feature => new Trail(
            +feature['id'],
            feature['properties']['name'],
            feature['properties']['description'],
            feature,
            {},
            feature['properties']['photo']
          )
        )
        let trails: Trails = {}
        for (let t of tl) {
          trails[t.id] = t
        }
        return trails
      }
    ))
  }

  getAllTreeTrails(): Observable<TreeTrail[]> {
    return this.httpClient.get<TreeTrail[]>('v1/tree-trail').pipe(
      map(data => {
        this.dataService.set_tree_trail(data)
        return data
      })
    )
  }

  getAllZones(): Observable<Zones> {
    return this.getAllGeoJSONZones().pipe(map(
      source => Object.fromEntries(
        source['features'].map(
          zone => new Zone(
            +zone['id'],
            zone,
            zone['properties']['name'],
            zone['properties']['type'],
            zone['properties']['description'],
            zone['properties']['photo'],
            zone['properties']['data'],
          )
        ).map((t: Zone) => [t.id, t]))
    ))
  }

  getAllStyles(): Observable<Styles> {
    return this.httpClient.get<Style[]>('v1/style').pipe(map(
      styles => Object.fromEntries(
        styles.map(
          style => new Style(
            style['layer'],
            style['paint'] || {},
            style['layout'] || {},
          )
        ).map((t: Style) => [t.layer, t]))
    ))
  }

  fetchData(): Observable<void> {
    return forkJoin([
      this.getAllTrails(),
      this.getAllTrees(),
      this.getAllTreeTrails(),
      this.getAllPois(),
      this.getAllZones(),
      this.getAllStyles(),
      this.plantekeyService.getAllPlants(),
    ]).pipe(map(
      ([trails, trees, tts, pois, zones, styles, plants]) => {
        Object.values(trails).forEach(
          trail => {
            let tl = tts.filter(tt => tt.trail_id == trail.id)
              .map(tt => trees[tt.tree_id])
            trail.trees = Object.fromEntries(
              tts.filter(tt => tt.trail_id == trail.id).map(
                (tt => [tt.tree_id, trees[tt.tree_id]])
              ))
          }
        )
        tts.forEach(
          tt => {
            if (tt.tree_id in trees) {
              let plantId = trees[tt.tree_id].plantekeyId
              if (!this.dataService.plant_trail[plantId]) {
                this.dataService.plant_trail[plantId] = {}
              }
              this.dataService.plant_trail[plantId][tt.trail_id] = trails[tt.trail_id]
            }
          }
        )
        this.dataService.all.next(new All(trees, trails, tts, plants, pois, zones, styles))
      }
    ))
  }

  getUpdates(): Observable<Observable<any>> {
    return from(window.caches.open('v1')).pipe(
      map(
        cache =>
          forkJoin([
            this.getSimpleUpdates(cache),
            //this.getComplexUpdates(cache)
          ])
      )
    )
  }

  cacheImages(): Observable<Observable<any>> {
    return from(window.caches.open('attachments')).pipe(
      map(
        cache => forkJoin([
          this.getPlantekeyImagesTarFile(cache),
          this.getAllAttachmentsTarFile(cache),
        ])
      )
    )
  }

  cacheMapData(): Observable<Observable<any>> {
    return from(window.caches.open(CACHE_NAME)).pipe(
      map(
        cache => this.getMapData(cache)
      )
    )
  }

  getMapData(cache: Cache, style = 'osm'): Observable<void> {
    return this.httpClient.get(`/tiles/${style}/all.tar`, {
      'responseType': 'blob'
    }).pipe(
      map(
        (data: any) => {
          from(data.arrayBuffer()).subscribe(
            buf => from(untar.default(buf)).subscribe(
              (tiles: any) => {
                for (let tile of tiles) {
                  cache.put(
                    `/tiles/${tile.name}`,
                    new Response(
                      tile.blob,
                      {
                        headers: {
                          'content-type': tile.name == `style/${style}` ? 'application/json' : 'application/octet-stream',
                          'content-length': tile.size
                        }
                      }
                    )
                  )
                }
              }
            )
          )
        }
      )
    )
  }

  getPlantekeyImagesTarFile(cache: Cache): Observable<void> {
    return this.httpClient.get('/static/cache/plantekey/thumbnails.tar', {
      'responseType': 'blob'
    }).pipe(
      map(
        (data: any) => {
          from(data.arrayBuffer()).subscribe(
            buf => from(untar.default(buf)).subscribe(
              (imgs: any) => {
                for (let img of imgs) {
                  cache.put(
                    `/attachment/plantekey/thumb/${img.name.split('/').pop()}`,
                    new Response(
                      img.blob,
                      {
                        headers: {
                          'content-type': 'image/jpeg',
                          'content-length': img.size
                        }
                      }
                    )
                  )
                }
              }
            )
          )
        }
      )
    )
  }

  getAllAttachmentsTarFile(cache: Cache): Observable<void> {
    return this.httpClient.get('/static/cache/attachments.tar', {
      'responseType': 'blob'
    }).pipe(
      map(
        (data: any) => {
          from(data.arrayBuffer()).subscribe(
            buf => from(untar.default(buf)).subscribe(
              (imgs: any) => {
                for (let img of imgs) {
                  let splitPath = img['name'].split('/')
                  let fileName = splitPath.pop()
                  let id = splitPath.pop()
                  let type = splitPath.pop()
                  cache.put(
                    `/attachment/${type}/${id}/${fileName}`,
                    new Response(
                      img.blob,
                      {
                        headers: {
                          'content-type': 'image/jpeg',
                          'content-length': img.size
                        }
                      }
                    )
                  )
                }
              }
            )
          )
        }
      )
    )
  }

  clearCache() {
    return from(window.caches.delete('v1'))
  }

  updatePlantekeyData(): Observable<any> {
    return this.httpClient.get('v1/plantekey/updateData')
  }

  updatePlantekeyImages(): Observable<any> {
    return this.httpClient.get('v1/plantekey/updateImages')
  }

  makeAttachmentsTarFile(): Observable<any> {
    return this.httpClient.get('v1/makeAttachmentsTarFile')
  }

  /*
   * Get simple (atomic) requests
   */
  getSimpleUpdates(cache: Cache): Observable<void> {
    return from(cache.addAll([
      'v1/trail',
      'v1/tree',
      'v1/plantekey/details',
      //'v1/plantekey/plant/info',
      'v1/tree-trail',
    ]))
  }

  upload(type: string, field: string, id: string, formData: FormData) {
    return this.httpClient.post(`v1/upload/${type}/${field}/${id}`, formData)
  }

  /*
   * Get complex objects from the server, that are expanded and
   * put in the cache in separate entities
   */
  // XXX: Unused
  getComplexUpdates(cache: Cache): Observable<any> {
    return this.httpClient.get('v1/plantekey/plant/info').pipe(
      map(
        resp => {
          for (let id in resp['plant']) {
            let plant = resp['plant'][id]
            let family = plant['family'].replace(' ', '-').toLowerCase()
            let characteristics = resp['characteristics'][id]
            let images = resp['image'][id]
            cache.put(
              `v1/plantekey/plant/info/${family}/${id}`,
              new Response(
                plant,
                { headers: { 'content-type': 'application/json' } }
              )
            )
          }
          return of()
        }
      )
    )
  }

  /*
  skipIntro() {
    this.configService.conf.value.skipIntro = true
    this.configService.storeUserData()
    this.router.navigate([''], {relativeTo: this.route});
  }
  */

  vibrate() {
    if (this.configService.conf.value.vibrate) {
      window.navigator.vibrate([200, 100, 200])
    }
  }

  updateLocalData() {
    this.getUpdates().subscribe(
      dbFetch => {
        dbFetch.subscribe({
          complete: () => {
            this.messageService.message.next('Update local data successful')
          },
          error: error => {
            console.error(error)
            this.messageService.message.next('Update failed')
          }
        })
      }
    )
  }

  updateLocalImages() {
    this.cacheImages().subscribe(
      dbFetch => {
        dbFetch.subscribe({
          complete: () => {
            this.messageService.message.next('Update local images successful')
          },
          error: error => {
            console.error(error)
            this.messageService.message.next('Update local images failed')
          }
        })
      }
    )
  }

  updateLocalMapData() {
    this.cacheMapData().subscribe(
      dbFetch => {
        dbFetch.subscribe({
          complete: () => {
            this.messageService.message.next('Update map data successful')
          },
          error: error => {
            console.error(error)
            this.messageService.message.next('Update map data failed')
          }
        })
      }
    )
  }

  clearLocalData() {
    this.clearCache().subscribe(
      result => {
        this.messageService.message.next('Cache cleared')
      }
    )
  }

  /*
   * Get all data from server needed for offline browsing.
   * Insert these in the cache storage
   */
  getPicturesForOffline() {
    this.updateLocalMapData()
    this.updateLocalImages()
    this.updateLocalData()
  }
}
