/*********************************************************************
 *        _       _         _
 *  _ __ | |_  _ | |  __ _ | |__   ___
 * | '__|| __|(_)| | / _` || '_ \ / __|
 * | |   | |_  _ | || (_| || |_) |\__ \
 * |_|    \__|(_)|_| \__,_||_.__/ |___/
 *
 * www.rt-labs.com
 * Copyright 2021 rt-labs AB, Sweden.
 *
 * This software is dual-licensed under GPLv3 and a commercial
 * license. See the file LICENSE.md distributed with this software for
 * full license information.
 ********************************************************************/

#define _GNU_SOURCE /* For asprintf() */

#include "app_utils.h"
#include "app_log.h"
#include "app_gsdml.h"
#include "sampleapp_common.h"
#include "osal.h"
#include "osal_log.h" /* For LOG_LEVEL */
#include "pnal.h"
#include <pnet_api.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define GET_HIGH_BYTE(id) ((id >> 8) & 0xFF)
#define GET_LOW_BYTE(id)  (id & 0xFF)

void app_utils_ip_to_string (pnal_ipaddr_t ip, char * outputstring)
{
   snprintf (
      outputstring,
      PNAL_INET_ADDRSTR_SIZE,
      "%u.%u.%u.%u",
      (uint8_t)((ip >> 24) & 0xFF),
      (uint8_t)((ip >> 16) & 0xFF),
      (uint8_t)((ip >> 8) & 0xFF),
      (uint8_t)(ip & 0xFF));
}

void app_utils_mac_to_string (pnet_ethaddr_t mac, char * outputstring)
{
   snprintf (
      outputstring,
      PNAL_ETH_ADDRSTR_SIZE,
      "%02X:%02X:%02X:%02X:%02X:%02X",
      mac.addr[0],
      mac.addr[1],
      mac.addr[2],
      mac.addr[3],
      mac.addr[4],
      mac.addr[5]);
}

const char * app_utils_submod_dir_to_string (pnet_submodule_dir_t direction)
{
   const char * s = "<error>";

   switch (direction)
   {
   case PNET_DIR_NO_IO:
      s = "NO_IO";
      break;
   case PNET_DIR_INPUT:
      s = "INPUT";
      break;
   case PNET_DIR_OUTPUT:
      s = "OUTPUT";
      break;
   case PNET_DIR_IO:
      s = "INPUT_OUTPUT";
      break;
   }

   return s;
}

const char * app_utils_ioxs_to_string (pnet_ioxs_values_t ioxs)
{
   const char * s = "<error>";
   switch (ioxs)
   {
   case PNET_IOXS_BAD:
      s = "IOXS_BAD";
      break;
   case PNET_IOXS_GOOD:
      s = "IOXS_GOOD";
      break;
   }

   return s;
}

void app_utils_get_error_code_strings (
   uint16_t err_cls,
   uint16_t err_code,
   const char ** err_cls_str,
   const char ** err_code_str)
{
   if (err_cls_str == NULL || err_cls_str == NULL)
   {
      return;
   }
   *err_cls_str = "Not decoded";
   *err_code_str = "Not decoded";

   switch (err_cls)
   {
   case PNET_ERROR_CODE_1_RTA_ERR_CLS_PROTOCOL:
      *err_cls_str = "Real-Time Acyclic Protocol";
      switch (err_code)
      {
      case PNET_ERROR_CODE_2_ABORT_AR_CONSUMER_DHT_EXPIRED:
         *err_code_str = "Device missed cyclic data "
                         "deadline, device terminated AR";
         break;
      case PNET_ERROR_CODE_2_ABORT_AR_CMI_TIMEOUT:
         *err_code_str = "Communication initialization "
                         "timeout, device terminated AR";
         break;
      case PNET_ERROR_CODE_2_ABORT_AR_RELEASE_IND_RECEIVED:
         *err_code_str = "AR release indication received";
         break;
      case PNET_ERROR_CODE_2_ABORT_DCP_STATION_NAME_CHANGED:
         *err_code_str = "DCP station name changed, "
                         "device terminated AR";
         break;
      case PNET_ERROR_CODE_2_ABORT_DCP_RESET_TO_FACTORY:
         *err_code_str = "DCP reset to factory or factory "
                         "reset, device terminated AR";
         break;
      }
      break;

   case PNET_ERROR_CODE_1_CTLDINA:
      *err_cls_str = "CTLDINA = Name and IP assignment from controller";
      switch (err_code)
      {
      case PNET_ERROR_CODE_2_CTLDINA_ARP_MULTIPLE_IP_ADDRESSES:
         *err_code_str = "Multiple users of same IP address";
         break;
      }
      break;
   }
}

void app_utils_copy_ip_to_struct (
   pnet_cfg_ip_addr_t * destination_struct,
   pnal_ipaddr_t ip)
{
   destination_struct->a = ((ip >> 24) & 0xFF);
   destination_struct->b = ((ip >> 16) & 0xFF);
   destination_struct->c = ((ip >> 8) & 0xFF);
   destination_struct->d = (ip & 0xFF);
}

const char * app_utils_dcontrol_cmd_to_string (
   pnet_control_command_t control_command)
{
   const char * s = NULL;

   switch (control_command)
   {
   case PNET_CONTROL_COMMAND_PRM_BEGIN:
      s = "PRM_BEGIN";
      break;
   case PNET_CONTROL_COMMAND_PRM_END:
      s = "PRM_END";
      break;
   case PNET_CONTROL_COMMAND_APP_RDY:
      s = "APP_RDY";
      break;
   case PNET_CONTROL_COMMAND_RELEASE:
      s = "RELEASE";
      break;
   default:
      s = "<error>";
      break;
   }

   return s;
}

const char * app_utils_event_to_string (pnet_event_values_t event)
{
   const char * s = "<error>";

   switch (event)
   {
   case PNET_EVENT_ABORT:
      s = "PNET_EVENT_ABORT";
      break;
   case PNET_EVENT_STARTUP:
      s = "PNET_EVENT_STARTUP";
      break;
   case PNET_EVENT_PRMEND:
      s = "PNET_EVENT_PRMEND";
      break;
   case PNET_EVENT_APPLRDY:
      s = "PNET_EVENT_APPLRDY";
      break;
   case PNET_EVENT_DATA:
      s = "PNET_EVENT_DATA";
      break;
   }

   return s;
}

int app_utils_pnet_cfg_init_default (pnet_cfg_t * cfg)
{
   memset (cfg, 0, sizeof (pnet_cfg_t));

   cfg->tick_us = APP_TICK_INTERVAL_US;

   /* Identification & Maintenance */

   cfg->im_0_data.im_vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_VENDOR_ID);
   cfg->im_0_data.im_vendor_id_lo = GET_LOW_BYTE (APP_GSDML_VENDOR_ID);

   cfg->im_0_data.im_hardware_revision = APP_GSDML_IM_HARDWARE_REVISION;
   cfg->im_0_data.im_sw_revision_prefix = APP_GSDML_SW_REV_PREFIX;
   cfg->im_0_data.im_sw_revision_functional_enhancement = PNET_VERSION_MAJOR;
   cfg->im_0_data.im_sw_revision_bug_fix = PNET_VERSION_MINOR;
   cfg->im_0_data.im_sw_revision_internal_change = PNET_VERSION_PATCH;
   cfg->im_0_data.im_revision_counter = APP_GSDML_IM_REVISION_COUNTER;
   cfg->im_0_data.im_profile_id = APP_GSDML_PROFILE_ID;
   cfg->im_0_data.im_profile_specific_type = APP_GSDML_PROFILE_SPEC_TYPE;
   cfg->im_0_data.im_version_major = 1; /** Always 1 */
   cfg->im_0_data.im_version_minor = 1; /** Always 1 */
   cfg->im_0_data.im_supported = APP_GSDML_IM_SUPPORTED;

   snprintf (
      cfg->im_0_data.im_order_id,
      sizeof (cfg->im_0_data.im_order_id),
      "%s",
      APP_GSDML_ORDER_ID);
   snprintf (
      cfg->im_0_data.im_serial_number,
      sizeof (cfg->im_0_data.im_serial_number),
      "%s",
      APP_GSDML_EXAMPLE_SERIAL_NUMBER);
   snprintf (
      cfg->im_1_data.im_tag_function,
      sizeof (cfg->im_1_data.im_tag_function),
      "%s",
      APP_GSDML_TAG_FUNCTION);
   snprintf (
      cfg->im_1_data.im_tag_location,
      sizeof (cfg->im_1_data.im_tag_location),
      "%s",
      APP_GSDML_TAG_LOCATION);
   snprintf (
      cfg->im_2_data.im_date,
      sizeof (cfg->im_2_data.im_date),
      "%s",
      APP_GSDML_IM_DATE);
   snprintf (
      cfg->im_3_data.im_descriptor,
      sizeof (cfg->im_3_data.im_descriptor),
      "%s",
      APP_GSDML_DESCRIPTOR);
   snprintf (
      cfg->im_4_data.im_signature,
      sizeof (cfg->im_4_data.im_signature),
      "%s",
      APP_GSDML_SIGNATURE);

   /* Device configuration */
   cfg->device_id.vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_VENDOR_ID);
   cfg->device_id.vendor_id_lo = GET_LOW_BYTE (APP_GSDML_VENDOR_ID);
   cfg->device_id.device_id_hi = GET_HIGH_BYTE (APP_GSDML_DEVICE_ID);
   cfg->device_id.device_id_lo = GET_LOW_BYTE (APP_GSDML_DEVICE_ID);
   cfg->oem_device_id.vendor_id_hi = GET_HIGH_BYTE (APP_GSDML_OEM_VENDOR_ID);
   cfg->oem_device_id.vendor_id_lo = GET_LOW_BYTE (APP_GSDML_OEM_VENDOR_ID);
   cfg->oem_device_id.device_id_hi = GET_HIGH_BYTE (APP_GSDML_OEM_DEVICE_ID);
   cfg->oem_device_id.device_id_lo = GET_LOW_BYTE (APP_GSDML_OEM_DEVICE_ID);

   snprintf (
      cfg->product_name,
      sizeof (cfg->product_name),
      "%s",
      APP_GSDML_PRODUCT_NAME);

   cfg->send_hello = true;

   /* Timing */
   cfg->min_device_interval = APP_GSDML_MIN_DEVICE_INTERVAL;

   /* Should be set by application as part of network configuration. */
   cfg->num_physical_ports = 1;

   snprintf (
      cfg->station_name,
      sizeof (cfg->station_name),
      "%s",
      APP_GSDML_DEFAULT_STATION_NAME);

   /* Diagnosis mechanism */
   /* We prefer using "Extended channel diagnosis" instead of
    * "Qualified channel diagnosis" format on the wire,
    * as this is better supported by Wireshark.
    */
   cfg->use_qualified_diagnosis = false;

   return 0;
}

int app_utils_get_netif_namelist (
   const char * arg_str,
   uint16_t max_port,
   app_utils_netif_namelist_t * p_if_list,
   uint16_t * p_num_ports)
{
   int ret = 0;
   uint16_t i = 0;
   uint16_t j = 0;
   uint16_t if_index = 0;
   uint16_t number_of_given_names = 1;
   uint16_t if_list_size = max_port + 1;
   char c;

   if (max_port == 0)
   {
      printf ("Error: max_port is 0.\n");
      return -1;
   }

   memset (p_if_list, 0, sizeof (*p_if_list));
   c = arg_str[i++];
   while (c != '\0')
   {
      if (c != ',')
      {
         if (if_index < if_list_size)
         {
            p_if_list->netif[if_index].name[j++] = c;
         }
      }
      else
      {
         if (if_index < if_list_size)
         {
            p_if_list->netif[if_index].name[j++] = '\0';
            j = 0;
            if_index++;
         }
         number_of_given_names++;
      }

      c = arg_str[i++];
   }

   if (max_port == 1 && number_of_given_names > 1)
   {
      printf ("Error: Only 1 network interface expected as max_port is 1.\n");
      return -1;
   }
   if (number_of_given_names == 2)
   {
      printf ("Error: It is illegal to give 2 interface names. Use 1, or one "
              "more than the number of physical interfaces.\n");
      return -1;
   }
   if (number_of_given_names > max_port + 1)
   {
      printf (
         "Error: You have given %u interface names, but max is %u as "
         "PNET_MAX_PHYSICAL_PORTS is %u.\n",
         number_of_given_names,
         max_port + 1,
         max_port);
      return -1;
   }

   if (number_of_given_names == 1)
   {
      if (strlen (p_if_list->netif[0].name) == 0)
      {
         printf ("Error: Zero length network interface name.\n");
         return -1;
      }
      else
      {
         p_if_list->netif[1] = p_if_list->netif[0];
         *p_num_ports = 1;
      }
   }
   else
   {
      for (i = 0; i < number_of_given_names; i++)
      {
         if (strlen (p_if_list->netif[i].name) == 0)
         {
            printf ("Error: Zero length network interface name (%d).\n", i);
            return -1;
         }
      }

      *p_num_ports = number_of_given_names - 1;
   }

   return ret;
}

int app_utils_pnet_cfg_init_netifs (
   const char * netif_list_str,
   app_utils_netif_namelist_t * if_list,
   uint16_t * number_of_ports,
   pnet_if_cfg_t * if_cfg)
{
   int ret = 0;
   int i = 0;
   pnal_ipaddr_t ip;
   pnal_ipaddr_t netmask;
   pnal_ipaddr_t gateway;

   ret = app_utils_get_netif_namelist (
      netif_list_str,
      PNET_MAX_PHYSICAL_PORTS,
      if_list,
      number_of_ports);
   if (ret != 0)
   {
      return ret;
   }
   if_cfg->main_netif_name = if_list->netif[0].name;

   for (i = 1; i <= *number_of_ports; i++)
   {
      if_cfg->physical_ports[i - 1].netif_name = if_list->netif[i].name;
      if_cfg->physical_ports[i - 1].default_mau_type =
         APP_GSDML_DEFAULT_MAUTYPE;
   }

   /* Read IP, netmask, gateway from operating system */
   ip = pnal_get_ip_address (if_cfg->main_netif_name);
   netmask = pnal_get_netmask (if_cfg->main_netif_name);
   gateway = pnal_get_gateway (if_cfg->main_netif_name);

   app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_addr, ip);
   app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_gateway, gateway);
   app_utils_copy_ip_to_struct (&if_cfg->ip_cfg.ip_mask, netmask);

   return ret;
}

static void app_utils_print_mac_address (const char * netif_name)
{
   pnal_ethaddr_t pnal_mac_addr;
   if (pnal_get_macaddress (netif_name, &pnal_mac_addr) == 0)
   {
      APP_LOG_INFO (
         "%02X:%02X:%02X:%02X:%02X:%02X\n",
         pnal_mac_addr.addr[0],
         pnal_mac_addr.addr[1],
         pnal_mac_addr.addr[2],
         pnal_mac_addr.addr[3],
         pnal_mac_addr.addr[4],
         pnal_mac_addr.addr[5]);
   }
   else
   {
      APP_LOG_ERROR ("Failed read mac address\n");
   }
}

void app_utils_print_network_config (
   pnet_if_cfg_t * if_cfg,
   uint16_t number_of_ports)
{
   uint16_t i;
   char hostname_string[PNAL_HOSTNAME_MAX_SIZE]; /* Terminated string */

   APP_LOG_INFO ("Management port:      %s ", if_cfg->main_netif_name);
   app_utils_print_mac_address (if_cfg->main_netif_name);
   for (i = 1; i <= number_of_ports; i++)
   {
      APP_LOG_INFO (
         "Physical port [%u]:    %s ",
         i,
         if_cfg->physical_ports[i - 1].netif_name);

      app_utils_print_mac_address (if_cfg->physical_ports[i - 1].netif_name);
   }

   if (pnal_get_hostname (hostname_string) != 0)
   {
      hostname_string[0] = '\0';
   }

   APP_LOG_INFO ("Hostname:             %s\n", hostname_string);
   APP_LOG_INFO (
      "IP address:           %u.%u.%u.%u\n",
      if_cfg->ip_cfg.ip_addr.a,
      if_cfg->ip_cfg.ip_addr.b,
      if_cfg->ip_cfg.ip_addr.c,
      if_cfg->ip_cfg.ip_addr.d);
   APP_LOG_INFO (
      "Netmask:              %u.%u.%u.%u\n",
      if_cfg->ip_cfg.ip_mask.a,
      if_cfg->ip_cfg.ip_mask.b,
      if_cfg->ip_cfg.ip_mask.c,
      if_cfg->ip_cfg.ip_mask.d);
   APP_LOG_INFO (
      "Gateway:              %u.%u.%u.%u\n",
      if_cfg->ip_cfg.ip_gateway.a,
      if_cfg->ip_cfg.ip_gateway.b,
      if_cfg->ip_cfg.ip_gateway.c,
      if_cfg->ip_cfg.ip_gateway.d);
}

void app_utils_print_ioxs_change (
   const app_subslot_t * subslot,
   const char * ioxs_str,
   uint8_t iocs_current,
   uint8_t iocs_new)
{
   if (iocs_current != iocs_new)
   {
      if (iocs_new == PNET_IOXS_BAD)
      {
         APP_LOG_DEBUG (
            "PLC reports %s BAD for slot %u subslot %u \"%s\"\n",
            ioxs_str,
            subslot->slot_nbr,
            subslot->subslot_nbr,
            subslot->submodule_name);
      }
      else if (iocs_new == PNET_IOXS_GOOD)
      {
         APP_LOG_DEBUG (
            "PLC reports %s GOOD for slot %u subslot %u \"%s\".\n",
            ioxs_str,
            subslot->slot_nbr,
            subslot->subslot_nbr,
            subslot->submodule_name);
      }
      else if (iocs_new != PNET_IOXS_GOOD)
      {
         APP_LOG_DEBUG (
            "PLC reports %s %u for input slot %u subslot %u \"%s\".\n"
            "  Is the PLC in STOP mode?\n",
            ioxs_str,
            iocs_new,
            subslot->slot_nbr,
            subslot->subslot_nbr,
            subslot->submodule_name);
      }
   }
}

int app_utils_plug_module (
   app_api_t * p_api,
   uint16_t slot_nbr,
   uint32_t id,
   const char * name)
{
   if (slot_nbr >= PNET_MAX_SLOTS)
   {
      return -1;
   }

   p_api->slots[slot_nbr].module_id = id;
   p_api->slots[slot_nbr].plugged = true;
   p_api->slots[slot_nbr].name = name;

   return 0;
}

int app_utils_pull_module (app_api_t * p_api, uint16_t slot_nbr)
{
   if (slot_nbr >= PNET_MAX_SLOTS)
   {
      return -1;
   }

   p_api->slots[slot_nbr].plugged = false;

   return 0;
}

app_subslot_t * app_utils_plug_submodule (
   app_api_t * p_api,
   uint16_t slot_nbr,
   uint16_t subslot_nbr,
   uint32_t submodule_ident,
   const pnet_data_cfg_t * p_data_cfg,
   const char * submodule_name,
   app_utils_cyclic_callback cyclic_callback,
   void * tag)
{
   uint16_t subslot_ix;

   if (slot_nbr >= PNET_MAX_SLOTS || p_api == NULL || p_data_cfg == NULL)
   {
      return NULL;
   }

   /** Find a free subslot */
   for (subslot_ix = 0; subslot_ix < PNET_MAX_SUBSLOTS; subslot_ix++)
   {
      if (p_api->slots[slot_nbr].subslots[subslot_ix].used == false)
      {
         app_subslot_t * p_subslot =
            &p_api->slots[slot_nbr].subslots[subslot_ix];

         p_subslot->used = true;
         p_subslot->plugged = true;
         p_subslot->slot_nbr = slot_nbr;
         p_subslot->subslot_nbr = subslot_nbr;
         p_subslot->submodule_name = submodule_name;
         p_subslot->submodule_id = submodule_ident;
         p_subslot->data_cfg = *p_data_cfg;
         p_subslot->cyclic_callback = cyclic_callback;
         p_subslot->tag = tag;
         p_subslot->indata_iocs = PNET_IOXS_BAD;
         p_subslot->outdata_iops = PNET_IOXS_BAD;
         return p_subslot;
      }
   }

   return NULL;
}

int app_utils_pull_submodule (
   app_api_t * p_api,
   uint16_t slot_nbr,
   uint16_t subslot_nbr)
{
   app_subslot_t * p_subslot = NULL;

   if (slot_nbr >= PNET_MAX_SUBSLOTS || p_api == NULL)
   {
      return -1;
   }

   p_subslot = app_utils_subslot_get (p_api, slot_nbr, subslot_nbr);
   if (p_subslot == NULL)
   {
      return -1;
   }

   memset (p_subslot, 0, sizeof (app_subslot_t));
   p_subslot->used = false;

   return 0;
}

app_subslot_t * app_utils_subslot_get (
   app_api_t * p_api,
   uint16_t slot_nbr,
   uint16_t subslot_nbr)
{
   uint16_t subslot_ix;

   if (slot_nbr >= PNET_MAX_SLOTS || p_api == NULL)
   {
      return NULL;
   }

   for (subslot_ix = 0; subslot_ix < PNET_MAX_SUBSLOTS; subslot_ix++)
   {
      if (p_api->slots[slot_nbr].subslots[subslot_ix].subslot_nbr == subslot_nbr)
      {
         return &p_api->slots[slot_nbr].subslots[subslot_ix];
      }
   }

   return NULL;
}

bool app_utils_subslot_is_input (const app_subslot_t * p_subslot)
{
   if (p_subslot == NULL || p_subslot->used == false)
   {
      return false;
   }

   if (
      p_subslot->data_cfg.data_dir == PNET_DIR_INPUT ||
      p_subslot->data_cfg.data_dir == PNET_DIR_IO)
   {
      return true;
   }

   return false;
}

bool app_utils_subslot_is_no_io (const app_subslot_t * p_subslot)
{
   if (p_subslot == NULL || p_subslot->used == false)
   {
      return false;
   }

   return p_subslot->data_cfg.data_dir == PNET_DIR_NO_IO;
}

bool app_utils_subslot_is_output (const app_subslot_t * p_subslot)
{
   if (p_subslot == NULL || p_subslot->used == false)
   {
      return false;
   }

   if (
      p_subslot->data_cfg.data_dir == PNET_DIR_OUTPUT ||
      p_subslot->data_cfg.data_dir == PNET_DIR_IO)
   {
      return true;
   }

   return false;
}

void app_utils_cyclic_data_poll (app_api_t * p_api)
{
   uint16_t slot_nbr;
   uint16_t subslot_index;
   app_subslot_t * p_subslot;

   for (slot_nbr = 0; slot_nbr < PNET_MAX_SLOTS; slot_nbr++)
   {
      for (subslot_index = 0; subslot_index < PNET_MAX_SUBSLOTS;
           subslot_index++)
      {
         p_subslot = &p_api->slots[slot_nbr].subslots[subslot_index];
         if (p_subslot->plugged && p_subslot->cyclic_callback != NULL)
         {
            p_subslot->cyclic_callback (p_subslot, p_subslot->tag);
         }
      }
   }
}
