/*
 * SPDX-FileCopyrightText: Copyright (C) 2004  Curtis L. Olson - http://www.flightgear.org/~curt
 * SPDX_FileComment: top level navaids management routines
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "config.h"

#include <string>
#include <vector>
#include <istream>
#include <cmath>
#include <cstddef>              // std::size_t
#include <cerrno>
#include <limits>
#include <stdexcept>

#include "navdb.hxx"

#include <simgear/compiler.h>
#include <simgear/constants.h>
#include <simgear/debug/logstream.hxx>
#include <simgear/math/SGGeod.hxx>
#include <simgear/math/SGMathFwd.hxx>
#include <simgear/math/SGVec3.hxx>
#include <simgear/misc/strutils.hxx>
#include <simgear/misc/sg_path.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/io/iostreams/sgstream.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/sg_inlines.h>

#include "navlist.hxx"
#include <Main/globals.hxx>
#include <Navaids/markerbeacon.hxx>
#include <Airports/airport.hxx>
#include <Airports/runways.hxx>
#include <Airports/xmlloader.hxx>
#include <Main/fg_props.hxx>
#include <Navaids/NavDataCache.hxx>
#include <Navaids/navrecord.hxx>

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

namespace strutils = simgear::strutils;

// Duplicate navaids with the same ident will be removed if the disance
// between them is less than this.
static const double DUPLICATE_DETECTION_RADIUS_NM = 10;


static void throwExceptionIfStreamError(const std::istream& inputStream,
                                        const SGPath& path)
{
  if (inputStream.bad()) {
    const std::string errMsg = simgear::strutils::error_string(errno);

    SG_LOG(SG_NAVAID, SG_ALERT,
           "Error while reading '" << path.utf8Str() << "': " << errMsg);
    throw sg_io_exception("Error reading file (" + errMsg + ")",
                          sg_location(path));
  }
}

static FGPositioned::Type
mapRobinTypeToFGPType(int aTy)
{
  switch (aTy) {
  case 2: return FGPositioned::NDB;
  case 3: return FGPositioned::VOR;
  case 4: return FGPositioned::ILS;
  case 5: return FGPositioned::LOC;
  case 6: return FGPositioned::GS;
  case 7: return FGPositioned::OM;
  case 8: return FGPositioned::MM;
  case 9: return FGPositioned::IM;
  case 12:
  case 13: return FGPositioned::DME;
  default: return FGPositioned::INVALID;
  }
}

static double defaultNavRange(const string& ident, FGPositioned::Type type)
{
  // Ranges are included with the latest data format, no need to
  // assign our own defaults, unless the range is not set for some
  // reason.
  SG_LOG(SG_NAVAID, SG_DEBUG, "navaid " << ident << " has no range set, using defaults");
  switch (type) {
    case FGPositioned::NDB:
    case FGPositioned::VOR:
      return FG_NAV_DEFAULT_RANGE;

    case FGPositioned::LOC:
    case FGPositioned::ILS:
    case FGPositioned::GS:
      return FG_LOC_DEFAULT_RANGE;

    case FGPositioned::DME:
      return FG_DME_DEFAULT_RANGE;

    case FGPositioned::TACAN:
    case FGPositioned::MOBILE_TACAN:
      return FG_TACAN_DEFAULT_RANGE;

    default:
      return FG_LOC_DEFAULT_RANGE;
  }
}


namespace flightgear
{

static bool isNearby(const SGGeod& pos1, const SGGeod& pos2) {
  double distNm = dist(SGVec3d::fromGeod(pos1),
                       SGVec3d::fromGeod(pos2)) * SG_METER_TO_NM;
  return distNm <= DUPLICATE_DETECTION_RADIUS_NM;
}

static bool canBeDuplicate(FGPositionedRef ref, FGPositioned::Type type,
                           const std::string& name,
                           const SGGeod& pos, int freq)
{
  if ((type >= FGPositioned::ILS) && (type <= FGPositioned::IM)) {
    NavDataCache* cache = NavDataCache::instance();
    AirportRunwayPair AR = cache->findAirportRunway(name);
    PositionedID navaidId = cache->findNavaidForRunway(AR.second, type);
    return navaidId != 0;
  } else if (type == FGPositioned::DME) {
    FGNavRecord* navRecord = dynamic_cast<FGNavRecord*>(ref.ptr());
    return navRecord->get_freq() == freq;
  } else {
    return true;
  }
}

// Parse a line from a file such as nav.dat or carrier_nav.dat. Load the
// corresponding data into the NavDataCache.
PositionedID NavLoader::processNavLine(
  const string& line, const string& utf8Path, unsigned int lineNum,
  FGPositioned::Type type, unsigned int version)
{
  NavDataCache* cache = NavDataCache::instance();
  int rowCode, elev_ft, freq, range;
  // 'multiuse': different meanings depending on the record's row code
  double lat, lon, multiuse;
  // Short identifier and longer name for a navaid (e.g., 'OLN' and
  // 'LFPO 02 OM')
  string ident, name, arpt_code;

  if (simgear::strutils::starts_with(line, "#")) {
    // carrier_nav.dat has a comment line using this syntax...
    return 0;
  }

  int num_splits;
  if (version < 1100) {
    // At most 9 fields (the ninth field may contain spaces)
    num_splits = 8;
  } else {
    // At most 11 fields (the eleventh field may contain spaces)
    num_splits = 10;
  }

  vector<string> fields(simgear::strutils::split(line, 0, num_splits));
  vector<string>::size_type nbFields = fields.size();
  static const string endOfData = "99"; // special code in the nav.dat spec

  if (nbFields == 0) {       // blank line
    return 0;
  } else if (nbFields == 1) {
    if (fields[0] != endOfData) {
      SG_LOG( SG_NAVAID, SG_WARN,
              utf8Path << ":" << lineNum << ": malformed line: only one "
              "field, but it is not '99'" );
    }

    return 0;
  } else if (nbFields < 9) {
    SG_LOG( SG_NAVAID, SG_WARN,
            utf8Path << ":"  << lineNum << ": invalid line "
            "(at least 9 fields are required)" );
    return 0;
  }

  // When their string argument can't be properly converted, std::stoi(),
  // std::stof() and std::stod() all raise an exception which is always a
  // subclass of std::logic_error.
  try {
    rowCode = std::stoi(fields[0]);
    lat = std::stod(fields[1]);
    lon = std::stod(fields[2]);
    elev_ft = std::stoi(fields[3]);
    freq = std::stoi(fields[4]);
    range = std::stoi(fields[5]);
    multiuse = std::stod(fields[6]);
    ident = fields[7];
    if (version >= 1100) {
      // Convert names to the format present in 810 version.

      // 1. fields[9] is ICAO region code, we skip over it.
      // 2. For NDB, VOR and DMEs not associated with ILS,
      //    fields[8] is always ENRT, we skip over this too, to match
      //    the naming with version 810.
      if ((rowCode == 2 || rowCode == 3 || rowCode == 12 || rowCode == 13)
          && fields[8] == "ENRT") {
        name = fields[10];
      } else {
        name = fields[8] + " " + fields[10];
      }
    } else {
      name = fields[8];
    }
    // Canonicalize name, removing whitespace from the beginning, the end
    // and extraneous spaces between tokens.
    name = simgear::strutils::simplify(name);
  } catch (const std::logic_error& exc) {
    // On my system using GNU libstdc++, exc.what() is limited to the function
    // name (e.g., 'stod')!
    SG_LOG( SG_NAVAID, SG_WARN,
            utf8Path << ":"  << lineNum << ": unable to parse (" <<
            exc.what() << "): '" <<
            simgear::strutils::stripTrailingNewlines(line) << "'" );
    return 0;
  }

  SGGeod pos(SGGeod::fromDegFt(lon, lat, static_cast<double>(elev_ft)));

  // The type can be forced by our caller, but normally we use the value
  // supplied in the .dat file.
  if (type == FGPositioned::INVALID) {
    type = mapRobinTypeToFGPType(rowCode);
    if (type == FGPositioned::INVALID) {
      static std::set<int> ignoredCodes;
      if (ignoredCodes.insert(rowCode).second) {
        SG_LOG(SG_NAVAID, SG_WARN,
               utf8Path << ":"  << lineNum << ": unrecognized row code "
               << rowCode << ", ignoring this line and all further lines "
               << "with the same code");
      }
      return 0;
    }
  }

  // Silently multiply ADF frequencies by 100 so that ADF
  // vs. NAV/LOC frequency lookups can use the same code.
  if (type == FGPositioned::NDB) {
    freq *= 100;
  }

  //
  // Deduplication rules:
  //
  // 1. Navaid files are loaded according to the order in $FG_SCENERY,
  //    followed by the default file in $FG_ROOT/Navaids.
  //
  // 2. Navaids from each of these files are considered one by one and added
  //    to the cache, except a navaid is *not* added if another one lies within
  //    DUPLICATE_DETECTION_RADIUS_NM and:
  //
  //     - it has the same name, type and ident, or
  //     - it has the same type and ident, and:
  //       * is either attached to the same runway (for navaid types LOC, ILS,
  //         GS, IM, MM, OM), or
  //       * has type FGPositioned::DME and the same frequency
  //         (this ensures that colocated DMEs and TACANs are *not* considered
  //          as duplicates, since they normally have different frequencies)
  //
  // For this logic to work reasonably, each set of nav.dat files in a given
  // scenery path must be self-contained regarding the colocated navaids --
  // if one of the colocated navaids is present, the other one must be too,
  // and the files must be sorted by row codes as mandated in XP-NAV1100 spec.
  //

  // First, eliminate nearby with the same name, type and ident.
  auto loadedNavsKey = std::make_tuple(type, ident, name);
  auto matchingNavs = _loadedNavs.equal_range(loadedNavsKey);
  for (auto it = matchingNavs.first; it != matchingNavs.second; ++it) {
    if (isNearby(pos, it->second)) {
      SG_LOG(SG_NAVAID, SG_INFO,
             utf8Path << ":"  << lineNum << ": skipping navaid '" <<
             name << "' (already defined nearby)");
      return 0;
    }
  }
  _loadedNavs.emplace(loadedNavsKey, pos);

  // Then, eliminate nearby with the same type and ident.
  FGPositioned::TypeFilter dupTypeFilter(type);
  FGPositionedRef ref = FGPositioned::findClosestWithIdent(ident, pos,
                                                           &dupTypeFilter);
  if (ref.valid()) {
    if (isNearby(pos, ref->geod())
        && canBeDuplicate(ref, type, name, pos, freq)) {
      SG_LOG(SG_NAVAID, SG_INFO,
             utf8Path << ":"  << lineNum << ": skipping navaid '" <<
             name << "' (nearby suspected duplicate '" << ref->name() << "')");
      return 0;
    }
  }

  if ((type >= FGPositioned::OM) && (type <= FGPositioned::IM)) {
    AirportRunwayPair arp(cache->findAirportRunway(name));

    if (!arp.first || !arp.second) {
      SG_LOG(SG_NAVAID, SG_INFO,
             utf8Path << ":" << lineNum << ": couldn't find matching runway " <<
             "for marker '" << name << "', skipping.");
      return 0;
    }

    if (arp.second && (elev_ft <= 0)) {
      // snap to runway elevation
      FGPositionedRef runway = cache->loadById(arp.second);
      assert(runway);
      pos.setElevationFt(runway->geod().getElevationFt());
    }

    return cache->insertNavaid(type, string(), name, pos, 0, 0, 0,
                               arp.first, arp.second);
  }

  if (range < 1) {
    range = defaultNavRange(ident, type);
  }

  AirportRunwayPair arp;
  FGRunwayRef runway;
  PositionedID navaid_dme = 0;

  if (type == FGPositioned::DME) {
      // complication here: the database doesn't record TACAN sites at all,
      // we simply infer their existence from DMEs whose name includes 'VORTAC'
      // or 'TACAN' (since all TACAN stations include DME)
      // hence the cache never actually includes entries of type TACAN
    FGPositioned::TypeFilter f(FGPositioned::INVALID);
    if ( name.find("VOR-DME") != std::string::npos ) {
      f.addType(FGPositioned::VOR);
    } else if ( name.find("DME-ILS") != std::string::npos ) {
      f.addType(FGPositioned::ILS);
      f.addType(FGPositioned::LOC);
    } else if ( name.find("VORTAC") != std::string::npos ) {
      f.addType(FGPositioned::VOR);
    } else if ( name.find("NDB-DME") != std::string::npos ) {
      f.addType(FGPositioned::NDB);
    }

    if (f.maxType() > 0) {
      FGPositionedRef ref = FGPositioned::findClosestWithIdent(ident, pos, &f);
      if (ref.valid()) {
        string_list dme_part = simgear::strutils::split(name , 0 ,1);
        string_list navaid_part = simgear::strutils::split(ref.get()->name(), 0 ,1);

        if ( simgear::strutils::uppercase(navaid_part[0]) == simgear::strutils::uppercase(dme_part[0]) ) {
          navaid_dme = ref.get()->guid();
        } else {
            SG_LOG(SG_NAVAID, SG_INFO,
                   utf8Path << ":" << lineNum << ": DME '" << ident << "' (" <<
                   name << "): while looking for a colocated navaid, found " <<
                   "that the closest match has wrong name: '" <<
                   ref->ident() << "' (" << ref->name() << ")");
        }
      } else {
          SG_LOG(SG_NAVAID, SG_INFO,
                 utf8Path << ":" << lineNum << ": couldn't find any colocated "
                 "navaid for DME '" << ident << "' (" << name << ")");
      }
    } // of have a co-located navaid to locate
  }

  if ((type >= FGPositioned::ILS) && (type <= FGPositioned::GS)) {
    arp = cache->findAirportRunway(name);
    if (!arp.first || !arp.second) {
      SG_LOG(SG_NAVAID, SG_INFO,
             utf8Path << ":" << lineNum << ": couldn't find matching runway " <<
             "for ILS/LOC/GS navaid '" << name << "', ignoring it.");
      return 0;
    }
    if (arp.second) {
      runway = FGPositioned::loadById<FGRunway>(arp.second);
      assert(runway);
#if 0
      // code is disabled since it's causing some problems, see
      // https://sourceforge.net/p/flightgear/codetickets/926/
      if (elev_ft <= 0) {
        // snap to runway elevation
        pos.setElevationFt(runway->geod().getElevationFt());
      }
#endif
    } // of found runway in the DB
  } // of type is runway-related

  bool isLoc = (type == FGPositioned::ILS) || (type == FGPositioned::LOC);
  PositionedID r = cache->insertNavaid(type, ident, name, pos, freq, range,
                                       multiuse, arp.first, arp.second);

  if (isLoc) {
    cache->setRunwayILS(arp.second, r);
  }

  if (navaid_dme) {
    cache->setNavaidColocated(navaid_dme, r);
  }

  return r;
}

// load and initialize the navigational databases
void NavLoader::loadNav(const NavDataCache::SceneryLocation& sceneryLocation,
                        std::size_t bytesReadSoFar,
                        std::size_t totalSizeOfAllDatFiles)
{
  NavDataCache* cache = NavDataCache::instance();
  const SGPath path = sceneryLocation.datPath;
  const string utf8Path = path.utf8Str();
  sg_gzifstream in(path);

  if ( !in.is_open() ) {
    throw sg_io_exception(
      "Cannot open file (" + simgear::strutils::error_string(errno) + ")",
      sg_location(path));
  }

  string line;

  // Skip the first two lines
  for (int i = 0; i < 2; i++) {
    std::getline(in, line);
    throwExceptionIfStreamError(in, path);
  }

  unsigned int lineNumber;
  unsigned int version;
  vector<string> fields(simgear::strutils::split(line, 0, 1));

  try {
    if (fields.empty()) {
      throw sg_format_exception();
    }
    version = strutils::readNonNegativeInt<unsigned int>(fields[0]);
  } catch (const sg_exception& exc) {
    std::string strippedLine = simgear::strutils::stripTrailingNewlines(line);
    std::string errMsg = utf8Path + ": ";

    if (fields.empty()) {
      errMsg += "unable to parse format version: empty line";
      SG_LOG(SG_NAVAID, SG_ALERT, errMsg);
    } else {
      errMsg += "unable to parse format version";
      SG_LOG(SG_NAVAID, SG_ALERT,
             errMsg << " (" << exc.what() << "): " << strippedLine);
    }

    throw sg_format_exception(errMsg, strippedLine);
  }

  SG_LOG(SG_NAVAID, SG_INFO,
         "nav.dat format version (" << utf8Path << "): " << version);

  for (lineNumber = 3; std::getline(in, line); lineNumber++) {
    processNavLine(line, utf8Path, lineNumber, FGPositioned::INVALID, version);

    if ((lineNumber % 100) == 0) {
      // every 100 lines
      unsigned int percent = ((bytesReadSoFar + in.approxOffset()) * 100)
        / totalSizeOfAllDatFiles;
      cache->setRebuildPhaseProgress(NavDataCache::REBUILD_NAVAIDS, percent);
    }

  } // of stream data loop

  throwExceptionIfStreamError(in, path);
}

void NavLoader::loadCarrierNav(const SGPath& path)
{
  SG_LOG( SG_NAVAID, SG_DEBUG, "Opening file: " << path );
  const string utf8Path = path.utf8Str();
  sg_gzifstream in(path);

  if ( !in.is_open() ) {
    throw sg_io_exception(
      "Cannot open file (" + simgear::strutils::error_string(errno) + ")",
      sg_location(path));
  }

  string line;
  unsigned int lineNumber;

  for (lineNumber = 1; std::getline(in, line); lineNumber++) {
    // Force the navaid type to be MOBILE_TACAN
    processNavLine(line, utf8Path, lineNumber, FGPositioned::MOBILE_TACAN);
  }

  throwExceptionIfStreamError(in, path);
}

bool NavLoader::loadTacan(const SGPath& path, FGTACANList *channellist)
{
    SG_LOG( SG_NAVAID, SG_DEBUG, "opening file: " << path );
    sg_gzifstream inchannel( path );

    if ( !inchannel.is_open() ) {
        SG_LOG( SG_NAVAID, SG_ALERT, "Cannot open file: " << path );
      return false;
    }

    // skip first line
    inchannel >> skipeol;
    while ( ! inchannel.eof() ) {
        FGTACANRecord *r = new FGTACANRecord;
        inchannel >> (*r);
        channellist->add ( r );

    } // end while

    return true;
}

} // of namespace flightgear
