#include <Arduino.h>

#define _DEBUG_

#define _DISPLAY_

#ifdef _DISPLAY_
#define _BUTTON_
#define _LUXOMETER_
#include "TM1637_Mini.h"
#endif

#include "GPSport_config.h"
#include <NMEAGPS.h>
//#include <GPSport.h>

#ifdef _DEBUG_
#define DEBUG_PRINT(args...) DEBUG_PORT.print(args)
#define DEBUG_PRINTLN(args...) DEBUG_PORT.println(args)
#else
#define DEBUG_PRINT(args...)
#define DEBUG_PRINTLN(args...)
#endif

NMEAGPS  gps; // This parses the GPS characters
gps_fix  fix; // This holds on to the latest values

#include <SPI.h>
#include "SdFat_config.h"
#include <SdFat.h>
SdFat SD;
File32 trkFile;

#ifdef _DISPLAY_
const byte TM_CLK = 4;
const byte TM_DIO = 5;

#ifdef _BUTTON_
const byte BTN_PIN = 8;
#endif

#ifdef _LUXOMETER_
#include <Wire.h>
#include <BH1750.h>

BH1750 luxometro;
//const BH1750::Mode luxMode = CONTINUOUS_HIGH_RES_MODE;
#endif

TM1637 display(TM_CLK, TM_DIO);
#endif

const byte SD_LED = 6;
const byte PWR_LED = 7;
const byte PWR_PIN = A0;
// const byte SDLED_HI = 255;
// const byte SDLED_LO = 64;

#ifdef _DISPLAY_
const char SCANNER[6][4] = {{45, 32, 32, 32}, {32, 45, 32, 32}, //  "-   ", " -  "
                            {32, 32, 45, 32}, {32, 32, 32, 45}, //  "  - ", "   -"
                            {32, 32, 45, 32}, {32, 45, 32, 32}  //  "  - ", " -  "
                           };

#ifdef _LUXOMETER_
const int THRESHOLD[7] = {1, 5, 50, 200, 500, 1000, 2000};
#endif
#endif

const int LOCAL_TIMEZONE = -3;
const int SECONDS_PER_HOUR = 3600;

const int32_t TIMEZONE_OFFSET = LOCAL_TIMEZONE * SECONDS_PER_HOUR;
const uint32_t DEBOUNCER_TIME = 20UL;
const uint32_t BATTERY_SAMPLE_TIME = 1 * 60000UL; // 5 min en mseg
const uint32_t BLINK_INTERVAL = 1000UL;
const uint32_t CHKBATT_INTERVAL = 1000UL;

const float VREF = 5.0;
const float K_FACTOR = VREF / 1024.0;
const float VOLTS_PCT[21] = { 3.31, 3.68, 3.69, 3.71, 3.74, 3.76, 3.77,
                              3.78, 3.79, 3.81, 3.82, 3.84, 3.88, 3.92,
                              3.95, 3.98, 4.00, 4.05, 4.09, 4.13, 4.18
                            };

bool validSDcard = true;
bool createFile = true;
bool validFix = false;
bool validTime = false;
bool validDate = false;
bool sdIndicator = false;
bool lowBattery = false;
bool chkBattery = true;
bool pwrLedStatus = true;

byte battPct;

#ifdef _BUTTON_
bool lastButtonState = false;
uint32_t showtimeTime;
#endif

uint32_t tkpCounter;
uint32_t sdIndicatorTime;
uint32_t repeatsCounter;
uint32_t repeatsControl;
uint32_t repeatsInterval;
uint32_t batteryTimeOld;
uint32_t blinkTimeOld;
uint32_t chkTimeOld;

char fileName[20] = "00000000_000000.csv";
char tpTime[20] = "0000/00/00,00:00:00";

struct memory_s {
  float lat;
  float lng;
  float altitude;
  float speed;
  float course;
} memory;

#ifdef _DISPLAY_
enum dmode_e {
  SHOWSPEED,
  SHOWHOUR,
  SHOWSECS,
  SHOWBATT,
  SHOWBATT_OK
};

dmode_e displayMode = SHOWSPEED;
#endif

NeoGPS::time_t localTime;

//------------------------------------------------------------------------------
// convierte UTC (del GPS) a hora local
void get_local_time(NeoGPS::time_t dt) {
  NeoGPS::clock_t seconds = dt; // convert date/time structure to seconds
  seconds += TIMEZONE_OFFSET;
  localTime = seconds; // convert seconds back to a date/time structure
}

//------------------------------------------------------------------------------

#ifdef _BUTTON_
bool get_button(byte trigger) { // return true on pressed
  bool state = false;
  static bool oldState = false;
  static bool debouncer = false;
  static uint32_t oldTime = 0;

  if (debouncer) {
    if (millis() - oldTime >= DEBOUNCER_TIME) {
      debouncer = false;
    }
  }
  else {
    state = (digitalRead(BTN_PIN) == trigger);
    if (state != oldState) {
      oldState = state;
      debouncer = true;
      oldTime = millis();
    }
  }
  return oldState;
}
#endif

//------------------------------------------------------------------------------

void check_battery() {
  static int count = 0;
  static int samplesAcc = 0;

  if (count < 10) {
    samplesAcc += analogRead(PWR_PIN);
    //    Serial.println(analogRead(PWR_PIN));
    count++;
  }
  else {
    float vBatt = samplesAcc / count * K_FACTOR;

    DEBUG_PRINT(vBatt, 2);
    DEBUG_PRINT(F("V - ADC: "));
    DEBUG_PRINTLN(samplesAcc / count);

    for (int i = 20; i >= 0; i--) {
      if (vBatt >= VOLTS_PCT[i]) {
        battPct = i * 5;
        break;
      }
      else {
        battPct = 0;
      }
    }
    lowBattery = battPct <= 5;
    samplesAcc = 0;
    count = 0;
    chkBattery = false;
  }
}

//------------------------------------------------------------------------------

#ifdef _LUXOMETER_
uint8_t get_brightness() {
  int lux = luxometro.readLightLevel();
  for (uint8_t i = 0; i < 7; i++) {
    if (lux <= THRESHOLD[i]) return i;
  }
  return 7;
}
#endif

//------------------------------------------------------------------------------

void read_gps_fix() {
  while (gps.available(gpsPort)) {
    fix = gps.read();
    validFix = gps.is_safe();
  }
}

//------------------------------------------------------------------------------
// call back for file timestamps
void dateTime(uint16_t* date, uint16_t* time) {
  // return date using FAT_DATE macro to format fields
  *date = FAT_DATE(localTime.year + 2000, localTime.month, localTime.date);

  // return time using FAT_TIME macro to format fields
  *time = FAT_TIME(localTime.hours, localTime.minutes, localTime.seconds);
}

//------------------------------------------------------------------------------

void setup() {
  pinMode(PWR_LED, OUTPUT);
  digitalWrite(PWR_LED, HIGH);
  pinMode(SD_LED, OUTPUT);
  digitalWrite(SD_LED, LOW);

#ifdef _BUTTON_
  pinMode(BTN_PIN, INPUT_PULLUP);
#endif

  // DEBUG_PORT.begin(19200);
  gpsPort.begin(19200);

  //  SPI.setClockDivider(SPI_CLOCK_DIV2);
  validSDcard = SD.begin(); //(SD_CS);
  digitalWrite(SD_LED, validSDcard ? HIGH : LOW);

  /*
    if(validSDcard) {
      fileName = "adc.txt";
      trkFile = SD.open(fileName, FILE_WRITE);
      if(trkFile) {
        for(int i=0; i<20;i++){
          digitalWrite(SD_LED, LOW);
          delay(100);
          trkFile.println(analogRead(A0));
          digitalWrite(SD_LED, HIGH);
          delay(100);
        }
        trkFile.close();
      }
    }
  */
  while (chkBattery) {
    check_battery();
  }
//  lowBattery = false;

  DEBUG_PRINT(F("Bateria: "));
  DEBUG_PRINT(battPct);
  DEBUG_PRINTLN(F("%"));

#ifdef _DISPLAY_
  display.init();
#ifdef _LUXOMETER_
  Wire.begin();
  luxometro.begin (BH1750::CONTINUOUS_HIGH_RES_MODE); // (BH1750::ONE_TIME_HIGH_RES_MODE);

  DEBUG_PRINTLN(luxometro.readLightLevel());

  display.brightness(get_brightness());
#else
  display.brightness(BRIGHT_TYPICAL);
#endif
  display.point();
  display.showStr(F("8888"));
  delay(200);
  display.displayOff();
  //  display.clear();
  delay(200);
  display.displayOn();
  //  display.point();
  //  display.showNum(8888);
  delay(250);
  display.clear();
  delay(500);
  uint32_t oldtime = millis();
  byte i = 0;
  while (!validFix) {
    if (millis() - oldtime >= 200) {
      oldtime = millis();
      display.showChr(SCANNER[i]);
      i++;
      i %= 6;
    }
    read_gps_fix();
  }
  display.clear();
  delay(250);
#else
  read_gps_fix();
#endif
}

//------------------------------------------------------------------------------

void loop() {

#ifdef _BUTTON_
  static uint32_t lastColonBlinkTime;
  static uint32_t pressTime;
  static bool hold = false;

  bool newButtonState = get_button(LOW);     // boton activo bajo
  if (newButtonState && !lastButtonState) {  // pulsado
    pressTime = millis();
  } else if (newButtonState && lastButtonState) {  // retenido
    if (!hold) {
      if (millis() - pressTime >= 500UL) {
        displayMode = SHOWBATT;
        display.clear();
        hold = true;
      }
    }
  } else if (!newButtonState && lastButtonState) {  // liberado
    if (hold) {
      hold = false;
    }
    display.clear();
    switch (displayMode) {
      case SHOWSPEED:
        displayMode = SHOWHOUR;
        lastColonBlinkTime = millis();
        showtimeTime = lastColonBlinkTime;
        break;
      case SHOWHOUR:
        displayMode = SHOWSECS;
        lastColonBlinkTime = millis();
        showtimeTime = lastColonBlinkTime;
        break;
//      case SHOWSECS:
//        displayMode = SHOWSPEED;
//        break;
//      case SHOWBATT:
//        displayMode = SHOWSPEED;
//        break;
//      case SHOWBATT_OK:
      default:
        displayMode = SHOWSPEED;
//        break;
    }
  }
  lastButtonState = newButtonState;
#endif

  if (validFix) {
    validDate = fix.valid.date && ((fix.dateTime.year + 2000) > 2020);
  }
  if (fix.valid.time && validDate) {
    get_local_time(fix.dateTime);
  }

#ifdef _DISPLAY_
#ifdef _LUXOMETER_
  if (validFix) {
    display.brightness(get_brightness());
  }
#endif
#ifdef _BUTTON_
  switch (displayMode) {
    case SHOWSPEED:
      if (validFix && fix.valid.speed) {
        int speed = fix.speed_kph() + 0.5;
        display.showNum(speed);
      }
      break;
    case SHOWHOUR:
      if (fix.valid.time && validDate) {
        int16_t time = localTime.hours * 100 + localTime.minutes;
        display.showClk(time);
        lastColonBlinkTime = millis();
      }
      break;
    case SHOWSECS:
      if (fix.valid.time && validDate) {
        display.point();
        display.showNum(localTime.seconds, true, 2);
        lastColonBlinkTime = millis();
      }
      break;
    case SHOWBATT:
      display.showBar(battPct);
      displayMode = SHOWBATT_OK;
      break;
    default:
      break;
  }
#else
  if (validFix && fix.valid.speed) {
    int speed = fix.speed_kph() + 0.5;
    display.showNum(speed);
  }
#endif
#endif

  if (validSDcard) {
    if (createFile) {
      if (fix.valid.time && validDate) {
        int ltYear = localTime.year + 2000;
        fileName[0] = ltYear / 1000 + 48;
        fileName[1] = ltYear / 100 % 10 + 48;
        fileName[2] = ltYear / 10 % 10 + 48;
        fileName[3] = ltYear % 10 + 48;
        fileName[4] = localTime.month / 10 + 48;
        fileName[5] = localTime.month % 10 + 48;
        fileName[6] = localTime.date / 10 + 48;
        fileName[7] = localTime.date % 10 + 48;
        //fileName[8] = '_';
        fileName[9] = localTime.hours / 10 + 48;
        fileName[10] = localTime.hours % 10 + 48;
        fileName[11] = localTime.minutes / 10 + 48;
        fileName[12] = localTime.minutes % 10 + 48;
        fileName[13] = localTime.seconds / 10 + 48;
        fileName[14] = localTime.seconds % 10 + 48;
        // set date time callback function
        SdFile::dateTimeCallback(dateTime);
        trkFile = SD.open(fileName, FILE_WRITE);

        if (trkFile) {
          DEBUG_PRINT(fileName);
          DEBUG_PRINTLN(F(" creado"));

          sdIndicator = true;
          sdIndicatorTime = millis();
          digitalWrite(SD_LED, LOW);
          trkFile.println(F("trackpoint,date,time,latitude,longitude,alt,speed,course"));

          DEBUG_PRINTLN(F("trackpoint,date,time,latitude,longitude,alt,speed,course"));

          //trkFile.sync();
          trkFile.close();
          createFile = false;
        }
      }
    }

//    DEBUG_PRINTLN("validFix = " + String(validFix));

    if (validFix) {
      bool newGpsData = fix.latitude() != memory.lat;  // verifica si los datos actuales son iguales a los anteriores
      newGpsData |= fix.longitude() != memory.lng;
      newGpsData |= fix.altitude() != memory.altitude;
      newGpsData |= fix.speed_kph() != memory.speed;
      newGpsData |= fix.heading() != memory.course;

      repeatsCounter++;
      repeatsControl++;

      if (repeatsControl > 610UL) repeatsInterval = 60UL;       // después de 10'10" detenido graba cada 60" (hasta que se reinicie movimiento)
      else if (repeatsControl > 340UL) repeatsInterval = 45UL;  // después de 5'40" detenido graba cada 45" (6 veces como máximo)
      else if (repeatsControl > 160UL) repeatsInterval = 30UL;  // después de 2'40" detenido graba cada 30" (6 veces como máximo)
      else if (repeatsControl > 70UL) repeatsInterval = 15UL;   // después de 1'10" detenido graba cada 15" (6 veces como máximo)
      else if (repeatsControl > 10UL) repeatsInterval = 10UL;   // después de 10" detenido graba cada 10" (6 veces como máximo)
      else repeatsInterval = 1UL;                               // graba cada 1" (lo normal)

      if ((newGpsData) || (repeatsCounter >= repeatsInterval)) {  // si los datos son repetitivos los guarda cada "repeatsInterval" seg
        repeatsCounter = 0;
        if (newGpsData) repeatsControl = 0;
        memory.lat = fix.latitude();
        memory.lng = fix.longitude();
        memory.altitude = fix.altitude();
        memory.speed = fix.speed_kph();
        memory.course = fix.heading();

        trkFile = SD.open(fileName, FILE_WRITE);
        if (trkFile) {
          sdIndicatorTime = millis();
          sdIndicator = true;
          digitalWrite(SD_LED, LOW);

          trkFile.print(++tkpCounter);
          trkFile.print(F(","));

          DEBUG_PRINT(tkpCounter);
          DEBUG_PRINT(F(","));

          int utcYear = fix.dateTime.year + 2000;
          tpTime[0] = utcYear / 1000 % 10 + 48;
          tpTime[1] = utcYear / 100 % 10 + 48;
          tpTime[2] = utcYear / 10 % 10 + 48;
          tpTime[3] = utcYear % 10 + 48;
          //tpTime[4] = '/';
          tpTime[5] = fix.dateTime.month / 10 + 48;
          tpTime[6] = fix.dateTime.month % 10 + 48;
          //tpTime[7] = '/';
          tpTime[8] = fix.dateTime.date / 10 + 48;
          tpTime[9] = fix.dateTime.date % 10 + 48;
          //tpTime[10] = ',';
          tpTime[11] = fix.dateTime.hours / 10 + 48;
          tpTime[12] = fix.dateTime.hours % 10 + 48;
          //tpTime[13] = ':';
          tpTime[14] = fix.dateTime.minutes / 10 + 48;
          tpTime[15] = fix.dateTime.minutes % 10 + 48;
          //tpTime[16] = ':';
          tpTime[17] = fix.dateTime.seconds / 10 + 48;
          tpTime[18] = fix.dateTime.seconds % 10 + 48;
          //tpTime[19] = '\0';
          trkFile.print(tpTime);

          trkFile.print(F(","));
          trkFile.print(fix.latitude(), 6);
          trkFile.print(F(","));
          trkFile.print(fix.longitude(), 6);
          trkFile.print(F(","));
          trkFile.print(fix.altitude(), 2);
          trkFile.print(F(","));
          trkFile.print(fix.speed_kph(), 2);
          trkFile.print(F(","));
          trkFile.println(fix.heading(), 2);

          DEBUG_PRINT(tpTime);
          DEBUG_PRINT(F(","));
          DEBUG_PRINT(fix.latitude(), 6);
          DEBUG_PRINT(F(","));
          DEBUG_PRINT(fix.longitude(), 6);
          DEBUG_PRINT(F(","));
          DEBUG_PRINT(fix.altitude(), 2);
          DEBUG_PRINT(F(","));
          DEBUG_PRINT(fix.speed_kph(), 2);
          DEBUG_PRINT(F(","));
          DEBUG_PRINTLN(fix.heading(), 2);

          //trkFile.sync();
          trkFile.close();
        } else {
          createFile = true;
        }
      }
    }
  }

  if (sdIndicator && (millis() - sdIndicatorTime >= 150UL)) {
    sdIndicator = false;
    digitalWrite(SD_LED, HIGH);
  }

  if (!chkBattery) {
    if (millis() - batteryTimeOld >= BATTERY_SAMPLE_TIME) {
      chkBattery = true;
      chkTimeOld = millis();
      batteryTimeOld += BATTERY_SAMPLE_TIME;
      check_battery();
    }
  } else {
    if (millis() - chkTimeOld >= CHKBATT_INTERVAL) {
      chkTimeOld += CHKBATT_INTERVAL;
      check_battery();
    }
  }

  if (lowBattery) {
    if (millis() - blinkTimeOld >= BLINK_INTERVAL) {
      pwrLedStatus = !pwrLedStatus;
      digitalWrite(PWR_LED, pwrLedStatus ? HIGH : LOW);
      blinkTimeOld += BLINK_INTERVAL;
    }
  }

#ifdef _BUTTON_
  if ((displayMode == SHOWHOUR) || (displayMode == SHOWSECS)) {
    if (millis() - lastColonBlinkTime >= 500UL) {
#ifdef _LUXOMETER_
      display.brightness(get_brightness());
#endif
      if (displayMode == SHOWHOUR) {
        int16_t time = localTime.hours * 100 + localTime.minutes;
        display.showClk(time, POINT_OFF);
      }
      else {  // displayMode == SHOWSECS
        display.noPoint();
        display.showNum(localTime.seconds, true, 2);
      }
      lastColonBlinkTime += 500;
    }
    if (millis() - showtimeTime >= 10000UL) {
      displayMode = SHOWSPEED;
      display.clear();
    }
  }
#endif

  validFix = false;
  validTime = false;
  validDate = false;

  read_gps_fix();
  delay(1);
}

//------------------------------------------------------------------------------
4-Digit Display
GPS-EmulatorBreakout
BH1750Breakout