// SPDX-FileCopyrightText: 2012 James Turner
// SPDX-License-Identifier: GPL-2.0-or-later

#include "config.h"

#include <algorithm>

#include "LevelDXML.hxx"

#include <simgear/structure/exception.hxx>
#include <simgear/misc/sg_path.hxx>
#include <simgear/misc/strutils.hxx>

#include <Navaids/waypoint.hxx>
#include <Airports/airport.hxx>
#include <Navaids/route.hxx>

using std::string;
using std::vector;

namespace flightgear
{

NavdataVisitor::NavdataVisitor(FGAirport* aApt, const SGPath& aPath):
  _airport(aApt),
  _path(aPath),
  _sid(NULL),
  _star(NULL),
  _approach(NULL),
  _transition(NULL),
  _procedure(NULL)
{
}

void NavdataVisitor::startXML()
{
}

void NavdataVisitor::endXML()
{
}

void NavdataVisitor::startElement(const char* name, const XMLAttributes &atts)
{
  _text.clear();
  string tag(name);
  if (tag == "Airport") {
    string icao(atts.getValue("ICAOcode"));
    if (_airport->ident() != icao) {
      throw sg_format_exception("Airport and ICAO mismatch", icao, _path.utf8Str());
    }
  } else if (tag == "Sid") {
    string ident(atts.getValue("Name"));
    _sid = new SID(ident, _airport);
    _procedure = _sid;
    _waypoints.clear();
    processRunways(_sid, atts);
  } else if (tag == "Star") {
    string ident(atts.getValue("Name"));
    _star = new STAR(ident, _airport);
    _procedure = _star;
    _waypoints.clear();
    processRunways(_star, atts);
  } else if ((tag == "Sid_Waypoint") ||
      (tag == "App_Waypoint") ||
      (tag == "Star_Waypoint") ||
      (tag == "AppTr_Waypoint") ||
      (tag == "SidTr_Waypoint") ||
      (tag == "RwyTr_Waypoint"))
  {
    // reset waypoint data
    _speed = 0.0;
    _altRestrict = RESTRICT_NONE;
    _altitude = 0.0;
    _overflightWaypt = false; // default to Fly-by
    _courseFlag = false; // default to heading
  } else if (tag == "Approach") {
    _ident = atts.getValue("Name");
    _waypoints.clear();
    ProcedureType ty = PROCEDURE_APPROACH_RNAV;

    // if the name of the procedure starts with the correct name, set the procedure type
    if (_ident.find("ILS") == 0) {
        ty = PROCEDURE_APPROACH_ILS;
    } else if (_ident.find("VOR") == 0 || _ident.find("VDM") == 0) {
        ty = PROCEDURE_APPROACH_VOR;
    } else if (_ident.find("NDB") == 0 || _ident.find("NDM") == 0) {
        ty = PROCEDURE_APPROACH_NDB;
    }

    _approach = new Approach(_ident, ty);
    _procedure = _approach;
  } else if ((tag == "Sid_Transition") ||
             (tag == "App_Transition") ||
             (tag == "Star_Transition")) {
      _transIdent = atts.getValue("Name");
      _transition = new Transition(_transIdent, PROCEDURE_TRANSITION, _procedure);
      _transWaypts.clear();
  } else if (tag == "RunwayTransition") {
      _transIdent = atts.getValue("Runway");
      if (!_airport->hasRunwayWithIdent(_transIdent)) {
          _transIdent.clear();
          _transition = nullptr;
      } else {
          _transition = new Transition(_transIdent, PROCEDURE_RUNWAY_TRANSITION, _procedure);
          _transWaypts.clear();
      }
  } else {
      // nothing here, we warn on unrecognized in endElement
  }
}

void NavdataVisitor::processRunways(ArrivalDeparture* aProc, const XMLAttributes &atts)
{
  string v("All");
  if (atts.hasAttribute("Runways")) {
    v = atts.getValue("Runways");
  }

  if (v == "All") {
    for (unsigned int r=0; r<_airport->numRunways(); ++r) {
      aProc->addRunway(_airport->getRunwayByIndex(r));
    }
    return;
  }

  auto rwys = simgear::strutils::split_on_any_of(v, " ,");
  for (auto rwy : rwys) {
      if (!_airport->hasRunwayWithIdent(rwy)) {
          const auto renamed = _airport->findAPTRunwayForNewName(rwy);
          if (!renamed.empty()) {
              rwy = renamed;
          } else {
              SG_LOG(SG_NAVAID, SG_DEV_WARN, "Procedure file " << _path << " references unknown airport runway:" << rwy);
              continue;
          }
      }

    aProc->addRunway(_airport->getRunwayByIdent(rwy));
  }
}

void NavdataVisitor::endElement(const char* name)
{
  string tag(name);
  if ((tag == "Sid_Waypoint") ||
      (tag == "App_Waypoint") ||
      (tag == "Star_Waypoint"))
  {
    _waypoints.push_back(buildWaypoint(_procedure));
  } else if ((tag == "AppTr_Waypoint") ||
             (tag == "SidTr_Waypoint") ||
             (tag == "RwyTr_Waypoint") ||
             (tag == "StarTr_Waypoint")) {
      _transWaypts.push_back(buildWaypoint(_transition));
  } else if (tag == "Sid_Transition") {
      assert(_sid);
      // SID waypoints are stored backwards, to share code with STARs
      std::reverse(_transWaypts.begin(), _transWaypts.end());
      _transition->setPrimary(_transWaypts);
      _sid->addTransition(_transition);
  } else if (tag == "Star_Transition") {
      assert(_star);
      _transition->setPrimary(_transWaypts);
      _star->addTransition(_transition);
  } else if (tag == "App_Transition") {
      assert(_approach);
      _transition->setPrimary(_transWaypts);
      _approach->addTransition(_transition);
  } else if (tag == "RunwayTransition") {
      if (!_transition) {
          // transition was skipped for some reason
          return;
      }

    ArrivalDeparture* ad;
    if (_sid) {
      // SID waypoints are stored backwards, to share code with STARs
      std::reverse(_transWaypts.begin(), _transWaypts.end());
      ad = _sid;
    } else {
      ad = _star;
    }

    _transition->setPrimary(_transWaypts);
    FGRunwayRef rwy = _airport->getRunwayByIdent(_transIdent);
    ad->addRunwayTransition(rwy, _transition);
  } else if (tag == "Approach") {
      finishApproach();
  } else if (tag == "Sid") {
      finishSid();
  } else if (tag == "Star") {
      finishStar();
  } else if (tag == "Longitude") {
      _longitude = atof(_text.c_str());
  } else if (tag == "Latitude") {
      _latitude = atof(_text.c_str());
  } else if (tag == "Name") {
      _wayptName = _text;
  } else if (tag == "Type") {
      _wayptType = _text;
  } else if (tag == "Speed") {
      _speed = atoi(_text.c_str());
  } else if (tag == "Altitude") {
      _altitude = atof(_text.c_str());
  } else if (tag == "AltitudeRestriction") {
      _altRestrict = restrictionFromString(_text);
  } else if (tag == "Hld_Rad_or_Inbd") {
      if (_text == "Inbd") {
          _holdRadial = -1.0;
      }
  } else if (tag == "Hld_Time_or_Dist") {
      _holdDistance = (_text == "Dist");
  } else if (tag == "Hld_Rad_value") {
      _holdRadial = atof(_text.c_str());
  } else if (tag == "Hld_Turn") {
      _holdRighthanded = (_text == "Right");
  } else if (tag == "Hld_td_value") {
      _holdTD = atof(_text.c_str());
  } else if (tag == "Hdg_Crs") {
      _courseFlag = atoi(_text.c_str());
  } else if (tag == "Hdg_Crs_value") {
      _courseOrHeading = atof(_text.c_str());
  } else if (tag == "DMEtoIntercept") {
      _dmeDistance = atof(_text.c_str());
  } else if (tag == "RadialtoIntercept") {
      _radial = atof(_text.c_str());
  } else if (tag == "Flytype") {
      // values are 'Fly-by' and 'Fly-over'
      _overflightWaypt = (_text == "Fly-over");
  } else if ((tag == "AltitudeCons") ||
             (tag == "BankLimit") ||
             (tag == "Sp_Turn") ||
             (tag == "Airport") ||
             (tag == "ProceduresDB")) {
      // ignored but don't warn
  } else {
      SG_LOG(SG_IO, SG_INFO, "unrecognized Level-D XML element:" << tag);
  }
}

Waypt* NavdataVisitor::buildWaypoint(RouteBase* owner)
{
  Waypt* wp = NULL;
  if (_wayptType == "Normal") {
    // new LatLonWaypoint
    SGGeod pos(SGGeod::fromDeg(_longitude, _latitude));
    wp = new BasicWaypt(pos, _wayptName, owner);
  } else if (_wayptType == "Runway") {
    string ident = _wayptName.substr(2);
    if (!_airport->hasRunwayWithIdent(ident)) {
        const auto renamed = _airport->findAPTRunwayForNewName(ident);
        if (renamed.empty()) {
            SG_LOG(SG_NAVAID, SG_DEV_WARN, "Missing runway " << ident << " reading " << _path);
            SGGeod pos(SGGeod::fromDeg(_longitude, _latitude));
            // fall back to a basic WP
            wp = new BasicWaypt(pos, _wayptName, owner);
            ident.clear();
        } else {
            ident = renamed;
        }
    }

    if (!ident.empty()) {
        FGRunwayRef rwy = _airport->getRunwayByIdent(ident);
        wp = new RunwayWaypt(rwy, owner);
    }
  } else if (_wayptType == "Hold") {
    SGGeod pos(SGGeod::fromDeg(_longitude, _latitude));
    Hold* h = new Hold(pos, _wayptName, owner);
    wp = h;
    if (_holdRighthanded) {
      h->setRightHanded();
    } else {
      h->setLeftHanded();
    }

    if (_holdDistance) {
      h->setHoldDistance(_holdTD);
    } else {
      h->setHoldTime(_holdTD * 60.0);
    }

    if (_holdRadial >= 0.0) {
      h->setHoldRadial(_holdRadial);
    }
  } else if (_wayptType == "Vectors") {
    wp = new ATCVectors(owner, _airport);
  } else if ((_wayptType == "Intc") || (_wayptType == "VorRadialIntc")) {
    SGGeod pos(SGGeod::fromDeg(_longitude, _latitude));
    wp = new RadialIntercept(owner, _wayptName, pos, _courseOrHeading, _radial);
  } else if (_wayptType == "DmeIntc") {
    SGGeod pos(SGGeod::fromDeg(_longitude, _latitude));
    wp = new DMEIntercept(owner, _wayptName, pos, _courseOrHeading, _dmeDistance);
  } else if (_wayptType == "ConstHdgtoAlt") {
    wp = new HeadingToAltitude(owner, _wayptName, _courseOrHeading);
  } else if (_wayptType == "PBD") {
    SGGeod pos(SGGeod::fromDeg(_longitude, _latitude)), pos2;
    double az2;
    SGGeodesy::direct(pos, _courseOrHeading, _dmeDistance, pos2, az2);
    wp = new BasicWaypt(pos2, _wayptName, owner);
  } else {
    SG_LOG(SG_NAVAID, SG_ALERT, "implement waypoint type:" << _wayptType);
    throw sg_format_exception("Unrecognized waypt type", _wayptType);
  }

  assert(wp);
  if ((_altitude > 0.0) && (_altRestrict != RESTRICT_NONE)) {
    wp->setAltitude(_altitude,_altRestrict);
  }

  if (_speed > 0.0) {
    wp->setSpeed(_speed, RESTRICT_AT); // or _BELOW?
  }

  if (_overflightWaypt) {
    wp->setFlag(WPT_OVERFLIGHT);
  }

  return wp;
}

void NavdataVisitor::finishApproach()
{
  WayptVec::iterator it;
  FGRunwayRef rwy;

// find the runway node
  for (it = _waypoints.begin(); it != _waypoints.end(); ++it) {
    FGPositionedRef navid = (*it)->source();
    if (!navid) {
      continue;
    }

    if (navid->type() == FGPositioned::RUNWAY) {
      rwy = (FGRunway*) navid.get();
      break;
    }
  }

  if (!rwy) {
      SG_LOG(SG_NAVAID, SG_DEV_WARN, "Parsing:" << _path << " found approach without a runway:" << _ident);
      delete _approach;
      _approach = nullptr;
      return;
  }

  WayptVec primary(_waypoints.begin(), it);
  // erase all points up to and including the runway, to leave only the
  // missed segments
  _waypoints.erase(_waypoints.begin(), ++it);

  _approach->setRunway(rwy);
  _approach->setPrimaryAndMissed(primary, _waypoints);
  _airport->addApproach(_approach);
  _approach = NULL;
}

void NavdataVisitor::finishSid()
{
  // reverse order, because that's how we deal with commonality between
  // STARs and SIDs. SID::route undoes  this
  std::reverse(_waypoints.begin(), _waypoints.end());
  _sid->setCommon(_waypoints);
  _airport->addSID(_sid);
  _sid = NULL;
}

void NavdataVisitor::finishStar()
{
  _star->setCommon(_waypoints);
  _airport->addSTAR(_star);
  _star = NULL;
}

void NavdataVisitor::data (const char * s, int len)
{
  _text += string(s, len);
}


void NavdataVisitor::pi (const char * target, const char * data) {
  //cout << "Processing instruction " << target << ' ' << data << endl;
}

void NavdataVisitor::warning (const char * message, int line, int column) {
  SG_LOG(SG_NAVAID, SG_WARN, "Warning: " << message << " (" << line << ',' << column << ')');
}

void NavdataVisitor::error (const char * message, int line, int column) {
  SG_LOG(SG_NAVAID, SG_ALERT, "Error: " << message << " (" << line << ',' << column << ')');
}

}
