import "./utils/extensions.ts";
import * as geolib from "geolib";
import geojson from "geojson";
import { TerminatorsCF } from "./terminators/CF.ts";
import { TerminatorsAF } from "./terminators/AF.ts";
import { TerminatorsCR } from "./terminators/CR.ts";
import { TerminatorsVM } from "./terminators/VM.ts";
import { TerminatorsFM } from "./terminators/FM.ts";
import { TerminatorsCI } from "./terminators/CI.ts";
import { TerminatorsVA } from "./terminators/VA.ts";
import { TerminatorsTF } from "./terminators/TF.ts";
import { TerminatorsVI } from "./terminators/VI.ts";
import { TerminatorsVD } from "./terminators/VD.ts";
import { TerminatorsRF } from "./terminators/RF.ts";

class Parser {
  private static _instance: Parser;

  private waypoints: Waypoint[];
  private runways: Runway[];
  private terminals: Terminal[];

  public static AC_SPEED = 250;
  public static AC_VS = 1500;

  private constructor(
    waypoints: Waypoint[],
    runways: Runway[],
    terminals: Terminal[]
  ) {
    this.waypoints = waypoints;
    this.runways = runways;
    this.terminals = terminals;
  }

  public static instance = async () => {
    if (!Parser._instance) {
      const waypoints = await (await fetch("NavData/Waypoints.json")).json();
      const runways = await (await fetch("NavData/Runways.json")).json();
      const terminals = await (await fetch("NavData/Terminals.json")).json();

      Parser._instance = new Parser(waypoints, runways, terminals);
    }

    return Parser._instance;
  };

  public parse = async (terminalID: number) => {
    // Private functions
    /**
     * @param line Line segments
     */
    const updateLastCourse = (line: LineSegment[]) => {
      lastCourse = geolib.getGreatCircleBearing(
        {
          latitude: line.at(-2)![1],
          longitude: line.at(-2)![0],
        },
        {
          latitude: line.at(-1)![1],
          longitude: line.at(-1)![0],
        }
      );
    };

    // Get Procedure main
    const terminal = this.terminals.find(({ ID }) => ID === terminalID);
    if (!terminal) throw new Error("Procedure does not exists");
    // Get runway this procedure is for
    const runway = this.runways.find(({ ID }) => ID === terminal.RwyID);
    if (!runway) throw new Error("Procedure links to non existent Runway");
    // Load procedure
    const procedure = (await (
      await fetch(`NavData/TermID_${terminalID}.json`)
    ).json()) as TerminalEntry[];

    // Output variables
    const navFixes: NavFix[] = [];
    const lineSegments: { line: LineSegment[]; [x: string]: unknown }[] = [];

    // Initials
    navFixes.push({
      latitude: runway.Latitude,
      longitude: runway.Longitude,
      altitude: runway.Elevation,
      speed: 0,
      name: runway.Ident,
    });
    let lastCourse = runway.TrueHeading;

    // Main
    for (let index = 0; index < procedure.length; index++) {
      const leg = procedure[index];
      const previousFix = navFixes.at(-1)!;
      const waypoint = this.waypoints.filter(({ ID }) => ID === leg.WptID)[0];

      switch (leg.TrackCode) {
        case "AF": {
          const [fixToAdd, lineToAdd] = TerminatorsAF(
            leg as AFTerminalEntry,
            previousFix,
            waypoint
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "CA":
        case "CD":
          console.error("Unknown TrackCode", leg.TrackCode);
          break;
        case "CF": {
          const [fixToAdd, lineToAdd] = TerminatorsCF(
            leg as CFTerminalEntry,
            previousFix,
            lastCourse,
            waypoint
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "CI": {
          const [fixToAdd, lineToAdd] = TerminatorsCI(
            leg as CITerminalEntry,
            procedure[index + 1],
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "CR": {
          const [fixToAdd, lineToAdd] = TerminatorsCR(
            leg as CRTerminalEntry,
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "DF":
        case "FA":
        case "FC":
        case "FD":
          console.error("Unknown TrackCode", leg.TrackCode);
          break;
        case "FM": {
          const [fixToAdd, lineToAdd] = TerminatorsFM(
            leg as FMTerminalEntry,
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd, isManual: true });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "HA":
        case "HF":
        case "HM":
        case "IF":
        case "PI":
          console.error("Unknown TrackCode", leg.TrackCode);
          break;
        case "RF": {
          const [fixToAdd, lineToAdd] = TerminatorsRF(
            leg as RFTerminalEntry,
            procedure[index + 1],
            previousFix,
            lastCourse,
            waypoint
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "TF": {
          const [fixToAdd, lineToAdd] = TerminatorsTF(
            leg as TFTerminalEntry,
            previousFix,
            lastCourse,
            waypoint
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "VA": {
          const [fixToAdd, lineToAdd] = TerminatorsVA(
            leg as VATerminalEntry,
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "VD": {
          const [fixToAdd, lineToAdd] = TerminatorsVD(
            leg as VDTerminalEntry,
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "VI": {
          const [fixToAdd, lineToAdd] = TerminatorsVI(
            leg as VITerminalEntry,
            procedure[index + 1],
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "VM": {
          const [fixToAdd, lineToAdd] = TerminatorsVM(
            leg as VMTerminalEntry,
            previousFix,
            lastCourse
          );
          if (fixToAdd) navFixes.push(fixToAdd);
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd, isManual: true });
            updateLastCourse(lineToAdd);
          }
          break;
        }
        case "VR":
        default:
          console.error("Unknown TrackCode", leg.TrackCode);
          break;
      }
    }

    return geojson.parse([...navFixes, ...lineSegments], {
      LineString: "line",
      Point: ["latitude", "longitude"],
    });
  };
}

export default Parser;
