import geojson from 'geojson';
import * as geolib from 'geolib';
import { TerminatorsAF } from './terminators/AF.ts';
import { TerminatorsCA } from './terminators/CA.ts';
import { TerminatorsCD } from './terminators/CD.ts';
import { TerminatorsCF } from './terminators/CF.ts';
import { TerminatorsCI } from './terminators/CI.ts';
import { TerminatorsCR } from './terminators/CR.ts';
import { TerminatorsDF } from './terminators/DF.ts';
import { TerminatorsFA } from './terminators/FA.ts';
import { TerminatorsFC } from './terminators/FC.ts';
import { TerminatorsFD } from './terminators/FD.ts';
import { TerminatorsFM } from './terminators/FM.ts';
import { TerminatorsIF } from './terminators/IF.ts';
import { TerminatorsRF } from './terminators/RF.ts';
import { TerminatorsTF } from './terminators/TF.ts';
import { TerminatorsVA } from './terminators/VA.ts';
import { TerminatorsVD } from './terminators/VD.ts';
import { TerminatorsVI } from './terminators/VI.ts';
import { TerminatorsVM } from './terminators/VM.ts';
import { TerminatorsVR } from './terminators/VR.ts';
import './utils/extensions.ts';

/*
Runway IDs for LIED
26156 - 34L
26157 - 34R
*/

class Parser {
  private static _instance: Parser;

  private _waypoints: Waypoint[];
  private _runways: Runway[];
  private _terminals: Terminal[];

  public static AC_SPEED = 250;
  public static AC_BANK = 30;
  public static AC_VS = 1400;

  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 get terminals() {
    return this._terminals;
  }
  public get waypoints() {
    return this._waypoints;
  }
  public get runways() {
    return this._runways;
  }

  public parse = async (terminalID: number) => {
    // 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
    let runway = this.runways.find(({ ID }) => ID === terminal.RwyID);
    if (!runway) {
      let id = 26156;
      if (typeof prompt !== 'undefined')
        //  throw new Error("Prompt not defined, cannot continue");

        id = Number.parseInt(prompt('Runway ID') ?? '');
      runway = this.runways.find(({ ID }) => ID === id);
      if (!runway) throw new Error('Procedure links to non existent Runway');
    }
    // Load procedure
    const procedures = (await (await fetch(`NavData/TermID_${terminalID}.json`)).json()) as TerminalEntry[];
    // Split into transitions
    const transitions = new Set(procedures.map((proc) => proc.Transition));

    const output: object[] = [];

    // Main
    transitions.forEach((transition) => {
      // 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],
          }
        );
      };

      /**
       * @param fix New fix
       * @param line New line
       * @param options Options for line rendering
       */
      const update = (fix?: NavFix, line?: LineSegment[], options?: Record<string, unknown>) => {
        if (fix) navFixes.push(fix);
        if (line) {
          lineSegments.push({ line, ...options });
          updateLastCourse(line);
        }
      };

      // 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;

      const procedure = procedures.filter((proc) => proc.Transition === transition);
      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);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'CA': {
            const [fixToAdd, lineToAdd] = TerminatorsCA(leg as CATerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'CD': {
            const [fixToAdd, lineToAdd] = TerminatorsCD(leg as CDTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'CF': {
            const [fixToAdd, lineToAdd] = TerminatorsCF(leg as CFTerminalEntry, previousFix, lastCourse, waypoint);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'CI': {
            const [fixToAdd, lineToAdd] = TerminatorsCI(
              leg as CITerminalEntry,
              procedure[index + 1],
              previousFix,
              lastCourse
            );
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'CR': {
            const [fixToAdd, lineToAdd] = TerminatorsCR(leg as CRTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'DF': {
            const [fixToAdd, lineToAdd] = TerminatorsDF(leg as DFTerminalEntry, previousFix, lastCourse, waypoint);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'FA': {
            const [fixToAdd, lineToAdd] = TerminatorsFA(leg as FATerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'FC': {
            const [fixToAdd, lineToAdd] = TerminatorsFC(leg as FCTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'FD': {
            const [fixToAdd, lineToAdd] = TerminatorsFD(leg as FDTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'FM': {
            const [fixToAdd, lineToAdd] = TerminatorsFM(leg as FMTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd, { isManual: true });
            break;
          }
          case 'HA':
          case 'HF':
          case 'HM':
            console.error('Unknown TrackCode', leg.TrackCode);
            break;
          case 'IF': {
            const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint);
            navFixes.length = 0;
            navFixes.push(fixToAdd);
            break;
          }
          case 'PI':
            console.error('Unknown TrackCode', leg.TrackCode);
            break;
          case 'RF': {
            const [fixToAdd, lineToAdd] = TerminatorsRF(
              leg as RFTerminalEntry,
              procedure[index + 1],
              previousFix,
              lastCourse,
              waypoint
            );
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'TF': {
            const [fixToAdd, lineToAdd] = TerminatorsTF(leg as TFTerminalEntry, previousFix, lastCourse, waypoint);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'VA': {
            const [fixToAdd, lineToAdd] = TerminatorsVA(leg as VATerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'VD': {
            const [fixToAdd, lineToAdd] = TerminatorsVD(leg as VDTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'VI': {
            const [fixToAdd, lineToAdd] = TerminatorsVI(
              leg as VITerminalEntry,
              procedure[index + 1],
              previousFix,
              lastCourse
            );
            update(fixToAdd, lineToAdd);
            break;
          }
          case 'VM': {
            const [fixToAdd, lineToAdd] = TerminatorsVM(leg as VMTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd, { isManual: true });
            break;
          }
          case 'VR': {
            const [fixToAdd, lineToAdd] = TerminatorsVR(leg as VRTerminalEntry, previousFix, lastCourse);
            update(fixToAdd, lineToAdd);
            break;
          }
          default:
            console.error('Unknown TrackCode', leg.TrackCode);
            break;
        }
      }

      output.push(
        geojson.parse([...navFixes, ...lineSegments], {
          LineString: 'line',
          Point: ['latitude', 'longitude'],
        })
      );
    });

    return output;
  };
}

export default Parser;
