package leigh.astro.astrometrics;

import leigh.astro.catalog.CatalogObject;
import leigh.astro.catalog.StellarCatalog;
import leigh.astro.unit.Rect;
import leigh.astro.unit.SphericalCoordinate;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.util.NumberUtil;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
 * This class contains various astrometrics related calculations.
 *
 * @author Alex Leigh
 */
public class Astrometrics {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(Astrometrics.class);

    public static void main(String args[]) throws IOException {
        StellarCatalog ngc = new StellarCatalog();
        double mst = meanSiderealTime(new Date(), -111);

        int totalObj = 0;
        int visibleObj = 0;
        int windowObj = 0;
        int filtered = 0;
        Rect window = new Rect(new SphericalCoordinate(225, 20), new SphericalCoordinate(315, 45));

        for (CatalogObject object : ngc.getObjects()) {
            totalObj++;

            SphericalCoordinate cel = object.getJ2000Coordinate();
            SphericalCoordinate horiz = calc(mst - cel.getX(), cel.getY(), 33.0);

            if (horiz.getY() > 0) visibleObj++;
            if (window.isWithin(horiz)) {
                windowObj++;
                logger.info("Window target: [name: {}] [airmass: {}] [hcs: {}]",
                        object.getName(),
                        NumberUtil.precision(AirMass.airMass(horiz.getY()), 2), horiz);
            }
        }

        logger.info("Catalog processed. [total: {}] [visible: {}] [window: {}] [rejected: {}]",
                totalObj, visibleObj, windowObj, filtered);
    }

    /**
     * Calculate the horizon alt/az for an object's ra/dec at a given hour angle.
     */
    public static SphericalCoordinate calc(double ha, double dec, double lat) {
        logger.debug("Called. [ha: {}] [dec: {}] [lat: {}]", ha, dec, lat);
        double haRad = Math.toRadians(ha);
        double sh = Math.sin(haRad);
        double ch = Math.cos(haRad);

        double decRad = Math.toRadians(dec);
        double sd = Math.sin(decRad);
        double cd = Math.cos(decRad);

        double latRad = Math.toRadians(lat);
        double sl = Math.sin(latRad);
        double cl = Math.cos(latRad);

        double x = -ch * cd * sl + sd * cl;
        double y = -sh * cd;
        double z = ch * cd * cl + sd * sl;
        double r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));

        double az = Math.toDegrees(Math.atan2(y, x));
        double alt = Math.toDegrees(Math.atan2(z, r));

        az = normalizeAngle(az);

        return new SphericalCoordinate(az, alt);
    }

    /**
     * For the given date and a longitude, calculate the local sidereal time.
     */
    public static float meanSiderealTime(Date date, float longitude) {
        // First, calculate number of Julian days since J2000.0.
        double jd = calculateJulianDay(date);
        double delta = jd - 2451545.0f;

        // Calculate the global and local sidereal times
        double gst = 280.461f + 360.98564737f * delta;
        double lst = normalizeAngle(gst + longitude);

        return (float) lst;
    }

    /**
     * Calculate the Julian Day for a given date using the following formula:
     * JD = 367 * Y - INT(7 * (Y + INT((M + 9)/12))/4) + INT(275 * M / 9) + D + 1721013.5 + UT/24
     * <p>
     * Note that this is only valid for the year range 1900 - 2099.
     */
    public static double calculateJulianDay(Date date) {
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.setTime(date);

        double hour = cal.get(Calendar.HOUR_OF_DAY)
                + cal.get(Calendar.MINUTE) / 60.0f
                + cal.get(Calendar.SECOND) / 3600.0f;

        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);

        double jd = 367.0
                * year
                - Math.floor(7.0 * (year + Math.floor((month + 9.0) / 12.0)) / 4.0)
                + Math.floor(275.0 * month / 9.0) + day + 1721013.5 + hour
                / 24.0;

        return jd;
    }

    /**
     * Normalize the angle to the range 0 &lt;= value &lt; 360.
     */
    public static double normalizeAngle(double angle) {
        double remainder = angle % 360;
        if (remainder < 0)
            remainder += 360;
        return remainder;
    }

    public static double dmsToDeg(double d, double m, double s) {
        return Math.signum(d) * (Math.abs(d) + (m / 60.0) + (s / 3600.0));
    }

    public static double hmsToDeg(double h, double m, double s) {
        return (15 * h) + (15 * m / 60) + (15 * s / 3600);
    }


    /**
     * Parse a <code>[+|-]HH:MM;SS.SSS</code> format into a double.
     *
     * @param str The string to parse.
     * @return The parsed value.
     */
    public static double parseHms(String str) {
        String sign = "+";
        if (str.startsWith("+") || str.startsWith("-")) {
            sign = str.substring(0, 1);
            str = str.substring(1);
        }

        String[] tm = str.split(":", 3);
        double x = Astrometrics.hmsToDeg(Double.parseDouble(tm[0]), Double.parseDouble(tm[1]), Double.parseDouble(tm[2]));
        if (sign.equals("-")) x = -x;

        return x;
    }

    public static double parseDms(String str) {
        String sign = "+";
        if (str.startsWith("+") || str.startsWith("-")) {
            sign = str.substring(0, 1);
            str = str.substring(1);
        }

        String[] tm = str.split(":", 3);
        double x = Astrometrics.dmsToDeg(Double.parseDouble(tm[0]), Double.parseDouble(tm[1]), Double.parseDouble(tm[2]));
        if (sign.equals("-")) x = -x;

        return x;
    }
}
