#include <Arduino.h>
#include <Wire.h>
#include <HardwareSerial.h>
#include <RunningMedian.h>

#define LED_PIN 2 // should be using builtin LED
#define LED_INTERVAL_OFF 2500 // in ms
#define LED_INTERVAL_ON 500 // in ms

#define SRV_PWM_FREQ 50
#define SRV_PWM_RES_BITS 10
#define SRV_PWM_REFRESH_USEC 20000

#define PAN_SRV_CHAN 1
#define PAN_SRV_PIN 32
#define PAN_SRV_PWM_MIN 1000
#define PAN_SRV_PWM_DEFAULT 1505 // the trim is on 77% duty
#define PAN_SRV_PWM_MAX 2000
#define PAN_SRV_PID_ITVL 100 // controller compute interval

#define TILT_SRV_CHAN 2
#define TILT_SRV_PIN 33
#define TILT_SRV_PWM_MIN 1180 // corresp with 0 deg
#define TILT_SRV_PWM_DEFAULT TILT_SRV_PWM_MIN
#define TILT_SRV_PWM_MAX 2090 // corresp with 90 deg

#define MAG_ADDRESS 0x1E
#define MAG_REG_CONFIG_A 0x00
#define MAG_REG_CONFIG_B 0x01
#define MAG_REG_MODE 0x02
#define MAG_REG_DATA 0x03
#define MAG_REG_IDTF 0x0A
#define MAG_PRINT_NUM 300 // approximately 30 seconds

#define GPS_BAUDRATE 9600
#define GPS_RX_PIN 19 // connected to GPS' TX pin
#define GPS_TX_PIN 18 // connected to GPS' RX pin (optional)

#define CONFIG_FREQ_HB 3
#define CONFIG_FREQ_AGL 4
#define CONFIG_MAG_UPD_FREQ 7
#define CONFIG_USE_GPS 8
#define CONFIG_KP 11
#define CONFIG_KI 12
#define CONFIG_KD 13

#define EARTH_RADIUS 6371

#define SERIAL_BAUDRATE 115200
#define CMD_PARAMS_NUM 3

enum Mode {
  OFF, // antenna tracker movement off
  LOCATION, // automatically determine the angles by UAV location
  SETPOINT_ANGLE, // manually set bearing & elevation angles
  SERVO_PWM // manually set pan & tilt servo pwm
};

struct Location {
  double latitude;
  double longitude;
  float altitude; // in meters
};

HardwareSerial SerialGPS(2);
RunningMedian bearingSamples(5); // remove spikes from data

uint32_t timePassed = millis();
bool isMagExists = false;
uint16_t magPrintRemaining = 0;

Mode mode = OFF;
Location atLoc = {
  -7.7738408, // location default to D13 (GAMAFORCE HQ)
  110.3783263,
  1.00 // altitude above ground level in meters
}; // antenna tracker location
Location uavLoc; // UAV/vehicle location
float bearActual = -1.0f; // actual bearing from sensor
float bearSetpoint; // bearing/azimuth angle setpoint
float elevSetpoint; // elevation angle setpoint
uint16_t panPwm = PAN_SRV_PWM_DEFAULT;
uint16_t tiltPwm = TILT_SRV_PWM_DEFAULT;
struct {
  uint8_t freqHB = 1; // in Hz
  uint8_t freqAGL = 10;
  float Kp = 3.1f;
  float Ki = 0.0f;
  float Kd = 0.02f;
  uint8_t magUpdFreq = 20; // magnetometer update freq
  float magDec = 1.46607f; // magnetic declination
  bool useGpsAsLoc = false; // override AT location using GPS reading
} configs;

int64_t strToInt64(String str) {
  uint8_t DIGITS_PER_PART = 9; // int32 max has >9 digits
  bool isNegative = str.charAt(0) == '-';
  if (isNegative) str = str.substring(1);
  uint8_t digitsLen = str.length();
  int64_t result = 0;
  
  for (int8_t e = digitsLen; e > 0; e -= DIGITS_PER_PART) {
    int8_t pos = (digitsLen - e) / DIGITS_PER_PART;
    int8_t s = e - DIGITS_PER_PART;
    if (s < 0) s = 0;

    int64_t partNum = str.substring(s, e).toInt();
    for (int8_t p = 0; p < pos; p++) partNum *= 1000000000;

    result += partNum;
  }

  if (isNegative) result *= -1;

  return result;
}

uint16_t usPwmToTicks(uint16_t usec) {
  return (uint16_t)((float)usec *
    (pow(2, SRV_PWM_RES_BITS) / (float)SRV_PWM_REFRESH_USEC));
}

// convert lat/lon format from DMM to DD
double convertDmmStrToDd (String &dmmStr) {
  double dmm = dmmStr.toDouble();
  int16_t numPart = static_cast<uint16_t>(dmm) / 100;
  double fracPart = dmm - static_cast<double>(numPart * 100);
  fracPart = fracPart / 60.0;
  double dd = static_cast<double>(numPart) + fracPart;

  if (dmmStr.endsWith("S")) dd *= -1.0; // for latitude
  if (dmmStr.endsWith("W")) dd *= -1.0; // for longitude

  return dd;
}

void sendMsgHeartbeat() {
  Serial.print("HB,");
  Serial.print(timePassed);
  Serial.print(",");
  Serial.println(mode);
}

void sendMsgAngles() {
  Serial.print("AGL,");
  Serial.print(bearActual);
  Serial.print(",");
  Serial.print(bearSetpoint);
  Serial.print(",");
  Serial.println(elevSetpoint);
}

void sendMsgUAVLocation() {
  Serial.print("UAV,");
  Serial.print(uavLoc.latitude, 7);
  Serial.print(",");
  Serial.print(uavLoc.longitude, 7);
  Serial.print(",");
  Serial.println(uavLoc.altitude, 3);
}

void sendMsgATLocation() {
  Serial.print("AT,");
  Serial.print(atLoc.latitude, 7);
  Serial.print(",");
  Serial.print(atLoc.longitude, 7);
  Serial.print(",");
  Serial.println(atLoc.altitude, 3);
}

void sendMsgSetpointAngles() {
  Serial.print("SP,");
  Serial.print(bearSetpoint);
  Serial.print(",");
  Serial.println(elevSetpoint);
}

void sendMsgServoPwm() {
  Serial.print("SRV,");
  Serial.print(panPwm);
  Serial.print(",");
  Serial.println(tiltPwm);
}

void sendMsgPIDGains() {
  Serial.print("PID,");
  Serial.print(configs.Kp, 3);
  Serial.print(",");
  Serial.print(configs.Ki, 3);
  Serial.print(",");
  Serial.println(configs.Kd, 3);
}

void sendMsgGPSReading(double latitude, double longitude,
  float altitude, float hdop) {
  Serial.print("GPS,");
  Serial.print(latitude, 7);
  Serial.print(",");
  Serial.print(longitude, 7);
  Serial.print(",");
  Serial.print(altitude, 2);
  Serial.print(",");
  Serial.println(hdop, 2);
}

void setPanServoPwm(uint16_t pwmValue) {
  panPwm = pwmValue;
  #ifdef ESP32
  ledcWrite(PAN_SRV_CHAN, usPwmToTicks(pwmValue));
  #endif
}

void setTiltServoPwm(uint16_t pwmValue) {
  tiltPwm = pwmValue;
  #ifdef ESP32
  ledcWrite(TILT_SRV_CHAN, usPwmToTicks(pwmValue));
  #endif
}

void resetServosPosition() {
  setPanServoPwm(PAN_SRV_PWM_DEFAULT);
  setTiltServoPwm(TILT_SRV_PWM_DEFAULT);
}

void initServo() {  
  #ifdef ESP32
  // setup pan servo
  ledcSetup(PAN_SRV_CHAN, SRV_PWM_FREQ, SRV_PWM_RES_BITS);
  ledcAttachPin(PAN_SRV_PIN, PAN_SRV_CHAN);

  // setup tilt servo
  ledcSetup(TILT_SRV_CHAN, SRV_PWM_FREQ, SRV_PWM_RES_BITS);
  ledcAttachPin(TILT_SRV_PIN, TILT_SRV_CHAN);
  #endif

  resetServosPosition();
}

void setBearingAngleSetpoint(float deg) {
  bearSetpoint = deg;
}

void setElevationAngleSetpoint(float deg) {
  elevSetpoint = deg;
  
  // TODO: implement algorithm to smoothing servo movement
  uint16_t newPwm = map(deg, 0, 90, TILT_SRV_PWM_MIN, TILT_SRV_PWM_MAX);
  setTiltServoPwm(newPwm);
}

void calculateNewSetpointAngles() {
  double startLat = radians(atLoc.latitude);
  double startLng = radians(atLoc.longitude);
  double destLat = radians(uavLoc.latitude);
  double destLng = radians(uavLoc.longitude);

  double y = sin(destLng - startLng) * cos(destLat);
  double x = cos(startLat) * sin(destLat) -
    sin(startLat) * cos(destLat) * cos(destLng - startLng);
  float bear = degrees(atan2(y, x));
  bear = bear < 0 ? bear + 360.0 : bear;

  double dist = EARTH_RADIUS * acos(
    sin(startLat) * sin(destLat) +
    cos(startLat) * cos(destLat) * cos(destLng - startLng));
  float altDiff = uavLoc.altitude - atLoc.altitude;
  float elev = degrees(atan2(altDiff, dist * 1000.0));
  elev = elev < 0 ? 0 : elev;

  setBearingAngleSetpoint(bear);
  setElevationAngleSetpoint(elev);
}

void setMode(Mode newMode) {
  mode = newMode;
  if (newMode == OFF) {
    resetServosPosition();
  }
  if (newMode == LOCATION) {
    calculateNewSetpointAngles();
  }
}

void scanI2CDevices() {
  uint8_t nDevices = 0;

  Serial.println("> Scanning I2C devices");

  for (byte address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    byte error = Wire.endTransmission();

    if (error == 0) {
      Serial.print("> Found at address 0x");
      Serial.println(address, HEX);
      nDevices++;
    } else if (error == 4) {
      Serial.print("> Unknown error at address 0x");
      Serial.println(address, HEX);
    }
  }

  Serial.print("> I2C devices found: ");
  Serial.println(nDevices);
}

// blink LED every n seconds
void blinkLED() {
  static uint32_t prevTime;
  uint16_t blinkInterval = mode == OFF
    ? LED_INTERVAL_OFF
    : LED_INTERVAL_ON;

  if (timePassed - prevTime >= blinkInterval) {
    digitalWrite(LED_PIN, HIGH);
    prevTime = timePassed;
  }
  if (timePassed - prevTime >= 100) {
    digitalWrite(LED_PIN, LOW);
  }
}

void printHeartbeatMsg() {
  static uint32_t prevTime;

  if (configs.freqHB == 0) return;
  if (timePassed - prevTime >= (1000 / configs.freqHB)) {
    sendMsgHeartbeat();
    prevTime = timePassed;
  }
}

void printAnglesMsg() {
  static uint32_t prevTime;

  if (configs.freqAGL == 0 || mode == OFF) return;
  if (timePassed - prevTime >= (1000 / configs.freqAGL)) {
    sendMsgAngles();
    prevTime = timePassed;
  }
}

void initMagnetometer() {
  Wire.beginTransmission(MAG_ADDRESS);
  Wire.write(MAG_REG_IDTF);
  byte error = Wire.endTransmission();

  // check if communication with the I2C device success
  if (error != 0) return;

  Wire.requestFrom(MAG_ADDRESS, 3);
  // HMC5883l identification bytes
  if (Wire.read() != 'H') return;
  if (Wire.read() != '4') return;
  if (Wire.read() != '3') return;

  isMagExists = true;

  // configure mag/compass
  Wire.beginTransmission(MAG_ADDRESS);
  Wire.write(MAG_REG_CONFIG_A);
  Wire.write(0x70);
  Wire.endTransmission();
  
  Wire.beginTransmission(MAG_ADDRESS);
  Wire.write(MAG_REG_CONFIG_B);
  Wire.write(0xA0);
  Wire.endTransmission();
  
  Wire.beginTransmission(MAG_ADDRESS);
  Wire.write(MAG_REG_MODE);
  Wire.write(0x00);
  Wire.endTransmission();
}

void readMagnetometer() {
  static uint32_t prevTime;

  if (!isMagExists) return;
  if (timePassed - prevTime >= (1000 / configs.magUpdFreq)) {
    int16_t x, y, z;

    Wire.beginTransmission(MAG_ADDRESS);
    Wire.write(MAG_REG_DATA); // start to read from this register
    Wire.endTransmission();

    byte retNum = Wire.requestFrom(MAG_ADDRESS, 6);
    if (Wire.available() < 6) return;

    x = Wire.read() << 8; // MSB x
    x |= Wire.read(); // LSB x
    z = Wire.read() << 8; // MSB z
    z |= Wire.read(); // LSB z
    y = Wire.read() << 8; // MSB y
    y |= Wire.read(); // LSB y

    float bearing = atan2(y, x);
    bearing += configs.magDec; // account for declination angle
    if (bearing < 0) bearing += 2*PI; // correct reversed signs
    if (bearing > 2*PI) bearing -= 2*PI; // check for wrap
    bearing = 2*PI - bearing; // reverse rotation

    float bearingDeg = degrees(bearing);
    bearingSamples.add(bearingDeg);

    // Serial.print("Raw: ");
    // Serial.print(x); Serial.print(" ");
    // Serial.print(y); Serial.print(" ");   
    // Serial.print(z);
    // Serial.print("\tBear: ");
    // Serial.println(bearingDeg);

    if (magPrintRemaining > 0) {
      uint16_t printProgress = MAG_PRINT_NUM-magPrintRemaining+1;
      float percentProgress = (float)printProgress/MAG_PRINT_NUM*100;
      Serial.print("> ");
      Serial.print(x); Serial.print(",");
      Serial.print(y); Serial.print(",");
      Serial.print(z); Serial.print(",");
      Serial.print(percentProgress, 1);
      Serial.println("%");
      magPrintRemaining--;
    }

    bearActual = bearingSamples.getMedian();
    prevTime = timePassed;
  }
}

void processNmeaMessage(String &msg) {
  msg.setCharAt(2, 'X'); // for compatibility with other GNSS sats

  if (msg.startsWith("$GXGGA")) {
    struct {
      String latitude, longitude, heightMsl, geoidSep, hdop;
    } usefulParts;
    uint8_t msgFieldIdx = 0;

    for (uint16_t i = 0; i < msg.length(); i++) {
      char &c = msg[i];
      if (c == ',') {
        msgFieldIdx++;
        continue;
      }
      if (msgFieldIdx == 2 || msgFieldIdx == 3)
        usefulParts.latitude += c;
      if (msgFieldIdx == 4 || msgFieldIdx == 5)
        usefulParts.longitude += c;
      if (msgFieldIdx == 9)
        usefulParts.heightMsl += c;
      if (msgFieldIdx == 11)
        usefulParts.geoidSep += c;
      if (msgFieldIdx == 8)
        usefulParts.hdop += c;
    }

    float hdop = usefulParts.hdop.toFloat();

    if (hdop < 10.0 && usefulParts.latitude != "") {
      double latitude = convertDmmStrToDd(usefulParts.latitude);
      double longitude = convertDmmStrToDd(usefulParts.longitude);
      float heightMsl = usefulParts.heightMsl.toFloat();
      float geoidSep = usefulParts.geoidSep.toFloat();
      float altitude = heightMsl + geoidSep; // ellipsoidal height

      sendMsgGPSReading(latitude, longitude, altitude, hdop);

      // a good HDOP to use is considered to be below 2
      if (configs.useGpsAsLoc && hdop < 2.0) {
        atLoc.latitude = latitude;
        atLoc.longitude = longitude;
      }
    }
  }
}

void readGPS() {
  static bool isReceivingNmea = false;
  static String receivedNmea = "";

  if (SerialGPS.available() > 0) {
    char rc = SerialGPS.read();

    if (!isReceivingNmea && rc == '$') {
      isReceivingNmea = true;
    }

    if (isReceivingNmea) {
      if (rc != '\n') {
        receivedNmea += rc;
      } else {
        receivedNmea.trim();
        processNmeaMessage(receivedNmea);

        isReceivingNmea = false;
        receivedNmea = "";
      }
    }
  }
}

void runCommand(String &cmd) {
  uint16_t cmdStrLen = cmd.length();
  char cmdType = '\0';
  int64_t cmdParams[CMD_PARAMS_NUM];
  String paramStrTemp = "";
  uint8_t paramIdx = 0;

  // fill array with the same initial value
  for(uint8_t i = 0; i < CMD_PARAMS_NUM; i++) {
    cmdParams[i] = INT64_MIN;
  }

  for (uint16_t i = 0; i < cmdStrLen && paramIdx < CMD_PARAMS_NUM; i++) {
    char c = cmd[i];
    if (i == 0) {
      cmdType = c;
      continue;
    }
    if (i == 1 && c == ',') continue;
    if ((c >= '0' && c <= '9') || c == '-') {
      paramStrTemp += c;
    }
    if (c == ',' || i == cmdStrLen-1) {
      cmdParams[paramIdx] = strToInt64(paramStrTemp);
      paramStrTemp = "";
      paramIdx++;
    }
  }

  int64_t &p1 = cmdParams[0];
  int64_t &p2 = cmdParams[1];
  int64_t &p3 = cmdParams[2];

  switch(cmdType) {
    case 'M': // set mode
      if (p1 >= 0 && p1 <= 3) {
        setMode(static_cast<Mode>(p1));
      }
      break;
    case 'A': // set AT location
      if (p1 != INT64_MIN && p2 != INT64_MIN) {
        atLoc.latitude = static_cast<double>(p1) / pow(10, 7);
        atLoc.longitude = static_cast<double>(p2) / pow(10, 7);
        if (p3 != INT64_MIN) {
          atLoc.altitude = static_cast<float>(p3) / pow(10, 3);
        }
        if (mode == LOCATION) {
          calculateNewSetpointAngles();
        }
        sendMsgATLocation();
      }
      break;
    case 'V': // set vehicle location (mode == LOCATION)
      if (mode != LOCATION) break;
      if (p1 != INT64_MIN && p2 != INT64_MIN) {
        uavLoc.latitude = static_cast<double>(p1) / pow(10, 7);
        uavLoc.longitude = static_cast<double>(p2) / pow(10, 7);
        if (p3 != INT64_MIN) {
          uavLoc.altitude = static_cast<float>(p3) / pow(10, 3);
        }
        calculateNewSetpointAngles();
        sendMsgUAVLocation();
      }
      break;
    case 'B': // set bearing setpoint angle (mode == SETPOINT_ANGLE)
      if (mode != SETPOINT_ANGLE) break;
      if (p1 >= 0 && p1 <= 359) {
        setBearingAngleSetpoint(static_cast<float>(p1));
        sendMsgSetpointAngles();
      }
      break;
    case 'E': // set elevation setpoint angle (mode == SETPOINT_ANGLE)
      if (mode != SETPOINT_ANGLE) break;
      if (p1 >= 0 && p1 <= 90) {
        setElevationAngleSetpoint(static_cast<float>(p1));
        sendMsgSetpointAngles();
      }
      break;
    case 'P': // set servo pan pwm value (mode == SERVO_PWM)
      if (mode != SERVO_PWM) break;
      if (p1 >= PAN_SRV_PWM_MIN && p1 <= PAN_SRV_PWM_MAX) {
        setPanServoPwm(p1);
        sendMsgServoPwm();
      }
      break;
    case 'T': // set servo tilt pwm value (mode == SERVO_PWM)
      if (mode != SERVO_PWM) break;
      if (p1 >= TILT_SRV_PWM_MIN && p1 <= TILT_SRV_PWM_MAX) {
        setTiltServoPwm(p1);
        sendMsgServoPwm();
      }
      break;
    case 'C': // set config
      if (p1 == INT64_MIN || p2 == INT64_MIN) break;
      if (p1 == CONFIG_FREQ_HB) {
        configs.freqHB = p2;
      }
      if (p1 == CONFIG_FREQ_AGL) {
        configs.freqAGL = p2;
      }
      if (p1 == CONFIG_MAG_UPD_FREQ) {
        configs.magUpdFreq = p2;
      }
      if (p1 == CONFIG_USE_GPS) {
        configs.useGpsAsLoc = static_cast<bool>(p2);
      }
      if (p1 >= CONFIG_KP && p1 <= CONFIG_KD) { // PID gains
        float newGain = static_cast<float>(p2) / pow(10, 3);
        if (p1 == CONFIG_KP) configs.Kp = newGain;
        if (p1 == CONFIG_KI) configs.Ki = newGain;
        if (p1 == CONFIG_KD) configs.Kd = newGain;
        sendMsgPIDGains();
      }
      break;
    case 'X': // run specific command
      if (p1 == 0) { // request all messages
        sendMsgHeartbeat();
        sendMsgAngles();
        sendMsgUAVLocation();
        sendMsgATLocation();
        sendMsgSetpointAngles();
        sendMsgServoPwm();
        sendMsgPIDGains();
      }
      if (p1 == 1) { // scan I2C address
        scanI2CDevices();
      }
      if (p1 == 2) { // print raw mag readings
        if (magPrintRemaining == 0) {
          magPrintRemaining = MAG_PRINT_NUM;
        } else {
          // stop printing
          magPrintRemaining = 0;
        }
      }
      break;
  }
}

// read command from serial
void readSerial() {
  static bool isReceivingCmd = false;
  static String receivedCmd = "";

  if (Serial.available() > 0) {
    char rc = Serial.read();

    if (!isReceivingCmd && rc >= 'A' && rc <= 'Z') {
      isReceivingCmd = true;
    }

    if (isReceivingCmd) {
      if (rc != '\n') {
        receivedCmd += rc;
      } else {
        runCommand(receivedCmd);

        isReceivingCmd = false;
        receivedCmd = "";
      }
    }
  }
}

void controlPanPID() {
  static uint32_t prevTime;
  static float error, integralError, deltaError, prevError;

  if (mode != LOCATION && mode != SETPOINT_ANGLE) return;
  if (bearActual == -1.0f) return; // not getting actual mag data
  if (timePassed - prevTime >= PAN_SRV_PID_ITVL) {
    error = bearSetpoint - bearActual;
    if (error > 180) error = -1 * (360.0 - error);
    else if (error < -180) error = 360.0 + error;
    integralError += ((error + prevError) *
      ((float)PAN_SRV_PID_ITVL / 1000.0)) / 2;
    deltaError = abs(error - prevError);

    float proportional = configs.Kp * error;
    float integral = configs.Ki * integralError;
    float derivative = configs.Kd * deltaError;

    uint16_t outputPwm = PAN_SRV_PWM_DEFAULT;
    outputPwm += proportional + integral + derivative;
    outputPwm = constrain(outputPwm, PAN_SRV_PWM_MIN, PAN_SRV_PWM_MAX);

    setPanServoPwm(outputPwm);

    // Serial.print("actu: ");
    // Serial.print(bearActual);
    // Serial.print("\tsetp: ");
    // Serial.print(bearSetpoint);
    // Serial.print("\tout: ");
    // Serial.println(outputPwm);

    prevError = error;
    prevTime = timePassed;
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(SERIAL_BAUDRATE);
  #ifdef ESP32
  SerialGPS.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
  #endif
  Wire.begin();

  initMagnetometer();
  initServo();
}

void loop() {
  timePassed = millis();
  
  // don't run blocking codes here
  blinkLED();
  printHeartbeatMsg();
  printAnglesMsg();
  readMagnetometer();
  readGPS();
  readSerial();
  controlPanPID();

  delay(1); // prevent high processor usage
}
esp:0
esp:2
esp:4
esp:5
esp:12
esp:13
esp:14
esp:15
esp:16
esp:17
esp:18
esp:19
esp:21
esp:22
esp:23
esp:25
esp:26
esp:27
esp:32
esp:33
esp:34
esp:35
esp:3V3
esp:EN
esp:VP
esp:VN
esp:GND.1
esp:D2
esp:D3
esp:CMD
esp:5V
esp:GND.2
esp:TX
esp:RX
esp:GND.3
esp:D1
esp:D0
esp:CLK
servo1:GND
servo1:V+
servo1:PWM
servo2:GND
servo2:V+
servo2:PWM
led1:A
led1:C
GPS-CompassBreakout
chip1:VCC
chip1:TX
chip1:RX
chip1:GND
chip1:SCL
chip1:SDA
Pan (bearing)
Tilt (elevation)
vcc1:VCC
gnd2:GND