#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>

RTC_DS1307 rtc;
LiquidCrystal_I2C lcd(0x27, 16, 2);  // Pas het I2C-adres aan indien nodig

double latitude = 51.499998;     // Bergen op Zoom
double longitude = 4.2999988;    // Bergen op Zoom
double timezone = -2.0;           // Bergen op Zoom (CET, UTC+1 in winter, +2 in summer)
double julian_date;

enum CalculationMethod { Custom, CalculationMethodsCount };
enum JuristicMethod { Shafii, Hanafi };
enum AdjustingMethod { None, MidNight, OneSeventh, AngleBased };
enum { Fajr, Sunrise, Dhuhr, Asr, Sunset, Maghrib, Isha, TimesCount };

struct MethodConfig {
  double fajr_angle;
  double maghrib_value;
  bool maghrib_is_minutes;
  double isha_value;
  bool isha_is_minutes;
};

struct DoublePair {
  double first;
  double second;
};

MethodConfig method_params[CalculationMethodsCount];
CalculationMethod calc_method = Custom;
JuristicMethod asr_juristic = Shafii;
AdjustingMethod adjust_high_lats = None;
double dhuhr_minutes = 0;  // minutes after mid-day for Dhuhr


void setup() {
  rtc.begin();
  rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  // Hardcode the time to a specific value
  //rtc.adjust(DateTime(2024, 5, 29, 13, 39, 40)); // Set the time to 21:47:50 on 29 May 2024

  lcd.init();
  lcd.backlight();


  // Initialize prayer calculation method parameters
  method_params[Custom] = { 18.0, 4.0, true, 17.0, false }; // Set Isha angle to 17 degrees
}

void loop() {
  DateTime now = rtc.now();

  // Check for Daylight Saving Time
  bool isDST = isDaylightSavingTime(now);
  timezone = isDST ? 2.0 : 1.0; // Adjust timezone for DST

  double times[TimesCount];
  get_prayer_times(now.year(), now.month(), now.day(), latitude, longitude, timezone, times);

  // Display current date and time
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Datum:");
  lcd.print(two_digits_format(now.day()));
  lcd.print("-");
  lcd.print(two_digits_format(now.month()));
  lcd.print("-");
  lcd.print(now.year());

  lcd.setCursor(0, 1);
  lcd.print("Tijd:   ");
  lcd.print(two_digits_format(now.hour()));
  lcd.print(":");
  lcd.print(two_digits_format(now.minute()));
  lcd.print(":");
  lcd.print(two_digits_format(now.second()));
  delay(2000);

  // Display prayer times in the specified format
  displayPrayerTimes(times);
  
}

void displayPrayerTimes(double times[]) {
  const char* timeNames[] = {"Fajr", "Sunrise", "Dhuhr", "Asr", "Sunset", "Maghrib", "Isha"};

  int hours, minutes;

  // Display Fajr and Sunrise
  lcd.clear();
  lcd.setCursor(0, 0);
  get_float_time_parts(times[Fajr], hours, minutes);
  lcd.print("Fajr:      ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));

  lcd.setCursor(0, 1);
  get_float_time_parts(times[Sunrise], hours, minutes);
  lcd.print("Sunrise:   ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));
  delay(1000);

  // Display Dhuhr and Asr
  lcd.clear();
  lcd.setCursor(0, 0);
  get_float_time_parts(times[Dhuhr], hours, minutes);
  lcd.print("Dhuhr:     ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));

  lcd.setCursor(0, 1);
  get_float_time_parts(times[Asr], hours, minutes);
  lcd.print("Asr:       ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));
  delay(1000);

  // Display Sunset and Maghrib
  lcd.clear();
  lcd.setCursor(0, 0);
  get_float_time_parts(times[Sunset], hours, minutes);
  lcd.print("Sunset:    ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));

  lcd.setCursor(0, 1);
  get_float_time_parts(times[Maghrib], hours, minutes);
  lcd.print("Maghrib:   ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));
  delay(1000);

  // Display Isha
  lcd.clear();
  lcd.setCursor(0, 0);
  get_float_time_parts(times[Isha], hours, minutes);
  lcd.print("Isha:      ");
  lcd.print(two_digits_format(hours) + ":" + two_digits_format(minutes));
  delay(1000);
}

// Check if a given date is within Daylight Saving Time period
bool isDaylightSavingTime(DateTime dt) {
  // DST starts last Sunday of March
  DateTime startDST(dt.year(), 3, lastSunday(3, dt.year()), 2, 0, 0);
  // DST ends last Sunday of October
  DateTime endDST(dt.year(), 10, lastSunday(10, dt.year()), 3, 0, 0);

  return dt >= startDST && dt < endDST;
}

// Calculate the last Sunday of a given month
int lastSunday(int month, int year) {
  DateTime dt(year, month + 1, 1);
  int dayOfWeek = dt.dayOfTheWeek(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
  return 31 - ((dayOfWeek + 1) % 7);
}

// Prayer time calculation functions
double dsin(double d) { return sin(deg2rad(d)); }
double dcos(double d) { return cos(deg2rad(d)); }
double dtan(double d) { return tan(deg2rad(d)); }
double darcsin(double x) { return rad2deg(asin(x)); }
double darccos(double x) { return rad2deg(acos(x)); }
double darctan(double x) { return rad2deg(atan(x)); }
double darctan2(double y, double x) { return rad2deg(atan2(y, x)); }
double darccot(double x) { return rad2deg(atan(1.0 / x)); }
double deg2rad(double d) { return d * M_PI / 180.0; }
double rad2deg(double r) { return r * 180.0 / M_PI; }
double fix_angle(double a) { a = a - 360.0 * floor(a / 360.0); return a < 0.0 ? a + 360.0 : a; }
double fix_hour(double a) { a = a - 24.0 * floor(a / 24.0); return a < 0.0 ? a + 24.0 : a; }

double get_julian_date(int year, int month, int day) {
  if (month <= 2) {
    year -= 1;
    month += 12;
  }

  double a = floor(year / 100.0);
  double b = 2 - a + floor(a / 4.0);

  return floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + day + b - 1524.5;
}

void compute_day_times(double times[]) {
  double default_times[] = { 5, 6, 12, 13, 18, 18, 18 };
  for (int i = 0; i < TimesCount; ++i) times[i] = default_times[i];

  for (int i = 0; i < 1; ++i) compute_times(times);
  adjust_times(times);
}

DoublePair sun_position(double jd) {
  double d = jd - 2451545.0;
  double g = fix_angle(357.529 + 0.98560028 * d);
  double q = fix_angle(280.459 + 0.98564736 * d);
  double l = fix_angle(q + 1.915 * dsin(g) + 0.020 * dsin(2 * g));
  double e = 23.439 - 0.00000036 * d;

  double dd = darcsin(dsin(e) * dsin(l));
  double ra = darctan2(dcos(e) * dsin(l), dcos(l)) / 15.0;
  ra = fix_hour(ra);
  double eq_t = q / 15.0 - ra;
  DoublePair dp = {dd, eq_t};
  return dp;
}

void set_asr_method(JuristicMethod method_id) {
  asr_juristic = method_id;
}

void set_high_lats_adjust_method(AdjustingMethod method_id) {
  adjust_high_lats = method_id;
}

double sun_declination(double jd) {
  return sun_position(jd).first;
}

double equation_of_time(double jd) {
  return sun_position(jd).second;
}

double compute_mid_day(double _t) {
  double t = equation_of_time(julian_date + _t);
  double z = fix_hour(12 - t);
  return z;
}

double compute_time(double g, double t) {
  double d = sun_declination(julian_date + t);
  double z = compute_mid_day(t);
  double v = 1.0 / 15.0 * darccos((-dsin(g) - dsin(d) * dsin(latitude)) / (dcos(d) * dcos(latitude)));
  return z + (g > 90.0 ? -v : v);
}

double compute_asr(int step, double t) {
  double d = sun_declination(julian_date + t);
  double g = -darccot(step + dtan(fabs(latitude - d)));
  return compute_time(g, t);
}

void compute_times(double times[]) {
  day_portion(times);

  times[Fajr]    = compute_time(180.0 - method_params[calc_method].fajr_angle, times[Fajr]);
  times[Sunrise] = compute_time(180.0 - 0.833, times[Sunrise]);
  times[Dhuhr]   = compute_mid_day(times[Dhuhr]);
  times[Asr]     = compute_asr(1 + asr_juristic, times[Asr]);
  times[Sunset]  = compute_time(0.833, times[Sunset]);
  times[Maghrib] = compute_time(method_params[calc_method].maghrib_value, times[Maghrib]);
  times[Isha]    = compute_time(method_params[calc_method].isha_value, times[Isha]);
}

double time_diff(double time1, double time2) {
  return fix_hour(time2 - time1);
}

String int_to_string(int num) {
  return String(num);
}

String two_digits_format(int num) {
  char tmp[16];
  tmp[0] = '\0';
  sprintf(tmp, "%2.2d", num);
  return String(tmp);
}

double night_portion(double angle) {
  switch (adjust_high_lats) {
    case AngleBased:
      return angle / 60.0;
    case MidNight:
      return 1.0 / 2.0;
    case OneSeventh:
      return 1.0 / 7.0;
    default:
      return 0;
  }
}

void adjust_high_lat_times(double times[]) {
  double night_time = time_diff(times[Sunset], times[Sunrise]);

  double fajr_diff = night_portion(method_params[calc_method].fajr_angle) * night_time;
  if (isnan(times[Fajr]) || time_diff(times[Fajr], times[Sunrise]) > fajr_diff)
    times[Fajr] = times[Sunrise] - fajr_diff;

  double isha_angle = method_params[calc_method].isha_is_minutes ? 18.0 : method_params[calc_method].isha_value;
  double isha_diff = night_portion(isha_angle) * night_time;
  if (isnan(times[Isha]) || time_diff(times[Sunset], times[Isha]) > isha_diff)
    times[Isha] = times[Sunset] + isha_diff;

  double maghrib_angle = method_params[calc_method].maghrib_is_minutes ? 4.0 : method_params[calc_method].maghrib_value;
  double maghrib_diff = night_portion(maghrib_angle) * night_time;
  if (isnan(times[Maghrib]) || time_diff(times[Sunset], times[Maghrib]) > maghrib_diff)
    times[Maghrib] = times[Sunset] + maghrib_diff;
}

void adjust_times(double times[]) {
  for (int i = 0; i < TimesCount; ++i)
    times[i] += timezone - longitude / 15.0;
  times[Dhuhr] += dhuhr_minutes / 60.0;
  if (method_params[calc_method].maghrib_is_minutes)
    times[Maghrib] = times[Sunset] + method_params[calc_method].maghrib_value / 60.0;
  if (method_params[calc_method].isha_is_minutes)
    times[Isha] = times[Maghrib] + method_params[calc_method].isha_value / 60.0;

  if (adjust_high_lats != None)
    adjust_high_lat_times(times);
}

void day_portion(double times[]) {
  for (int i = 0; i < TimesCount; ++i)
    times[i] /= 24.0;
}

void get_prayer_times(int year, int month, int day, double _latitude, double _longitude, double _timezone, double times[]) {
  latitude = _latitude;
  longitude = _longitude;
  timezone = _timezone;
  julian_date = get_julian_date(year, month, day) - longitude / (double)(15 * 24);
  compute_day_times(times);

  // Check vaste tijden voor fajr en Isha.
  //Fajr is op 03:30 uur van 5 mei tot en met 4 augustus // Set Fajr to 03:30 between 5-5 and 4-8
  //Isha is op 23:30 uur van 3 mei tot en met 10 augustus  // Set Isha to 23:30 between 3-5 and 10-8
  if ((month == 5 && day >= 5) || (month == 6) || (month == 8 && day <= 4)) {
        times[Fajr] = 3.5; // 03:30 in decimal hours
  }
   if ((month == 5 && day >= 3) || (month == 6) || (month == 7) || (month == 8 && day <= 10)) {
    times[Isha] = 23.5; // 23:30 in decimal hours
  }
}

void set_calc_method(CalculationMethod method_id) {
  calc_method = method_id;
}

void set_fajr_angle(double angle) {
  method_params[Custom].fajr_angle = angle;
  calc_method = Custom;
}

void set_maghrib_angle(double angle) {
  method_params[Custom].maghrib_is_minutes = false;
  method_params[Custom].maghrib_value = angle;
  calc_method = Custom;
}

void set_isha_angle(double angle) {
  method_params[Custom].isha_is_minutes = false;
  method_params[Custom].isha_value = angle;
  calc_method = Custom;
}

void set_dhuhr_minutes(double minutes) {
  dhuhr_minutes = minutes;
}

void set_maghrib_minutes(double minutes) {
  method_params[Custom].maghrib_is_minutes = true;
  method_params[Custom].maghrib_value = minutes;
  calc_method = Custom;
}

void set_isha_minutes(double minutes) {
  method_params[Custom].isha_is_minutes = true;
  method_params[Custom].isha_value = minutes;
  calc_method = Custom;
}

void get_float_time_parts(double time, int& hours, int& minutes) {
  time = fix_hour(time + 0.5 / 60);  // add 0.5 minutes to round
  hours = floor(time);
  minutes = floor((time - hours) * 60);
}


GND5VSDASCLSQWRTCDS1307+