/*
 *
 * Conky, a system monitor, based on torsmo
 *
 * Any original torsmo code is licensed under the BSD license
 *
 * All code written since the fork of torsmo is licensed under the GPL
 *
 * Please see COPYING for details
 *
 * Copyright (c) 2004, Hannu Saransaari and Lauri Hakkarainen
 * Copyright (c) 2005-2024 Brenden Matthews, Philip Kovacs, et. al.
 *	(see AUTHORS)
 * All rights reserved.
 *
 * 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/>.
 *
 */

#include "net_stat.h"
#include <ctype.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include "../../conky.h"
#include "../../content/specials.h"
#include "../../content/text_object.h"
#include "../../logging.h"
#include "net/if.h"
#if defined(__sun)
#include <sys/sockio.h>
#endif
#if defined(__HAIKU__)
#include <sys/sockio.h>
#define IFF_RUNNING IFF_LINK
#endif
#ifndef SOCK_CLOEXEC
#define SOCK_CLOEXEC O_CLOEXEC
#endif /* SOCK_CLOEXEC */

#if defined(__linux__)
#include "../os/linux.h"
#else
static char e_iface[50] = "empty";
static char interfaces_arr[MAX_NET_INTERFACES][64] = {""};
#endif /* __linux__ */

/* network interface stuff */

enum if_up_strictness_ { IFUP_UP, IFUP_LINK, IFUP_ADDR };

template <>
conky::lua_traits<if_up_strictness_>::Map
    conky::lua_traits<if_up_strictness_>::map = {
        {"up", IFUP_UP}, {"link", IFUP_LINK}, {"address", IFUP_ADDR}};

static conky::simple_config_setting<if_up_strictness_> if_up_strictness(
    "if_up_strictness", IFUP_UP, true);
/**
 * global array of structs containing network statistics for each interface
 **/
struct net_stat netstats[MAX_NET_INTERFACES];
struct net_stat foo_netstats;

/**
 * Returns pointer to specified interface in netstats array.
 * If not found then add the specified interface to the array.
 * The added interface will have all its members initialized to 0,
 * because clear_net_stats() is called from main() in conky.cc!
 *
 * @param[in] dev  device / interface name. Silently ignores char * == nullptr
 **/
struct net_stat *get_net_stat(const char *dev, void * /*free_at_crash1*/,
                              void * /*free_at_crash2*/) {
  unsigned int i;

  if (dev == nullptr) { return nullptr; }

  /* find interface stat */
  for (i = 0; i < MAX_NET_INTERFACES; i++) {
    if ((netstats[i].dev != nullptr) && strcmp(netstats[i].dev, dev) == 0) {
      return &netstats[i];
    }
  }

  /* wasn't found? add it */
  for (i = 0; i < MAX_NET_INTERFACES; i++) {
    if (netstats[i].dev == nullptr) {
      netstats[i].dev = strndup(dev, text_buffer_size.get(*state));
      /* initialize last_read_recv and last_read_trans to -1 denoting
       * that they were never read before */
      netstats[i].last_read_recv = -1;
      netstats[i].last_read_trans = -1;
      return &netstats[i];
    }
  }

  clear_net_stats(&foo_netstats);
  foo_netstats.dev = strndup(dev, text_buffer_size.get(*state));
  /* initialize last_read_recv and last_read_trans to -1 denoting
   * that they were never read before */
  foo_netstats.last_read_recv = -1;
  foo_netstats.last_read_trans = -1;
  return &foo_netstats;
}

void parse_net_stat_arg(struct text_object *obj, const char *arg,
                        void *free_at_crash) {
#ifdef BUILD_IPV6
  bool shownetmask = false;
  bool showscope = false;
#endif               /* BUILD_IPV6 */
  char nextarg[21];  // longest arg possible is a devname (max 20 chars)
  int i = 0;
  struct net_stat *netstat = nullptr;
  long int x = 0;
  unsigned int found = 0;
  char *arg_ptr = const_cast<char *>(arg);
  char buf[64];
  char *buf_ptr = buf;

  if (arg == nullptr) { arg = DEFAULTNETDEV; }

  if (0 == (strcmp("$gw_iface", arg)) || 0 == (strcmp("${gw_iface}", arg))) {
#if defined(__linux__)
    update_gateway_info();
#endif
    arg = e_iface;
  }

  if (0 == strncmp(arg, "${iface", 7)) {
    if (nullptr != arg_ptr) {
      for (; *arg_ptr; arg_ptr++) {
        if (isdigit(static_cast<unsigned char>(*arg_ptr))) {
          *buf_ptr++ = *arg_ptr;
          found = 1;
        }
      }
    }
    if (1U == found) {
      *buf_ptr = '\0';
      x = strtol(buf, (char **)NULL, 10);
      if (63L > x) { arg = interfaces_arr[x]; }
    }
  }

  while (sscanf(arg + i, " %20s", nextarg) == 1) {
#ifdef BUILD_IPV6
    if (strcmp(nextarg, "-n") == 0 || strcmp(nextarg, "--netmask") == 0) {
      shownetmask = true;
    } else if (strcmp(nextarg, "-s") == 0 || strcmp(nextarg, "--scope") == 0) {
      showscope = true;
    } else if (nextarg[0] == '-') {  // multiple flags in 1 arg
      for (int j = 1; nextarg[j] != 0; j++) {
        if (nextarg[j] == 'n') { shownetmask = true; }
        if (nextarg[j] == 's') { showscope = true; }
      }
    } else {
#endif /* BUILD_IPV6 */
      netstat = get_net_stat(nextarg, obj, free_at_crash);
#ifdef BUILD_IPV6
    }
#endif                     /* BUILD_IPV6 */
    i += strlen(nextarg);  // skip this arg
    while (
        !((isspace(static_cast<unsigned char>(arg[i])) != 0) || arg[i] == 0)) {
      i++;  // and skip the spaces in front of it
    }
  }
  if (netstat == nullptr) {
    netstat = get_net_stat(DEFAULTNETDEV, obj, free_at_crash);
  }

#ifdef BUILD_IPV6
  netstat->v6show_nm = shownetmask;
  netstat->v6show_sc = showscope;
#endif /* BUILD_IPV6 */
  obj->data.opaque = netstat;
}

void parse_net_stat_bar_arg(struct text_object *obj, const char *arg,
                            void *free_at_crash) {
  if (arg == nullptr) {
    // default to DEFAULTNETDEV
    char *buf = strndup(DEFAULTNETDEV, text_buffer_size.get(*state));
    obj->data.opaque = get_net_stat(buf, obj, free_at_crash);
    free(buf);
    return;
  }

  /* The interface name and bar dimensions may appear in either order.
   * scan_command() yields the leading interface name for the documented
   * "(net) (height),(width)" order (e.g. "wlp3s0 3,260", mirroring cpubar and
   * the net speed graphs), and returns nullptr when the argument starts with a
   * digit, i.e. when the "(height),(width)" spec comes first. Parsing the
   * interface out first lets scan_bar see the dimensions regardless of order,
   * so small heights are no longer dropped. */
  auto [dev, skip] = scan_command(arg);
  const char *rest = scan_bar(obj, arg + skip, 1);
  if (dev == nullptr) { dev = scan_command(rest).first; }

  if (dev != nullptr && dev[0] != '\0') {
    obj->data.opaque = get_net_stat(dev, obj, free_at_crash);
  } else {
    char *buf = strndup(DEFAULTNETDEV, text_buffer_size.get(*state));
    obj->data.opaque = get_net_stat(buf, obj, free_at_crash);
    free(buf);
  }
  free(dev);
}

void print_downspeed(struct text_object *obj, char *p,
                     unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  human_readable(ns->recv_speed, p, p_max_size);
}

void print_downspeedf(struct text_object *obj, char *p,
                      unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  spaced_print(p, p_max_size, "%.1f", 8, ns->recv_speed / 1024.0);
}

void print_upspeed(struct text_object *obj, char *p, unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  human_readable(ns->trans_speed, p, p_max_size);
}

void print_upspeedf(struct text_object *obj, char *p, unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  spaced_print(p, p_max_size, "%.1f", 8, ns->trans_speed / 1024.0);
}

void print_totaldown(struct text_object *obj, char *p,
                     unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  human_readable(ns->recv, p, p_max_size);
}

void print_totalup(struct text_object *obj, char *p, unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  human_readable(ns->trans, p, p_max_size);
}

void print_addr(struct text_object *obj, char *p, unsigned int p_max_size) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  if (ns == nullptr) { return; }

  if ((ns->addr.sa_data[2] & 255) == 0 && (ns->addr.sa_data[3] & 255) == 0 &&
      (ns->addr.sa_data[4] & 255) == 0 && (ns->addr.sa_data[5] & 255) == 0) {
    snprintf(p, p_max_size, "%s", "No Address");
  } else {
    snprintf(p, p_max_size, "%u.%u.%u.%u", ns->addr.sa_data[2] & 255,
             ns->addr.sa_data[3] & 255, ns->addr.sa_data[4] & 255,
             ns->addr.sa_data[5] & 255);
  }
}

#ifdef __linux__
void print_addrs(struct text_object *obj, char *p, unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  if (0 != ns->addrs[0] && strlen(ns->addrs) > 2) {
    ns->addrs[strlen(ns->addrs) - 2] = 0; /* remove ", " from end of string */
    strncpy(p, ns->addrs, p_max_size);
  } else {
    strncpy(p, "0.0.0.0", p_max_size);
  }
}

#ifdef BUILD_IPV6
void print_v6addrs(struct text_object *obj, char *p, unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;
  char tempaddress[INET6_ADDRSTRLEN];
  struct v6addr *current_v6 = ns->v6addrs;

  if (p_max_size == 0) return;
  if (!ns->v6addrs) {
    snprintf(p, p_max_size, "%s", "No Address");
    return;
  }
  *p = 0;
  while (current_v6) {
    inet_ntop(AF_INET6, &(current_v6->addr), tempaddress, INET6_ADDRSTRLEN);
    strncat(p, tempaddress, p_max_size);
    // netmask
    if (ns->v6show_nm) {
      char netmaskstr[5];  // max 5 chars (/128 + null-terminator)
      snprintf(netmaskstr, sizeof(netmaskstr), "/%u", current_v6->netmask);
      strncat(p, netmaskstr, p_max_size);
    }
    // scope
    if (ns->v6show_sc) {
      char scopestr[4];
      snprintf(scopestr, sizeof(scopestr), "(%c)", current_v6->scope);
      strncat(p, scopestr, p_max_size);
    }
    // next (or last) address
    current_v6 = current_v6->next;
    if (current_v6) strncat(p, ", ", p_max_size);
  }
}
#endif /* BUILD_IPV6 */

#endif /* __linux__ */

#ifdef BUILD_GUI

/**
 * This function is called periodically to update the download and upload graphs
 *
 * - evaluates argument strings like 'eth0 50,120 #FFFFFF #FF0000 0 -l'
 * - sets the obj->data.opaque pointer to the interface specified
 *
 * @param[out] obj  struct which will hold evaluated arguments
 * @param[in]  arg  argument string to evaluate
 **/
void parse_net_stat_graph_arg(struct text_object *obj, const char *arg,
                              void *free_at_crash) {
  /* scan arguments and get interface name back */
  auto [buf, skip] = scan_command(arg);

  graph_data_key key = graph_parent_obj_key;
  if (buf == nullptr) {
    key = fmt::format("net:{}", DEFAULTNETDEV);
  } else if (strcmp("$gw_iface", buf) != 0 && strcmp("${gw_iface}", buf) != 0) {
    key = fmt::format("net:{}", buf);
  } /* gw_iface resolves dynamically; fall back to obj-address keying */

  scan_graph(obj, arg + skip, 0, TRUE, key);

  // default to DEFAULTNETDEV
  if (buf != nullptr) {
#if defined(__linux__)
    if (0 == (strcmp("$gw_iface", buf)) || 0 == (strcmp("${gw_iface}", buf))) {
      update_gateway_info();
      obj->data.opaque = get_net_stat(e_iface, obj, free_at_crash);
      free(buf);
      return;
    }
#endif
    obj->data.opaque = get_net_stat(buf, obj, free_at_crash);
    free(buf);
    return;
  }
  obj->data.opaque = get_net_stat(DEFAULTNETDEV, obj, free_at_crash);
}

/**
 * returns the download speed in B/s for the interface referenced by obj
 *
 * @param[in] obj struct containing a member data, which is a struct
 *                containing a void * to a net_stat struct
 **/
double downspeedgraphval(struct text_object *obj) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  return (ns != nullptr ? ns->recv_speed : 0);
}

double upspeedgraphval(struct text_object *obj) {
  auto *ns = static_cast<struct net_stat *>(obj->data.opaque);

  return (ns != nullptr ? ns->trans_speed : 0);
}
#endif /* BUILD_GUI */

#ifdef BUILD_WLAN
void print_wireless_essid(struct text_object *obj, char *p,
                          unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) {
    for (unsigned int i = 0; *(netstats[i].dev) != 0; i++) {
      if (*(netstats[i].essid) != 0) {
        snprintf(p, p_max_size, "%s", netstats[i].essid);
        return;
      }
    }
    return;
  }

  snprintf(p, p_max_size, "%s", ns->essid);
}
void print_wireless_mode(struct text_object *obj, char *p,
                         unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  snprintf(p, p_max_size, "%s", ns->mode);
}
void print_wireless_channel(struct text_object *obj, char *p,
                            unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  if (ns->channel != 0) {
    snprintf(p, p_max_size, "%i", ns->channel);
  } else {
    snprintf(p, p_max_size, "%s", "/");
  }
}
void print_wireless_frequency(struct text_object *obj, char *p,
                              unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  if (ns->freq[0] != 0) {
    snprintf(p, p_max_size, "%s", ns->freq);
  } else {
    snprintf(p, p_max_size, "/");
  }
}
void print_wireless_bitrate(struct text_object *obj, char *p,
                            unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  snprintf(p, p_max_size, "%s", ns->bitrate);
}
void print_wireless_ap(struct text_object *obj, char *p,
                       unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  snprintf(p, p_max_size, "%s", ns->ap);
}
void print_wireless_link_qual(struct text_object *obj, char *p,
                              unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  spaced_print(p, p_max_size, "%d", 4, ns->link_qual);
}
void print_wireless_link_qual_max(struct text_object *obj, char *p,
                                  unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  spaced_print(p, p_max_size, "%d", 4, ns->link_qual_max);
}
void print_wireless_link_qual_perc(struct text_object *obj, char *p,
                                   unsigned int p_max_size) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return;

  if (ns->link_qual_max > 0) {
    spaced_print(p, p_max_size, "%.0f", 5,
                 (double)ns->link_qual / ns->link_qual_max * 100);
  } else {
    spaced_print(p, p_max_size, "unk", 5);
  }
}
double wireless_link_barval(struct text_object *obj) {
  struct net_stat *ns = (struct net_stat *)obj->data.opaque;

  if (!ns) return 0;

  return (double)ns->link_qual / ns->link_qual_max;
}
#endif /* BUILD_WLAN */

/**
 * Clears the global array of net_stat structs which contains networks
 * statistics for every interface.
 **/
void clear_net_stats() {
#ifdef BUILD_IPV6
  struct v6addr *nextv6;
#endif /* BUILD_IPV6 */
  int i;
  for (i = 0; i < MAX_NET_INTERFACES; i++) {
    free_and_zero(netstats[i].dev);
#ifdef BUILD_IPV6
    while (netstats[i].v6addrs) {
      nextv6 = netstats[i].v6addrs;
      netstats[i].v6addrs = netstats[i].v6addrs->next;
      free_and_zero(nextv6);
    }
#endif /* BUILD_IPV6 */
  }
  memset(netstats, 0, sizeof(netstats));
}

void clear_net_stats(net_stat *in) {
#ifdef BUILD_IPV6
  struct v6addr *nextv6;
#endif /* BUILD_IPV6 */
  free_and_zero(in->dev);
#ifdef BUILD_IPV6
  while (in->v6addrs) {
    nextv6 = in->v6addrs;
    in->v6addrs = in->v6addrs->next;
    free_and_zero(nextv6);
  }
#endif /* BUILD_IPV6 */
}

void parse_if_up_arg(struct text_object *obj, const char *arg) {
  obj->data.opaque = strndup(arg, text_buffer_size.get(*state));
}

void free_if_up(struct text_object *obj) { free_and_zero(obj->data.opaque); }

/* We should check if this is ok with OpenBSD and NetBSD as well. */
int interface_up(struct text_object *obj) {
  int fd;
  struct ifreq ifr{};
  auto *dev = static_cast<char *>(obj->data.opaque);

  if (dev == nullptr) { return 0; }

#if defined(__APPLE__) && defined(__MACH__)
  if ((fd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
#else
  if ((fd = socket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)) < 0) {
#endif
    SYSTEM_ERR("could not create socket for network interface query");
    return 0;
  }
  strncpy(ifr.ifr_name, dev, IFNAMSIZ);
  if (ioctl(fd, SIOCGIFFLAGS, &ifr) != 0) {
    /* if device does not exist, treat like not up */
    if (errno != ENODEV && errno != ENXIO) {
      LOG_ERROR("SIOCGIFFLAGS: {}", strerror(errno));
    }
    goto END_FALSE;
  }

  if ((ifr.ifr_flags & IFF_UP) == 0) { /* iface is not up */
    goto END_FALSE;
  }
  if (if_up_strictness.get(*state) == IFUP_UP) { goto END_TRUE; }

#ifdef IFF_RUNNING
  if ((ifr.ifr_flags & IFF_RUNNING) == 0) { goto END_FALSE; }
#endif
  if (if_up_strictness.get(*state) == IFUP_LINK) { goto END_TRUE; }

  if (ioctl(fd, SIOCGIFADDR, &ifr) != 0) {
    LOG_ERROR("SIOCGIFADDR: {}", strerror(errno));
    goto END_FALSE;
  }
  if ((reinterpret_cast<struct sockaddr_in *>(&(ifr.ifr_addr)))
          ->sin_addr.s_addr != 0u) {
    goto END_TRUE;
  }

END_FALSE:
  close(fd);
  return 0;
END_TRUE:
  close(fd);
  return 1;
}

class _dns_data {
 public:
  _dns_data() = default;
  int nscount{0};
  char **ns_list{nullptr};
  void reset() {
    nscount = 0;
    ns_list = nullptr;
  }
};

static _dns_data dns_data;

void free_dns_data(struct text_object *obj) {
  int i;

  (void)obj;

  for (i = 0; i < dns_data.nscount; i++) { free(dns_data.ns_list[i]); }
  free(dns_data.ns_list);
  dns_data.reset();
}

int update_dns_data() {
  FILE *fp;
  char line[256];
  // static double last_dns_update = 0.0;

  /* maybe updating too often causes higher load because of /etc lying on a real
  FS if (current_update_time - last_dns_update < 10.0) return 0;

  last_dns_update = current_update_time;
  */

  free_dns_data(nullptr);

  if ((fp = fopen("/etc/resolv.conf", "re")) == nullptr) { return 0; }
  while (feof(fp) == 0) {
    if (fgets(line, 255, fp) == nullptr) { break; }
    if (strncmp(line, "nameserver ", 11) == 0) {
      line[strlen(line) - 1] = '\0';  // remove trailing newline
      dns_data.nscount++;
      dns_data.ns_list = static_cast<char **>(
          realloc(dns_data.ns_list, dns_data.nscount * sizeof(char *)));
      dns_data.ns_list[dns_data.nscount - 1] =
          strndup(line + 11, text_buffer_size.get(*state));
    }
  }
  fclose(fp);
  return 0;
}

void parse_nameserver_arg(struct text_object *obj, const char *arg) {
  obj->data.l = arg != nullptr ? strtol(arg, nullptr, 10) : 0;
}

void print_nameserver(struct text_object *obj, char *p,
                      unsigned int p_max_size) {
  if (dns_data.nscount > obj->data.l) {
    snprintf(p, p_max_size, "%s", dns_data.ns_list[obj->data.l]);
  }
}
