#include "poll_modbus.h"

#include <dlfcn.h>
#include <limits.h>
#include <math.h>
#include <regex.h>
#include <stdarg.h>
#include <sys/types.h>

LOG_BUFFER(1000)
struct options_t g_opt;

struct cmdline optv[] = {
  { 'v', "version", NULL, NULL, NULL, NULL, "print this usage text" },
  /*OPT_LOG_L,*/
  OPT_UTIL_F,
  { 'i', "interval", "INTERVAL", "seconds", "1.0", NULL, "poll the devices each <seconds>" },
  { 'c', "count", "COUNT", "number", "0", NULL, "if <number> > 0, only poll <number> times" },
  { 'p', "proc", "POSTPROC", "library", NULL, NULL, "load <library> and run postinit(), postproc(), and postfini()" },
  { 'a', "procarg", "PROCARG", "arg", NULL, NULL, "give <arg> to postinit()" },
};
#define OPTIX_V (0)
#define OPTIX_L (OPTIX_V+1)
#define OPTIX_F (OPTIX_L+1)
#define OPTIX_I (OPTIX_F+1)
#define OPTIX_C (OPTIX_I+1)
#define OPTIX_P (OPTIX_C+1)
#define OPTIX_A (OPTIX_P+1)

void Usage(int exit_status, const char *fmt, ...);
void Usage(int exit_status, const char *fmt, ...) {
  FILE *fp;

  if (exit_status) fp = stderr;
  else fp = stdout;

  if (fmt) {
    if (*fmt) {
      va_list ap;
      fprintf(fp, "%s: ", cmdline_pgm);
      va_start(ap, fmt);
      vfprintf(fp, fmt, ap);
      va_end(ap);
      fprintf(fp, "\n");
    }
  } else {
    fprintf(fp, "Name:\n\t%s - daemon to get values from Modbus/TCP/UDP devices\n\n", cmdline_pgm);
    fprintf(fp, "Synopsis:\n\t%s [OPTION...] HOST:[U][PORT]:STA:CNT ...\n\n", cmdline_pgm);
    fprintf(fp, "Description:\n");
    fprintf(fp, "\tPoll Modbus devices for data, and save it in a log file\n\n");
    fprintf(fp, "\tHOST is the hostname of the device\n");
    fprintf(fp, "\tthe optional \"U\" indicates use of UDP instead of TCP\n");
    fprintf(fp, "\tPORT default is 502\n");
    fprintf(fp, "\tSTA is the starting register address (default is 0, valid is 0 .. 0xffff)\n");
    fprintf(fp, "\tCNT is the quantity of registers to read (default is 1, valid is 1 .. 125)\n");
    fprintf(fp, "\n");
    fprintf(fp, "Options:\n\t(-short --long (env_var) (default) =curr_value descr.)\n");
    LOG_IF(cmdline_fusage(fp, CMDLINE_SZ(optv), optv, "\t", " | "), LOG_ERR) {}
    fprintf(fp, "\n\tYou have to specify a log file unless you want to run in the foreground\n");
  }
  exit(exit_status);
}

int main(int argc, char *argv[]) {
  const char *logfile;

  (void) timespec_update();
  (void) cmdline_extrpgm(argv);
  (void) log_open(NULL, LOG_PRIO | LOG_SIGHUP | 2, 0);

  if ( cmdline_getopt(argc, argv, CMDLINE_SZ(optv), optv) < 0) Usage(1, "");

  if (optv[OPTIX_V].arg_value) Usage(0, NULL);

  logfile = optv[OPTIX_L].arg_value;
  if ((logfile == NULL || logfile[0] == '\0') && optv[OPTIX_F].arg_value == NULL ) {
    Usage(1, "No log file specified and not running in foregrund");
  }

  {
    char *pp;
    const char *arg = optv[OPTIX_I].arg_value;
    long val;
    regex_t rx;

    LOG_IF( regcomp(&rx, "^[0-9]+(\\.[0-9]+)?$", REG_EXTENDED | REG_NOSUB), LOG_CRIT) exit(2);
    if ( regexec(&rx, arg, 0, NULL, 0))
      Usage(1, "strange interval <%s>", arg);
    regfree(&rx);

    LOG_IFERRNO( (val = strtol(arg, &pp, 0), errno && errno != ERANGE), LOG_ERR) {
    } else if ( (errno == ERANGE && val > 0) || (val > 361*24*60*60) ) {
      Usage(1, "for intervals longer than one year, use a crontab entry instead");
    }
    g_opt.interval.tv_sec = val;

    val = 0;
    if (*pp == '.') {
      const int precision = 3;
      int ix;
      char *str;

      LOG_IFERRNO( (str = malloc(precision+1)) == NULL, LOG_ERR) {
	return -1;
      }
      pp++; /* skip the '.' */
      strncpy(str, pp, precision);
      for (ix = strlen(pp); ix < precision; ++ix) str[ix] = '0';
      /*printf("digits: <%s>\n", str); */
      val = strtol(str, NULL, 10);
      free(str);
    }
    g_opt.interval.tv_nsec = val * 1000 * 1000;
    {
      double sec = timespec_ts2dbl(g_opt.interval);
      if (sec < 0.1) {
	fprintf(stderr, "Warning: interval less than 100ms, don't exspect this to work reliably\n\n");
      }
    }
    /*timespec_printf(g_opt.interval); */
  }

  {
    char *pp;
    if (optv[OPTIX_C].arg_value && *(optv[OPTIX_C].arg_value)) {
      long val;
      errno = 0;
      LOG_IFERRNO( (val = strtol(optv[OPTIX_C].arg_value, &pp, 0), errno && errno != ERANGE), LOG_ERR) {
      } else if (*pp) {
	Usage(1, "strange count <%s>", optv[OPTIX_C].arg_value);
      }
      if (errno == ERANGE && val > 0) {
	Usage(1, "count overflowed, please give a lesser value");
      }
#if LONG_MAX > INT_MAX
      if (val > INT_MAX) {
	Usage(1, "count overflowed, please give a smaller value");
      }
#endif
      if (val > 0) g_opt.count = val;
      else         g_opt.count = 0;
    }
    /*fprintf(stderr,"%5.2f %d\n", sec, count); */
  }

  {
    int devsz = argc - optind;
    struct net_state *netstate;
    uint16_t *starting_address;
    uint16_t *quantity;
    const char port[] = "502";

    int ix;
    struct net_state *ns;

    LOG_IFERRNO( (netstate = calloc(devsz,sizeof(struct net_state))) == NULL, LOG_ERR) return -1;
    LOG_IFERRNO( (starting_address = calloc(devsz,sizeof(uint16_t))) == NULL, LOG_ERR) return -1;
    LOG_IFERRNO( (quantity = calloc(devsz,sizeof(uint16_t))) == NULL, LOG_ERR) return -1;

    if (devsz <= 0) {
      Usage(1, "no device arguments found");
    }

    for (ix = 0, ns = netstate; ix < devsz; ++ix, ++ns) {
      char *arg = argv[optind+ix];
      int len = strlen(arg) + 1;
      char *str; /* copy of original argument, for error reporting */
      const int nsa = 4; /* 0:1:2:3 host:port:sta:cnt number of subarguments */
      char **strv;
      int strc;
      int socktype = SOCK_STREAM;

      LOG_IFERRNO( (str  = malloc(len)) == NULL, LOG_ERR) return -1;
      LOG_IFERRNO( (strv = calloc(nsa+1,sizeof(char*))) == NULL, LOG_ERR) return -1;
      strcpy(str, arg);

      (void) splitc(arg, ":", nsa+1, &strc, strv);

      if (strc != nsa) {
	Usage(1, "each argument needs four parts separated by \":\", found %d in <%s>", strc, str);
      }

      /* HOST = strv[0]; */
      /* [U][PORT] */
      if (strv[1][0] == 'U') {
	socktype = SOCK_DGRAM;
	strv[1]++;
      }
      if (strv[1][0]) {
	LOG_IF( netclient_init( ns, strv[0], strv[1], socktype), LOG_ERR) { }
      } else {
	LOG_IF( netclient_init( ns, strv[0], port   , socktype), LOG_ERR) { }
      }
      /* STA */
      starting_address[ix] = 0;
      if (strv[2][0]) {
	long val;
	char *pp;
	val = strtol(strv[2], &pp, 0);
	if (*pp != '\0' || val < 0 || val > 0xffff ) {
	  Usage(1, "\"%s\" is not a valid starting address (in \"%s\")", strv[2], str);
	}
	starting_address[ix] = val;
      }
      /* CNT */
      quantity[ix] = 1;
      if (strv[3][0]) {
	long val;
	char *pp;
	val = strtol(strv[3], &pp, 0);
	if (*pp != '\0' || val <= 0 || val > 125 ) {
	  Usage(1, "\"%s\" is not a valid quantity of registers (in \"%s\")", strv[3], str);
	}
	quantity[ix] = val;
      }

      /*printf("<%s:%s%s:%d:%d>\n", ns->host, ns->hints.ai_socktype == SOCK_STREAM ? "" : "U", */
      /*   ns->port, starting_address[ix], quantity[ix]); */
      free(str);
      free(strv);
    }

    {
      const char *opt_p = optv[OPTIX_P].arg_value;
      if (opt_p && opt_p[0]) {
	void *handle = dlopen(opt_p, RTLD_LAZY);
	if (handle == NULL) Usage(1, dlerror());

	dlerror(); /* Clear any existing error */
	if ( (*(void **) (&g_opt.dl_init) = dlsym(handle, "proc_init")) == NULL) Usage(1, dlerror());
	if ( (*(void **) (&g_opt.dl_proc) = dlsym(handle, "proc_run" )) == NULL) Usage(1, dlerror());
	if ( (*(void **) (&g_opt.dl_fini) = dlsym(handle, "proc_fini")) == NULL) Usage(1, dlerror());

	if (g_opt.dl_init) {
	  if ( (*g_opt.dl_init)(optv[OPTIX_A].arg_value) < 0) exit(1);
	}
	/* TODO: proc_fini() at SIGTERM etc. */
      }
    }

    if (logfile && logfile[0]) {
      LOG_VIF( log_open(logfile, LOG_PRIO | LOG_SIGHUP | 2, 0) < 0, LOG_ERR, " logfile = %s", logfile) {
	exit(1);
      }
    }

    {
      pid_t me = getpid();
      pid_t pp = getppid();
      if (optv[OPTIX_F].arg_value == NULL) {
	LOG_IFERRNO( daemon(0,0), LOG_ERR ) { exit(1); }
	NOTICE("Became daemon (ppid: %d pid: %d)", (int) pp, (int) me);
      } else {
	NOTICE("Running i foreground (ppid: %d pid: %d)", (int) pp, (int) me);
	LOG_IFERRNO(setvbuf(stdout, NULL, _IOLBF, 0) < 0, LOG_ERR) { }
      }
    }

    (void) signal_set_single(sighandler, SIGINT, NULL);
    (void) signal_set_single(sighandler, SIGTERM, NULL);

    run( devsz, netstate, starting_address, quantity );
    if (g_opt.dl_fini) {
      LOG_IF( (*g_opt.dl_fini)() < 0, LOG_ERR) { }
    }
    free(netstate);
    free(starting_address);
    free(quantity);
  }

  return 0;
}
