#include "epics_tool.h"

#include <cerrno>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <limits>
#include <sstream>

namespace epics_tool {

void *ScalarValue::data(chtype type)
{
   switch (type) {
   case DBR_STRING: return string_value;
   case DBR_SHORT:  return &short_value;
   case DBR_FLOAT:  return &float_value;
   case DBR_ENUM:   return &enum_value;
   case DBR_CHAR:   return &char_value;
   case DBR_LONG:   return &long_value;
   case DBR_DOUBLE: return &double_value;
   default:         return nullptr;
   }
}

const void *ScalarValue::data(chtype type) const
{
   return const_cast<ScalarValue *>(this)->data(type);
}

bool parse_double(const char *text, double &value)
{
   if (!text || !text[0])
      return false;
   char *end = nullptr;
   errno = 0;
   value = std::strtod(text, &end);
   return errno == 0 && end && *end == 0 && std::isfinite(value);
}

bool parse_priority(const char *text, capri &priority)
{
   double value;
   if (!parse_double(text, value) || value < 0 || value > 99 ||
       std::floor(value) != value)
      return false;
   priority = static_cast<capri>(value);
   return true;
}

bool parse_type(const char *text, chtype &type)
{
   if (!text)
      return false;
   std::string name(text);
   if (name.compare(0, 4, "DBR_") == 0)
      name.erase(0, 4);
   for (char &ch : name) {
      if (ch >= 'a' && ch <= 'z')
         ch = static_cast<char>(ch - 'a' + 'A');
   }

   if (name == "STRING") type = DBR_STRING;
   else if (name == "SHORT" || name == "INT") type = DBR_SHORT;
   else if (name == "FLOAT") type = DBR_FLOAT;
   else if (name == "ENUM") type = DBR_ENUM;
   else if (name == "CHAR") type = DBR_CHAR;
   else if (name == "LONG") type = DBR_LONG;
   else if (name == "DOUBLE") type = DBR_DOUBLE;
   else {
      char *end = nullptr;
      long number = std::strtol(name.c_str(), &end, 10);
      if (!end || *end || number < DBR_STRING || number > DBR_DOUBLE)
         return false;
      type = number;
   }
   return true;
}

template <typename Integer>
bool parse_integer(const std::string &text, Integer &result)
{
   char *end = nullptr;
   errno = 0;
   long long value = std::strtoll(text.c_str(), &end, 0);
   if (errno || !end || *end ||
       value < static_cast<long long>(std::numeric_limits<Integer>::min()) ||
       value > static_cast<long long>(std::numeric_limits<Integer>::max()))
      return false;
   result = static_cast<Integer>(value);
   return true;
}

bool parse_value(chtype type, const std::string &text, ScalarValue &value,
                 std::string &error)
{
   switch (type) {
   case DBR_STRING:
      if (text.size() >= sizeof(value.string_value)) {
         error = "string values are limited to 39 characters";
         return false;
      }
      std::memcpy(value.string_value, text.c_str(), text.size() + 1);
      return true;
   case DBR_SHORT:
      if (parse_integer(text, value.short_value))
         return true;
      break;
   case DBR_ENUM:
      if (parse_integer(text, value.enum_value))
         return true;
      break;
   case DBR_CHAR:
      if (parse_integer(text, value.char_value))
         return true;
      break;
   case DBR_LONG:
      if (parse_integer(text, value.long_value))
         return true;
      break;
   case DBR_FLOAT: {
      double number;
      if (parse_double(text.c_str(), number) &&
          std::fabs(number) <= std::numeric_limits<float>::max()) {
         value.float_value = static_cast<float>(number);
         return true;
      }
      break;
   }
   case DBR_DOUBLE:
      if (parse_double(text.c_str(), value.double_value))
         return true;
      break;
   default:
      error = "unsupported DBR type";
      return false;
   }

   error = "invalid " + std::string(type_name(type)) + " value '" + text + "'";
   return false;
}

std::string format_value(chtype type, const ScalarValue &value)
{
   std::ostringstream output;
   switch (type) {
   case DBR_STRING: return value.string_value;
   case DBR_SHORT:  output << value.short_value; break;
   case DBR_FLOAT:
      output << std::setprecision(std::numeric_limits<float>::max_digits10)
             << value.float_value;
      break;
   case DBR_ENUM: output << value.enum_value; break;
   case DBR_CHAR: output << static_cast<unsigned>(value.char_value); break;
   case DBR_LONG: output << value.long_value; break;
   case DBR_DOUBLE:
      output << std::setprecision(std::numeric_limits<double>::max_digits10)
             << value.double_value;
      break;
   default: return "";
   }
   return output.str();
}

double timeout_from_environment()
{
   double timeout;
   const char *text = std::getenv("EPICS_CA_TIMEOUT");
   if (parse_double(text, timeout) && timeout >= 0)
      return timeout;
   return kDefaultTimeout;
}

const char *type_name(chtype type)
{
   switch (type) {
   case DBR_STRING: return "STRING";
   case DBR_SHORT:  return "SHORT";
   case DBR_FLOAT:  return "FLOAT";
   case DBR_ENUM:   return "ENUM";
   case DBR_CHAR:   return "CHAR";
   case DBR_LONG:   return "LONG";
   case DBR_DOUBLE: return "DOUBLE";
   default:         return "unknown";
   }
}

void print_ca_error(const char *operation, const char *pv, int status)
{
   std::fprintf(stderr, "%s '%s': %s\n", operation, pv, ca_message(status));
}

} // namespace epics_tool
