#include "LoRaWan_APP.h"
#include <Wire.h>
#include "HT_SSD1306Wire.h"

#include "gps_sniffer.h"

#define RXD2 37
#define UPDATE_EVERY_MS 3000
#define LORA_SPREADING_FACTOR 9



gps_sniffer *gsniff;
gps_sniffer::ubx_nav_posllh *pos;
int32_t speed = 0;
// Initialized high in order to send the first position
double distance_moved = 50;
RTC_DATA_ATTR int RTC_OLD_LAT = 0;
RTC_DATA_ATTR int RTC_OLD_LON = 0;
const int MIN_DISTANCE_MOVED = 3;
RTC_DATA_ATTR int RTC_BEEN_STATIONARY = 0;
const int SLEEP_AFTER_STATIONARY = 4;


/* You MUST replace these three with the values for YOUR board, in YOUR Helium account. */
uint8_t devEui[] = { 0x60, 0x81, 0xF9, 0x9A, 0xBF, 0x1C, 0xF8, 0x43 };
uint8_t appEui[] = { 0x60, 0x81, 0xF9, 0xF9, 0x9D, 0x37, 0x56, 0x18 };
uint8_t appKey[] = { 0xDE, 0xD5, 0x9B, 0xD7, 0x33, 0x7B, 0x36, 0x32, 0x0A, 0xB4, 0x78, 0x21, 0x95, 0xD5, 0x96, 0x80 };


/* ABP para*/
uint8_t nwkSKey[] = { 0x15, 0xb1, 0xd0, 0xef, 0xa4, 0x63, 0xdf, 0xbe, 0x3d, 0x11, 0x18, 0x1e, 0x1e, 0xc7, 0xda,0x85 };
uint8_t appSKey[] = { 0xd7, 0x2c, 0x78, 0x75, 0x8c, 0xdc, 0xca, 0xbf, 0x55, 0xee, 0x4a, 0x77, 0x8d, 0x16, 0xef,0x67 };
uint32_t devAddr =  ( uint32_t )0x007e6ae1;

/*LoraWan Class, Class A and Class C are supported*/
DeviceClass_t  loraWanClass = CLASS_A;

/*the application data transmission duty cycle.  value in [ms].*/
RTC_DATA_ATTR uint32_t appTxDutyCycle = 20000;

/*OTAA or ABP*/
bool overTheAirActivation = true;

/*ADR enable*/
bool loraWanAdr = false;

/* Indicates if the node is sending confirmed or unconfirmed messages */
bool isTxConfirmed = false;

/*LoraWan channelsmask, default channels 0-7*/
uint16_t userChannelsMask[6]={ 0xFF00,0x0000,0x0000,0x0000,0x0000,0x0000 };


/* Application port */
uint8_t appPort = 2;

uint8_t confirmedNbTrials = 4;


LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;
extern SSD1306Wire display;


static void prepareTxFrame( uint8_t port )
{
  if (pos != NULL) {

    appDataSize = 20; //AppDataSize max value is 64

    appData[3] = pos->latitude >> 24;
    appData[2] = pos->latitude >> 16;
    appData[1] = pos->latitude >>  8;
    appData[0] = pos->latitude;

    appData[7] = pos->longitude >> 24;
    appData[6] = pos->longitude >> 16;
    appData[5] = pos->longitude >>  8;
    appData[4] = pos->longitude;

    appData[11] = pos->altitude_ellipsoid >> 24;
    appData[10] = pos->altitude_ellipsoid >> 16;
    appData[9] = pos->altitude_ellipsoid >> 8;
    appData[8] = pos->altitude_ellipsoid;

    appData[15] = speed >> 24;
    appData[14] = speed >> 16;
    appData[13] = speed >> 8;
    appData[12] = speed;

    int32_t accuracy = pos->horizontal_accuracy / 1000; // UBX reports it in mm.

    appData[19] = accuracy >> 24;
    appData[18] = accuracy >> 16;
    appData[17] = accuracy >>  8;
    appData[16] = accuracy;

  } else {
    appDataSize = 1;
    appData[0] = 0;
  }
}


double distanceBetween(double lat1, double long1, double lat2, double long2)
{
  // returns distance in meters between two positions, both specified
  // as signed decimal-degrees latitude and longitude. Uses great-circle
  // distance computation for hypothetical sphere of radius 6372795 meters.
  // Because Earth is no exact sphere, rounding errors may be up to 0.5%.
  // Courtesy of Maarten Lamers
  double delta = radians(long1-long2);
  double sdlong = sin(delta);
  double cdlong = cos(delta);
  lat1 = radians(lat1);
  lat2 = radians(lat2);
  double slat1 = sin(lat1);
  double clat1 = cos(lat1);
  double slat2 = sin(lat2);
  double clat2 = cos(lat2);
  delta = (clat1 * slat2) - (slat1 * clat2 * cdlong);
  delta = sq(delta);
  delta += sq(clat2 * sdlong);
  delta = sqrt(delta);
  double denom = (slat1 * slat2) + (clat1 * clat2 * cdlong);
  delta = atan2(delta, denom);
  return delta * 6372795;
}



void setup()
{

  Mcu.begin();
  Serial.begin(115200);
  while (!Serial);
  SPI.begin(SCK,MISO,MOSI,SS);
  deviceState = DEVICE_STATE_INIT;

  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, RXD2, -1);
  gsniff = new gps_sniffer(&Serial2);
}

void update_location() {

  static unsigned long updatetimer = 0;
  gps_sniffer::ubx_nav_posllh *newpos;

    // if (updatetimer + UPDATE_EVERY_MS < millis()) {
        updatetimer = millis();
        newpos = gsniff->GetLocation(&speed);
        while( (newpos == NULL) && millis() < updatetimer + 100) {
          newpos = gsniff->GetLocation(&speed);
        }


        if (newpos != NULL) {
          if (RTC_OLD_LAT != 0 && RTC_OLD_LON != 0) {
            distance_moved = distanceBetween( (double) newpos->latitude / 10000000, (double) newpos->longitude / 10000000, (double) RTC_OLD_LAT / 10000000,  (double) RTC_OLD_LON / 10000000);
            Serial.printf("distance_moved: %.2f\n", distance_moved);
            if (distance_moved < MIN_DISTANCE_MOVED) {
              if (RTC_BEEN_STATIONARY++ > SLEEP_AFTER_STATIONARY) {
                // Do not double past 6 hours.
                if( appTxDutyCycle < (6 * 60 * 60 * 1000) ) {
                  appTxDutyCycle = appTxDutyCycle * 2;
                  Serial.printf("Been stationary for %i cycles, doubling period to %i seconds\n", RTC_BEEN_STATIONARY - 1, (int) appTxDutyCycle / 1000);
                }
                RTC_BEEN_STATIONARY = 0;
              }
            } else {
              if (appTxDutyCycle > 60000) {
                appTxDutyCycle = 30000;
              }
              RTC_BEEN_STATIONARY = 0;
            }
          }


          int old_lat = RTC_OLD_LAT;
          int old_lon = RTC_OLD_LON;

          pos = newpos;

          printf("long: %.6f, latitude: %.6f aka 0x%08x, h_accuracy: %i, distance: %.2f\n", (float) pos->longitude / 10000000, (float) pos->latitude / 10000000, pos->latitude, pos->horizontal_accuracy / 1000, (float) distance_moved);
          RTC_OLD_LAT = pos->latitude;
          RTC_OLD_LON = pos->longitude;
        }
    // }
  printf("returning from update_location, pos is: %i\n", pos);
}



void loop()
{
  switch( deviceState )
  {
    case DEVICE_STATE_INIT:
    {
#if(LORAWAN_DEVEUI_AUTO)
      LoRaWAN.generateDeveuiByChipID();
#endif
      LoRaWAN.init(loraWanClass,loraWanRegion);
      break;
    }
    case DEVICE_STATE_JOIN:
    {
      LoRaWAN.join();
      break;
    }
    case DEVICE_STATE_SEND:
    {
        update_location();
        if (distance_moved >= MIN_DISTANCE_MOVED) {
          LoRaWAN.displaySending();
          prepareTxFrame( appPort );
          LoRaWAN.send();
        }
      deviceState = DEVICE_STATE_CYCLE;
      break;
    }
    case DEVICE_STATE_CYCLE:
    {
      // Schedule next packet transmission
      txDutyCycleTime = appTxDutyCycle + randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND );
      LoRaWAN.cycle(txDutyCycleTime);
      deviceState = DEVICE_STATE_SLEEP;
      break;
    }
    case DEVICE_STATE_SLEEP:
    {
      LoRaWAN.displayAck();
      // if (pos != NULL) {
        // pos = gsniff->GetLocation(&speed);
        LoRaWAN.sleep(loraWanClass);
        // pos = gsniff->GetLocation(&speed);
        // update_location();
      // }
      break;
    }
    default:
    {
      deviceState = DEVICE_STATE_INIT;
      break;
    }
  }
}