from datetime import date, timedelta
import csv
from skyfield.api import load, wgs84
from skyfield import almanac
from skyfield.units import Angle
from skyfield.earthlib import refraction
from numpy import arccos
import pandas as pd
import math

ts = load.timescale()
eph = load('de440s.bsp')
earth, sun, moon = eph['earth'], eph['sun'], eph['moon']

def tabledata(lat, lon, ele, tz, year, m, d, csv_filename="crescent_data.csv"):


    # Observer (works across Skyfield versions)
    try:
        location = wgs84.latlon(lat, lon, elevation_m=ele, center=earth)
    except TypeError:
        location = earth + wgs84.latlon(lat, lon, elevation_m=ele)

    # Local-day window in UTC
    t0 = ts.utc(year, m, d, 0 - tz, 0, 0)
    t1 = ts.utc(year, m, d, 24 - tz, 0, 0)

    # Apparent horizon for upper-limb sunset, with height & refraction
    earth_radius_m = 6378136.6
    side_over_hyp = earth_radius_m / (earth_radius_m + ele)
    h_geom = Angle(radians=-arccos(side_over_hyp))           # dip from observer height
    r = refraction(0.0, temperature_C=15.0, pressure_mbar=1013.25)
    solar_radius_deg = 16/60
    horizon_deg = -r + h_geom.degrees - solar_radius_deg

    # Sunset within the local-day window
    set_times_sun, _ = almanac.find_settings(location, sun, t0, t1, horizon_degrees=horizon_deg)
    if len(set_times_sun) == 0:
        raise RuntimeError("No sunset found in this local-day window.")
    t_sunset = set_times_sun[0]
    dt_sunset_local = t_sunset.utc_datetime() + timedelta(hours=tz)
    sunset_str = dt_sunset_local.strftime("%H:%M:%S")

    # Moonset within the local-day window (may be missing)
    set_times_moon, _ = almanac.find_settings(location, moon, t0, t1, horizon_degrees=horizon_deg)
    if len(set_times_moon) == 0:
        t_moonset = None
        moonset_str = "—"
        lag_str = "—"
    else:
        t_moonset = set_times_moon[0]
        dt_moonset_local = t_moonset.utc_datetime() + timedelta(hours=tz)
        moonset_str = dt_moonset_local.strftime("%H:%M:%S")

        # Lag = moonset - sunset (ensure positive same-evening)
        lag_td = (t_moonset.utc_datetime() - t_sunset.utc_datetime())
        if lag_td.total_seconds() < 0:
            lag_td += timedelta(days=1)
        hh = int(lag_td.total_seconds() // 3600)
        mm = int((lag_td.total_seconds() % 3600) // 60)
        ss = int(lag_td.total_seconds() % 60)
        lag_str = f"{hh:02d}:{mm:02d}:{ss:02d}"

    # Alt/Az at actual sunset
    alt_moon, az_moon, _ = location.at(t_sunset).observe(moon).apparent().altaz()
    alt_sun,  az_sun,  _ = location.at(t_sunset).observe(sun).apparent().altaz()

    moon_alt_deg = float(alt_moon.degrees)
    daz_deg = abs(float(az_moon.degrees) - float(az_sun.degrees))
    arcv_deg = abs(moon_alt_deg - float(alt_sun.degrees))    # Arc of Vision
    arcl_deg = float(
        location.at(t_sunset).observe(sun).apparent()
        .separation_from(location.at(t_sunset).observe(moon).apparent()).degrees
    )  # Arc of Light

    # Moon age at sunset: (JD_sunset - JD_conjunction_before)*24
    jd_sunset = t_sunset.tt
    # Search ±5 days around date; pick last conjunction BEFORE sunset
    t0c = ts.utc(year, m, d - 5)
    t1c = ts.utc(year, m, d + 5)
    oc_func = almanac.oppositions_conjunctions(eph, eph['Moon'])
    times_oc, events_oc = almanac.find_discrete(t0c, t1c, oc_func)
    jd_conj = None
    for ti, ei in zip(times_oc, events_oc):
        if ei == 1 and ti.tt <= jd_sunset:  # 1 = conjunction
            jd_conj = ti.tt
    moon_age_hours = (jd_sunset - jd_conj) * 24.0 if jd_conj is not None else float('nan')

    # Build row
    date_str = date(year, m, d).strftime("%Y-%m-%d")
    header = ["Date", "Sunset", "Moonset", "Lag Time", "Moon Age", "Moon Alt", "DAZ", "ArcV", "ArcL"]
    row = [
        date_str,
        sunset_str,
        moonset_str,
        lag_str,
        f"{moon_age_hours:.2f}",
        f"{moon_alt_deg:.2f}",
        f"{daz_deg:.2f}",
        f"{arcv_deg:.2f}",
        f"{arcl_deg:.2f}",
    ]

    # Pretty print
    print("\n+------------+----------+----------+----------+----------+----------+----------+----------+----------+")
    print("|    Date    |  Sunset  |  Moonset | LagTime  | MoonAge  | MoonAlt  |   DAZ    |   ArcV   |   ArcL   |")
    print("+------------+----------+----------+----------+----------+----------+----------+----------+----------+")
    print(f"| {date_str} | {sunset_str:8} | {moonset_str:8} | {lag_str:8} | "
          f"{moon_age_hours:8.2f} | {moon_alt_deg:8.2f} | {daz_deg:8.2f} | {arcv_deg:8.2f} | {arcl_deg:8.2f} |")
    print("+------------+----------+----------+----------+----------+----------+----------+----------+----------+")

    # Save CSV
    with open(csv_filename, "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(header)
        writer.writerow(row)

    print(f"\n✅ Saved to CSV file: {csv_filename}")

    return {
        "Date": date_str,
        "Sunset": sunset_str,
        "Moonset": moonset_str,
        "Lag Time": lag_str,
        "Moon Age (h)": f"{moon_age_hours:.2f}",
        "Moon Alt (deg)": f"{moon_alt_deg:.2f}",
        "DAZ (deg)": f"{daz_deg:.2f}",
        "ArcV (deg)": f"{arcv_deg:.2f}",
        "ArcL (deg)": f"{arcl_deg:.2f}",
    }

from datetime import date, timedelta
import csv
from skyfield.api import load, wgs84
from skyfield import almanac
from skyfield.units import Angle
from skyfield.earthlib import refraction
from numpy import arccos

ts = load.timescale()
eph = load('de440s.bsp')
earth, sun, moon = eph['earth'], eph['sun'], eph['moon']

def observedata(lat, lon, ele, tz, year, m, d):


    # Observer (works across Skyfield versions)
    try:
        location = wgs84.latlon(lat, lon, elevation_m=ele, center=earth)
    except TypeError:
        location = earth + wgs84.latlon(lat, lon, elevation_m=ele)

    # Local-day window in UTC
    t0 = ts.utc(year, m, d, 0 - tz, 0, 0)
    t1 = ts.utc(year, m, d, 24 - tz, 0, 0)

    # Apparent horizon for upper-limb sunset, with height & refraction
    earth_radius_m = 6378136.6
    side_over_hyp = earth_radius_m / (earth_radius_m + ele)
    h_geom = Angle(radians=-arccos(side_over_hyp))           # dip from observer height
    r = refraction(0.0, temperature_C=15.0, pressure_mbar=1013.25)
    solar_radius_deg = 16/60
    horizon_deg = -r + h_geom.degrees - solar_radius_deg

    # Sunset within the local-day window
    set_times_sun, _ = almanac.find_settings(location, sun, t0, t1, horizon_degrees=horizon_deg)
    if len(set_times_sun) == 0:
        raise RuntimeError("No sunset found in this local-day window.")
    t_sunset = set_times_sun[0]
    dt_sunset_local = t_sunset.utc_datetime() + timedelta(hours=tz)
    sunset_str = dt_sunset_local.strftime("%H:%M:%S")

    # Moonset within the local-day window (may be missing)
    set_times_moon, _ = almanac.find_settings(location, moon, t0, t1, horizon_degrees=horizon_deg)
    if len(set_times_moon) == 0:
        t_moonset = None
        moonset_str = "—"
        lag_str = "—"
    else:
        t_moonset = set_times_moon[0]
        dt_moonset_local = t_moonset.utc_datetime() + timedelta(hours=tz)
        moonset_str = dt_moonset_local.strftime("%H:%M:%S")

        # Lag = moonset - sunset (ensure positive same-evening)
        lag_time = (t_moonset.utc_datetime() - t_sunset.utc_datetime())



    # Alt/Az at actual sunset
    alt_moon, az_moon, _ = location.at(t_sunset).observe(moon).apparent().altaz()
    alt_sun,  az_sun,  _ = location.at(t_sunset).observe(sun).apparent().altaz()

    moon_alt_deg = float(alt_moon.degrees)
    daz_deg = abs(float(az_moon.degrees) - float(az_sun.degrees))
    arcv_deg = abs(moon_alt_deg - float(alt_sun.degrees))    # Arc of Vision
    arcl_deg = float(
        location.at(t_sunset).observe(sun).apparent()
        .separation_from(location.at(t_sunset).observe(moon).apparent()).degrees
    )  # Arc of Light

    # Moon age at sunset: (JD_sunset - JD_conjunction_before)*24
    jd_sunset = t_sunset.tt
    # Search ±5 days around date; pick last conjunction BEFORE sunset
    t0c = ts.utc(year, m, d - 5)
    t1c = ts.utc(year, m, d + 5)
    oc_func = almanac.oppositions_conjunctions(eph, eph['Moon'])
    times_oc, events_oc = almanac.find_discrete(t0c, t1c, oc_func)
    jd_conj = None
    for ti, ei in zip(times_oc, events_oc):
        if ei == 1 and ti.tt <= jd_sunset:  # 1 = conjunction
            jd_conj = ti.tt
    moon_age_hours = (jd_sunset - jd_conj) * 24.0 if jd_conj is not None else float('nan')

    # --- ADDED: Crescent Width Calculation ---
    moon_dist_km = location.at(t_sunset).observe(moon).apparent().distance().km
    moon_radius_km = 1737.4

    # Semidiameter
    sd_deg = math.degrees(math.asin(moon_radius_km / moon_dist_km))

    # Width = SD * (1 - cos(ArcL))
    width_deg = sd_deg * (1 - math.cos(math.radians(arcl_deg)))

    width_arcm = width_deg * 60.0
    width_arcs = width_deg * 3600.0

    return  sunset_str, moonset_str, lag_time,moon_age_hours,moon_alt_deg, daz_deg,arcv_deg,arcl_deg,width_arcm,width_arcs

def criteriavisibility ( lat, lon, ele, tz, year, m, d):

  sunset_str, moonset_str, lag_time,moon_age_hours,moon_alt_deg, daz,arcv,arcl,width_arcm,width_arcs = observedata(lat, lon, ele, tz, year, m, d)
  print(arcl_deg)
  # List to store results
  results = []

  # Function to append results to the list
  def append_result(kriteria, expression, arcv_cerapan, arcv_kriteria, visibility):

    results.append({
        "Kriteria": kriteria,
        "Expression": expression,
        "Observation Value": arcv_cerapan,
        "Criterion Value": arcv_kriteria,
        "Visibility": visibility
    })
  v="Visible"
  i = "Not Visibile"
  # a. Kriteria Fotheringham
  kriteria = "Fotheringham"
  expression = "ARCV = 12.0 - 0.008 DAZ"
  fotheringham_arcv = float(12.0 - 0.008 * daz)

  visibility = i if fotheringham_arcv >= arcv else v
  append_result(kriteria, expression, arcv, fotheringham_arcv, visibility)

  # b. Kriteria Maunder
  kriteria = "Maunder"
  expression = "ARCV = 11 - 0.005*daz - 0.01*daz**2"
  maunder_arcv =  float(11 - 0.005*daz - 0.01*daz**2)

  visibility = i if maunder_arcv >= arcv else v
  append_result(kriteria, expression, arcv, maunder_arcv, visibility)

  # c. Kriteria Ilyas
  kriteria = "Ilyas"
  expression = "ARCV = -0.0027356815*daz - 0.0136648716*daz**2 + 0.0002119205*daz**3 + 10.2832719598"
  ilyas_arcv = float(-0.0027356815 * daz - 0.0136648716 * daz**2 + 0.0002119205 * daz**3 + 10.2832719598)

  visibility = i if ilyas_arcv >= arcv else v
  append_result(kriteria, expression, arcv, ilyas_arcv, visibility)

  # d. Kriteria Fatoohi
  kriteria = "Fatoohi"
  expression = "ARCV = 9.2714-0.0644*daz-0.0058*daz**2 + 0.0002*daz**3"
  fatoohi_arcv = float(9.2714 - 0.0644 * daz - 0.0058 * daz**2 + 0.0002 * daz**3)

  visibility = i if fatoohi_arcv >= arcv else v
  append_result(kriteria, expression, arcv, fatoohi_arcv, visibility)

  # e. Kriteria Krauss
  kriteria = "Krauss"
  expression = "ARCV = 0.0291254840*daz - 0.0098347831*daz**2 + 0.0000475196*daz**3 + 10.5981838905"
  krauss_arcv = float(0.0291254840 * daz - 0.0098347831 * daz**2 + 0.0000475196 * daz**3 + 10.5981838905)

  visibility = i if krauss_arcv >= arcv else v
  append_result(kriteria, expression, arcv, krauss_arcv, visibility)

  # f. Kriteria Faid Naked Eye
  kriteria = "Faid et.al (2024) Mata Kasar"
  expression = "ARCV = -0.09222*DAZ - 0.00629*DAZ^2 + 0.0002078*DAZ^3 + 6.792"
  faidne_arcv = float(-0.09222 * daz - 0.00629 * daz**2 + 0.0002078 * daz**3 + 6.792)

  visibility = i  if faidne_arcv >= arcv else v
  append_result(kriteria, expression, arcv, faidne_arcv, visibility)

  # g. Kriteria Faid CCD Imaging
  kriteria = "Faid et.al (2024) Optical Aid"
  expression = "ARCV = -1.3590*DAZ + 0.081710*DAZ^2 - 0.0015330*DAZ^3 + 7.391"
  faidCCD_arcv = float(-1.3590 * daz + 0.081710 * daz**2 - 0.0015330 * daz**3 + 7.391)

  visibility = i if faidCCD_arcv >= arcv else v
  append_result(kriteria, expression, arcv, faidCCD_arcv, visibility)

   #a. Kriteria MABIMS
  kriteria = "MABIMS 1995"
  expression = "ArcL ≥ 3° & MAlt ≥ 2°"
  k_arcl = 3  # Arc Length Criterion
  k_malt = 2  # Moon Altitude Criterion
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # b. Kriteria MABIMS 2021
  kriteria = "MABIMS 2021"
  expression = "ArcL ≥ 6.4° & MAlt ≥ 3°"
  k_arcl = 6.4  # Arc Length Criterion
  k_malt = 3  # Moon Altitude Criterion

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # c. Kriteria Istanbul 2015
  kriteria = "Istanbul 2015"
  expression = "ArcL ≥ 8° & MAlt ≥ 5°"
  k_arcl = 8  # Arc Length Criterion
  k_malt = 5  # Moon Altitude Criterion

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # d. Kriteria Danjon
  kriteria = "Danjon"
  expression = "ArcL ≥ 7° "
  k_arcl = 7  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # e. Kriteria Ilyas Elongation
  kriteria = "Ilyas"
  expression = "ArcL ≥ 10.5° "
  k_arcl = 10.5  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # f. Kriteria McNally
  kriteria = "McNally"
  expression = "ArcL ≥ 5° "
  k_arcl = 5  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # g. Kriteria McNally
  kriteria = "Amir Hasanzadeh"
  expression = "ArcL ≥ 5° "
  k_arcl = 5  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  # Check visibility based on Nawawi's criteria
  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # g. Kriteria Sultan
  kriteria = "Sultan"
  expression = "ArcL ≥ 5° "
  k_arcl = 5  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # g. Kriteria Schaefer
  kriteria = "Schaefer"
  expression = "ArcL ≥ 7.5° "
  k_arcl = 7.5  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # h. Kriteria Fatoohi
  kriteria = "Fatoohi"
  expression = "ArcL ≥ 7.5° "
  k_arcl = 7.5  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # i. Kriteria Odeh
  kriteria = "Odeh"
  expression = "ArcL ≥ 6.4° "
  k_arcl = 6.4  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # i. Kriteria Odeh
  kriteria = "Odeh"
  expression = "ArcL ≥ 6.4° "
  k_arcl = 6.4  # Arc Length Criterion
  k_malt = 0  # Moon Altitude Criterion

  visibility = i if (k_arcl > arcl or k_malt > moon_alt_deg) else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"

  # Append the result
  append_result(kriteria, expression, obs_string, criterion_string, visibility)

  # j. Kriteria Faid Naked Eye
  kriteria = "Faid Naked Eye"
  expression = "MAlt = − 0.3351 ArcL + 0.0023 ArcL2 + 0.000064 ArcL3 + 7.78"
  faidne_malt = float(- 0.3351 * arcl + 0.0023 * arcl**2 + 0.000064 * arcl**3 + 7.78)

  visibility = i if faidne_arcv >= moon_alt_deg else v
  obs_string = f"{arcl_deg:.2f}, {moon_alt_deg:.2f}" # "12.45, 4.50"
  criterion_string = f"{k_arcl}, {k_malt}" # "12.45, 4.50"
  append_result(kriteria, expression, obs_string, criterion_string, visibility)


  # a. Kriteria Yallop
  kriteria = "Yallop Q1 (Lunar Crescent Easily Visible)"
  expression = "ArcV =- 0.1018*W^3 +0.7319*W^2 -6.3226*W + 13.9971"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 13.9971)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # b. Kriteria Yallop
  kriteria = "Yallop Q2 (Lunar Crescent Visibile Under Perfect Condition)"
  expression = "ArcV =- 0.1018W3 +0.7319W2 -6.3226W' + 11.6971"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 11.6971)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # c. Kriteria Yallop
  kriteria = "Yallop Q3 (May Need Optical Aid to Find Crescent)"
  expression = "ArcV =- 0.1018W3 +0.7319W2 -6.3226W' + 10.2371"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 10.2371)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # d. Kriteria Yallop
  kriteria = "Yallop Q4 (Will Need Optical Aid to Find Crescent)"
  expression = "ArcV =- 0.1018W3 +0.7319W2 -6.3226W' + 9.5171"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 9.5171)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # e. Kriteria Yallop
  kriteria = "Yallop Q5 (Not visible with a telescope)"
  expression = "ArcV =- 0.1018W3 +0.7319W2 -6.3226W' + 8.9071"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 8.9071)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Yallop
  kriteria = "Yallop Q6 (Not visible, below Danjon limit)"
  expression = "ArcV =- 0.1018W3 +0.7319W2 -6.3226W' + 8.9071"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 8.9071)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Odeh
  kriteria = "Odeh V1 (Crescent is visible by naked eyes)"
  expression = "ArcV =- 0.1018W3 +0.7319W^2 -6.3226W' + 12.8151"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 12.8151)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Odeh
  kriteria = "Odeh V2 (Crescent is visible by optical aid, and it could be seen by naked eyes)"
  expression = "ArcV =- 0.1018W^3 +0.7319W^2 -6.3226W' + 9.1651"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm + 9.1651)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Odeh
  kriteria = "Odeh V3 (Crescent is visible by optical aid only)"
  expression = "ArcV =- 0.1018W^3 +0.7319W^2 -6.3226W' +  6.2051"
  kriteria_arcv_w = float(- 0.1018*width_arcm**3 +0.7319*width_arcm**2 -6.3226*width_arcm +  6.2051)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Alrefay
  kriteria = "Alrefay v1 (Visible by Naked Eye)"
  expression = "ArcV =− 1.01W^3 +3.3W^2 − 4.51W +  9.34"
  kriteria_arcv_w = float(- 1.01*width_arcm**3 +3.3*width_arcm**2 + -4.51*width_arcm +  9.34)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Alrefay
  kriteria = "Alrefay v2 (Visible by Optical Aided Only)"
  expression = "ArcV =− 1.02W^3 +3.22W^2 − 4.35W +  7.83"
  kriteria_arcv_w = float(- 1.02*width_arcm**3 +3.22*width_arcm**2 + -4.35*width_arcm +  7.83)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Faid
  kriteria = "Faid v1 (Visible by Naked Eye))"
  expression = "ArcV = − 0.2387791499 W + 0.0053517999 W2 − 0.0000422340 W3 + 7.9662653619"
  kriteria_arcv_w = float(- 0.0000422340*width_arcm**3 ++ 0.0053517999 *width_arcm**2 + - 0.2387791499*width_arcm +  7.9662653619)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Faid
  kriteria = "Faid v2 (Visible by CCD Imaging Only))"
  expression = "ArcV = − 0.2372 W + 0.0064324 W2 − 0.0000523 W3 + 2.957"
  kriteria_arcv_w = float(-  0.2372*width_arcm**3 + 0.0064324 *width_arcm**2 + - 0.0000523*width_arcm +  2.957)

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

  # Kriteria Faid
  kriteria = "Bruin"
  expression = "ArcV =11.5621745317 − 7.944238328 W + 3.2608487770 W2 − 0.4559413249 W3"
  kriteria_arcv_w = float(-0.4559413249*width_arcm**3 + 3.2608487770 *width_arcm**2 + - 7.944238328*width_arcm +  11.5621745317 )

  visibility = i if kriteria_arcv_w  >= arcv else v
  append_result(kriteria, expression, arcv, kriteria_arcv_w , visibility)

    # Convert the timedelta object to total minutes (float)
  lag_time = lag_time.total_seconds() / 60.0

  # Kriteria Caldwell
  kriteria = "Caldwell (Naked Eye Visible)"
  expression = "lag (min) = − 0.9709 ArcL + 44.65"
  kriteria_lt = float(-0.9709*arcl + 44.65)

  visibility = i if kriteria_lt  >= lag_time else v
  append_result(kriteria, expression, lag_time, kriteria_lt , visibility)

  # Kriteria Caldwell
  kriteria = "Caldwell (Only Optical Aided Visible)"
  expression = "lag (min) =  − 1.9230 ArcL + 43.13 "
  kriteria_lt = float(- 1.9230*arcl + 43.13)

  visibility = i if kriteria_lt  >= lag_time else v
  append_result(kriteria, expression, lag_time, kriteria_lt , visibility)

  # Kriteria Gautchy
  kriteria = "Gautchy "
  expression = "lag (min) =  0.3342328913 DAZ + − 0.0715608980 DAZ2 + 0.0009924422 DAZ3 + 33.8890455442  "
  kriteria_lt = float(0.3342328913 * daz + - 0.0715608980 * daz**2 + 0.0009924422* daz**3 + 33.8890455442 )

  visibility = i if kriteria_lt  >= lag_time else v
  append_result(kriteria, expression, lag_time, kriteria_lt , visibility)


  # Create DataFrame
  df = pd.DataFrame(results)

  # Display the DataFrame
  print(df)

  csv_path = 'kriteria_arcv_vs_daz.csv'
  df.to_csv(csv_path, index=False)

  return df
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cartopy.io.shapereader as shpreader
from matplotlib.colors import ListedColormap, BoundaryNorm
from skyfield.api import load, Topos
from skyfield.almanac import find_settings
from shapely.geometry import Point, MultiPolygon
from shapely.prepared import prep
from tqdm import tqdm
from timezonefinder import TimezoneFinder
from datetime import datetime, timedelta
import pytz

def globalvisibilitymap(year,month,day, LAT_RES = 100, LON_RES = 200):
  # -----------------------------------------------------------------------------
  # 1. CONFIGURATION
  # -----------------------------------------------------------------------------
  YEAR, MONTH, DAY = year,month,day # Eid al-Fitr 2024 sighting date
  csv_filename = f"visibility_data_adjusted_{YEAR}_{MONTH}_{DAY}.csv"

  # Resolution (100x200 is a good balance for speed vs accuracy)


  # -----------------------------------------------------------------------------
  # 2. CALCULATION
  # -----------------------------------------------------------------------------
  def run_calculation():
      print(f"--- Starting Calculation for {DAY}-{MONTH}-{YEAR} ---")

      # Load Ephemeris
      try:
          eph = load('de421.bsp')
      except:
          eph = load('de421.bsp')

      earth, moon, sun_obj = eph['earth'], eph['moon'], eph['sun']
      ts = load.timescale()
      tf = TimezoneFinder()

      # Grid generation
      lat_values = np.linspace(-60, 60, LAT_RES)
      lon_values = np.linspace(-179.9, 179.9, LON_RES)

      csv_data = []

      for lat in tqdm(lat_values, desc="Calculating Visibility"):
          for lon in lon_values:
              try:
                  # 1. Timezone & Sunset
                  try:
                      tz_str = tf.timezone_at(lng=lon, lat=lat)
                      tz_offset = pytz.timezone(tz_str).utcoffset(datetime(YEAR, MONTH, DAY)).total_seconds() / 3600.0 if tz_str else 0
                  except:
                      tz_offset = 0

                  observer = earth + Topos(latitude_degrees=lat, longitude_degrees=lon)

                  # Search for sunset
                  t0_utc = datetime(YEAR, MONTH, DAY) - timedelta(hours=tz_offset)
                  t1_utc = t0_utc + timedelta(days=1)
                  t0_sf = ts.utc(t0_utc.year, t0_utc.month, t0_utc.day, t0_utc.hour)
                  t1_sf = ts.utc(t1_utc.year, t1_utc.month, t1_utc.day, t1_utc.hour)

                  t_set, _ = find_settings(observer, sun_obj, t0_sf, t1_sf, horizon_degrees=-0.8333)

                  if not t_set: continue

                  t_observe = t_set[0]

                  # 2. Positions at Sunset
                  obs_sl = earth + Topos(latitude_degrees=lat, longitude_degrees=lon, elevation_m=0)
                  moon_app = obs_sl.at(t_observe).observe(moon).apparent()
                  sun_app = obs_sl.at(t_observe).observe(sun_obj).apparent()

                  moon_alt = moon_app.altaz()[0].degrees
                  sun_alt = sun_app.altaz()[0].degrees
                  arcl = sun_app.separation_from(moon_app).degrees
                  arcv = moon_alt - sun_alt # Altitude Difference
                  daz = abs(sun_app.altaz()[1].degrees - moon_app.altaz()[1].degrees)

                  # -------------------------------------------------------------
                  # 3. APPLY ADJUSTED CRITERION (Passes through 0,4 and 6.4,3)
                  # Equation: Malt = -0.1736 * ArcL + 0.0023 * ArcL^2 + 0.000064 * ArcL^3 + 4.0
                  # -------------------------------------------------------------
                  min_malt = -0.1736 * arcl + 0.0023 * (arcl**2) + 0.000064 * (arcl**3) + 4.0

                  # Visibility Check
                  is_visible = moon_alt >= min_malt

                  csv_data.append([
                      lat, lon, arcv, arcl, daz,
                      "Visible" if is_visible else "Not Visible",
                      1 if is_visible else 0
                  ])

              except Exception:
                  continue

      # Save CSV
      columns = ["Latitude", "Longitude", "ArcV", "ArcL", "Daz", "Visibility", "Map Value"]
      df = pd.DataFrame(csv_data, columns=columns)
      df.to_csv(csv_filename, index=False)
      print(f"✅ CSV Saved: {csv_filename}")
      return df

  # -----------------------------------------------------------------------------
  # 3. FIND EASTERNMOST LAND POINT
  # -----------------------------------------------------------------------------
  def find_easternmost_land(df):
      print("Searching for easternmost land visibility...")

      # Filter Visible & Sort by Longitude (Descending = East to West)
      visible_df = df[df['Visibility'] == 'Visible'].sort_values(by='Longitude', ascending=False)

      if visible_df.empty:
          print("No visibility found.")
          return None

      # Load Land Data
      reader = shpreader.Reader(shpreader.natural_earth(resolution='110m', category='physical', name='land'))
      land_geom = prep(MultiPolygon(list(reader.geometries())))

      # Check points
      for _, row in visible_df.iterrows():
          if land_geom.contains(Point(row['Longitude'], row['Latitude'])):
              print(f"✅ Found: {row['Latitude']:.2f}N, {row['Longitude']:.2f}E")
              return row

      print("No visible points on land.")
      return None

  # -----------------------------------------------------------------------------
  # 4. PLOTTING
  # -----------------------------------------------------------------------------
  def plot_map(df, east_point):
      print("Generating Map...")

      # Grid Setup
      pivot = df.pivot(index='Latitude', columns='Longitude', values='Map Value')
      lat_grid, lon_grid = np.meshgrid(pivot.columns, pivot.index) # Meshgrid might need swap depending on pivot
      # Correct pivoting: columns=Longitude (X), index=Latitude (Y)
      # meshgrid(X, Y)
      lon_grid, lat_grid = np.meshgrid(pivot.columns, pivot.index)

      # Plot
      fig, ax = plt.subplots(figsize=(20, 10), subplot_kw={'projection': ccrs.Robinson()})
      ax.set_global()
      ax.add_feature(cfeature.LAND, color='#f4f4f4')
      ax.add_feature(cfeature.OCEAN, color='#e0f7fa')
      ax.coastlines(linewidth=0.5)

      # Colors: Red (0) -> Green (1)
      cmap = ListedColormap([(1, 0, 0, 0.3), (0, 1, 0, 0.4)])
      mesh = ax.pcolormesh(lon_grid, lat_grid, pivot.values, cmap=cmap, transform=ccrs.PlateCarree(), shading='nearest')

      # Marker
      if east_point is not None:
          elat, elon = east_point['Latitude'], east_point['Longitude']
          ax.plot(elon, elat, marker='*', color='yellow', markersize=20, markeredgecolor='black', transform=ccrs.PlateCarree(), zorder=10)
          ax.text(elon + 3, elat - 2, f"First Land Visibility\n{elat:.2f}°N, {elon:.2f}°E", transform=ccrs.PlateCarree(),
                  fontsize=10, weight='bold', bbox=dict(facecolor='white', alpha=0.9, boxstyle='round,pad=0.3'), zorder=11)

      # Legend
      patches = [mpatches.Patch(color=(1, 0, 0, 0.3), label="Not Visible"), mpatches.Patch(color=(0, 1, 0, 0.4), label="Visible")]
      ax.legend(handles=patches, loc="lower right", title="Adjusted Criterion")

      plt.title(f"Global Visibility Map (Adjusted) - {DAY}/{MONTH}/{YEAR}", fontsize=16)
      plt.savefig("adjusted_visibility_map.png", dpi=150, bbox_inches='tight')
      plt.show()

  # Run
  if __name__ == "__main__":
      df = run_calculation()
      east_point = find_easternmost_land(df)
      plot_map(df, east_point)

from datetime import datetime, timedelta
from skyfield.api import load, wgs84
import pandas as pd
from tqdm import tqdm
import math
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import matplotlib.colors as mcolors
import matplotlib.patheffects as path_effects
import matplotlib.patches as mpatches 
import numpy as np 

# Load ephemeris data once
eph = load('de440s.bsp') 
ts = load.timescale()



def moonpositionanalysis(year,month,day,tz,lat,long,ele,  IMAGE_PATH):

  def parameter_calculator(year, month, day, tz):
      from skyfield import almanac
      from skyfield.api import N, E


      # 1. Setup Observer
      location = wgs84.latlon(lat * N, long * E, elevation_m=ele)
      observer = eph['Earth'] + location
      sun, moon = eph['sun'], eph['moon']

      # 2. Find Sunset Time
      t0 = ts.utc(year, month, day)
      t1 = ts.utc(year, month, day + 1)
      
      # Standard refraction logic
      t, y = almanac.find_settings(observer, sun, t0, t1)
      
      if len(t) == 0:
          return 0, 0, 0, 0, 0 # Fail-safe
          
      t_sunset = t[0]

      # 3. Observe Sun and Moon at Sunset
      sun_obs = observer.at(t_sunset).observe(sun).apparent()
      moon_obs = observer.at(t_sunset).observe(moon).apparent()

      # 4. Extract Parameters
      # sun_alt should be close to -0.83 (horizon)
      sun_alt, sun_az, _ = sun_obs.altaz()
      moon_alt, moon_az, _ = moon_obs.altaz()
      elongation = sun_obs.separation_from(moon_obs).degrees

      return (elongation, 
              moon_alt.degrees, 
              sun_alt.degrees, 
              sun_az.degrees, 
              moon_az.degrees)

  def generate_hijri_calendar():
      # --- CONFIGURATION ---
      # Start Date (Gregorian)
      start_date = datetime(2027, 1, 1)
      
      # Start Date (Hijri) - Anchor Point
      # Example: Jan 1, 2027 is roughly 22 Rajab 1448
      h_day = 22
      h_month = 7  # Rajab
      h_year = 1448
      
      duration_days = 365*19 # One year of data
      
      # MABIMS Criteria (2021)
      LIMIT_ALT = 3.0
      LIMIT_ELONG = 6.4

      nama_bulan_hijri = [
          "Muharram", "Safar", "Rabiulawal", "Rabiulakhir",
          "Jamadilawal", "Jamadilakhir", "Rejab", "Syaaban",
          "Ramadan", "Syawal", "Zulkaedah", "Zulhijah"
      ]
      
      data = []
      current_date = start_date

      # --- MAIN LOOP ---
      for _ in tqdm(range(duration_days), desc="Simulating Calendar"):
          
          # 1. Initialize Row with Default Values
          row = {
              "Gregorian": current_date.strftime("%Y-%m-%d"),
              "Hijri Day": h_day,
              "Hijri Month": nama_bulan_hijri[h_month - 1],
              "Hijri Year": h_year,
              "Elongation": None,
              "Moon Alt": None,
              "Sun Alt": None,
              "Sun Az": None,
              "Moon Az": None,
              "Status": "Normal"
          }

          # 2. Determine TOMORROW'S Hijri Date
          next_h_day = h_day + 1
          next_h_month = h_month
          next_h_year = h_year

          # --- CRITICAL LOGIC: Check Visibility ONLY on Day 29 ---
          if h_day == 29:
              # Run the heavy calculation
              elong, m_alt, s_alt, s_az, m_az = parameter_calculator(
                  current_date.year, current_date.month, current_date.day, tz
              )
              
              # Store Calculation Results
              row["Elongation"] = round(elong, 2)
              row["Moon Alt"] = round(m_alt, 2)
              row["Sun Alt"] = round(s_alt, 3)
              row["Sun Az"] = round(s_az, 2)
              row["Moon Az"] = round(m_az, 2)

              # Check Criteria (MABIMS)
              if m_alt > LIMIT_ALT and elong > LIMIT_ELONG:
                  # Visible -> Month ends at 29
                  next_h_day = 1
                  next_h_month += 1
                  row["Status"] = "New Moon Visible (End 29)"
              else:
                  # Not Visible -> Month extends to 30
                  next_h_day = 30
                  row["Status"] = "Not Visible (Extend to 30)"
          
          elif h_day == 30:
              # Month ends automatically
              next_h_day = 1
              next_h_month += 1
              row["Status"] = "End of Month (30)"

          # 3. Handle Year Rollover
          if next_h_month > 12:
              next_h_month = 1
              next_h_year += 1

          # 4. Save and Advance
          data.append(row)
          
          h_day = next_h_day
          h_month = next_h_month
          h_year = next_h_year
          current_date += timedelta(days=1)

      return pd.DataFrame(data)

  # Run the function
  df = generate_hijri_calendar()

  # Show the critical transition days (Day 29) to verify output
  cols_to_show = ['Gregorian', 'Hijri Day', 'Hijri Month', 'Moon Alt', 'Sun Az', 'Moon Az', 'Status']
  print(df[df['Hijri Day'] == 29][cols_to_show].head(10))

  # Save to CSV
  df.to_csv("hijri_simulation_detailed.csv", index=False)



  # --- 1. CALIBRATION: TWEAK THESE NUMBERS TO MATCH YOUR PHOTO ---
  IMG_CENTER_AZ = 270              
  IMG_FOV = 65                     

  # Horizon Position Adjustment
  HORIZON_PCT = 0.25       

  # Zoom/Headroom
  Y_MAX = 25 

  # ------------------------------------------------------------------

  # 1. Load Data
  try:
      df = pd.read_csv("hijri_simulation_detailed.csv")
      obs_days = df[df['Hijri Day'] == 29].copy()
  except FileNotFoundError:
      print("Error: CSV not found. Generating dummy data for simulation.")
      data = {
          'Hijri Day': [29]*12,
          'Sun Az': np.linspace(240, 300, 12),
          'Sun Alt': [-0.83]*12,
          'Moon Az': np.linspace(245, 295, 12),
          'Moon Alt': np.random.uniform(1, 15, 12),
          'Elongation': np.random.uniform(5, 12, 12),
          'Hijri Month': ['Muh', 'Saf', 'Rab', 'Rab', 'Jam', 'Jam', 'Rej', 'Sya', 'Ram', 'Sya', 'Zul', 'Zul']
      }
      obs_days = pd.DataFrame(data)

  # 2. Setup the Plot
  fig, ax = plt.subplots(figsize=(14, 8))

  # Calculate Extents
  img_h_dummy = 3000
  img_w_dummy = 4000
  aspect_ratio = img_h_dummy / img_w_dummy 
  fov_v = IMG_FOV * aspect_ratio
  bottom_deg = - (fov_v * HORIZON_PCT)
  top_deg = fov_v * (1 - HORIZON_PCT)

  extent = [
      IMG_CENTER_AZ - (IMG_FOV / 2),
      IMG_CENTER_AZ + (IMG_FOV / 2),
      bottom_deg,
      top_deg
  ]

  # 3. Load Image OR Create Gradient
  try:
      img = mpimg.imread(IMAGE_PATH)
      
      img_h, img_w, _ = img.shape
      aspect_ratio = img_h / img_w
      fov_v = IMG_FOV * aspect_ratio
      bottom_deg = - (fov_v * HORIZON_PCT)
      top_deg = fov_v * (1 - HORIZON_PCT)
      
      extent = [
          IMG_CENTER_AZ - (IMG_FOV / 2),
          IMG_CENTER_AZ + (IMG_FOV / 2),
          bottom_deg,
          top_deg
      ]
      
      ax.imshow(img, extent=extent, aspect='auto', zorder=0)

  except FileNotFoundError:
      print(f"Warning: {IMAGE_PATH} not found. Generating sunset sky simulation.")
      
      # --- GRADIENT GENERATION START ---
      total_height_deg = top_deg - bottom_deg
      horizon_ratio = (0 - bottom_deg) / total_height_deg
      h_pos = max(0.0, min(1.0, horizon_ratio))
      
      nodes = [
          (0.0,  '#001133'),        
          (h_pos - 0.01, '#001133'), 
          (h_pos, '#FF4500'),       
          (min(1.0, h_pos + 0.15), '#FF8C00'), 
          (min(1.0, h_pos + 0.35), '#FFD700'), 
          (1.0,  '#87CEEB')         
      ]
      
      sunset_cmap = mcolors.LinearSegmentedColormap.from_list("SimulatedSunset", nodes)
      gradient = np.linspace(0, 1, 500).reshape(-1, 1)
      ax.imshow(gradient, extent=extent, aspect='auto', cmap=sunset_cmap, origin='lower', zorder=0)
      # --- GRADIENT GENERATION END ---

  # 4. Plot Sun and Moon
  LIMIT_ALT = 3.0
  LIMIT_ELONG = 6.4

  # Calculate Statistics
  total_moons = len(obs_days)
  above_criteria_count = len(obs_days[(obs_days['Moon Alt'] > LIMIT_ALT) & (obs_days['Elongation'] > LIMIT_ELONG)])
  below_criteria_count = total_moons - above_criteria_count

  if total_moons > 0:
      pct_above = (above_criteria_count / total_moons) * 100
      pct_below = (below_criteria_count / total_moons) * 100
  else:
      pct_above = 0
      pct_below = 0

  # Horizon Reference Line
  ax.axhline(0, color='white', linestyle='-', linewidth=1.0, alpha=0.5)

  # Plot Sun
  ax.scatter(obs_days['Sun Az'], obs_days['Sun Alt'], 
            c='orange', s=180, marker='o', edgecolors='red', linewidth=1.5,
            zorder=2)

  # Plot Moon
  colors = ['#00FF00' if (r['Moon Alt'] > LIMIT_ALT and r['Elongation'] > LIMIT_ELONG) 
            else '#CCCCCC' for i, r in obs_days.iterrows()]

  ax.scatter(obs_days['Moon Az'], obs_days['Moon Alt'], 
            c=colors, s=100, marker='o', edgecolors='black', linewidth=1,
            zorder=3)

  # 5. Draw Connectors
  for index, row in obs_days.iterrows():
      ax.plot([row['Sun Az'], row['Moon Az']], [row['Sun Alt'], row['Moon Alt']], 
              color='white', linestyle=':', linewidth=1, alpha=0.6)

  # 6. Final Formatting
  ax.set_title(f'Sun & Moon at Sunset: Horizon Simulation', color='black', fontsize=14)
  ax.set_xlabel('Azimuth (Compass Direction)', fontsize=12)
  ax.set_ylabel('Altitude (Degrees)', fontsize=12)

  # --- CUSTOM LEGEND START ---
  legend_handles = [
      mpatches.Patch(facecolor='#00FF00', edgecolor='black', label=f'Moon Above Criteria: {pct_above:.1f}%'),
      mpatches.Patch(facecolor='#CCCCCC', edgecolor='black', label=f'Moon Below Criteria: {pct_below:.1f}%'),
      plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='orange', markeredgecolor='red', markersize=10, label='Sun'),
      plt.Line2D([0], [0], color='white', lw=1, label='True Horizon (0°)')
  ]

  ax.legend(handles=legend_handles, loc='upper left', frameon=True, framealpha=0.8, fontsize=10)
  # --- CUSTOM LEGEND END ---

  # Set X-Axis Limits
  x_min = IMG_CENTER_AZ - (IMG_FOV / 2)
  x_max = IMG_CENTER_AZ + (IMG_FOV / 2)
  ax.set_xlim(x_min, x_max)

  # Set Y-Axis Limits
  ax.set_ylim(bottom_deg, Y_MAX)

  # Add "West" Marker
  ax.text(270, bottom_deg + 1, 'WEST (270°)', color='yellow', fontsize=12, 
          ha='center', fontweight='bold', bbox=dict(facecolor='black', alpha=0.3))

  plt.tight_layout()
  plt.show()

import os
import csv
import math
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from skyfield.api import load, wgs84, Topos
from skyfield.earthlib import refraction
from skyfield.units import Angle
from numpy import arccos



def visibilitycontrast (lat,long,year,month,day,ele,tz,temperature_celcius, relative_humidity, light_pollution_magsec, location):
  direction_obs = "N"
  lokasi = location

  # --- Initialization ---
  # Ensure 'de440s.bsp' exists in your directory, or Skyfield will download it.
  ts = load.timescale()
  eph = load('de440s.bsp')
  earth, sun, moon = eph['earth'], eph['sun'], eph['moon']

  # Define time range
  local_midnight = datetime(year, month, day, 0, 0, 0)
  t0_utc = local_midnight - timedelta(hours=tz)
  t1_utc = t0_utc + timedelta(days=1)

  t0 = ts.utc(t0_utc.year, t0_utc.month, t0_utc.day, t0_utc.hour, t0_utc.minute, t0_utc.second)
  t1 = ts.utc(t1_utc.year, t1_utc.month, t1_utc.day, t1_utc.hour, t1_utc.minute, t1_utc.second)

  # Calculate Horizon Dip based on elevation
  altitude_m = ele
  earth_radius_m = 6378136.6
  side_over_hypotenuse = earth_radius_m / (earth_radius_m + altitude_m)
  h = Angle(radians=-arccos(side_over_hypotenuse))

  # Standard solar radius approximation
  solar_radius_degrees = 16 / 60

  # --- Find Sunset ---
  observer_topo = earth + Topos(latitude_degrees=lat, longitude_degrees=long, elevation_m=ele)
  from skyfield.almanac import find_settings
  t, y = find_settings(observer_topo, sun, t0, t1, horizon_degrees=h.degrees - solar_radius_degrees)

  if not t:
      print("Sun does not set in this time range.")
      exit()

  sunset_time_utc = t[0].utc_datetime()
  sunset_local = sunset_time_utc + timedelta(hours=tz)

  # Initial UTC tuple for the loop
  utc_x = (
      sunset_time_utc.year,
      sunset_time_utc.month,
      sunset_time_utc.day,
      sunset_time_utc.hour,
      sunset_time_utc.minute,
      sunset_time_utc.second
  )

  # Initial Moon Altitude check
  current_time_ts = ts.utc(*utc_x)
  moon_app = observer_topo.at(current_time_ts).observe(moon).apparent()
  moon_alt, _, _ = moon_app.altaz()

  data = []

  print(f"Sunset calculated at: {sunset_local} (Local)")
  print("Starting visibility calculation loop...")

  # --- Main Calculation Loop ---
  while moon_alt.degrees >= -1.0: # Run until moon is 1 degree below horizon
      
      current_time_ts = ts.utc(*utc_x)
      
      # Observe Sun and Moon
      sun_astro = observer_topo.at(current_time_ts).observe(sun)
      sun_app = sun_astro.apparent()
      sun_alt, sun_az, sun_dist = sun_app.altaz()
      sun_ra, sun_dec, _ = sun_app.radec()

      moon_astro = observer_topo.at(current_time_ts).observe(moon)
      moon_app = moon_astro.apparent()
      moon_alt, moon_az, moon_dist = moon_app.altaz()
      
      # Break if moon is too low
      if moon_alt.degrees < -1.0:
          break

      daz = abs(moon_az.degrees - sun_az.degrees)

      # 1. Geometry Calculations
      moon_earth_dist_km = (moon.at(current_time_ts) - earth.at(current_time_ts)).distance().km
      horizontal_parallax = math.degrees(math.asin(6378.14 / moon_earth_dist_km))
      
      moon_sun_distance_ratio = 0.272481
      semidiameter_geocentric = math.degrees(math.asin(moon_sun_distance_ratio * math.sin(math.radians(horizontal_parallax))))
      semidiameter_topocentric = semidiameter_geocentric * (1 + math.sin(math.radians(moon_alt.degrees)) * math.sin(math.radians(horizontal_parallax)))
      
      separation_angle = sun_app.separation_from(moon_app).degrees
      
      # 2. Phase Angle
      v_moon = eph['moon'].at(current_time_ts).position.km
      v_sun = eph['sun'].at(current_time_ts).position.km
      moon_sun_vector = eph['moon'].at(current_time_ts) - eph['sun'].at(current_time_ts)
      moon_sun_dist_km = moon_sun_vector.distance().km
      sun_earth_dist_km = sun_dist.km
      
      arcl_rad = math.radians(separation_angle)
      denom = sun_earth_dist_km - moon_sun_dist_km * math.cos(arcl_rad)
      if denom == 0: denom = 1e-9
      
      phase_angle = 180 + math.degrees(math.atan((moon_sun_dist_km * math.sin(arcl_rad)) / denom))

      # 3. Illuminated Fraction
      k_frac = (1 + math.cos(math.radians(phase_angle))) / 2
      moon_area = math.pi * semidiameter_topocentric**2
      ilum_area = k_frac * moon_area

      # 4. Moon Brightness (Extra-Atmospheric)
      m = -12.73 + 0.026 * phase_angle + 4 * 10**-9 * phase_angle**4
      
      if ilum_area <= 0:
          out_moonbrightness_mag = 999
      else:
          out_moonbrightness_s10 = (1 / ilum_area) * 2.51**(10 - m)
          if out_moonbrightness_s10 <= 0:
              out_moonbrightness_mag = 999
          else:
              out_moonbrightness_mag = -((math.log10(out_moonbrightness_s10)) * 2.5 - 26.33)

      # 5. Atmospheric Extinction (k)
      # Rayleigh
      kr = 0.1066 * math.exp(-(ele) / 8200) * (0.031 / 0.55)**(-4)

      # Aerosol
      ke1 = 0.10 * (0.031 / 0.55)**-1.3
      ke2 = math.exp(-ele / 1500)
      ke3 = (1 - (0.32 / math.log(relative_humidity)))**(4/3) if relative_humidity > 0 and math.log(relative_humidity) != 0 else 1
      ke4 = 1 + 0.33 * math.sin(math.radians(sun_ra.hours * 15))
      ke = ke1 * ke2 * ke3 * ke4

      # Ozone
      matara = float((sun_ra.hours * 15))
      ko1 = (lat * math.cos(math.radians(matara)) - math.cos(math.radians(3 * matara)))
      ko2 = 1 + 0.13 * ko1
      ko = 0.031 * ko2

      # Water Vapour
      k_W = 0.0031 * 0.94 * relative_humidity * math.exp(temperature_celcius / 15) * math.exp(-ele / 8200)

      # Air Mass
      moon_zenith_dist = 90 - moon_alt.degrees
      rad_z = np.radians(min(moon_zenith_dist, 89.9)) 
      air_mass = (np.cos(rad_z) + 0.025 * np.exp(-11 * np.cos(rad_z)))**-1

      # 6. Inner Atmospheric Moon Brightness
      difmag = float(kr * air_mass) 
      bulanmagket = out_moonbrightness_mag * 10**(-0.4 * difmag) 

      # 7. Twilight Brightness
      tw1 = -(7.5 * (10**-5) * moon_zenith_dist + 5.05 * 10**-3) * daz
      tw2 = (3.67 * 10**-4 * moon_zenith_dist - 0.458) * -1 * (sun_alt.degrees)
      tw3 = (9.17 * 10**-3) * moon_zenith_dist + 3.525
      tw = tw1 + tw2 + tw3
      twmag1 = 10**tw
      twmag2 = 9.17 * 10**4 * twmag1
      tw_no_lightpollution = -(math.log10(twmag2) * 2.5 - 27.78)

      # Light Pollution Model
      poly_a, poly_b = 319.186511755673, -46.9419919726453
      poly_c = 0.434369283318305 
      poly_d, poly_e = 2.39138122541257, -0.00121311966265414
      poly_f, poly_g = -0.0488735671014705, -0.0396262989070584
      poly_h, poly_i = -0.000000928758741258742, 0.0000548215089277455
      poly_j = 0.001346675592325110
      
      dir_obs_val = 1 if direction_obs == "Y" else -1
      theta = dir_obs_val * moon_zenith_dist
      Zlp = light_pollution_magsec

      tw_lightpollution = (poly_a + poly_b*Zlp + poly_c*theta + poly_d*Zlp**2 + 
                          poly_e*theta**2 + poly_f*Zlp*theta + poly_g*Zlp**3 + 
                          poly_h*theta**3 + poly_i*Zlp*theta**2 + poly_j*Zlp**2*theta)

      twmag = min(tw_no_lightpollution, tw_lightpollution)

      # 8. Contrast Calculation
      moon_brightness_nL = 10**(-0.4 * (bulanmagket - 26.33))
      
      # Calculate twilight nL with scaling
      twilight_brightness_nL_raw = 10**(-0.4 * (twmag - 26.33))
      
      factor_nl = 1.0
      if light_pollution_magsec < 15:
          factor_nl = 5000 * (np.log10(100000 / (light_pollution_magsec + 1)))
      elif light_pollution_magsec < 16:
          factor_nl = 50 * (np.log10(1000 / (light_pollution_magsec + 1)))
      elif light_pollution_magsec < 18:
          factor_nl = 25 * (np.log10(1000 / (light_pollution_magsec + 1)))
      
      twilight_brightness_nL = twilight_brightness_nL_raw * factor_nl

      contrast_c = ((moon_brightness_nL - twilight_brightness_nL) / twilight_brightness_nL)

      # 9. Contrast Threshold
      twilight_brightness_cd_raw = 10**((12.58 - twmag) / 2.5)
      twilight_brightness_cd = twilight_brightness_cd_raw * factor_nl

      r1, r2 = 6.505 * 10**-4, -8.461 * 10**-4
      k1, k2 = 7.633 * 10**-3, -7.174 * 10**-3

      term1 = (r1 * twilight_brightness_cd**(-0.25) + r2)**2 / semidiameter_topocentric
      term2 = k1 * twilight_brightness_cd**(-0.25) + k2
      
      try:
          contrast_threshold = (term1**(0.6) + term2**(0.6))**(5/3)
      except Exception:
          contrast_threshold = 999 

      if isinstance(contrast_threshold, complex) or math.isnan(contrast_threshold):
          contrast_threshold = 999

      # Determine visibility
      visibility = "No" if contrast_c <= contrast_threshold else "Yes"

      utc_tz = datetime(*utc_x) + timedelta(hours=tz)
      time_str = utc_tz.strftime("%H:%M")
      
      # Save Data
      data.append((time_str, twmag, contrast_c, contrast_threshold, visibility))

      # Increment time by 1 minute
      utc_x_dt = datetime(*utc_x) + timedelta(minutes=1)
      utc_x = list(utc_x_dt.timetuple())[:6]

  # --- Output Results ---
  csv_filename = "twilight_sky_brightness_results.csv"
  headers = ["Time", "Twilight Mag", "Contrast", "Contrast Threshold", "Visibility"]

  with open(csv_filename, mode='w', newline='') as file:
      writer = csv.writer(file)
      writer.writerow(headers)
      writer.writerows(data)

  df_v = pd.DataFrame(data, columns=headers)
  print("\n--- Results Table ---")
  print(df_v)

  # --- Analysis ---
  df_yes = df_v[df_v["Visibility"] == "Yes"].copy()

  if not df_yes.empty:
      # Helper to calculate minutes from sunset safely
      def calc_delta(t_str):
          # 1. Parse the time string into a naive datetime object
          cur_time = datetime.strptime(t_str, "%H:%M").replace(
              year=sunset_local.year, month=sunset_local.month, day=sunset_local.day)
          
          # 2. Handle case where calculation crosses midnight (next day)
          if cur_time.hour < 12 and sunset_local.hour > 12:
              cur_time += timedelta(days=1)
              
          # 3. FIX: Strip timezone info from sunset_local so both are "naive"
          sunset_naive = sunset_local.replace(tzinfo=None)
          
          return int((cur_time - sunset_naive).total_seconds() / 60)

      df_yes["Δt after Sunset (min)"] = df_yes["Time"].apply(calc_delta)
      
      first_min = df_yes["Δt after Sunset (min)"].iloc[0]
      last_min = df_yes["Δt after Sunset (min)"].iloc[-1]
      duration = last_min - first_min + 1

      print(f"\nFirst 'Yes' at {df_yes['Time'].iloc[0]} (Δt = {first_min} min after sunset).")
      print(f"Duration of window: {duration} minutes.")
  else:
      print("\nVisibility never reaches 'Yes' within the computed window.")

  # --- Analysis & Visualization ---
  import matplotlib.pyplot as plt
  import matplotlib.dates as mdates

  # 1. Calculate Moonset for final stats
  # We search for moonset after sunset
  t_moonset, y_moonset = find_settings(observer_topo, moon, t0, t1, horizon_degrees=-0.57) # -0.57 for refraction+parallax
  if t_moonset:
      moonset_time_utc = t_moonset[0].utc_datetime()
      moonset_local = moonset_time_utc + timedelta(hours=tz)
      minutes_sunset_to_moonset = int((moonset_local - sunset_local).total_seconds() / 60)
  else:
      moonset_local = "N/A"
      minutes_sunset_to_moonset = 0

  # 2. Filter for Visibility
  df_yes = df_v[df_v["Visibility"] == "Yes"].copy()
  duration = 0

  if not df_yes.empty:
      # Get first and last visible times
      time1_str = df_yes["Time"].iloc[0]
      time2_str = df_yes["Time"].iloc[-1]
      
      # Calculate duration
      def calc_delta(t_str):
          cur_time = datetime.strptime(t_str, "%H:%M").replace(
              year=sunset_local.year, month=sunset_local.month, day=sunset_local.day)
          if cur_time.hour < 12 and sunset_local.hour > 12:
              cur_time += timedelta(days=1)
          # Fix: Remove timezone from sunset_local for subtraction
          return int((cur_time - sunset_local.replace(tzinfo=None)).total_seconds() / 60)

      df_yes["Δt"] = df_yes["Time"].apply(calc_delta)
      duration = df_yes["Δt"].iloc[-1] - df_yes["Δt"].iloc[0] + 1
      
      vis_text = f"The lunar crescent will be visible from {time1_str} to {time2_str}."
  else:
      vis_text = "The lunar crescent will not be visible during this period."

  # 3. Prepare Data for Plotting
  # We use the full dataframe 'df_v'
  df_plot = df_v.copy()
  # Convert 'Time' string to datetime objects for matplotlib
  df_plot["Datetime"] = pd.to_datetime(df_plot["Time"], format="%H:%M")

  # --- Plotting ---
  plt.figure(figsize=(12, 6), dpi=120)

  contrast_color = "#1f77b4"  # Soft blue
  threshold_color = "#ff7f0e"  # Warm orange

  # Plot Contrast and Threshold
  # Note: We filter out infinite values for log plotting if necessary
  plt.plot(df_plot["Datetime"], df_plot["Contrast"],
          label="Contrast (Moon/Sky)", color=contrast_color, linestyle='-', linewidth=2.5)
  plt.plot(df_plot["Datetime"], df_plot["Contrast Threshold"],
          label="Visibility Threshold", color=threshold_color, linestyle='--', linewidth=2.5)

  plt.yscale('log')

  # Labels and Title
  plt.xlabel("Time (Local)", fontsize=12, fontweight='bold', color="#333333")
  plt.ylabel("Contrast (Log Scale)", fontsize=12, fontweight='bold', color="#333333")
  plt.title(f"Lunar Crescent Visibility at {lokasi}\n{day}/{month}/{year}", fontsize=14, fontweight='bold', color="#222222", pad=15)

  # X-Axis Formatting
  plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
  plt.xticks(rotation=45)
  plt.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.7)

  # Add Visibility Text Box
  # We place it in the center of the plot
  y_pos = df_plot["Contrast"].max() * 0.1 # Adjust vertical position
  plt.text(df_plot["Datetime"].iloc[len(df_plot)//2], y_pos, vis_text,
          fontsize=10, color="black", 
          bbox=dict(facecolor='white', edgecolor='gray', boxstyle='round,pad=0.5', alpha=0.9),
          horizontalalignment='center')

  plt.legend(fontsize=10, loc='best')
  plt.tight_layout()
  plt.show()

  # --- Final Printout ---
  print("-" * 30)
  print(f"Sunset (local):  {sunset_local.strftime('%Y-%m-%d %H:%M:%S')}")
  if isinstance(moonset_local, datetime):
      print(f"Moonset (local): {moonset_local.strftime('%Y-%m-%d %H:%M:%S')}")
  else:
      print(f"Moonset (local): {moonset_local}")

  print(f"Lag (Sunset→Moonset): {minutes_sunset_to_moonset} minutes")
  print(f"Visibility Window: {duration} minutes")
  print(f"Visibility Status: {vis_text}")

  if minutes_sunset_to_moonset > 0:
      ratio = duration / minutes_sunset_to_moonset
      print(f"Visibility Ratio: {ratio:.2f}")
  else:
      print("Visibility Ratio: 0.00")
  print("-" * 30)
