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

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

class Parser {
  private static _instance: Parser;

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

  private _procedures: TerminalEntry[] = [];

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

  private constructor(airports: Airport[], waypoints: Waypoint[], runways: Runway[], terminals: Terminal[]) {
    this._airports = airports;
    this._waypoints = waypoints;
    this._runways = runways;
    this._terminals = terminals;
  }

  public static instance = async () => {
    if (!Parser._instance) {
      const airports = await (await fetch('NavData/Airports.json')).json();
      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(airports, waypoints, runways, terminals);
    }

    return Parser._instance;
  };

  public get airports() {
    return this._airports;
  }
  public get terminals() {
    return this._terminals;
  }
  public get waypoints() {
    return this._waypoints;
  }
  public get runways() {
    return this._runways;
  }

  public get procedures() {
    return this._procedures;
  }

  public loadTerminal = async (terminalID: number) => {
    this._procedures = (await (
      await fetch(`NavData/ProcedureLegs/TermID_${terminalID}.json`)
    ).json()) as TerminalEntry[];
  };

  public parse = (runway: Runway, transition: string) => {
    // Private functions
    /**
     * @param line Line segments
     */
    const updateLastCourse = (line: LineSegment[]) => {
      lastCourse = 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 legOptions = { isMAP: false };
    const procedure = this._procedures.filter(
      ({ Transition }) => !Transition || Transition === transition || Transition === 'ALL'
    );

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

      switch (leg.TrackCode) {
        case 'AF': {
          const [fixToAdd, lineToAdd] = TerminatorsAF(leg as AFTerminalEntry, previousFix, waypoint);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'CA': {
          const [fixToAdd, lineToAdd] = TerminatorsCA(leg as CATerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'CD': {
          const [fixToAdd, lineToAdd] = TerminatorsCD(leg as CDTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'CF': {
          const [fixToAdd, lineToAdd] = TerminatorsCF(
            leg as CFTerminalEntry,
            { ...previousFix }, // COPY
            lastCourse,
            waypoint
          );
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'CI': {
          const [fixToAdd, lineToAdd] = TerminatorsCI(
            leg as CITerminalEntry,
            procedure[index + 1],
            { ...previousFix }, // COPY
            lastCourse
          );
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'CR': {
          const [fixToAdd, lineToAdd] = TerminatorsCR(leg as CRTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'DF': {
          const [fixToAdd, lineToAdd] = TerminatorsDF(leg as DFTerminalEntry, previousFix, lastCourse, waypoint);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'FA': {
          const [fixToAdd, lineToAdd] = TerminatorsFA(leg as FATerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'FC': {
          const [fixToAdd, lineToAdd] = TerminatorsFC(leg as FCTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'FD': {
          const [fixToAdd, lineToAdd] = TerminatorsFD(leg as FDTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'FM': {
          const [fixToAdd, lineToAdd] = TerminatorsFM(leg as FMTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, { isManual: true, ...legOptions });
          // Make overfly
          navFixes.at(-1)!.isFlyOver = true;
          break;
        }
        case 'HA':
        case 'HF':
        case 'HM':
          console.error('Unknown TrackCode', leg.TrackCode);
          break;
        case 'IF': {
          const fixToAdd = TerminatorsIF(leg as RFTerminalEntry, waypoint);
          // Only Runway, replace
          if (navFixes.length <= 1) {
            navFixes.push(fixToAdd);
            lastCourse = -1;
          }
          break;
        }
        case 'PI':
          console.error('Unknown TrackCode', leg.TrackCode);
          break;
        case 'RF': {
          const [fixToAdd, lineToAdd] = TerminatorsRF(leg as RFTerminalEntry, previousFix, lastCourse, waypoint);
          if (fixToAdd) {
            navFixes.push(fixToAdd);
            lastCourse = (leg as RFTerminalEntry).Course?.toTrue(fixToAdd);
          }
          if (lineToAdd) {
            lineSegments.push({ line: lineToAdd, ...legOptions });
          }
          break;
        }
        case 'TF': {
          const [fixToAdd, lineToAdd] = TerminatorsTF(
            leg as TFTerminalEntry,
            { ...previousFix }, // COPY
            lastCourse,
            waypoint
          );
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'VA': {
          const [fixToAdd, lineToAdd] = TerminatorsVA(leg as VATerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'VD': {
          const [fixToAdd, lineToAdd] = TerminatorsVD(leg as VDTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'VI': {
          const [fixToAdd, lineToAdd] = TerminatorsVI(
            leg as VITerminalEntry,
            procedure[index + 1],
            { ...previousFix }, // COPY
            lastCourse
          );
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        case 'VM': {
          const [fixToAdd, lineToAdd] = TerminatorsVM(leg as VMTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, { isManual: true, ...legOptions });
          // Make overfly
          navFixes.at(-1)!.isFlyOver = true;
          break;
        }
        case 'VR': {
          const [fixToAdd, lineToAdd] = TerminatorsVR(leg as VRTerminalEntry, previousFix, lastCourse);
          update(fixToAdd, lineToAdd, legOptions);
          break;
        }
        default:
          console.error('Unknown TrackCode', leg.TrackCode);
          break;
      }
    }

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

export default Parser;
