// airways.cxx - storage of airways network, and routing between nodes
// Written by James Turner, started 2009.
//
// SPDX-FileCopyrightText: 2009 James Turner
// SPDX-License-Identifier: GPL-2.0-or-later

#include "config.h"
#include "simgear/debug/debug_types.h"

#include "airways.hxx"

#include <tuple>
#include <algorithm>
#include <set>

#include <simgear/sg_inlines.h>
#include <simgear/structure/exception.hxx>
#include <simgear/io/iostreams/sgstream.hxx>
#include <simgear/misc/sg_path.hxx>

#include <Main/globals.hxx>
#include <Navaids/positioned.hxx>
#include <Navaids/waypoint.hxx>
#include <Navaids/NavDataCache.hxx>

using std::make_pair;
using std::string;
using std::set;
using std::vector;

//#define DEBUG_AWY_SEARCH 1

namespace flightgear
{

static std::vector<AirwayRef> static_airwaysCache;
typedef SGSharedPtr<FGPositioned> FGPositionedRef;

//////////////////////////////////////////////////////////////////////////////

class AStarOpenNode : public SGReferenced
{
public:
    AStarOpenNode(FGPositionedRef aNode, double aLegDist,
                  int aAirway,
                  FGPositionedRef aDest, AStarOpenNode* aPrev) : node(aNode),
                                                                 previous(aPrev),
                                                                 airway(aAirway)
    {
        distanceFromStart = aLegDist;
        if (previous) {
            distanceFromStart += previous->distanceFromStart;
        }

        directDistanceToDestination = SGGeodesy::distanceM(node->geod(), aDest->geod());
    }

  virtual ~AStarOpenNode()
  {
  }

  FGPositionedRef node;
  SGSharedPtr<AStarOpenNode> previous;
  int airway;
  double distanceFromStart; // aka 'g(x)'
  double directDistanceToDestination; // aka 'h(x)'

  /**
	 * aka 'f(x)'
	 */
	double totalCost() const {
		return distanceFromStart + directDistanceToDestination;
	}
};

using AStarOpenNodeRef = SGSharedPtr<AStarOpenNode>;

////////////////////////////////////////////////////////////////////////////

Airway::Network* Airway::lowLevel()
{
  static Network* static_lowLevel = nullptr;

  if (!static_lowLevel) {
      static_lowLevel = new Network;
      static_lowLevel->_networkID = Airway::LowLevel;
  }

  return static_lowLevel;
}

Airway::Network* Airway::highLevel()
{
  static Network* static_highLevel = nullptr;
  if (!static_highLevel) {
    static_highLevel = new Network;
      static_highLevel->_networkID = Airway::HighLevel;
  }

  return static_highLevel;
}

Airway::Airway(const std::string& aIdent,
               const Level level,
               int dbId,
               int aTop, int aBottom) :
  _ident(aIdent),
  _level(level),
  _cacheId(dbId),
  _topAltitudeFt(aTop),
  _bottomAltitudeFt(aBottom)
{
    assert((level == HighLevel) || (level == LowLevel));
    static_airwaysCache.push_back(this);
}

void Airway::loadAWYDat(const SGPath& path)
{
  std::string identStart, identEnd, name;
  double latStart, lonStart, latEnd, lonEnd;
  int type, base, top;

  sg_gzifstream in( path );
  if ( !in.is_open() ) {
    SG_LOG( SG_NAVAID, SG_ALERT, "Cannot open file: " << path );
    throw sg_io_exception("Could not open airways data", path);
  }
// toss the first two lines of the file
  in >> skipeol;
  in >> skipeol;

// read in each remaining line of the file
  while (!in.eof()) {
    in >> identStart;

    if (identStart == "99") {
      break;
    }

    in >> latStart >> lonStart >> identEnd >> latEnd >> lonEnd >> type >> base >> top >> name;
    in >> skipeol;

    // type = 1; low-altitude (victor)
    // type = 2; high-altitude (jet)
    Network* net = (type == 1) ? lowLevel() : highLevel();

    SGGeod startPos(SGGeod::fromDeg(lonStart, latStart)),
      endPos(SGGeod::fromDeg(lonEnd, latEnd));

    if (type == 1) {

    } else if (type == 2) {

    } else {
        SG_LOG(SG_NAVAID, SG_DEV_WARN, "unknown airway type:" << type << " for " << name);
        continue;
    }

    auto pieces = simgear::strutils::split(name, "-");
    for (auto p : pieces) {
        int awy = net->findAirway(p);
        net->addEdge(awy, startPos, identStart, endPos, identEnd);
    }
  } // of file line iteration
}

WayptVec::const_iterator Airway::find(WayptRef wpt) const
{
    assert(!_elements.empty());
    if (wpt->type() == "via") {
      // map vias to their end navaid / fix, so chaining them
      // together works. (Temporary waypoint is discarded after search)
      wpt = new NavaidWaypoint(wpt->source(), wpt->owner());
    }

    return std::find_if(_elements.begin(), _elements.end(),
                        [wpt] (const WayptRef& w)
                        {
                            if (!w) return false;
                            return w->matches(wpt);
                        });
}

bool Airway::canVia(const WayptRef& from, const WayptRef& to) const
{
    loadWaypoints();

    auto fit = find(from);
    auto tit = find(to);

    if ((fit == _elements.end()) || (tit == _elements.end())) {
        return false;
    }

    if (fit < tit) {
        // forward progression
        for (++fit; fit != tit; ++fit) {
            if (*fit == nullptr) {
                // traversed an airway discontinuity
                return false;
            }
        }
    } else {
        // reverse progression
        for (--fit; fit != tit; --fit) {
            if (*fit == nullptr) {
                // traversed an airway discontinuity
                return false;
            }
        }
    }

    return true;
}

WayptVec Airway::via(const WayptRef& from, const WayptRef& to) const
{
    loadWaypoints();

    WayptVec v;
    auto fit = find(from);
    auto tit = find(to);

    if ((fit == _elements.end()) || (tit == _elements.end())) {
        throw sg_exception("bad VIA transition points");
    }

    if (fit == tit) {
        // will cause duplicate point but that seems better than
        // return an empty
        v.push_back(*tit);
        return v;
    }

    // establish the ordering of the transitions, i.e are we moving forward or
    // backward along the airway.
    if (fit < tit) {
        // forward progression
        for (++fit; fit != tit; ++fit) {
            v.push_back(*fit);
        }
    } else {
        // reverse progression
        for (--fit; fit != tit; --fit) {
            v.push_back(*fit);
        }
    }

    v.push_back(*tit);
    return v;
}

bool Airway::containsNavaid(const FGPositionedRef &navaid) const
{
    if (!navaid)
        return false;

    loadWaypoints();
    auto it = std::find_if(_elements.begin(), _elements.end(),
                           [navaid](WayptRef w)
    {
        if (!w) return false;
        return w->matches(navaid);
    });
    return (it != _elements.end());
}

int Airway::Network::findAirway(const std::string& aName)
{
    const Level level = _networkID;
    auto it = std::find_if(static_airwaysCache.begin(), static_airwaysCache.end(),
                           [aName, level](const AirwayRef& awy)
    { return (awy->_level == level) && (awy->ident() == aName); });
    if (it != static_airwaysCache.end()) {
        return (*it)->_cacheId;
    }

    return NavDataCache::instance()->findAirway(_networkID, aName, true);
}

AirwayRef Airway::findByIdent(const std::string& aIdent, Level level)
{
    auto it = std::find_if(static_airwaysCache.begin(), static_airwaysCache.end(),
                           [aIdent, level](const AirwayRef& awy) {
                               if (level != Level::Both) {
                                   if (level != awy->level()) {
                                       return false;
                                   }
                               }
                               return (awy->ident() == aIdent);
                           });
    if (it != static_airwaysCache.end()) {
        return *it;
    }

    auto ndc = NavDataCache::instance();
    int airwayId = 0;
    if (level == Both) {
        airwayId = ndc->findAirway(HighLevel, aIdent, false);
        if (airwayId == 0) {
            level = LowLevel; // not found in HighLevel, try LowLevel
        } else {
            level = HighLevel; // fix up, so Airway ctro see a valid value
        }
    }

    if (airwayId == 0) {
        airwayId = ndc->findAirway(level, aIdent, false);
        if (airwayId == 0) {
            return {};
        }
    }

    return ndc->loadAirway(airwayId);
}

    AirwayRef Airway::loadByCacheId(int cacheId)
    {
        auto it = std::find_if(static_airwaysCache.begin(), static_airwaysCache.end(),
                               [cacheId](const AirwayRef& awy)
                               { return (awy->_cacheId == cacheId); });
        if (it != static_airwaysCache.end()) {
            return *it;
        }

        return NavDataCache::instance()->loadAirway(cacheId);
        ;
    }

void Airway::loadWaypoints() const
{
    NavDataCache* ndc = NavDataCache::instance();
    for (auto id : ndc->airwayWaypts(_cacheId)) {
        if (id == 0) {
            _elements.push_back({});
        } else {
            FGPositionedRef pos = ndc->loadById(id);
            auto wp = new NavaidWaypoint(pos, const_cast<Airway*>(this));
            wp->setFlag(WPT_VIA);
            wp->setFlag(WPT_GENERATED);
            _elements.push_back(wp);
        }
    }
}

AirwayRef Airway::findByIdentAndVia(const std::string& aIdent, const WayptRef& from, const WayptRef& to)
{
    AirwayRef hi = findByIdent(aIdent, HighLevel);
    if (hi && hi->canVia(from, to)) {
        return hi;
    }

    AirwayRef low = findByIdent(aIdent, LowLevel);
    if (low && low->canVia(from, to)) {
        return low;
    }

    return nullptr;
}

AirwayRef Airway::findByIdentAndEnroute(const std::string& aIdent, Level level, const std::string& enroute)
{
    AirwayRef awy = findByIdent(aIdent, level);
    if (awy && awy->findEnroute(enroute)) {
        return awy;
    }

    // in case there's both airways, *and* the high level doesn't contain the enroute,
    // fall back to the low level. See eg L620 where the high-level is basically empty.
    if (level == Level::Both) {
        AirwayRef awy = findByIdent(aIdent, Level::LowLevel);
        if (awy && awy->findEnroute(enroute)) {
            return awy;
        }
    }


    return nullptr;
}


AirwayRef Airway::findByIdentAndNavaid(const std::string& aIdent, const FGPositionedRef nav)
{
    AirwayRef hi = findByIdent(aIdent, HighLevel);
    if (hi && hi->containsNavaid(nav)) {
        return hi;
    }

    AirwayRef low = findByIdent(aIdent, LowLevel);
    if (low && low->containsNavaid(nav)) {
        return low;
    }

    return nullptr;
}

WayptRef Airway::findEnroute(const std::string &aIdent) const
{
    loadWaypoints();
    auto it = std::find_if(_elements.begin(), _elements.end(),
                           [&aIdent](WayptRef w)
    {
        if (!w) return false;
        return w->ident() == aIdent;
    });

    if (it != _elements.end())
        return *it;
    return {};
}

WayptRef Airway::findEnroute(const FGPositionedRef& nav) const
{
    loadWaypoints();
    auto it = std::find_if(_elements.begin(), _elements.end(),
                           [&nav](WayptRef w)
    {
        if (!w) return false;
        return w->source() == nav;
    });

    if (it != _elements.end())
        return *it;
    return {};
}

void Airway::Network::addEdge(int aWay, const SGGeod& aStartPos,
                              const std::string& aStartIdent,
                              const SGGeod& aEndPos, const std::string& aEndIdent)
{
  FGPositionedRef start = FGPositioned::findClosestWithIdent(aStartIdent, aStartPos);
  FGPositionedRef end = FGPositioned::findClosestWithIdent(aEndIdent, aEndPos);

  if (!start) {
    SG_LOG(SG_NAVAID, SG_DEBUG, "unknown airways start pt: '" << aStartIdent << "'");
    start = FGPositioned::createWaypoint(FGPositioned::WAYPOINT, aStartIdent, aStartPos);
  }

  if (!end) {
    SG_LOG(SG_NAVAID, SG_DEBUG, "unknown airways end pt: '" << aEndIdent << "'");
    end = FGPositioned::createWaypoint(FGPositioned::WAYPOINT, aEndIdent, aEndPos);
  }

  NavDataCache::instance()->insertEdge(_networkID, aWay, start->guid(), end->guid());
}

//////////////////////////////////////////////////////////////////////////////

static double headingDiffDeg(double a, double b)
{
    double rawDiff = b - a;
    SG_NORMALIZE_RANGE(rawDiff, -180.0, 180.0);
    return rawDiff;
}

bool Airway::Network::inNetwork(PositionedID posID) const
{
  NetworkMembershipDict::iterator it = _inNetworkCache.find(posID);
  if (it != _inNetworkCache.end()) {
    return it->second; // cached, easy
  }

  bool r =  NavDataCache::instance()->isInAirwayNetwork(_networkID, posID);
  _inNetworkCache.insert(it, std::make_pair(posID, r));
  return r;
}

bool Airway::Network::route(WayptRef aFrom, WayptRef aTo,
                            WayptVec& aPath)
{
  if (!aFrom || !aTo) {
    throw sg_exception("invalid waypoints to route between");
  }

// find closest nodes on the graph to from/to
// if argument waypoints are directly on the graph (which is frequently the
// case), note this so we don't duplicate them in the output.

  FGPositionedRef from, to;
  bool exactTo, exactFrom;
  std::tie(from, exactFrom) = findClosestNode(aFrom);
  std::tie(to, exactTo) = findClosestNode(aTo);

#ifdef DEBUG_AWY_SEARCH
  SG_LOG(SG_NAVAID, SG_INFO, "from:" << from->ident() << "/" << from->name());
  SG_LOG(SG_NAVAID, SG_INFO, "to:" << to->ident() << "/" << to->name());
#endif

  bool ok = search2(from, to, aPath);
  if (!ok) {
    return false;
  }

  return cleanGeneratedPath(aFrom, aTo, aPath, exactTo, exactFrom);
}

bool Airway::Network::cleanGeneratedPath(WayptRef aFrom, WayptRef aTo, WayptVec& aPath,
                                bool exactTo, bool exactFrom)
{
  // path cleaning phase : various cases to handle here.
  // if either the TO or FROM waypoints were 'exact', i.e part of the enroute
  // structure, we don't want to duplicate them. This happens frequently with
  // published SIDs and STARs.
  // secondly, if the waypoints are NOT on the enroute structure, the course to
  // them may be a significant dog-leg. Check how the leg course deviates
  // from the direct course FROM->TO, and delete the first/last leg if it's more
  // than 90 degrees out.
  // note we delete a maximum of one leg, and no more. This is a heuristic - we
  // could check the next (previous) legs, but at some point we'll end up
  // deleting too much.

  const double MAX_DOG_LEG = 90.0;
  double enrouteCourse = SGGeodesy::courseDeg(aFrom->position(), aTo->position()),
  finalLegCourse = SGGeodesy::courseDeg(aPath.back()->position(), aTo->position());

  bool isDogLeg = fabs(headingDiffDeg(enrouteCourse, finalLegCourse)) > MAX_DOG_LEG;
  if (exactTo || isDogLeg) {
    aPath.pop_back();
  }

  // edge case - if from and to are equal, which can happen, don't
  // crash here. This happens routing EGPH -> EGCC; 'DCS' is common
  // to the EGPH departure and EGCC STAR.
  if (aPath.empty()) {
    return true;
  }

  double initialLegCourse = SGGeodesy::courseDeg(aFrom->position(), aPath.front()->position());
  isDogLeg = fabs(headingDiffDeg(enrouteCourse, initialLegCourse)) > MAX_DOG_LEG;
  if (exactFrom || isDogLeg) {
    aPath.erase(aPath.begin());
  }

  return true;
}

std::pair<FGPositionedRef, bool>
Airway::Network::findClosestNode(WayptRef aRef)
{
    if (aRef->source()) {
        // we can check directly
        if (inNetwork(aRef->source()->guid())) {
            return std::make_pair(aRef->source(), true);
        }
    }

    return findClosestNode(aRef->position());
}

class InAirwayFilter : public FGPositioned::Filter
{
public:
  InAirwayFilter(const Airway::Network* aNet) :
    _net(aNet)
  { ; }

  virtual bool pass(FGPositioned* aPos) const
  {
    return _net->inNetwork(aPos->guid());
  }

  virtual FGPositioned::Type minType() const
  { return FGPositioned::WAYPOINT; }

  virtual FGPositioned::Type maxType() const
  { return FGPositioned::VOR; }

private:
  const Airway::Network* _net;
};

std::pair<FGPositionedRef, bool>
Airway::Network::findClosestNode(const SGGeod& aGeod)
{
  InAirwayFilter f(this);
  FGPositionedRef r = FGPositioned::findClosest(aGeod, 800.0, &f);
  bool exact = false;

  if (r && (SGGeodesy::distanceM(aGeod, r->geod()) < 100.0)) {
    exact = true; // within 100 metres, let's call that exact
  }

  return make_pair(r, exact);
}

FGPositionedRef
Airway::Network::findNodeByIdent(const std::string& ident, const SGGeod& near) const
{
    InAirwayFilter f(this);
    return FGPositioned::findClosestWithIdent(ident, near, &f);
}

/////////////////////////////////////////////////////////////////////////////

typedef vector<AStarOpenNodeRef> OpenNodeHeap;

static void buildWaypoints(AStarOpenNodeRef aNode, WayptVec& aRoute)
{
// count the route length, and hence pre-size aRoute
  size_t count = 0;
  AStarOpenNodeRef n = aNode;
  for (; n != nullptr; ++count, n = n->previous) {;}
  aRoute.resize(count);

// run over the route, creating waypoints
  for (n = aNode; n; n=n->previous) {
      // get / create airway to be the owner for this waypoint
      AirwayRef awy = Airway::loadByCacheId(n->airway);
      auto wp = new NavaidWaypoint(n->node, awy);
      if (awy) {
          wp->setFlag(WPT_VIA);
      }
      wp->setFlag(WPT_GENERATED);
      aRoute[--count] = wp;
  }
}

/**
 * Inefficient (linear) helper to find an open node in the heap
 */
static AStarOpenNodeRef
findInOpen(const OpenNodeHeap& aHeap, FGPositioned* aPos)
{
  for (unsigned int i=0; i<aHeap.size(); ++i) {
    if (aHeap[i]->node == aPos) {
      return aHeap[i];
    }
  }

  return nullptr;
}

class HeapOrder
{
public:
  bool operator()(AStarOpenNode* a, AStarOpenNode* b)
  {
    return a->totalCost() > b->totalCost();
  }
};

bool Airway::Network::search2(FGPositionedRef aStart, FGPositionedRef aDest,
  WayptVec& aRoute)
{
    typedef set<PositionedID> ClosedNodeSet;

    OpenNodeHeap openNodes;
    ClosedNodeSet closedNodes;
    HeapOrder ordering;

    openNodes.push_back(new AStarOpenNode(aStart, 0.0, 0, aDest, nullptr));

    // A* open node iteration
    while (!openNodes.empty()) {
        std::pop_heap(openNodes.begin(), openNodes.end(), ordering);
        AStarOpenNodeRef x = openNodes.back();
        FGPositioned* xp = x->node;
        openNodes.pop_back();
        closedNodes.insert(xp->guid());

#ifdef DEBUG_AWY_SEARCH
    SG_LOG(SG_NAVAID, SG_INFO, "x:" << xp->ident() << ", f(x)=" << x->totalCost());
#endif

  // check if xp is the goal; if so we're done, since there cannot be an open
  // node with lower f(x) value.
    if (xp == aDest) {
      buildWaypoints(x, aRoute);
      return true;
    }

  // adjacent (neighbour) iteration
    NavDataCache* cache = NavDataCache::instance();
    for (auto other : cache->airwayEdgesFrom(_networkID, xp->guid())) {
      if (closedNodes.count(other.second)) {
        continue; // closed, ignore
      }

      FGPositioned* yp = cache->loadById(other.second);
      double edgeDistanceM = SGGeodesy::distanceM(xp->geod(), yp->geod());
      AStarOpenNodeRef y = findInOpen(openNodes, yp);
      if (y) { // already open
        double g = x->distanceFromStart + edgeDistanceM;
        if (g > y->distanceFromStart) {
          // worse path, ignore
#ifdef DEBUG_AWY_SEARCH
          SG_LOG(SG_NAVAID, SG_INFO, "\tabandoning " << yp->ident() <<
           " path is worse: g(y)" << y->distanceFromStart << ", g'=" << g);
#endif
          continue;
        }

      // we need to update y. Unfortunately this means rebuilding the heap,
      // since y's score can change arbitrarily
#ifdef DEBUG_AWY_SEARCH
        SG_LOG(SG_NAVAID, SG_INFO, "\tfixing up previous for new path to " << yp->ident() << ", d =" << g);
#endif
        y->previous = x;
        y->distanceFromStart = g;
        y->airway = other.first;
        std::make_heap(openNodes.begin(), openNodes.end(), ordering);
      } else { // not open, insert a new node for y into the heap
        y = new AStarOpenNode(yp, edgeDistanceM, other.first, aDest, x);
#ifdef DEBUG_AWY_SEARCH
        SG_LOG(SG_NAVAID, SG_INFO, "\ty=" << yp->ident() << ", f(y)=" << y->totalCost());
#endif
        openNodes.push_back(y);
        std::push_heap(openNodes.begin(), openNodes.end(), ordering);
      }
    } // of neighbour iteration
    } // of open node iteration

  SG_LOG(SG_NAVAID, SG_INFO, "A* failed to find route");
  return false;
}

} // of namespace flightgear
