// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/********************************************************************
 * COPYRIGHT:
 * Copyright (c) 1996-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 ********************************************************************/

/* Test CalendarAstronomer for C++ */

#include "unicode/utypes.h"
#include "string.h"
#include "unicode/locid.h"

#if !UCONFIG_NO_FORMATTING

#include "astro.h"
#include "astrotst.h"
#include "cmemory.h"
#include "gregoimp.h" // for Math
#include "unicode/simpletz.h"


#define CASE(id,test) case id: name = #test; if (exec) { logln(#test "---"); logln((UnicodeString)""); test(); } break

AstroTest::AstroTest(): astro(nullptr), gc(nullptr) {
}

void AstroTest::runIndexedTest( int32_t index, UBool exec, const char* &name, char* /*par*/ )
{
    if (exec) logln("TestSuite AstroTest");
    switch (index) {
      // CASE(0,FooTest);
      CASE(0,TestSolarLongitude);
      CASE(1,TestLunarPosition);
      CASE(2,TestCoordinates);
      CASE(3,TestCoverage);
      CASE(4,TestSunriseTimes);
      CASE(5,TestBasics);
      CASE(6,TestMoonAge);
    default: name = ""; break;
    }
}

#undef CASE

#define ASSERT_OK(x) UPRV_BLOCK_MACRO_BEGIN { \
    if(U_FAILURE(x)) { \
        dataerrln("%s:%d: %s\n", __FILE__, __LINE__, u_errorName(x)); \
        return; \
    } \
} UPRV_BLOCK_MACRO_END


void AstroTest::initAstro(UErrorCode &status) {
  if(U_FAILURE(status)) return;

  if((astro != nullptr) || (gc != nullptr)) {
    dataerrln("Err: initAstro() called twice!");
    closeAstro(status);
    if(U_SUCCESS(status)) {
      status = U_INTERNAL_PROGRAM_ERROR;
    }
  }

  if(U_FAILURE(status)) return;

  astro = new CalendarAstronomer();
  gc = Calendar::createInstance(TimeZone::getGMT()->clone(), status);
}

void AstroTest::closeAstro(UErrorCode &/*status*/) {
  if(astro != nullptr) {
    delete astro;
    astro = nullptr;
  }
  if(gc != nullptr) {
    delete gc;
    gc = nullptr;
  }
}

void AstroTest::TestSolarLongitude() {
  UErrorCode status = U_ZERO_ERROR;
  initAstro(status);
  ASSERT_OK(status);

  struct {
    int32_t d[5]; double f ;
  } tests[] = {
    { { 1980, 7, 27, 0, 00 },  124.114347 },
    { { 1988, 7, 27, 00, 00 },  124.187732 }
  };

  logln("");
  for (uint32_t i = 0; i < UPRV_LENGTHOF(tests); i++) {
    gc->clear();
    gc->set(tests[i].d[0], tests[i].d[1]-1, tests[i].d[2], tests[i].d[3], tests[i].d[4]);

    astro->setDate(gc->getTime(status));

    double longitude = astro->getSunLongitude();
    //longitude = 0;
    CalendarAstronomer::Equatorial result;
    astro->getSunPosition(result);
    logln((UnicodeString)"Sun position is " + result.toString() + (UnicodeString)";  " /* + result.toHmsString()*/ + " Sun longitude is " + longitude );
  }
  closeAstro(status);
  ASSERT_OK(status);
}



void AstroTest::TestLunarPosition() {
  UErrorCode status = U_ZERO_ERROR;
  initAstro(status);
  ASSERT_OK(status);

  static const double tests[][7] = {
    { 1979, 2, 26, 16, 00,  0, 0 }
  };
  logln("");

  for (int32_t i = 0; i < UPRV_LENGTHOF(tests); i++) {
    gc->clear();
    gc->set((int32_t)tests[i][0], (int32_t)tests[i][1]-1, (int32_t)tests[i][2], (int32_t)tests[i][3], (int32_t)tests[i][4]);
    astro->setDate(gc->getTime(status));

    const CalendarAstronomer::Equatorial& result = astro->getMoonPosition();
    logln((UnicodeString)"Moon position is " + result.toString() + (UnicodeString)";  " /* + result->toHmsString()*/);
  }

  closeAstro(status);
  ASSERT_OK(status);
}



void AstroTest::TestCoordinates() {
  UErrorCode status = U_ZERO_ERROR;
  initAstro(status);
  ASSERT_OK(status);

  CalendarAstronomer::Equatorial result;
  astro->eclipticToEquatorial(result, 139.686111 * CalendarAstronomer::PI / 180.0, 4.875278* CalendarAstronomer::PI / 180.0);
  logln((UnicodeString)"result is " + result.toString() + (UnicodeString)";  " /* + result.toHmsString()*/ );
  closeAstro(status);
  ASSERT_OK(status);
}



void AstroTest::TestCoverage() {
  UErrorCode status = U_ZERO_ERROR;
  initAstro(status);
  ASSERT_OK(status);
  GregorianCalendar *cal = new GregorianCalendar(1958, UCAL_AUGUST, 15,status);
  UDate then = cal->getTime(status);
  CalendarAstronomer *myastro = new CalendarAstronomer(then);
  ASSERT_OK(status);

  //Latitude:  34 degrees 05' North
  //Longitude:  118 degrees 22' West
  double laLat = 34 + 5./60, laLong = 360 - (118 + 22./60);
  CalendarAstronomer *myastro2 = new CalendarAstronomer(laLong, laLat);

  double eclLat = laLat * CalendarAstronomer::PI / 360;
  double eclLong = laLong * CalendarAstronomer::PI / 360;

  CalendarAstronomer::Ecliptic ecl(eclLat, eclLong);
  CalendarAstronomer::Equatorial eq;
  CalendarAstronomer::Horizon hor;

  logln("ecliptic: " + ecl.toString());
  CalendarAstronomer *myastro3 = new CalendarAstronomer();
  myastro3->setJulianDay((4713 + 2000) * 365.25);

  CalendarAstronomer *astronomers[] = {
    myastro, myastro2, myastro3, myastro2 // check cache
  };

  for (uint32_t i = 0; i < UPRV_LENGTHOF(astronomers); ++i) {
    CalendarAstronomer *anAstro = astronomers[i];

    //logln("astro: " + astro);
    logln((UnicodeString)"   date: " + anAstro->getTime());
    logln((UnicodeString)"   cent: " + anAstro->getJulianCentury());
    logln((UnicodeString)"   gw sidereal: " + anAstro->getGreenwichSidereal());
    logln((UnicodeString)"   loc sidereal: " + anAstro->getLocalSidereal());
    logln((UnicodeString)"   equ ecl: " + (anAstro->eclipticToEquatorial(eq,ecl)).toString());
    logln((UnicodeString)"   equ long: " + (anAstro->eclipticToEquatorial(eq, eclLong)).toString());
    logln((UnicodeString)"   horiz: " + (anAstro->eclipticToHorizon(hor, eclLong)).toString());
    logln((UnicodeString)"   sunrise: " + (anAstro->getSunRiseSet(true)));
    logln((UnicodeString)"   sunset: " + (anAstro->getSunRiseSet(false)));
    logln((UnicodeString)"   moon phase: " + anAstro->getMoonPhase());
    logln((UnicodeString)"   moonrise: " + (anAstro->getMoonRiseSet(true)));
    logln((UnicodeString)"   moonset: " + (anAstro->getMoonRiseSet(false)));
    logln((UnicodeString)"   prev summer solstice: " + (anAstro->getSunTime(CalendarAstronomer::SUMMER_SOLSTICE(), false)));
    logln((UnicodeString)"   next summer solstice: " + (anAstro->getSunTime(CalendarAstronomer::SUMMER_SOLSTICE(), true)));
    logln((UnicodeString)"   prev full moon: " + (anAstro->getMoonTime(CalendarAstronomer::FULL_MOON(), false)));
    logln((UnicodeString)"   next full moon: " + (anAstro->getMoonTime(CalendarAstronomer::FULL_MOON(), true)));
  }

  delete myastro2;
  delete myastro3;
  delete myastro;
  delete cal;

  closeAstro(status);
  ASSERT_OK(status);
}



void AstroTest::TestSunriseTimes() {
  UErrorCode status = U_ZERO_ERROR;
  initAstro(status);
  ASSERT_OK(status);

  //  logln("Sunrise/Sunset times for San Jose, California, USA");
  //  CalendarAstronomer *astro2 = new CalendarAstronomer(-121.55, 37.20);
  //  TimeZone *tz = TimeZone::createTimeZone("America/Los_Angeles");

  // We'll use a table generated by the UNSO website as our reference
  // From: http://aa.usno.navy.mil/
  //-Location: W079 25, N43 40
  //-Rise and Set for the Sun for 2001
  //-Zone:  4h West of Greenwich
  int32_t USNO[] = {
    6,59, 19,45,
    6,57, 19,46,
    6,56, 19,47,
    6,54, 19,48,
    6,52, 19,49,
    6,50, 19,51,
    6,48, 19,52,
    6,47, 19,53,
    6,45, 19,54,
    6,43, 19,55,
    6,42, 19,57,
    6,40, 19,58,
    6,38, 19,59,
    6,36, 20, 0,
    6,35, 20, 1,
    6,33, 20, 3,
    6,31, 20, 4,
    6,30, 20, 5,
    6,28, 20, 6,
    6,27, 20, 7,
    6,25, 20, 8,
    6,23, 20,10,
    6,22, 20,11,
    6,20, 20,12,
    6,19, 20,13,
    6,17, 20,14,
    6,16, 20,16,
    6,14, 20,17,
    6,13, 20,18,
    6,11, 20,19,
  };

  logln("Sunrise/Sunset times for Toronto, Canada");
  // long = 79 25", lat = 43 40"
  CalendarAstronomer astro3(-(79+25/60), 43+40/60);

  // As of ICU4J 2.8 the ICU4J time zones implement pass-through
  // to the underlying JDK.  Because of variation in the
  // underlying JDKs, we have to use a fixed-offset
  // SimpleTimeZone to get consistent behavior between JDKs.
  // The offset we want is [-18000000, 3600000] (raw, dst).
  // [aliu 10/15/03]

  // TimeZone tz = TimeZone.getTimeZone("America/Montreal");
  SimpleTimeZone tz(-18000000 + 3600000, "Montreal(FIXED)");

  GregorianCalendar cal(tz.clone(), Locale::getUS(), status);
  GregorianCalendar cal2(tz.clone(), Locale::getUS(), status);
  cal.clear();
  cal.set(UCAL_YEAR, 2001);
  cal.set(UCAL_MONTH, UCAL_APRIL);
  cal.set(UCAL_DAY_OF_MONTH, 1);
  cal.set(UCAL_HOUR_OF_DAY, 12); // must be near local noon for getSunRiseSet to work

  LocalPointer<DateFormat> df_t(DateFormat::createTimeInstance(DateFormat::MEDIUM,Locale::getUS()));
  LocalPointer<DateFormat> df_d(DateFormat::createDateInstance(DateFormat::MEDIUM,Locale::getUS()));
  LocalPointer<DateFormat> df_dt(DateFormat::createDateTimeInstance(DateFormat::MEDIUM, DateFormat::MEDIUM, Locale::getUS()));
  if(!df_t.isValid() || !df_d.isValid() || !df_dt.isValid()) {
      dataerrln("couldn't create dateformats.");
      closeAstro(status);
      return;
  }
  df_t->adoptTimeZone(tz.clone());
  df_d->adoptTimeZone(tz.clone());
  df_dt->adoptTimeZone(tz.clone());

  for (int32_t i=0; i < 30; i++) {
    logln("setDate\n");
    astro3.setDate(cal.getTime(status));
    logln("getRiseSet(true)\n");
    UDate sunrise = astro3.getSunRiseSet(true);
    logln("getRiseSet(false)\n");
    UDate sunset  = astro3.getSunRiseSet(false);
    logln("end of getRiseSet\n");

    cal2.setTime(cal.getTime(status), status);
    cal2.set(UCAL_SECOND,      0);
    cal2.set(UCAL_MILLISECOND, 0);

    cal2.set(UCAL_HOUR_OF_DAY, USNO[4*i+0]);
    cal2.set(UCAL_MINUTE,      USNO[4*i+1]);
    UDate exprise = cal2.getTime(status);
    cal2.set(UCAL_HOUR_OF_DAY, USNO[4*i+2]);
    cal2.set(UCAL_MINUTE,      USNO[4*i+3]);
    UDate expset = cal2.getTime(status);
    // Compute delta of what we got to the USNO data, in seconds
    int32_t deltarise = (int32_t)uprv_fabs((sunrise - exprise) / 1000);
    int32_t deltaset = (int32_t)uprv_fabs((sunset - expset) / 1000);

    // Allow a deviation of 0..MAX_DEV seconds
    // It would be nice to get down to 60 seconds, but at this
    // point that appears to be impossible without a redo of the
    // algorithm using something more advanced than Duffett-Smith.
    int32_t MAX_DEV = 180;
    UnicodeString s1, s2, s3, s4, s5;
    if (deltarise > MAX_DEV || deltaset > MAX_DEV) {
      if (deltarise > MAX_DEV) {
        errln("FAIL: (rise) " + df_d->format(cal.getTime(status),s1) +
              ", Sunrise: " + df_dt->format(sunrise, s2) +
              " (USNO " + df_t->format(exprise,s3) +
              " d=" + deltarise + "s)");
      } else {
        logln(df_d->format(cal.getTime(status),s1) +
              ", Sunrise: " + df_dt->format(sunrise,s2) +
              " (USNO " + df_t->format(exprise,s3) + ")");
      }
      s1.remove(); s2.remove(); s3.remove(); s4.remove(); s5.remove();
      if (deltaset > MAX_DEV) {
        errln("FAIL: (set)  " + df_d->format(cal.getTime(status),s1) +
              ", Sunset:  " + df_dt->format(sunset,s2) +
              " (USNO " + df_t->format(expset,s3) +
              " d=" + deltaset + "s)");
      } else {
        logln(df_d->format(cal.getTime(status),s1) +
              ", Sunset: " + df_dt->format(sunset,s2) +
              " (USNO " + df_t->format(expset,s3) + ")");
      }
    } else {
      logln(df_d->format(cal.getTime(status),s1) +
            ", Sunrise: " + df_dt->format(sunrise,s2) +
            " (USNO " + df_t->format(exprise,s3) + ")" +
            ", Sunset: " + df_dt->format(sunset,s4) +
            " (USNO " + df_t->format(expset,s5) + ")");
    }
    cal.add(UCAL_DATE, 1, status);
  }

  //        CalendarAstronomer a = new CalendarAstronomer(-(71+5/60), 42+37/60);
  //        cal.clear();
  //        cal.set(cal.YEAR, 1986);
  //        cal.set(cal.MONTH, cal.MARCH);
  //        cal.set(cal.DATE, 10);
  //        cal.set(cal.YEAR, 1988);
  //        cal.set(cal.MONTH, cal.JULY);
  //        cal.set(cal.DATE, 27);
  //        a.setDate(cal.getTime());
  //        long r = a.getSunRiseSet2(true);
  closeAstro(status);
  ASSERT_OK(status);
}



void AstroTest::TestBasics() {
  UErrorCode status = U_ZERO_ERROR;
  initAstro(status);
  if (U_FAILURE(status)) {
    dataerrln("Got error: %s", u_errorName(status));
    return;
  }

  // Check that our JD computation is the same as the book's (p. 88)
  GregorianCalendar cal3(TimeZone::getGMT()->clone(), Locale::getUS(), status);
  LocalPointer<DateFormat> d3(DateFormat::createDateTimeInstance(DateFormat::MEDIUM,DateFormat::MEDIUM,Locale::getUS()));
  if (d3.isNull()) {
      dataerrln("Got error: %s", u_errorName(status));
      closeAstro(status);
      return;
  }
  d3->setTimeZone(*TimeZone::getGMT());
  cal3.clear();
  cal3.set(UCAL_YEAR, 1980);
  cal3.set(UCAL_MONTH, UCAL_JULY);
  cal3.set(UCAL_DATE, 2);
  logln("cal3[a]=%.1lf, d=%d\n", cal3.getTime(status), cal3.get(UCAL_JULIAN_DAY,status));
  {
    UnicodeString s;
    logln(UnicodeString("cal3[a] = ") + d3->format(cal3.getTime(status),s));
  }
  cal3.clear();
  cal3.set(UCAL_YEAR, 1980);
  cal3.set(UCAL_MONTH, UCAL_JULY);
  cal3.set(UCAL_DATE, 27);
  logln("cal3=%.1lf, d=%d\n", cal3.getTime(status), cal3.get(UCAL_JULIAN_DAY,status));

  ASSERT_OK(status);
  {
    UnicodeString s;
    logln(UnicodeString("cal3 = ") + d3->format(cal3.getTime(status),s));
  }
  astro->setTime(cal3.getTime(status));
  double jd = astro->getJulianDay() - 2447891.5;
  double exp = -3444.;
  if (jd == exp) {
    UnicodeString s;
    logln(d3->format(cal3.getTime(status),s) + " => " + jd);
  } else {
    UnicodeString s;
    errln("FAIL: " + d3->format(cal3.getTime(status), s) + " => " + jd +
          ", expected " + exp);
  }

  //        cal3.clear();
  //        cal3.set(cal3.YEAR, 1990);
  //        cal3.set(cal3.MONTH, Calendar.JANUARY);
  //        cal3.set(cal3.DATE, 1);
  //        cal3.add(cal3.DATE, -1);
  //        astro.setDate(cal3.getTime());
  //        astro.foo();

  ASSERT_OK(status);
  closeAstro(status);
  ASSERT_OK(status);

}

void AstroTest::TestMoonAge(){
	UErrorCode status = U_ZERO_ERROR;
	initAstro(status);
	ASSERT_OK(status);
	
	// more testcases are around the date 05/20/2012
	//ticket#3785  UDate ud0 = 1337557623000.0;
	static const double testcase[][10] = {{2012, 5, 20 , 16 , 48, 59},
	                {2012, 5, 20 , 16 , 47, 34},
	                {2012, 5, 21, 00, 00, 00},
	                {2012, 5, 20, 14, 55, 59},
	                {2012, 5, 21, 7, 40, 40},
	                {2023, 9, 25, 10,00, 00},
	                {2008, 7, 7, 15, 00, 33}, 
	                {1832, 9, 24, 2, 33, 41 },
	                {2016, 1, 31, 23, 59, 59},
	                {2099, 5, 20, 14, 55, 59}
	        };
	// Moon phase angle - Got from http://www.moonsystem.to/checkupe.htm
	static const double angle[] = {356.8493418421329, 356.8386760059673, 0.09625415252237701, 355.9986960782416, 3.5714026601303317, 124.26906744384183, 59.80247650195558,
									357.54163205513123, 268.41779281511094, 4.82340276581624};
	static const double precision = CalendarAstronomer::PI/32;
	for (int32_t i = 0; i < UPRV_LENGTHOF(testcase); i++) {
		gc->clear();
		logln((UnicodeString)"CASE["+i+"]: Year "+(int32_t)testcase[i][0]+" Month "+(int32_t)testcase[i][1]+" Day "+
		                                    (int32_t)testcase[i][2]+" Hour "+(int32_t)testcase[i][3]+" Minutes "+(int32_t)testcase[i][4]+
		                                    " Seconds "+(int32_t)testcase[i][5]);
		gc->set((int32_t)testcase[i][0], (int32_t)testcase[i][1]-1, (int32_t)testcase[i][2], (int32_t)testcase[i][3], (int32_t)testcase[i][4], (int32_t)testcase[i][5]);
		astro->setDate(gc->getTime(status));
		double expectedAge = (angle[i]*CalendarAstronomer::PI)/180;
		double got = astro->getMoonAge();
		//logln(testString);
		if(!(got>expectedAge-precision && got<expectedAge+precision)){
			errln((UnicodeString)"FAIL: expected " + expectedAge +
					" got " + got);
		}else{
			logln((UnicodeString)"PASS: expected " + expectedAge +
					" got " + got);
		}
	}
	closeAstro(status);
	ASSERT_OK(status);
}


// TODO: try finding next new moon after  07/28/1984 16:00 GMT


#endif



