#ifndef _SO_GLUE_H
#define _SO_GLUE_H

#include <stddef.h>
#include <stdbool.h>

typedef void (*glue_fetch_fn)(char *, char *, void *);

void glue_fetch(const char *query, size_t params_len, const char *params[params_len], glue_fetch_fn fn, void **data, size_t *len, size_t size);
void glue_execute(const char *query, size_t params_len, const char *params[params_len]);
void glue_get_string(char *src, char **dest);
void glue_get_int(char *src, int *dest);
void glue_get_double(char *src, double *dest);
void glue_get_float(char *src, float *dest);
void glue_get_bool(char *src, bool *dest);


#define TABLEDEF($name, $cols) \
  typedef struct { \
    $cols(X_COLDEF) \
  } $name##_t; \
  \
  void _##$name##_fetch_fn(char *column, char *value, void *data) { \
    $name##_t *self = ($name##_t *) data; \
    $cols(X_ASSIGN) \
  } \
  void $name##_fetch(const char *query, size_t params_len, const char *params[params_len], $name##_t **data, size_t *len) { \
    glue_fetch(query, params_len, params, _##$name##_fetch_fn, (void *) data, len, sizeof($name##_t)); \
  } \
  void $name##_free($name##_t *self) { \
    $cols(X_FREE) \
  } \
  \
  void $name##_list_free($name##_t *list, size_t len) { \
    if (!list) return; \
    for (size_t i = 0; i < len; i++) { \
      $name##_free(&list[i]); \
    } \
    free(list); \
  } \
  \
  void $name##_dbg($name##_t *self) { \
    fprintf(stdout, "(%s_t) {\n", #$name); \
    $cols(X_DEBUG) \
    fprintf(stdout, "}\n"); \
  }

#define X_COLDEF($type, $name, $getter, $freeable, $format) \
  $type $name;

#define X_FREE($type, $name, $getter, $freeable, $format) \
  if ($freeable) { \
    if (self->$name) { \
      void **name = (void **) &self->$name; \
      free(*name); \
      *name = NULL; \
    } \
  }

#define X_ASSIGN($type, $name, $getter, $freeable, $format) \
  if (strcmp(column, #$name) == 0) { \
    $getter(value, &self->$name); \
  }

#define X_DEBUG($type, $name, $getter, $freeable, $format) \
  fprintf(stdout, "  .%s = ", #$name); \
  $format($name) \
  fprintf(stdout, "\n");

#define FORMAT_INT($name) \
  fprintf(stdout, "%d,", (int) self->$name);

#define FORMAT_TEXT($name) \
  fprintf(stdout, "%s,", (const char *) self->$name);

#define COLDEF_INT(X, $name) \
  X(int, $name, glue_get_int, 0, FORMAT_INT)

#define COLDEF_TEXT(X, $name) \
  X(char *, $name, glue_get_string, 1, FORMAT_TEXT)

#ifdef SO_GLUE_IMPL

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <libpq-fe.h>

void glue_execute(const char *query, size_t params_len, const char *params[params_len]) {
  PGconn *conn = PQconnectdb(GLUE_DB_INFO);
  if (PQstatus(conn) != CONNECTION_OK) {
    fprintf(stderr, "%s\n", PQerrorMessage(conn));
    PQfinish(conn);
    return;
  }
  PGresult *result = PQexecParams(conn, query, params_len, NULL, params, NULL, NULL, 0);
  ExecStatusType status = PQresultStatus(result);
  if (status != PGRES_COMMAND_OK) {
    fprintf(stderr, "%s\n", PQerrorMessage(conn));
    PQclear(result);
    return;
  }
  PQclear(result);
  PQfinish(conn);
}

void glue_fetch(const char *query, size_t params_len, const char *params[params_len], glue_fetch_fn fn, void **data, size_t *len, size_t size) {
  PGconn *conn = PQconnectdb(GLUE_DB_INFO);
  if (PQstatus(conn) != CONNECTION_OK) {
    fprintf(stderr, "%s\n", PQerrorMessage(conn));
    PQfinish(conn);
    return;
  }
  PGresult *result = PQexecParams(conn, query, params_len, NULL, params, NULL, NULL, 0);
  ExecStatusType status = PQresultStatus(result);
  if (status != PGRES_TUPLES_OK) {
    fprintf(stderr, "%s\n", PQerrorMessage(conn));
    PQclear(result);
    PQfinish(conn);
    return;
  }
  size_t rows = PQntuples(result);
  size_t cols = PQnfields(result);
  if (rows > 0) {
    for (size_t i = 0; i < rows; i++) {
      void *ptr = realloc(*data, size * (*len + 1));
      if (ptr == NULL) {
        // TODO error message
        PQclear(result);
        PQfinish(conn);
        return;
      }
      *data = ptr;
      void *item = (char *) *data + size * (*len);
      memset(item, 0, size);
      (*len)++;
      for (size_t j = 0; j < cols; j++) {
        if (PQgetisnull(result, i, j)) {
          continue;
        }
        char *column = PQfname(result, j);
        char *value = PQgetvalue(result, i, j);
        fn(column, value, item);
      }
    }
  }
  PQclear(result);
  PQfinish(conn);
}

void glue_get_string(char *src, char **dest) {
  size_t value_len = strlen(src);
  if (value_len == 0) {
    *dest = NULL;
  }
  *dest = malloc(sizeof(char) * (value_len + 1));
  memcpy(*dest, src, value_len);
  (*dest)[value_len] = '\0';
}

void glue_get_int(char *src, int *dest) {
  *dest = strtoimax(src, NULL, 10);
}

void glue_get_double(char *src, double *dest) {
  *dest = strtod(src, NULL);
}

void glue_get_float(char *src, float *dest) {
  *dest = strtof(src, NULL);
}

void glue_get_bool(char *src, bool *dest) {
  *dest = strcmp("t", src) == 0;
}

#endif /* SO_GLUE_IMPL */
#endif /* _SO_GLUE_H */
