import {
  AstroTime,
  Observer,
  Body,
  Vector,
  Equator,
  Ecliptic,
} from "astronomy-engine";
import moment from "moment-timezone";

export function getJulianDay(date: Date): number {
  return date.getTime() / 86400000 + 2440587.5;
}

export function mod360(angle: number): number {
  return ((angle % 360) + 360) % 360;
}

export function getAngleDifference(angle1: number, angle2: number) {
  // Calculate the absolute difference and normalize it within 0-360 degrees
  let diff = Math.abs(angle1 - angle2) % 360;
  // If the difference is more than 180, take the complementary angle
  if (diff > 180) {
    diff = 360 - diff;
  }
  return diff;
}

export function formatTimeFromDate(date: Date): string {
  const hh = date.getHours().toString().padStart(2, "0");
  const mm = date.getMinutes().toString().padStart(2, "0");
  const ss = date.getSeconds().toString().padStart(2, "0");
  return `${hh}:${mm}:${ss}`;
}

export function adjustTimeByTimezone(
  date: Date,
  timezone: string | number
): Date {
  if (typeof timezone === "string" && isNaN(Number(timezone))) {
    try {
      const formatter = new Intl.DateTimeFormat("en-US", {
        timeZone: timezone,
        timeZoneName: "short",
      });
      return new Date(formatter.format(date));
    } catch (e) {
      console.warn(
        `Invalid timezone string: ${timezone}. Using numeric offset.`
      );
      return new Date(date.getTime() + Number(timezone) * 3600000);
    }
  } else {
    return new Date(date.getTime() + Number(timezone) * 3600000);
  }
}

export function adjustToLocalTime(date: Date, tz: string): Date {
  // Create a moment object from the UTC date, then convert to the specified timezone.
  return moment.utc(date).tz(tz).toDate();
}

export function astroTimeToISOString(
  time: AstroTime,
  timezone: string | number
): string {
  const localDate = adjustTimeByTimezone(time.date, timezone);
  return localDate.toISOString();
}

export function astroTimeToLocalTimeString(
  time: AstroTime,
  timezone: string | number
): string {
  const localDate = adjustTimeByTimezone(time.date, timezone);
  return formatTimeFromDate(localDate);
}

export function addDays(time: AstroTime, days: number): AstroTime {
  const d = new Date(time.date.getTime() + days * 86400000);
  return new AstroTime(d);
}
export function inverseLagrange(
  x: number[],
  y: number[],
  target: number
): number {
  let total = 0;
  for (let i = 0; i < x.length; i++) {
    let numer = 1;
    let denom = 1;
    for (let j = 0; j < x.length; j++) {
      if (j !== i) {
        numer *= target - y[j];
        denom *= y[i] - y[j];
      }
    }
    total += (numer * x[i]) / denom;
  }
  return total;
}

export async function interpolateTime(
  startTime: AstroTime,
  observer: Observer,
  target: number,
  f: (t: AstroTime) => number,
  offsets: number[] = [0, 0.25, 0.5, 0.75, 1.0]
): Promise<AstroTime> {
  const samples: number[] = [];
  for (const offset of offsets) {
    const sampleTime = addDays(startTime, offset);
    let value = f(sampleTime);
    value = mod360(value);
    samples.push(value);
  }
  const frac = inverseLagrange(offsets, samples, target);
  return addDays(startTime, frac);
}

// Get tropical ecliptic longitude of a body.
export function tropicalLongitude(
  body: Body,
  time: AstroTime,
  observer: Observer
): number {
  const eq = Equator(body, time, observer, true, false);
  // Convert equatorial coordinates (ra, dec in degrees) to a unit vector.
  const raRad = (eq.ra * Math.PI) / 180;
  const decRad = (eq.dec * Math.PI) / 180;
  const vector = new Vector(
    Math.cos(decRad) * Math.cos(raRad),
    Math.cos(decRad) * Math.sin(raRad),
    Math.sin(decRad),
    time
  );
  // Pass the vector to Ecliptic.
  const ec = Ecliptic(vector);
  return mod360(ec.elon);
}

// Approximate Lahiri Ayanamsa.
export function computeAyanamsa(time: AstroTime): number {
  const year = time.date.getUTCFullYear();
  return 24.07 + 0.014 * (year - 2000);
}

// Compute sidereal longitude: tropical minus ayanamsa.
export function siderealLongitude(
  body: Body,
  time: AstroTime,
  observer: Observer
): number {
  const trop = tropicalLongitude(body, time, observer);
  const ayan = computeAyanamsa(time);
  return mod360(trop - ayan);
}

export function parseInputDate(dateStr: string): Date {
  let inputDate = new Date(dateStr);
  if (isNaN(inputDate.getTime())) {
    const parts = dateStr.split("/");
    if (parts.length === 3) {
      const [d, m, y] = parts.map(Number);
      inputDate = new Date(Date.UTC(y, m - 1, d));
    } else {
      throw new Error("Invalid date format.");
    }
  }
  return inputDate;
}
