import { Injectable } from '@angular/core';
import { BackgroundGeolocation, BackgroundGeolocationEvents } from '@ionic-native/background-geolocation/ngx';
import { BackgroundGeolocationConfig, BackgroundGeolocationResponse } from '@ionic-native/background-geolocation/ngx';
import { UserLocalNotificationService } from '../user-local-notification/user-local-notification.service';
import { Platform } from '@ionic/angular';
import { LocationModel } from './models/location.model';
import { ConnectorService } from '../connector/connector.service';
import { NativeStorage } from '@ionic-native/native-storage/ngx';
import { AuthenticationService } from '../authentication/authentication.service';
import { ZoneModel } from './models/zone.model';
import { CommonService } from '../common/common.service';
import { InsertLocationRequest } from './models/insert-location.request';
import { GeoUserModel } from './models/geo-user.model';
import { TranslatorService } from '../translator/translator.service';

export enum zoneState {
  out = 0, enter = 1, exit = 2, in = 3
}

 @Injectable({
   providedIn: 'root'
 })
export class GeofencingService {
  public static ZONES_KEY = 'geofencing-zones';
  public static USER_DATA_KEY = 'geofencing-user-data';
  public static GEO_STATUS_KEY = 'geofencing-status';
  public static zonesUrl = 'Services/Patients.svc/REST/GeoF_GetAllPoints';
  public static loggingUrl = 'Services/Patients.svc/rest/GeoF_InsertPatientFileInfo';
  private zonesList: ZoneModel [];
  private userData: GeoUserModel;
  private isPaused: boolean;
  private debug: boolean;

  locations: any [];
  constructor(
    private backgroundGeolocation: BackgroundGeolocation,
    public notif: UserLocalNotificationService,
    public platform: Platform,
    public con: ConnectorService,
    public auth: AuthenticationService,
    public storage: NativeStorage,
    public cs: CommonService,
    public authService: AuthenticationService,
    public ts: TranslatorService,
    ) {
      this.debug = false;
    }
/*
* logs location on server.
*/
  private logLocation(id: number, type: number, userData: GeoUserModel, zone: ZoneModel) {
    this.toast('logging location..');
    this.setNotification('Logging location ' , 'location id: ' + id + ' type: ' + type);
    const req = new InsertLocationRequest();
    req.PointsID = id;
    req.GeoType = type;
    //req.PatientID = userData.PatientID;
    //req.PatientOutSA = userData.PatientOutSA;
    this.auth.setPublicFields(req);
    this.con.postNoLoad(GeofencingService.loggingUrl, req, () => {
      zone.pendingRequest = type; // failed to log, setup pending request
      this.storeZones(this.zonesList);
      this.log('pending request from onError');
      this.setNotification('Pending request from onError', 'zone: ' + zone.Description + ' request type: ' + type);
    }).subscribe((response) => {
      if (this.cs.validResponse(response)) {
        this.setNotification('Log success' , 'location id: ' + id + ' patientID: ' + userData.PatientID);
        this.log('logged successfuly');
        zone.pendingRequest = zoneState.out; // cancel pending requests if any
      } else {
        zone.pendingRequest = type; // failed to log, setup pending request
        this.log('pending request from invalid');
        this.setNotification('Pending request from invalid', 'zone: ' + zone.Description + ' request type: ' + type);
      }
      this.storeZones(this.zonesList);
    });
  }
/*
* Requests geofence zones list
*/
  private requestZones() {
    const req = this.auth.getPublicRequest();
    this.con.postNoLoad(GeofencingService.zonesUrl, req, _ => { // TODO: handle error
    }).subscribe((response: any) => {
      if (this.cs.validResponse(response)) {
        if (this.cs.hasData(response.GeoF_PointsList)) {
          // update zonelist if it doesnt exist or if the server has an updated one
          if (!this.cs.hasData(this.zonesList) || (this.zonesList.length !== response.GeoF_PointsList.length)) {
            this.log('setting new zone list');
            this.zonesList = response.GeoF_PointsList;
            for (const zone of this.zonesList) {
              zone.in = false;
            }
            this.log('storing zones');
            this.storeZones(this.zonesList);
          }
        }
      }
    });
  }
/*
* Store zones list in local storage
*/
  public async storeZones(zones: ZoneModel []) {
    return await this.storage.setItem(GeofencingService.ZONES_KEY, zones);
  }
/*
* Retrieve and set list of zones from local storage
*/
  public async setZones() {
    try {
      const data = await this.storage.getItem(GeofencingService.ZONES_KEY);
      if (this.cs.hasData(data)) {
        this.zonesList = data;
      } else {
        this.requestZones();  // zones list not stored in local storage
      }
    } catch {
      this.requestZones(); // zones list not stored in local storage
    }
  }

  public async storeUserData(user: GeoUserModel) {
    return await this.storage.setItem(GeofencingService.USER_DATA_KEY, user);
  }
/*
* Retrieve and set user data from local storage
*/
  public async setUserFromStorage() {
    const data = await this.storage.getItem(GeofencingService.USER_DATA_KEY);
    if (data) {
      this.userData = data;
    } else {
      this.userData = null;
    }
  }
/*
// turns location tracking on. Sets geo user data and zones list
*/
  public async turnTrackingOn(patientID?: number, outSa?: number): Promise<boolean> {
    this.toast('turning tracking on');
    // set zones list from storage
    await this.setZones();
    // update zone list from server
    this.requestZones();

    // Set user data
    if (patientID) {
      this.userData = new GeoUserModel;
      this.userData.PatientID = patientID;
      this.userData.PatientOutSA = outSa;
    } else if (!this.userData) {
      try {
        await this.setUserFromStorage();
      } catch {
        return false;
      }
    }
    this.userData.isTrackingOn = true;
    this.storeUserData(this.userData);
    this.startTracking();
    return true;
  }
/*
// turns location tracking off. Stops background tracking
*/
  public turnTrackingOff() {
    this.toast('turning tracking off');
    this.stopTracking();
    if (this.userData) {
      this.userData.isTrackingOn = false;
      this.storeUserData(this.userData);
    } else {
      this.setUserFromStorage().then(() => {
          this.userData.isTrackingOn = false;
          this.storeUserData(this.userData);
      });
    }
  }

/*
// Check if geofencing service is enabled
*/
  public async isTrackingEnabled(): Promise<boolean> {
 //   if (this.userData) {
 //     return this.userData.isTrackingOn;
 //   } else {
      try {
        await this.setUserFromStorage();
        if (this.userData) {
          return this.userData.isTrackingOn;
        } else {
          return false;
        }
      } catch {
        throw false;
      }
   // }
  }
/*
// setup and start background gelocation plugin
*/
  public startTracking() {
    const config: BackgroundGeolocationConfig = {
      desiredAccuracy: 10000, // HIGH_ACCURACY = 1 // low = 10000
      stationaryRadius: 20, // 20
      distanceFilter: 10, // 10
      debug: false, //  enable this hear sounds for background-geolocation life-cycle.
      stopOnTerminate: false, // enable this to clear background location settings when the app terminates
      // startOnBoot: true,
      // Android options
      notificationsEnabled: false,
      interval: 60000,  // interval vars for improved battery life
      fastestInterval: 120000,
      activitiesInterval: 10000,
      startForeground: false,
      notificationTitle: 'Running in the background',
      notificationText: '',
      // IOS options
      saveBatteryOnBackground: false
    };
    this.backgroundGeolocation.configure(config)
    .then((location: BackgroundGeolocationResponse) => {
    });

    this.backgroundGeolocation.on(BackgroundGeolocationEvents.location)
      .subscribe((location: BackgroundGeolocationResponse) => {
          this.reportLocation(location);
      });

    this.backgroundGeolocation.on(BackgroundGeolocationEvents.stationary)
      .subscribe((location: BackgroundGeolocationResponse) => {
        this.reportLocation(location);
    });

    this.storage.getItem(GeofencingService.GEO_STATUS_KEY).then(status => {
      if (!status) {
        this.backgroundGeolocation.start();
        this.storage.setItem(GeofencingService.GEO_STATUS_KEY, true);
      }
    }).catch(() => {
      this.toast('turning geofencing on');
      this.backgroundGeolocation.start();
      this.storage.setItem(GeofencingService.GEO_STATUS_KEY, true);
    });

  }
/*
// Helper to startTracking()
*/
  private async reportLocation(location: BackgroundGeolocationResponse) {
    // Check if zones and user is set
    if (!this.userData) {
      await this.setUserFromStorage();
    }
    if (!this.cs.hasData(this.zonesList)) {
      await this.setZones();
    }
    if (this.platform.is('ios')) {
      this.backgroundGeolocation.startTask().then((taskKey) => {
        this.checklocationInZones(location, this.zonesList, this.userData);
        this.backgroundGeolocation.endTask(taskKey);
      });
    } else {
      this.checklocationInZones(location, this.zonesList, this.userData);
    }
    this.storeZones(this.zonesList);
  }

/*
// stops background geoloacation plugin tracking
*/
  public stopTracking() {
    this.toast('Stop Tracking' );
    this.storage.setItem(GeofencingService.GEO_STATUS_KEY, false);
    this.backgroundGeolocation.stop().then(value => {
     // this.toast('Stop Tracking value returned: ' + value);

    });
  }

/*
// Checks if given location triggers any zone events (enter, exit) for all zones, and pauses the service if
// the location was far from all zones
*/
  private checklocationInZones(location: BackgroundGeolocationResponse, zones: ZoneModel [], user: GeoUserModel) {
    this.printLocation(location, 'check location in zones');
    let currentMinDist = 9999999;
    for (const zone of zones) {
      const userLocation = new LocationModel(location.latitude, location.longitude);
      const event = this.checkLocationInZone(userLocation, zone);
      const currentDistance = this.distanceFromZone(userLocation, zone);
      currentMinDist = (currentMinDist > currentDistance ? currentDistance : currentMinDist);
      if (zone.GEOF_ID === 16) {
        this.log(zone.Description);
        this.log('zone state: ' + zone.in + ' pending: ' + zone.pendingRequest + 'current event: ' + event);
      }
      if (event === zoneState.in) {
        zone.in = true;
      }
      // send enter request
      if (event === zoneState.enter ||
         (event === zoneState.in && zone.pendingRequest === zoneState.enter)) {
        zone.in = true;
        this.setNotification('Enter ZONE ' + zone.Description, 'location lat: ' + location.latitude + ' lng: ' + location.longitude);
        this.log('enter Zone' + zone.Description + ' ' + 'location: ' + location.latitude + ',' + location.longitude);
        this.logLocation(zone.GEOF_ID, zoneState.enter, user, zone);
      }
      // send exit request
      if (event === zoneState.exit ||
         (event === zoneState.out && zone.pendingRequest === zoneState.exit)) {
        zone.in = false;
        this.setNotification('Exit ZONE ' + zone.Description, 'location lat: ' + location.latitude + ' lng: ' + location.longitude);
        this.log('exit Zone' + zone.Description + ' ' + 'location: ' + location.latitude + ',' + location.longitude);
        this.logLocation(zone.GEOF_ID, zoneState.exit, user, zone);
      }
    }
    const minDist = this.toDecimalDegrees(5000); // minimum distance location needs to be to all zones to pause service
    const maxDist = this.toDecimalDegrees(10000); // Max distance used to have a factor between 0-1
    if (currentMinDist >= minDist) {
      const currentDis = (currentMinDist > maxDist ? maxDist : currentMinDist);
      this.pauseService((currentDis - minDist) / (maxDist - minDist));
    }
  }
/*
// Checks the state of given location for the given zone
*/
  private checkLocationInZone(userLocation: LocationModel, zone: ZoneModel): zoneState {
    const isIn = this.isPointInZone(userLocation.lng, userLocation.lat, zone.Longitude,
      zone.Latitude, this.toDecimalDegrees(zone.Radius));
    if (isIn === false && zone.in === true) {
      return zoneState.exit;
    }
    if (isIn === true && zone.in === false) {
      return zoneState.enter;
    }
    if (isIn) {
      return zoneState.in;
    }
    return zoneState.out;
  }
/*
// calculates the distance between given location and given zone
*/
  private distanceFromZone(userLocation: LocationModel, zone: ZoneModel): number {
    return this.distance(userLocation.lng, userLocation.lat, zone.Longitude, zone.Latitude);
  }
/*
// checks if a point p is in a circular zone with center z and radius r
*/
  public isPointInZone(xp, yp, xz, yz, r): boolean {
    return this.distance(xp, yp, xz, yz) < r;
  }
/*
// Measures distance between two points
*/
  public distance(xp, yp, xz, yz): number {
    return Math.sqrt(Math.pow(Math.abs(xp - xz), 2) + Math.pow(Math.abs(yp - yz) , 2));
  }
/*
// Convert meters to decimal degrees
*/
  public toDecimalDegrees(meters: number) {
    return meters / 1000 / 111.32;
  }
/*
// Pauses tracking service for a time multiplied by given factor
*/
  public pauseService(factor: number) {
    if (!this.isPaused) {
      this.stopTracking();
      this.isPaused = true;
      // time to pause min=120 sec max=1800 sec
      const time = (1680000 * factor); // time in milliseconds
      this.setNotification('pausing tracking service', 'Pause for ' + time + ' FYI factor is: ' + factor);
      setTimeout(() => {
        this.startTracking();
        this.isPaused = false;
        this.setNotification('resuming tracking service', 'Paused for ' + time + ' FYI factor is: ' + factor);
      }, time);
    }
  }

  public startGeoFromHome() {
     if (this.cs.isCordova()) {
       this.isTrackingEnabled().then((result) => {
        if (result) {
             this.turnTrackingOff();
             this.turnTrackingOn();
           }
         }).catch(() => {
           // first time login, setup geofencing
           // local storage should reset when user logs out
           if (this.authService.isAuthenticated()) {
             const user = this.authService.getAuthenticatedUser();
             this.turnTrackingOff();
             this.cs.presentConfirmDialog(this.ts.trPK('settings', 'enable-geo'), () => {
               //  this.turnTrackingOn(user.PatientID, +user.PatientOutSA);
             }, () => {
               //  this.turnTrackingOn(user.PatientID, +user.PatientOutSA);
                 this.turnTrackingOff();
             });
           }
         });
       }
  }

  // ---------- TESTING FUNCTIONS -------------- //
  public setNotification(title: string, text: string) {
    if (this.debug) {
      const date = new Date();
      date.setSeconds(date.getSeconds() + 2);
      this.notif.createNotification(title, text, date, 'geofencing');
    }
  }

  public printLocation(location: BackgroundGeolocationResponse, source: string) {
    if (this.debug) {
      console.log('-------------------------------------------------------------');
      console.log(source);
      console.log(this.zonesList);
      console.log(location);
      console.log('location: ' + location.latitude + ', ' + location.longitude);
    }
  }

  public printAllLocations() {
    if (this.debug) {
      this.backgroundGeolocation.getLocations().then(locations => {
        console.log(locations);
      });
    }
  }

  public log(msg: string) {
    if (this.debug) {
      console.log(msg);
    }
  }

  public toast(msg: string) {
    if (this.debug) {
      this.cs.toast(msg);
    }
  }
}
