// ==========================================================================
//             ____        _     _   _      _    ____      _
//            / ___| _   _| |__ | \ | | ___| |_ / ___|__ _| | ___
//            \___ \| | | | '_ \|  \| |/ _ \ __| |   / _` | |/ __|
//             ___) | |_| | |_) | |\  |  __/ |_| |__| (_| | | (__
//            |____/ \__,_|_.__/|_| \_|\___|\__|\____\__,_|_|\___|
//
//                    ---  IPv4/IPv6 Subnet Calculator  ---
//                   https://www.nntb.no/~dreibh/subnetcalc/
// ==========================================================================
//
// SubNetCalc - IPv4/IPv6 Subnet Calculator
// Copyright (C) 2024-2026 by Thomas Dreibholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// Contact: thomas.dreibholz@gmail.com

#include <cassert>
#include <cctype>
#include <clocale>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <getopt.h>
#include <iostream>
#include <netdb.h>
#include <unistd.h>
#include <vector>
#ifndef NI_IDN
#include <idn2.h>
#endif

#ifdef HAVE_MAXMINDDB
#include <maxminddb.h>
#endif

#ifdef ENABLE_NLS
#include <libintl.h>
#else
#define bindtextdomain(domain, dirname) { }
#define textdomain(domain) { }
#define gettext(string) string
#define ngettext(singular, plural, n) ((n) == 1 ? (singular) : (plural))
#endif

#include "tools.h"
#include "package-version.h"


// ###### Is given address an IPv4 address? #################################
inline bool isIPv4(const sockaddr_union& address)
{
   return (address.sa.sa_family == AF_INET);
}


// ###### Extract IPv4 address from address #################################
inline in_addr_t getIPv4Address(const sockaddr_union& address)
{
   assert(address.sa.sa_family == AF_INET);
   return address.in.sin_addr.s_addr;
}


// ###### Extract IPv4 address from address #################################
inline in6_addr getIPv6Address(const sockaddr_union& address)
{
   assert(address.sa.sa_family == AF_INET6);
   return address.in6.sin6_addr;
}


// ###### Is given address a multicast address? #############################
inline bool isMulticast(const sockaddr_union& address)
{
   if(isIPv4(address)) {
      return IN_CLASSD(ntohl(getIPv4Address(address)));
   }
   else {
      const in6_addr ipv6address = getIPv6Address(address);
      return IN6_IS_ADDR_MULTICAST(&ipv6address);
   }
}


// ###### Read prefix from parameter ########################################
int readPrefix(const char*           parameter,
               const sockaddr_union& forAddress,
               sockaddr_union&       netmask)
{
   const size_t parameterLength = strlen(parameter);
   for(size_t i = 0; i < parameterLength; i++) {
      if(!isdigit(static_cast<unsigned char>(parameter[i]))) {
         return -1;
      }
   }
   int prefix = atol(parameter);
   if(prefix < 0) {
      return -1;
   }
   netmask = forAddress;
   if(netmask.sa.sa_family == AF_INET) {
      if(prefix > 32) {
         return -1;
      }
      int p = prefix;
      netmask.in.sin_addr.s_addr = 0;
      for(int i = 31; i >= 0; i--) {
         if(p > 0) {
            netmask.in.sin_addr.s_addr |= (1U << i);
         }
         p--;
      }
      netmask.in.sin_addr.s_addr = ntohl(netmask.in.sin_addr.s_addr);
   }
   else {
      if(prefix > 128) {
         return -1;
      }
      int p = prefix;
      for(int j = 0; j < 16; j++) {
         netmask.in6.sin6_addr.s6_addr[j] = 0;
         for(int i = 7; i >= 0; i--) {
            if(p > 0) {
               netmask.in6.sin6_addr.s6_addr[j] |= (1U << i);
            }
            p--;
         }
      }
   }
   return prefix;
}


// ###### Generate a unique local IPv6 address ##############################
void generateUniqueLocal(sockaddr_union& address,
                         const bool      highQualityRng = false)
{
   uint8_t buffer[5];
   if(address.sa.sa_family != AF_INET6) {
      std::cerr << gettext("ERROR: An IPv6 address must be given to generate a unique local address!") << "\n";
      exit(1);
   }

#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
   // ====== Read random number from random device ==========================
   const char* randomFile = highQualityRng ? "/dev/random" : "/dev/urandom";

   std::ifstream randomStream(randomFile, std::ios::binary);
   if(randomStream) {
      std::cout << format(gettext("Generating Unique Local IPv6 address (using %s) ..."),
                          randomFile) << "\n";
      if(!randomStream.read(reinterpret_cast<char*>(buffer), sizeof(buffer))) {
         std::cerr << format(gettext("ERROR: Unable to read from %s!"), randomFile) << "\n";
         exit(1);
      }
   }
   else {
      std::cerr << format(gettext("ERROR: Unable to open %s!"), randomFile) << "\n";
      exit(1);
   }
#else
   // ====== Get random number using random() function ======================
#warning Using default random number generator!
   srandom((unsigned int)getMicroTime() ^ getpid());
   for(size_t i = 0; i < sizeof(buffer); i++) {
      buffer[i] = (uint8_t)(random() % 0xff);
   }
#endif

   // ====== Create IPv6 Unique Local address ===============================
   address.in6.sin6_addr.s6_addr[0] = 0xfd;
   memcpy(&address.in6.sin6_addr.s6_addr[1], buffer, sizeof(buffer));
}


// ###### Print IPv4 address in binary digits ###############################
void printAddressBinary(std::ostream&         os,
                        const sockaddr_union& address,
                        const unsigned int    prefix,
                        const bool            colourMode = true,
                        const char*           indent     = "")
{
   if(address.sa.sa_family == AF_INET) {
      os << indent;
      uint32_t a = ntohl(getIPv4Address(address));
      for(int i = 31; i >= 0; i--) {
         const uint32_t v = (uint32_t)1 << i;
         if(colourMode) {
            if(32 - i > (int)prefix) {   // Colourize output
               os << "\x1b[33m";
            }
            else {
               os << "\x1b[34m";
            }
         }
         if(a >= v) {
            os << "1";
            a -= v;
         }
         else {
            os << "0";
         }
         if(colourMode) {
            os << "\x1b[0m";   // Turn off colour printing
         }
         if( ((i % 8) == 0) && (i > 0) ) {
            os << " . ";
         }
      }
      os << "\n";
   }
   else {
      int            p           = 0;
      const in6_addr ipv6Address = getIPv6Address(address);
      for(int j = 0; j < 8; j++) {
         uint16_t a = (ipv6Address.s6_addr[j * 2] << 8) | ipv6Address.s6_addr[j * 2 + 1];
         char     str[16];
         snprintf(str, sizeof(str), "%04x", a);
         os << indent << str << " = ";
         for(int i = 15; i >= 0; i--) {
            const uint32_t v = (uint32_t)1 << i;
            if(colourMode) {
               if(p < (int)prefix) {   // Colourize output
                  os << "\x1b[33m";
               }
               else {
                  os << "\x1b[34m";
               }
            }
            if(a >= v) {
               os << "1";
               a -= v;
            }
            else {
               os << "0";
            }
            if(colourMode) {
               os << "\x1b[0m";   // Turn off colour printing
            }
            if( ((i % 8) == 0) && (i > 0) ) {
               os << " ";
            }
            p++;
         }
         os << "\n";
      }
   }
}


// ###### Is given netmask valid? ###########################################
int getPrefixLength(const sockaddr_union& netmask)
{
   int  prefixLength;
   bool belongsToNetwork = true;

   if(netmask.sa.sa_family == AF_INET) {
      prefixLength     = 32;
      const uint32_t a = ntohl(getIPv4Address(netmask));
      for(int i = 31; i >= 0; i--) {
         if(!(a & (1U << (uint32_t)i))) {
            belongsToNetwork = false;
            prefixLength--;
         }
         else {
            if(belongsToNetwork == false) {
               return -1;
            }
         }
      }
   }
   else {
      prefixLength = 128;
      const in6_addr ip6 = getIPv6Address(netmask);
      for(int j = 0; j < 4; j++) {
         const uint32_t a = ((uint32_t)ip6.s6_addr[j * 4]     << 24) |
                            ((uint32_t)ip6.s6_addr[j * 4 + 1] << 16) |
                            ((uint32_t)ip6.s6_addr[j * 4 + 2] << 8)  |
                            ((uint32_t)ip6.s6_addr[j * 4 + 3]);
         for(int i = 31; i >= 0; i--) {
            if(!(a & (1U << (uint32_t)i))) {
               belongsToNetwork = false;
               prefixLength--;
            }
            else {
               if(belongsToNetwork == false) {
                  return -1;
               }
            }
         }
      }
   }
   return prefixLength;
}


// ###### "&" operator for addresses ########################################
sockaddr_union operator&(const sockaddr_union& a1, const sockaddr_union& a2)
{
   assert(a1.sa.sa_family == a2.sa.sa_family);

   sockaddr_union a = a1;
   if(a.sa.sa_family == AF_INET) {
      a.in.sin_addr.s_addr &= a2.in.sin_addr.s_addr;
   } else {
      for(int j = 0; j < 16; j++) {
         a.in6.sin6_addr.s6_addr[j] &= a2.in6.sin6_addr.s6_addr[j];
      }
   }
   return a;
}


// ###### "|" operator for addresses ########################################
sockaddr_union operator|(const sockaddr_union& a1, const sockaddr_union& a2)
{
   assert(a1.sa.sa_family == a2.sa.sa_family);

   sockaddr_union a = a1;
   if(a.sa.sa_family == AF_INET) {
      a.in.sin_addr.s_addr |= a2.in.sin_addr.s_addr;
   }
   else {
      for(int j = 0; j < 16; j++) {
         a.in6.sin6_addr.s6_addr[j] |= a2.in6.sin6_addr.s6_addr[j];
      }
   }
   return a;
}


// ###### "~" operator for addresses ########################################
sockaddr_union operator~(const sockaddr_union& a1)
{
   sockaddr_union a = a1;
   if(a.sa.sa_family == AF_INET) {
      a.in.sin_addr.s_addr = ~a1.in.sin_addr.s_addr;
   }
   else {
      for(int j = 0; j < 16; j++) {
         a.in6.sin6_addr.s6_addr[j] = ~a1.in6.sin6_addr.s6_addr[j];
      }
   }
   return a;
}


// ###### Output operator for addresses #####################################
std::ostream& operator<<(std::ostream& os, const sockaddr_union& a)
{
   printAddress(os, &a.sa, false, true);
   return os;
}


// ###### "+" operator for addresses ########################################
sockaddr_union operator+(const sockaddr_union& a1, uint32_t n)
{
   sockaddr_union a = a1;
   if(a.sa.sa_family == AF_INET) {
      a.in.sin_addr.s_addr = htonl(ntohl(a.in.sin_addr.s_addr) + n);
   }
   else {
      union {
         uint8_t  b[16];
         uint32_t w[4];
      } local;
      std::memcpy(local.b, a.in6.sin6_addr.s6_addr, 16);
      for(int j = 3; j >= 0; j--) {
         const uint64_t sum = static_cast<uint64_t>(ntohl(local.w[j])) + static_cast<uint64_t>(n);
         local.w[j] = htonl(static_cast<uint32_t>(sum & 0xffffffffULL));
         n = static_cast<uint32_t>(sum >> 32);
      }
      std::memcpy(a.in6.sin6_addr.s6_addr, local.b, 16);
   }
   return a;
}


// ###### "-" operator for addresses ########################################
sockaddr_union operator-(const sockaddr_union& a1, uint32_t n)
{
   sockaddr_union a = a1;
   if(a.sa.sa_family == AF_INET) {
      a.in.sin_addr.s_addr = htonl(ntohl(a.in.sin_addr.s_addr) - n);
   }
   else {
      union {
         uint8_t  b[16];
         uint32_t w[4];
      } local;
      std::memcpy(local.b, a.in6.sin6_addr.s6_addr, 16);
      for(int j = 3; j >= 0; j--) {
         const uint64_t sum = static_cast<uint64_t>(ntohl(local.w[j])) - static_cast<uint64_t>(n);
         local.w[j] = htonl(static_cast<uint32_t>(sum & 0xffffffffULL));
         n = static_cast<uint32_t>(sum >> 32);
      }
      std::memcpy(a.in6.sin6_addr.s6_addr, local.b, 16);
   }
   return a;
}


// ###### "==" operator for addresses #######################################
bool operator==(const sockaddr_union& a1, const sockaddr_union& a2)
{
   assert(a1.sa.sa_family == a2.sa.sa_family);

   if(a1.sa.sa_family == AF_INET) {
      return (a1.in.sin_addr.s_addr == a2.in.sin_addr.s_addr);
   }
   else {
      for(int j = 0; j < 16; j++) {
         if(a1.in6.sin6_addr.s6_addr[j] != a2.in6.sin6_addr.s6_addr[j]) {
            return false;
         }
      }
      return true;
   }
}


// ###### Print IPv6 unicast properties of given address ####################
void printUnicastProperties(std::ostream&   os,
                            const in6_addr& ipv6address,
                            const bool      colourMode  = true,
                            const bool      hasSubnetID = true,
                            const bool      hasGlobalID = false)
{
   uint16_t word[8];
   for(int i = 0; i < 8; i++) {
      word[i] = (ipv6address.s6_addr[i * 2] << 8) | ipv6address.s6_addr[i * 2 + 1];
   }

   // ====== Global ID ======================================================
   if(hasGlobalID) {
      char globalIDString[16];
      snprintf(globalIDString, sizeof(globalIDString), "%02x%04x%04x",
               word[0] & 0xff, word[1], word[2]);
      os << "      + " << format(gettext("%-32s"), gettext("Global ID")) << " = " << globalIDString << "\n";
   }

   // ====== Subnet ID ======================================================
   if(hasSubnetID) {
      char           subnetIDString[16];
      const uint16_t subnetID = word[3];
      snprintf(subnetIDString, sizeof(subnetIDString), "%04x", subnetID);
      os << "      + " << format(gettext("%-32s"), gettext("Subnet ID")) << " = " <<  subnetIDString << "\n";
   }

   // ====== Interface ID ===================================================
   char           interfaceIDString[128];
   const uint16_t interfaceID[4] = { word[4], word[5], word[6], word[7] };
   snprintf(interfaceIDString, sizeof(interfaceIDString),
            ((colourMode == true) ? "\x1b[36m%04x:%02x\x1b[37m%02x:%02x\x1b[38m%02x:%04x\x1b[0m" :
                                    "%04x:%02x%02x:%02x%02x:%04x"),
            interfaceID[0],
            (interfaceID[1] & 0xff00) >> 8, (interfaceID[1] & 0x00ff),
            (interfaceID[2] & 0xff00) >> 8, (interfaceID[2] & 0x00ff),
            interfaceID[3]);
   os << "      + " << format(gettext("%-32s"), gettext("Interface ID")) << " = " <<  interfaceIDString << "\n";

   if( ((interfaceID[1] & 0x00ff) == 0x00ff) &&
       ((interfaceID[2] & 0xff00) == 0xfe00) ) {
      snprintf(interfaceIDString, sizeof(interfaceIDString),
               ((colourMode == true) ? "\x1b[36m%02x:%02x:%02x\x1b[0m:\x1b[38m%02x:%02x:%02x\x1b[0m" :
                                       "%02x:%02x:%02x:%02x:%02x:%02x"),
               ipv6address.s6_addr[8] ^ 0x02,
               ipv6address.s6_addr[9],
               ipv6address.s6_addr[10],
               ipv6address.s6_addr[13],
               ipv6address.s6_addr[14],
               ipv6address.s6_addr[15]);
      os << "      + " << format(gettext("%-32s"), gettext("MAC Address")) << " = " <<  interfaceIDString << "\n";
   }

   // ====== Solicited Node Multicast Address ===============================
   char snmcAddressString[32];
   snprintf(snmcAddressString, sizeof(snmcAddressString),
            ((colourMode == true) ? "\x1b[32mff02::1:ff\x1b[38m%02x:%04x\x1b[0m" :
                                    "ff02::1:ff%02x:%04x"),
            word[6] & 0xff,
            word[7]);
   os << "      + " << format(gettext("%-32s"), gettext("Solicited Node Multicast Address")) << " = " <<  snmcAddressString << "\n";
}


// ###### Print address properties ##########################################
void printAddressProperties(std::ostream&         os,
                            const sockaddr_union& address,
                            const sockaddr_union& netmask,
                            const unsigned int    prefix,
                            const sockaddr_union& network,
                            const sockaddr_union& broadcast,
                            const bool            colourMode)
{
   char addressString[64];
   address2string(&address.sa, addressString, sizeof(addressString), false, false);

   // ====== Common properties ==============================================
   os << format(gettext("%-14s"), gettext("Properties")) << " = \n";
   os << "   - ";
   if(isMulticast(address)) {
      os << format(gettext("%s is a MULTICAST address"), addressString);
   }
   else if( (isIPv4(address)) &&
            (prefix < 32) &&
            (address == broadcast) ) {
      char networkString[64];
      address2string(&network.sa, networkString, sizeof(networkString), false, false);
      os << format(gettext("%s is the BROADCAST address of %s/%u"),
                   addressString, networkString, prefix);
   }
   else if( (address == network) &&
            ( (isIPv4(address) && (prefix < 32))  ||
              (!isIPv4(address) && (prefix < 128)) ) ) {
      os << format(gettext("%s is a NETWORK address"), addressString);
   }
   else {
      char networkString[64];
      address2string(&network.sa, networkString, sizeof(networkString), false, false);
      os << format(gettext("%s is a HOST address in %s/%u"),
                   addressString, networkString, prefix);
   }
   os << "\n";


   // ====== IPv4 properties ================================================
   if(isIPv4(address)) {
      const in_addr_t    ipv4address = ntohl(getIPv4Address(address));
      const unsigned int a           = ipv4address >> 24;
      const unsigned int b           = (ipv4address & 0x00ff0000) >> 16;

      if(IN_CLASSA(ipv4address)) {
         os << "   - " << gettext("Class A") << "\n";
         if(ipv4address == INADDR_LOOPBACK) {
            os << "   - " << gettext("Loopback address") << "\n";
         }
         else if(a == IN_LOOPBACKNET) {
            os << "   - " << gettext("In loopback network") << "\n";
         }
         else if(a == 10) {
            os << "   - " << gettext("Private") << "\n";
         }
      }
      else if(IN_CLASSB(ipv4address)) {
         os << "   - " << gettext("Class B") << "\n";
         if((a == 172) && ((b >= 16) && (b <= 31))) {
            os << "   - " << gettext("Private") << "\n";
         }
         else if((a == 169) && (b == 254)) {
            os << "   - " << gettext("Link-local address") << "\n";
         }
      }
      else if(IN_CLASSC(ipv4address)) {
         os << "   - " << gettext("Class C") << "\n";
         if((a == 192) && (b == 168)) {
            os << "   - " << gettext("Private") << "\n";
         }
      }
      else if(IN_CLASSD(ipv4address)) {
         os << "   - " << gettext("Class D (Multicast)") << "\n";
         // ------ Multicast scope ------------------------------------------
         os << "      + " << gettext("Scope: ");
         if(a == 224) {
            os << gettext("link-local");
         }
         else if((a == 239) && (b >= 192) && (b <= 251)) {
            os << gettext("organization-local");
         }
         else if((a == 239) && (b >= 252) && (b <= 255)) {
            os << gettext("site-local");
         }
         else {
            os << gettext("global");
         }
         os << "\n";

         // ------ Corresponding MAC address --------------------------------
         char macAddressString[32];
         snprintf(macAddressString, sizeof(macAddressString),
                  "01:00:5e:%02x:%02x:%02x",
                  (ipv4address & 0x007f0000) >> 16,
                  (ipv4address & 0x0000ff00) >> 8,
                  (ipv4address & 0x000000ff));
         os << "      + " << format(gettext("Corresponding multicast MAC address: %s"),
                                    macAddressString) << "\n";

         // ------ Source-specific multicast --------------------------------
         if(a == 232) {
            os << "      + " << gettext("Source-specific multicast") << "\n";
         }
      }
      else {
         os << "   - " << gettext("Invalid (not in class A, B, C or D)") << "\n";
      }
   }


   // ====== IPv6 properties ================================================
   else {
      const in6_addr ipv6address = getIPv6Address(address);
      const uint16_t word0       = (ipv6address.s6_addr[0] << 8) | ipv6address.s6_addr[1];
      const uint16_t word1       = (ipv6address.s6_addr[2] << 8) | ipv6address.s6_addr[3];
      const uint16_t word5       = (ipv6address.s6_addr[10] << 8) | ipv6address.s6_addr[11];
      const uint16_t word6       = (ipv6address.s6_addr[12] << 8) | ipv6address.s6_addr[13];
      const uint16_t word7       = (ipv6address.s6_addr[14] << 8) | ipv6address.s6_addr[15];

      // ------ Special addresses -------------------------------------------
      if(IN6_IS_ADDR_LOOPBACK(&ipv6address)) {
         os << "   - " << gettext("Loopback address") << "\n";
      }
      else if(IN6_IS_ADDR_UNSPECIFIED(&ipv6address)) {
         os << "   - " << gettext("Unspecified address") << "\n";
      }
      else if(IN6_IS_ADDR_V4COMPAT(&ipv6address)) {
         os << "   - " << gettext("IPv4-compatible IPv6 address") << "\n";
      }
      else if(IN6_IS_ADDR_V4MAPPED(&ipv6address)) {
         os << "   - " << gettext("IPv4-mapped IPv6 address") << "\n";
      }
      else if(hasTranslationPrefix(&address.in6)) {
         os << "   - " << gettext("IPv4-embedded IPv6 address") << "\n";
      }

      // ------ Multicast addresses -----------------------------------------
      else if(IN6_IS_ADDR_MULTICAST(&ipv6address)) {
         // ------ Multicast scope ------------------------------------------
         os << "   - " << gettext("Multicast Properties") << "\n";
         os << "      + " << gettext("Scope: ");
         if(IN6_IS_ADDR_MC_NODELOCAL(&ipv6address)) {
            os << gettext("node-local");
         }
         else if(IN6_IS_ADDR_MC_LINKLOCAL(&ipv6address)) {
            os << gettext("link-local");
         }
         else if(IN6_IS_ADDR_MC_SITELOCAL(&ipv6address)) {
            os << gettext("site-local");
         }
         else if(IN6_IS_ADDR_MC_ORGLOCAL(&ipv6address)) {
            os << gettext("organization-local");
         }
         else if(IN6_IS_ADDR_MC_GLOBAL(&ipv6address)) {
            os << gettext("global");
         }
         else {
            os << gettext("unknown");
         }
         os << "\n";

         // ------ Multicast flags ------------------------------------------
         const uint8_t flags = (ipv6address.s6_addr[1] & 0xf0) >> 4;
         if(flags == 0x1) {
            os << "      + " << gettext("Temporarily-allocated address") << "\n";
         }

         // ------ Corresponding MAC address --------------------------------
         char macAddressString[32];
         snprintf(macAddressString, sizeof(macAddressString),
                  "33:33:%02x:%02x:%02x:%02x",
                  ipv6address.s6_addr[12],
                  ipv6address.s6_addr[13],
                  ipv6address.s6_addr[14],
                  ipv6address.s6_addr[15]);
         os << "      + " << format(gettext("Corresponding multicast MAC address: %s"),
                                    macAddressString) << "\n";

         // ------ Source-specific multicast --------------------------------
         if( ((word0 & 0xfff0) == 0xff30) && (word1 == 0x0000) ) {
            // FF0x:0::/32
            os << "      + " << gettext("Source-specific multicast") << "\n";
         }

         // ------ Solicited node multicast address -------------------------
         // FF02::1:FF00:0/104
         if( (word0 == 0xff02) &&
             (word5 == 0x0001) &&
             ((word6 & 0xff00) == 0xff00) ) {
            char nodeAddressString[64];
            snprintf(nodeAddressString, sizeof(nodeAddressString),
                     "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xx%02x:%04x",
                     word6 & 0xff, word7);
            os << "      + " << gettext("Address is solicited node multicast address for")
               << " " << nodeAddressString << "\n";
         }
      }

      // ------ Link-local Unicast ------------------------------------------
      else if(IN6_IS_ADDR_LINKLOCAL(&ipv6address)) {
         os << "   - " << gettext("Link-Local Unicast Properties:") << "\n";
         printUnicastProperties(std::cout, ipv6address, colourMode, false, false);
      }

      // ------ Site-Local Unicast ------------------------------------------
      else if(IN6_IS_ADDR_SITELOCAL(&ipv6address)) {
         os << "   - " << gettext("Site-Local Unicast Properties:") << "\n";
         printUnicastProperties(std::cout, ipv6address, colourMode, true, false);
      }

      // ------ Unique Local Unicast ----------------------------------------
      else if((word0 & 0xfc00) == 0xfc00) {
         os << "   - " << gettext("Unique Local Unicast Properties:") << "\n";
         if(word0 & 0x0100) {
            os << "      + " << gettext("Locally chosen") << "\n";
         }
         else {
            os << "      + " << gettext("Assigned by global instance") << "\n";
         }
         printUnicastProperties(std::cout, ipv6address, colourMode, true, true);
      }

      // ------ Global Unicast ----------------------------------------------
      else if((word0 & 0xe000) == 0x2000) {
         os << "   - " << gettext("Global Unicast Properties:") << "\n";
         printUnicastProperties(std::cout, ipv6address, colourMode, false, false);

         // ------ 6to4 Address ---------------------------------------------
         if((word0 & 0x2002) == 0x2002) {
            sockaddr_union sixToFour;
            sixToFour.sa.sa_family       = AF_INET;
            const uint32_t u = (ipv6address.s6_addr[2] << 8) | ipv6address.s6_addr[3];
            const uint32_t l = (ipv6address.s6_addr[4] << 8) | ipv6address.s6_addr[5];
            sixToFour.in.sin_addr.s_addr = htonl((u << 16) | l);
            os << "      + " << format("%-32s = ", gettext("6-to-4 Address"));
            printAddress(std::cout, &sixToFour.sa, false);
            os << "\n";
         }
      }
   }
}


#if defined(__SIZEOF_INT128__)
// ###### Convert unsigned 128 bit integer to string ########################
std::string toString(unsigned __int128 num) {
   std::string str;
   do {
      const int digit = num % 10;
      str = std::to_string(digit) + str;
      num = (num - digit) / 10;
   } while(num != 0);
   return str;
}
#endif


#ifdef HAVE_MAXMINDDB
// ###### Find location of MaxMindDB .mmdb file #############################
std::string find_mmdb_path(const std::string& mmdbFileName) {
   static const std::vector<std::string> mmdbDirectories = {
      "/usr/share/GeoIP",          // Debian, Ubuntu
      "/var/lib/GeoIP",            // CentOS, Fedora, RedHat
      "/usr/local/share/GeoIP",    // FreeBSD
      "/opt/homebrew/var/GeoIP",   // MacOS (Homebrew Apple Silicon)
      "/usr/local/var/GeoIP",      // MacOS (Homebrew Intel)
      "/etc/GeoIP"                 // Legacy
   };

   for (const auto& directory : mmdbDirectories) {
      const std::filesystem::path absolutePath =
         std::filesystem::path(directory) / mmdbFileName;
      if(std::filesystem::exists(absolutePath)) {
         return absolutePath.string();
      }
    }
    return "";
}
#endif


// ###### Version ###########################################################
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202000L)
[[ noreturn ]]
#endif
static void version(void)
{
   std::cout << "SubNetCalc " << SUBNETCALC_VERSION << "\n";
   exit(0);
}


// ###### Usage #############################################################
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202000L)
[[ noreturn ]]
#endif
static void usage(const char* program, const int exitCode)
{
   std::cerr << gettext("Usage:") << " "
             << program
             << " address/prefix | address/netmask | address [prefix] | address [netmask]\n"
                " [-u|--uniquelocal] [-U|--uniquelocalhq]\n"
                " [-n|--noreverselookup]\n"
                " [-g|--nogeoiplookup]\n"
                " [-c|--nocolour|--nocolor]\n"
                " [-h|--help] [-v|--version]\n";
   exit(exitCode);
}


// ###### Main program ######################################################
int main(int argc, char** argv)
{
   // ====== Initialise i18n support ========================================
   if(setlocale(LC_ALL, "") == nullptr) {
      setlocale(LC_ALL, "C.UTF-8");   // "C" should exist on all systems!
   }
   bindtextdomain("subnetcalc", nullptr);
   textdomain("subnetcalc");

   // ====== Handle arguments ===============================================
   static const struct option long_options[] = {
      { "uniquelocal",     no_argument, 0, 'u' },
      { "uniquelocalhq",   no_argument, 0, 'U' },
      { "nocolour",        no_argument, 0, 'c' },
      { "nocolor",         no_argument, 0, 'c' },
      { "noreverselookup", no_argument, 0, 'n' },
      { "nogeoiplookup",   no_argument, 0, 'g' },
      { "help",            no_argument, 0, 'h' },
      { "version",         no_argument, 0, 'v' },
      {  nullptr,          0,           0, 0   }
   };

   bool         colourMode      = true;
   bool         noReverseLookup = false;
   bool         noGeoIPLookup   = false;
   unsigned int uniqueLocal     = 0;
   int option;
   int longIndex;
   while( (option = getopt_long_only(argc, argv, "uUcnghv", long_options, &longIndex)) != -1 ) {
      switch(option) {
         case 'n':
            noReverseLookup = true;
            break;
         case 'g':
            noGeoIPLookup = true;
            break;
         case 'c':
            colourMode = false;
            break;
         case 'u':
            uniqueLocal = 1;
            break;
         case 'U':
            uniqueLocal = 2;
            break;
         case 'v':
            version();
            break;
         case 'h':
         case '?':
            // Exit with 0 on h/help, exit with 1 on '?' (unknown option):
            usage(argv[0], (option == 'h') ? 0 : 1);
            break;
         case '-':
            break;
         default:
            // This should not happen: wrong getopt parameters, or missing case?
            fprintf(stderr, "INTERNAL ERROR: Unhandled option c=%c code=%x!\n",
                    (isprint(option) ? (char)option : ' '), option);
            return 1;
      }
   }
   if( (optind + 1 != argc) && (optind + 2 != argc) ) {
      usage(argv[0], 1);
   }

   int                prefix;
   unsigned int       hostBits;
   unsigned int       reservedHosts;
#if defined(__SIZEOF_INT128__)
   unsigned __int128  maxHosts;
#else
   // There is no 128-bit type on 32-bit systems!
   unsigned long long maxHosts;
#endif
   sockaddr_union     network;
   sockaddr_union     address;
   sockaddr_union     netmask;
   sockaddr_union     broadcast;
   sockaddr_union     wildcard;
   sockaddr_union     host1;
   sockaddr_union     host2;

   // ====== Get address and netmask from one parameter =====================
   char* slash = strchr(argv[optind], '/');
   if(slash) {
      slash[0] = 0x00;
      if(string2address(argv[optind], &address) == false) {
         std::cerr << format(gettext("ERROR: Invalid address %s!"), argv[optind]) << "\n";
         exit(1);
      }
      if(readPrefix(&slash[1], address, netmask) < 0) {
         if(string2address(&slash[1], &netmask) == false) {
            std::cerr << format(gettext("ERROR: Invalid netmask %s!"), argv[optind]) << "\n";
            exit(1);
         }
      }
   }

   // ====== Get address and netmask from separate parameters ===============
   else if(slash == nullptr) {
      if(string2address(argv[optind], &address) == false) {
         std::cerr << format(gettext("ERROR: Invalid address %s!"), argv[optind]) << "\n";
         exit(1);
      }
      if(optind + 1 < argc) {
         // ------ Get netmask or prefix ------------------------------------
         if(argv[optind + 1][0] != '-') {
            if( readPrefix(argv[optind + 1], address, netmask) < 0 ) {
               if(string2address(argv[optind + 1], &netmask) == false) {
                  std::cerr << format(gettext("ERROR: Invalid netmask %s!"), argv[optind + 1]) << "\n";
                  exit(1);
               }
            }
         }
         else {
            // ------ No netmask or prefix => use default for convenience ---
            prefix = readPrefix((address.sa.sa_family == AF_INET) ? "32" : "128", address, netmask);
            assert(prefix >= 0);
         }
      }
      else {
         // ------ No netmask or prefix => use default for convenience ------
         prefix = readPrefix((address.sa.sa_family == AF_INET) ? "32" : "128", address, netmask);
         assert(prefix >= 0);
      }
   }


   // ====== Get prefix length ==============================================
   prefix = getPrefixLength(netmask);
   if(prefix < 0) {
      char addressString[64];
      address2string(&netmask.sa, addressString, sizeof(addressString), false, false);
      std::cerr << format(gettext("ERROR: Invalid netmask %s!"), addressString) << "\n";
      exit(1);
   }
   if(netmask.sa.sa_family != address.sa.sa_family) {
      char addressString[64];
      address2string(&netmask.sa, addressString, sizeof(addressString), false, false);
      std::cerr << format(gettext("ERROR: Incompatible netmask %s!"), addressString) << "\n";
      exit(1);
   }


   // ====== Unique Local IPv4 address generation ===========================
   if(uniqueLocal > 0) {
      generateUniqueLocal(address, (uniqueLocal > 1));
   }


   // ====== Calculate network address, hosts, etc. =========================
   network   = address & netmask;
   broadcast = network | (~netmask);
   wildcard  = ~netmask;
   if(isIPv4(address)) {
      reservedHosts = 2;
      hostBits      = 32 - prefix;
      host1         = network + 1;
      host2         = broadcast - 1;
      if(prefix >= 31) {   // Special case for Point-to-Point links
         reservedHosts = 0;
         host1 = network;
         host2 = broadcast;
      }
   }
   else {
      reservedHosts = 1;
      hostBits      = 128 - prefix;
      if(prefix < 128) {
         host1    = network + 1;
         host2    = broadcast;   // There is no broadcast address for IPv6!
      }
      else {
         reservedHosts = 0;
         host1         = network;
         host2         = broadcast;   // There is no broadcast address for IPv6!
      }
   }
#if defined(__SIZEOF_INT128__)
   maxHosts = (unsigned __int128)pow(2.0, (double)hostBits) - reservedHosts;
#else
   if(hostBits <= 64) {
      maxHosts = (unsigned long long)pow(2.0, (double)hostBits) - reservedHosts;
   }
   else {
      maxHosts = 0;   // Not enough accuracy for such a large number!
   }
#endif


   // ====== Print results ==================================================
   std::cout << format(gettext("%-14s"), gettext("Address")) << " = "
             << address << "\n";
   printAddressBinary(std::cout, address, prefix, colourMode,
                      (format(gettext("%-14s"), " ") + "      ").c_str());
   std::cout << format(gettext("%-14s"), gettext("Network")) << " = "
             << network << " / " << prefix << "\n"
             << format(gettext("%-14s"), gettext("Netmask")) << " = "
             << netmask << "\n";
   if(isIPv4(address)) {
      std::cout << format(gettext("%-14s"), gettext("Broadcast")) << " = ";
      if(reservedHosts == 2) {
         std::cout << broadcast;
      }
      else {
         std::cout << gettext("not needed on Point-to-Point links");
      }
      std::cout << "\n";
   }
   std::cout << format(gettext("%-14s"), gettext("Wildcard Mask"))
             << " = " << wildcard << "\n";
   if(isIPv4(address)) {
      char hex[16];
      snprintf(hex, sizeof(hex), "%08X", ntohl(address.in.sin_addr.s_addr));
      std::cout << format(gettext("%-14s"), gettext("Hex. Address"))
                << " = " << hex << "\n";
   }
   std::cout << format(gettext("%-14s"), gettext("Host Bits")) << " = "
             << hostBits << "\n";
   if(!isMulticast(address)) {
      char maxHostsString[128];
      if(maxHosts > 0) {
#if defined(__SIZEOF_INT128__)
         snprintf(maxHostsString, sizeof(maxHostsString),
                  "%s   (2^%u - %u)", toString(maxHosts).c_str(), hostBits, reservedHosts);
#else
         snprintf(maxHostsString, sizeof(maxHostsString),
                  "%llu   (2^%u - %u)", maxHosts, hostBits, reservedHosts);
#endif
      }
      else {
         snprintf(maxHostsString, sizeof(maxHostsString),
                  "2^%u - %u", hostBits, reservedHosts);
      }
      std::cout << format(gettext("%-14s"), gettext("Max. Hosts")) << " = "
                << maxHostsString << "\n"
                << format(gettext("%-14s"), gettext("Host Range")) << " = { "
                << host1 << " - " << host2 << " }" << "\n";
   }


   // ====== Properties =====================================================
   printAddressProperties(std::cout, address, netmask, prefix, network, broadcast, colourMode);


   // ====== GeoIP ==========================================================
#ifdef HAVE_MAXMINDDB
   MMDB_s               mmdb;
   MMDB_lookup_result_s mmdbLookupResult;
   MMDB_entry_data_s    mmdbEntryData;
   int                  mmdbErrorCode;
   int                  mmdbStatusCode;

   // ------ ASN Lookup -----------------------------------------------------
   if(!noGeoIPLookup) {
      const std::string mmdbASNDatabasePath = find_mmdb_path("GeoLite2-ASN.mmdb");
      if(!mmdbASNDatabasePath.empty()) {
         mmdbStatusCode = MMDB_open(mmdbASNDatabasePath.c_str(), MMDB_MODE_MMAP, &mmdb);
         if(mmdbStatusCode == MMDB_SUCCESS) {
            mmdbLookupResult = MMDB_lookup_sockaddr(&mmdb, &address.sa, &mmdbErrorCode);
            if(mmdbLookupResult.found_entry) {
               std::string organisation = gettext("Unknown");
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "autonomous_system_organization", nullptr) == MMDB_SUCCESS) &&
                   (mmdbEntryData.has_data) ) {
                  organisation = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }
               std::cout << format("%-14s = ", gettext("GeoIP AS Info")) << organisation << "\n";
            }
            MMDB_close(&mmdb);
         }
      }
   }

   // ------ Country and City Lookup ----------------------------------------
   if(!noGeoIPLookup) {
      const std::string mmdbCityDatabasePath = find_mmdb_path("GeoLite2-City.mmdb");
      if(!mmdbCityDatabasePath.empty()) {
         mmdbStatusCode = MMDB_open(mmdbCityDatabasePath.c_str(), MMDB_MODE_MMAP, &mmdb);
         if(mmdbStatusCode == MMDB_SUCCESS) {
            mmdbLookupResult = MMDB_lookup_sockaddr(&mmdb, &address.sa, &mmdbErrorCode);
            if(mmdbLookupResult.found_entry) {

               // ------ Country --------------------------------------------
               std::string country = gettext("Unknown");
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "country", "names", "en", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  country = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }

               std::string code = "??";
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "country", "iso_code", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  code = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }

               std::cout << format("%-14s = ", gettext("GeoIP Country"))
                        << country << " (" << code << ")\n";

               // ------ Region and City ------------------------------------
               std::string postalCode;
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "postal", "code", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  postalCode = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }

               std::string city = gettext("Unknown");
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "city", "names", "en", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  city = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }

               std::string region = gettext("Unknown");
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "subdivisions", "0", "names", "en", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  region = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }

               std::string timeZone;
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "location", "time_zone", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  timeZone = std::string(mmdbEntryData.utf8_string, mmdbEntryData.data_size);
               }

               double latitude = 0.0;
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "location", "latitude", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  latitude = mmdbEntryData.double_value;
               }

               double longitude = 0.0;
               if( (MMDB_get_value(&mmdbLookupResult.entry, &mmdbEntryData,
                                 "location", "longitude", nullptr) == MMDB_SUCCESS) &&
                  (mmdbEntryData.has_data) ) {
                  longitude = mmdbEntryData.double_value;
               }

               std::cout << format("%-14s = ", gettext("GeoIP Region"))
                        << postalCode << (!postalCode.empty() ? " " : "")
                        << city << ", " << region
                        << " ("
                        << std::fabs(latitude)  << "°" << ((latitude >= 0.0)  ? "N" : "S") << ", "
                        << std::fabs(longitude) << "°" << ((longitude >= 0.0) ? "E" : "W")
                        << (!timeZone.empty() ? ", " : "") << timeZone
                        << ")\n";
            }
            MMDB_close(&mmdb);
         }
      }
   }
#endif


   // ====== Reverse lookup =================================================
   if(noReverseLookup == false) {
      if(isatty(fileno(stdout))) {
         std::cout << gettext("Performing reverse DNS lookup ...");
         std::cout.flush();
      }
      char hostname[NI_MAXHOST];
      int error = getnameinfo(&address.sa,
                              (address.sa.sa_family == AF_INET6) ?
                                 sizeof(sockaddr_in6) : sizeof(sockaddr_in),
                              hostname, sizeof(hostname),
                              nullptr, 0,
#ifdef NI_IDN
                              NI_NAMEREQD|NI_IDN
#else
                              NI_NAMEREQD
#endif
                             );
      if(isatty(fileno(stdout))) {
         std::cout << "\r\x1b[K";
      }
      std::cout << format("%-14s = ", gettext("DNS Hostname"));
      std::cout.flush();
      if(error == 0) {
#ifndef NI_IDN
         char* utf8hostname = nullptr;
         if(idn2_to_unicode_8z8z(hostname, &utf8hostname, 0) == IDN2_OK) {
            std::cout << utf8hostname << "\n";
            idn2_free(utf8hostname);
         }
         else {
#endif
            std::cout << hostname << "\n";
#ifndef NI_IDN
         }
#endif
      }
      else {
         std::cout << "(" << gai_strerror(error) << ")" << "\n";
      }
   }
}
