/*
 *  Copyright (C) 2005-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include "Network.h"
#include "ServiceBroker.h"
#include "messaging/ApplicationMessenger.h"
#include "network/NetworkServices.h"
#include "settings/Settings.h"
#include "settings/SettingsComponent.h"
#include "utils/log.h"
#ifdef TARGET_WINDOWS
#include "platform/win32/WIN32Util.h"
#include "utils/CharsetConverter.h"
#endif
#include "utils/StringUtils.h"
#include "utils/XTimeUtils.h"

using namespace KODI::MESSAGING;

/* slightly modified in_ether taken from the etherboot project (http://sourceforge.net/projects/etherboot) */
bool in_ether (const char *bufp, unsigned char *addr)
{
  if (strlen(bufp) != 17)
    return false;

  char c;
  const char *orig;
  unsigned char *ptr = addr;
  unsigned val;

  int i = 0;
  orig = bufp;

  while ((*bufp != '\0') && (i < 6))
  {
    val = 0;
    c = *bufp++;

    if (isdigit(c))
      val = c - '0';
    else if (c >= 'a' && c <= 'f')
      val = c - 'a' + 10;
    else if (c >= 'A' && c <= 'F')
      val = c - 'A' + 10;
    else
      return false;

    val <<= 4;
    c = *bufp;
    if (isdigit(c))
      val |= c - '0';
    else if (c >= 'a' && c <= 'f')
      val |= c - 'a' + 10;
    else if (c >= 'A' && c <= 'F')
      val |= c - 'A' + 10;
    else if (c == ':' || c == '-' || c == 0)
      val >>= 4;
    else
      return false;

    if (c != 0)
      bufp++;

    *ptr++ = (unsigned char) (val & 0377);
    i++;

    if (*bufp == ':' || *bufp == '-')
      bufp++;
  }

  if (bufp - orig != 17)
    return false;

  return true;
}

CNetworkBase::CNetworkBase() :
  m_services(new CNetworkServices())
{
  CApplicationMessenger::GetInstance().PostMsg(TMSG_NETWORKMESSAGE, SERVICES_UP, 0);
}

CNetworkBase::~CNetworkBase()
{
  CApplicationMessenger::GetInstance().PostMsg(TMSG_NETWORKMESSAGE, SERVICES_DOWN, 0);
}

int CNetworkBase::ParseHex(char *str, unsigned char *addr)
{
   int len = 0;

   while (*str)
   {
      int tmp;
      if (str[1] == 0)
         return -1;
      if (sscanf(str, "%02x", (unsigned int *)&tmp) != 1)
         return -1;
      addr[len] = tmp;
      len++;
      str += 2;
   }

   return len;
}

bool CNetworkBase::GetHostName(std::string& hostname)
{
  char hostName[128];
  if (gethostname(hostName, sizeof(hostName)))
    return false;

#ifdef TARGET_WINDOWS
  std::string hostStr;
  g_charsetConverter.systemToUtf8(hostName, hostStr);
  hostname = hostStr;
#else
  hostname = hostName;
#endif
  return true;
}

bool CNetworkBase::IsLocalHost(const std::string& hostname)
{
  if (hostname.empty())
    return false;

  if (StringUtils::StartsWith(hostname, "127.")
      || (hostname == "::1")
      || StringUtils::EqualsNoCase(hostname, "localhost"))
    return true;

  std::string myhostname;
  if (GetHostName(myhostname)
      && StringUtils::EqualsNoCase(hostname, myhostname))
    return true;

  std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
  std::vector<CNetworkInterface*>::const_iterator iter = ifaces.begin();
  while (iter != ifaces.end())
  {
    CNetworkInterface* iface = *iter;
    if (iface && iface->GetCurrentIPAddress() == hostname)
      return true;

     ++iter;
  }

  return false;
}

CNetworkInterface* CNetworkBase::GetFirstConnectedInterface()
{
  CNetworkInterface* fallbackInterface = nullptr;
  for (CNetworkInterface* iface : GetInterfaceList())
  {
    if (iface && iface->IsConnected())
    {
      if (!iface->GetCurrentDefaultGateway().empty())
        return iface;
      else if (fallbackInterface == nullptr)
        fallbackInterface = iface;
    }
  }

  return fallbackInterface;
}

bool CNetworkBase::HasInterfaceForIP(unsigned long address)
{
   unsigned long subnet;
   unsigned long local;
   std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
   std::vector<CNetworkInterface*>::const_iterator iter = ifaces.begin();
   while (iter != ifaces.end())
   {
      CNetworkInterface* iface = *iter;
      if (iface && iface->IsConnected())
      {
         subnet = ntohl(inet_addr(iface->GetCurrentNetmask().c_str()));
         local = ntohl(inet_addr(iface->GetCurrentIPAddress().c_str()));
         if( (address & subnet) == (local & subnet) )
            return true;
      }
      ++iter;
   }

   return false;
}

bool CNetworkBase::IsAvailable(void)
{
  std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
  return (ifaces.size() != 0);
}

bool CNetworkBase::IsConnected()
{
   return GetFirstConnectedInterface() != NULL;
}

void CNetworkBase::NetworkMessage(EMESSAGE message, int param)
{
  switch( message )
  {
    case SERVICES_UP:
      CLog::Log(LOGDEBUG, "%s - Starting network services",__FUNCTION__);
      m_services->Start();
      break;

    case SERVICES_DOWN:
      CLog::Log(LOGDEBUG, "%s - Signaling network services to stop",__FUNCTION__);
      m_services->Stop(false); // tell network services to stop, but don't wait for them yet
      CLog::Log(LOGDEBUG, "%s - Waiting for network services to stop",__FUNCTION__);
      m_services->Stop(true); // wait for network services to stop
      break;
  }
}

bool CNetworkBase::WakeOnLan(const char* mac)
{
  int i, j, packet;
  unsigned char ethaddr[8];
  unsigned char buf [128];
  unsigned char *ptr;

  // Fetch the hardware address
  if (!in_ether(mac, ethaddr))
  {
    CLog::Log(LOGERROR, "%s - Invalid hardware address specified (%s)", __FUNCTION__, mac);
    return false;
  }

  // Setup the socket
  if ((packet = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
  {
    CLog::Log(LOGERROR, "%s - Unable to create socket (%s)", __FUNCTION__, strerror (errno));
    return false;
  }

  // Set socket options
  struct sockaddr_in saddr;
  saddr.sin_family = AF_INET;
  saddr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
  saddr.sin_port = htons(9);

  unsigned int value = 1;
  if (setsockopt (packet, SOL_SOCKET, SO_BROADCAST, (char*) &value, sizeof( unsigned int ) ) == SOCKET_ERROR)
  {
    CLog::Log(LOGERROR, "%s - Unable to set socket options (%s)", __FUNCTION__, strerror (errno));
    closesocket(packet);
    return false;
  }

  // Build the magic packet (6 x 0xff + 16 x MAC address)
  ptr = buf;
  for (i = 0; i < 6; i++)
    *ptr++ = 0xff;

  for (j = 0; j < 16; j++)
    for (i = 0; i < 6; i++)
      *ptr++ = ethaddr[i];

  // Send the magic packet
  if (sendto (packet, (char *)buf, 102, 0, (struct sockaddr *)&saddr, sizeof (saddr)) < 0)
  {
    CLog::Log(LOGERROR, "%s - Unable to send magic packet (%s)", __FUNCTION__, strerror (errno));
    closesocket(packet);
    return false;
  }

  closesocket(packet);
  CLog::Log(LOGINFO, "%s - Magic packet send to '%s'", __FUNCTION__, mac);
  return true;
}

// ping helper
static const char* ConnectHostPort(SOCKET soc, const struct sockaddr_in& addr, struct timeval& timeOut, bool tryRead)
{
  // set non-blocking
#ifdef TARGET_WINDOWS
  u_long nonblocking = 1;
  int result = ioctlsocket(soc, FIONBIO, &nonblocking);
#else
  int result = fcntl(soc, F_SETFL, fcntl(soc, F_GETFL) | O_NONBLOCK);
#endif

  if (result != 0)
    return "set non-blocking option failed";

  result = connect(soc, (const struct sockaddr *)&addr, sizeof(addr)); // non-blocking connect, will fail ..

  if (result < 0)
  {
#ifdef TARGET_WINDOWS
    if (WSAGetLastError() != WSAEWOULDBLOCK)
#else
    if (errno != EINPROGRESS)
#endif
      return "unexpected connect fail";

    { // wait for connect to complete
      fd_set wset;
      FD_ZERO(&wset);
      FD_SET(soc, &wset);

      result = select(FD_SETSIZE, 0, &wset, 0, &timeOut);
    }

    if (result < 0)
      return "select fail";

    if (result == 0) // timeout
      return ""; // no error

    { // verify socket connection state
      int err_code = -1;
      socklen_t code_len = sizeof (err_code);

      result = getsockopt(soc, SOL_SOCKET, SO_ERROR, (char*) &err_code, &code_len);

      if (result != 0)
        return "getsockopt fail";

      if (err_code != 0)
        return ""; // no error, just not connected
    }
  }

  if (tryRead)
  {
    fd_set rset;
    FD_ZERO(&rset);
    FD_SET(soc, &rset);

    result = select(FD_SETSIZE, &rset, 0, 0, &timeOut);

    if (result > 0)
    {
      char message [32];

      result = recv(soc, message, sizeof(message), 0);
    }

    if (result == 0)
      return ""; // no reply yet

    if (result < 0)
      return "recv fail";
  }

  return 0; // success
}

bool CNetworkBase::PingHost(unsigned long ipaddr, unsigned short port, unsigned int timeOutMs, bool readability_check)
{
  if (port == 0) // use icmp ping
    return PingHost (ipaddr, timeOutMs);

  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);
  addr.sin_addr.s_addr = ipaddr;

  SOCKET soc = socket(AF_INET, SOCK_STREAM, 0);

  const char* err_msg = "invalid socket";

  if (soc != INVALID_SOCKET)
  {
    struct timeval tmout;
    tmout.tv_sec = timeOutMs / 1000;
    tmout.tv_usec = (timeOutMs % 1000) * 1000;

    err_msg = ConnectHostPort (soc, addr, tmout, readability_check);

    (void) closesocket (soc);
  }

  if (err_msg && *err_msg)
  {
#ifdef TARGET_WINDOWS
    std::string sock_err = CWIN32Util::WUSysMsg(WSAGetLastError());
#else
    std::string sock_err = strerror(errno);
#endif

    CLog::Log(LOGERROR, "%s(%s:%d) - %s (%s)", __FUNCTION__, inet_ntoa(addr.sin_addr), port, err_msg, sock_err.c_str());
  }

  return err_msg == 0;
}

//creates, binds and listens tcp sockets on the desired port. Set bindLocal to
//true to bind to localhost only.
std::vector<SOCKET> CreateTCPServerSocket(const int port, const bool bindLocal, const int backlog, const char *callerName)
{
#ifdef WINSOCK_VERSION
  int yes = 1;
#else
  unsigned int yes = 1;
#endif

  std::vector<SOCKET> sockets;
  struct addrinfo* results = nullptr;

  std::string sPort = StringUtils::Format("%d", port);
  struct addrinfo hints = { 0 };
  hints.ai_family = PF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;
  hints.ai_protocol = 0;

  int error = getaddrinfo(bindLocal ? "localhost" : nullptr, sPort.c_str(), &hints, &results);
  if (error != 0)
    return sockets;

  for (struct addrinfo* result = results; result != nullptr; result = result->ai_next)
  {
    SOCKET sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (sock == INVALID_SOCKET)
      continue;

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof(yes));
    setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char*>(&yes), sizeof(yes));

    if (bind(sock, result->ai_addr, result->ai_addrlen) != 0)
    {
      closesocket(sock);
      CLog::Log(LOGDEBUG, "%s Server: Failed to bind %s serversocket", callerName, result->ai_family == AF_INET6 ? "IPv6" : "IPv4");
      continue;
    }

    if (listen(sock, backlog) == 0)
      sockets.push_back(sock);
    else
    {
      closesocket(sock);
      CLog::Log(LOGERROR, "%s Server: Failed to set listen", callerName);
    }
  }
  freeaddrinfo(results);

  if (sockets.empty())
    CLog::Log(LOGERROR, "%s Server: Failed to create serversocket(s)", callerName);

  return sockets;
}

void CNetworkBase::WaitForNet()
{
  const int timeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_WAITFORNETWORK);
  if (timeout <= 0)
    return; // wait for network is disabled

  // check if we have at least one network interface to wait for
  if (!IsAvailable())
    return;

  CLog::Log(LOGINFO, "%s: Waiting for a network interface to come up (Timeout: %d s)", __FUNCTION__,
            timeout);

  const static int intervalMs = 200;
  const int numMaxTries = (timeout * 1000) / intervalMs;

  for(int i=0; i < numMaxTries; ++i)
  {
    if (i > 0)
      KODI::TIME::Sleep(intervalMs);

    if (IsConnected())
    {
      CLog::Log(LOGINFO, "%s: A network interface is up after waiting %d ms", __FUNCTION__,
                i * intervalMs);
      return;
    }
  }

  CLog::Log(LOGINFO, "%s: No network interface did come up within %d s... Giving up...",
            __FUNCTION__, timeout);
}

std::string CNetworkBase::GetIpStr(const struct sockaddr* sa)
{
  std::string result;
  if (!sa)
    return result;

  char buffer[INET6_ADDRSTRLEN] = { 0 };
  switch (sa->sa_family)
  {
  case AF_INET:
    inet_ntop(AF_INET, &reinterpret_cast<const struct sockaddr_in *>(sa)->sin_addr, buffer, INET_ADDRSTRLEN);
    break;
  case AF_INET6:
    inet_ntop(AF_INET6, &reinterpret_cast<const struct sockaddr_in6 *>(sa)->sin6_addr, buffer, INET6_ADDRSTRLEN);
    break;
  default:
    return result;
  }

  result = buffer;
  return result;
}

std::string CNetworkBase::GetMaskByPrefixLength(uint8_t prefixLength)
{
  if (prefixLength > 32)
    return "";

  struct sockaddr_in sa;
  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = htonl(~((1 << (32u - prefixLength)) - 1));;
  return CNetworkBase::GetIpStr(reinterpret_cast<struct sockaddr*>(&sa));
}
