import { ApplicationRef, Injectable } from '@angular/core'
import { BehaviorSubject, Observable, Subject, combineLatest } from 'rxjs'
import { HttpClient } from '@angular/common/http'
import { Router } from '@angular/router'
import { catchError, first, map } from 'rxjs/operators'
import { throwError } from 'rxjs'

import { v1 as uuidv1 } from 'uuid'
import { GeoJSONSource, LngLat } from 'maplibre-gl'
import { NgxIndexedDBService } from 'ngx-indexed-db'

import { PlantsTrails, Trail, Trails, Trees, Pois,
         Zones, TreeTrail, All, Plant, Tree } from './models'
import { MessageService } from './message.service'
import { Feature } from 'geojson'

export const pendingTreeDbName = 'pendingTree'

export class TreeDef {
  constructor(
    public plant: Plant,
    public trails: Trail[],
  ) {}
}

export class NewTree {
  id: string
  constructor(
    public treeDef: TreeDef,
    public lngLat: LngLat,
    public dataService: DataService,
    public picture?: string,
    public uuid1?: string,
    public pending?: number,
    public details: Object = {}
  ) {
    if (!this.uuid1) {
      this.uuid1 = uuidv1()
    }
  }

  getFeature(): Feature {
    return {
      'type': 'Feature',
      'geometry': {
        'type': 'Point',
        'coordinates': [this.lngLat.lng, this.lngLat.lat, 0]
      },
      'properties': {
        'plantekey_id': this.treeDef.plant.id,
        'symbol': this.dataService.all.value.plants[this.treeDef.plant.id].symbol,
        'id': this.id || this.uuid1,
        'pending': this.pending
      },
    }
  }

  getForStorage(): Object {
    let so = {
      feature: this.getFeature(),
      trails: this.treeDef.trails.map(trail => trail.id),
      uuid1: this.uuid1,
      id: this.uuid1,
      details: this.details
    }
    if (this.picture) {
      so['picture'] = this.picture
    }
    return so
  }

  getFormData(): FormData {
    let formData = new FormData()
    formData.set('plantekey_id', this.treeDef.plant.id)
    if (this.treeDef.trails.length > 0) {
      formData.set('trail_ids', this.treeDef.trails.map(
        trail => trail.id.toString()).join(',')
      )
    }
    formData.set('lng', this.lngLat.lng.toString())
    formData.set('lat', this.lngLat.lat.toString())
    formData.set('uuid1', this.uuid1)
    if (Object.keys(this.details).length > 0) {
      formData.set('details', JSON.stringify(this.details))
    }
    if (this.picture) {
      formData.set('picture', this.picture)
    }
    return formData
  }
}

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(
    public httpClient: HttpClient,
    public messageService: MessageService,
    public dbService: NgxIndexedDBService,
    public router: Router,
    public appRef: ApplicationRef,
  ) {
    this.all.subscribe(
      all => {
        // Assign plants and trails to trees
        Object.values(all.trees).forEach(
          tree => {
            tree.plant = all.plants[tree.plantekeyId]
            let trail_ids = new Set(all.tree_trails.filter(
              tt => tt.tree_id == tree.id
            ).map(tt => tt.trail_id))
            tree.trails = Object.values(all.trails).filter(
              trail => trail_ids.has(+trail.id)
            )
          }
        )
      }
    )
  }

  public trailFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
  public treeFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
  public poisFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
  public zoneFeatures: BehaviorSubject<GeoJSON.FeatureCollection> = new BehaviorSubject<GeoJSON.FeatureCollection>(<GeoJSON.FeatureCollection>{})
  //public treesUpdated: Observable<void>

  public trails: BehaviorSubject<Trails> = new BehaviorSubject<Trails>({})
  //public trails$ = this.trails.asObservable()

  public tree_trail: BehaviorSubject<TreeTrail[]> = new BehaviorSubject<TreeTrail[]>([])
  //public tree_trail$ = this.tree_trail.asObservable()

  //public trees: BehaviorSubject<Tree[]> = new BehaviorSubject<Tree[]>([])
  public trees: BehaviorSubject<Trees> = new BehaviorSubject<Trees>({})
  //public trees$ = this.trees.asObservable()
  public pois: BehaviorSubject<Pois> = new BehaviorSubject<Pois>({})
  public zones: BehaviorSubject<Zones> = new BehaviorSubject<Zones>({})

  public all: BehaviorSubject<All> = new BehaviorSubject<All>(new All())

  public addTree$: Subject<NewTree> = new Subject<NewTree>()

  public updatePendingTrees$: Subject<void> = new Subject()

  set_tree_trail(data: TreeTrail[]) {
    this.tree_trail.next(data)
  }

  plant_trail: PlantsTrails = {}

  plant_in_some_trail(plantId: string): boolean {
    return plantId in this.plant_trail
  }

  // Try to send to server, falling back to local storage for later sync
  addTree(location: LngLat, treeDef: TreeDef, picture?: string, details: Object = {}) {
    const newTree = new NewTree(treeDef, location, this, picture, undefined, 1, details)
    this.httpClient.post('v1/tree', newTree.getFormData()).pipe(
      map(data => {
        // Got an id from the server for that tree
        newTree.id = data['id']
        this.addTree$.next(newTree)
      }),
      catchError(err => {
        this.messageService.message.next(`Cannot save: (${err.statusText}). ` +
                    'It is stored in the local device and will be saved later.')
        this.addTree$.next(newTree)
        this.addPendingTree(newTree)
        return new Observable()
      })
    ).subscribe()
  }

  // Store in local database to sync it with the server when possible
  addPendingTree(newTree: NewTree) {
    this.dbService.add(pendingTreeDbName, newTree.getForStorage()).subscribe()
  }

  // Get a geo feature source of pending trees (from local indexedDB)
  getPendingTrees(): Observable<Object[]> {
    return this.dbService.getAll(pendingTreeDbName)
  }

  getPendingTree(id: string): Observable<Tree> {
    return this.dbService.getByID(pendingTreeDbName, id).pipe(map(
      data => {
        const feature = data['feature']
        let tree = new Tree(
          feature['properties']['id'],
          feature,
          feature['properties']['plantekey_id'],
          data['picture'],
        )
        tree.plant = this.all.value.plants[feature['properties']['plantekey_id']]
        return tree
      }
    ))
  }

  deletePendingTrees() {
    this.getPendingTrees().subscribe(
      trees => this.dbService.bulkDelete(
        pendingTreeDbName,
        trees.map(tree => tree['id'])
        ).subscribe(_ => {
          this.updatePendingTrees$.next()
          this.appRef.tick()
        })
    )
  }

  // Attempts to send the pending trees to the server
  syncPendingTrees(canRedirectToLogin: boolean=false) {
    this.dbService.count(pendingTreeDbName).pipe(first()).subscribe(
      count => {
        if (count > 0) {
          combineLatest([
            this.getPendingTrees(),
            this.all
          ]).subscribe(
            ([treeList, all]) => {
              if (Object.keys(all.trees).length == 0) { return }
              treeList.map(
                tr => {
                  const feature = tr['feature']
                  let newTree = new NewTree(
                    new TreeDef(
                      all.plants[feature['properties']['plantekey_id']],
                      []
                      // this.all.getValue().trails.filter(
                      //   trail => trail.
                      // )
                    ),
                    new LngLat(
                      feature['geometry']['coordinates'][0],
                      feature['geometry']['coordinates'][1]
                    ),
                    this,
                    tr['picture'],
                    tr['uuid1'],
                    undefined,
                    tr['details'],
                  )
                  this.httpClient.post('v1/tree', newTree.getFormData()).pipe(
                    map(data => {
                      this.dbService.deleteByKey(
                        pendingTreeDbName, feature['properties']['id']
                      ).subscribe(
                        r => {
                          // Got an id from the server for that tree
                          newTree.id = data['id']
                          this.updatePendingTrees$.next()
                        }
                      )
                      this.appRef.tick()
                    })
                  ).subscribe({
                    error: (err) => {
                      if (err.status == 401) {
                        this.messageService.message.next(`Cannot save pending trees, you need to login again.`)
                        if (canRedirectToLogin) {
                          this.router.navigate(['/login'])
                        }
                        // TODO: resubmit pending trees
                      }
                      else {
                        this.messageService.message.next(`Cannot save pending trees (${err.statusText}).`)
                      }
                    }
                  })
                }
              )
            }
          )
        }
      }
    )
  }
}
