import { Point, Vector } from "@fboes/geojson";
import { Rand } from "../general/Rand.js";
import { Configuration } from "./Configuration.js";
import { Units } from "../../data/Units.js";
import { AeroflyAircraft } from "../../data/AeroflyAircraft.js";
import { Degree, degreeDifference } from "../general/Degree.js";
import { HoldingPatternFix } from "./HoldingPatternFix.js";

export type HoldingPatternEntry = "direct" | "parallel" | "offset";

/**
 * Represents a holding pattern for an aircraft.
 * This class encapsulates the properties and calculations needed to define a holding pattern,
 * including inbound heading, turn direction, DME distance, pattern altitude, and leg time.
 *
 * @see https://www.faa.gov/air_traffic/publications/atpubs/aip_html/part2_enr_section_1.5.html
 */
export class HoldingPattern {
  id: string;

  inboundHeading: number;
  inboundHeadingTrue: number;

  isLeftTurn: boolean;

  /**
   * In nautical miles
   * This is the distance from the holding fix to the DME fix.
   */
  dmeDistanceNm: number;

  /**
   * In nautical miles
   */
  dmeDistanceOutboundNm: number;

  /**
   * Indicates if the DME procedure is flown towards the VOR (false) or away from it (true).
   */
  dmeHoldingAwayFromNavaid: boolean;

  /**
   * The direction of the holding pattern.
   */
  holdingAreaDirection: number;
  holdingAreaDirectionTrue: number;

  /**
   * Pattern altitude in feet MSL.
   * This is the altitude at which the aircraft should hold.
   */
  patternAltitudeFt: number;
  patternSpeedKts: number;

  /**
   * In minutes
   * This is the time the aircraft should hold on each leg of the holding pattern.
   * It is used to calculate the distance flown during the holding pattern.
   */
  legTimeMin: number;

  /**
   * The fix around which the holding pattern is built.
   * This is the Navaid that the aircraft will hold at, or in case of a DME procedure, the DME fix.
   */
  holdingFix: Point;

  /**
   * The turn radius of the holding pattern in meters.
   */
  turnRadiusMeters: number;

  /**
   * The distance of the inbound leg in the holding pattern in meters.
   */
  legDistanceMeters: number;

  furtherClearanceInMin: number;

  constructor(configuration: Configuration, holdingNavAid: HoldingPatternFix, aircraft: AeroflyAircraft) {
    this.inboundHeading =
      configuration.inboundHeading === -1 ? Rand.getRandomInt(0, 359) : configuration.inboundHeading;
    this.inboundHeadingTrue = this.inboundHeading + holdingNavAid.mag_dec;
    this.isLeftTurn = Math.random() < configuration.leftHandPatternProbability;
    this.dmeDistanceNm =
      Math.random() < configuration.dmeProcedureProbability && ["VORTAC", "VOR/DME"].includes(holdingNavAid.type)
        ? Rand.getRandomInt(configuration.minimumDmeDistance, configuration.maximumDmeDistance)
        : 0;
    this.dmeHoldingAwayFromNavaid =
      this.dmeDistanceNm > 0 && Math.random() <= configuration.dmeHoldingAwayFromNavaidProbability;
    this.dmeDistanceOutboundNm =
      this.dmeDistanceNm > 0 ? this.dmeDistanceNm + (this.dmeHoldingAwayFromNavaid ? -4 : 4) : 0;
    this.patternAltitudeFt =
      Math.round(Rand.getRandomInt(configuration.minimumHoldingAltitude, configuration.maximumHoldingAltitude) / 100) *
      100;
    this.patternSpeedKts = Math.min(
      aircraft.cruiseSpeedKts + 10,
      this.#getMaxPatternSpeedKts(aircraft, this.patternAltitudeFt),
    );
    this.legTimeMin = this.#getLegTimeMin(this.patternAltitudeFt);
    this.id =
      this.dmeDistanceNm <= 0 ? holdingNavAid.id : `${holdingNavAid.id}+${String(this.dmeDistanceNm).padStart(2, "0")}`;
    this.holdingFix = this.#getHoldingFix(holdingNavAid);
    this.turnRadiusMeters = this.#getTurnRadiusMeters(this.patternSpeedKts);
    this.legDistanceMeters = this.#getLegDistanceMeters(this.patternSpeedKts, this.legTimeMin);
    this.holdingAreaDirection = Degree(this.inboundHeading + (this.dmeHoldingAwayFromNavaid ? 0 : 180));
    this.holdingAreaDirectionTrue = Degree(this.holdingAreaDirection + holdingNavAid.mag_dec);
    this.furtherClearanceInMin = Rand.getRandomInt(3, 5) * 5;
    //console.log(this);
  }

  #getHoldingFix(holdingNavAid: HoldingPatternFix): Point {
    return holdingNavAid.position.getPointBy(
      new Vector(this.dmeDistanceNm * Units.metersPerNauticalMile, Degree(this.inboundHeadingTrue + 180)),
    );
  }

  /**
   * @see https://www.code7700.com/holding.htm
   */
  #getMaxPatternSpeedKts(aircraft: AeroflyAircraft, patternAltitudeFt: number): number {
    // TODO: Turbulence: 280
    if (aircraft.tags.includes("helicopter")) {
      return patternAltitudeFt <= 6000 ? 100 : 170;
    }
    if (patternAltitudeFt <= 6000) {
      return 200; // FAA
    }
    if (patternAltitudeFt <= 14000) {
      return 230; // ICAO / FAA
    }
    if (patternAltitudeFt <= 20000) {
      return 240; // ICAO
    }
    return 265; // ICAO
  }

  /**
   * @see https://skybrary.aero/articles/holding-pattern
   * During entry and holding, pilots manually flying the aircraft are expected
   * to make all turns to achieve an average bank angle of at least 25˚ or
   * a rate of turn of 3˚ per second, whichever requires the lesser bank.
   */
  #getTurnRadiusMeters(patternSpeedKts: number): number {
    return (patternSpeedKts / (20 * Math.PI * 3)) * Units.metersPerNauticalMile; // turn radius at 3 degrees per second
    //return (patternSpeedKts ** 2 / (11.26 * Math.tan(25 * (Math.PI / 180)))) * Units.metersPerNauticalMile; // turn radius at 25 degrees bank angle
  }

  #getLegDistanceMeters(patternSpeedKts: number, legTimeMin: number): number {
    if (this.dmeDistanceOutboundNm !== 0) {
      return Math.abs(
        Math.sqrt((this.dmeDistanceOutboundNm * Units.metersPerNauticalMile) ** 2 - (this.turnRadiusMeters * 2) ** 2) -
          this.dmeDistanceNm * Units.metersPerNauticalMile,
      );
    }

    return (patternSpeedKts / 60) * legTimeMin * Units.metersPerNauticalMile;
  }

  #getLegTimeMin(patternAltitudeFt: number): number {
    return patternAltitudeFt > 14000 ? 1.5 : 1;
  }

  getEntry(bearing: number): HoldingPatternEntry {
    const delta = degreeDifference(this.holdingAreaDirectionTrue, bearing) * (this.isLeftTurn ? -1 : 1);
    if (delta >= 110) {
      return "offset";
    }
    if (delta <= -70) {
      return "parallel";
    }
    return "direct";
  }

  getFurtherClearance(date: Date): Date {
    const furtherClearanceInMs = this.furtherClearanceInMin * 60 * 1000;
    return new Date(date.getTime() + furtherClearanceInMs);
  }
}
