#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

#define LIB_STR_IMPL
#include <libstr.h>

#define CTL_TMP_C_PATH ".ctl_tmp.c"
#define CTL_TMP_SO_PATH ".ctl_tmp.so"

typedef enum {
  CTL_ERR_OK,
  CTL_ERR_NO_ARGS,
  CTL_ERR_NO_FILE,
  CTL_ERR_NO_WRITE,
  CTL_ERR_UNCLOSED_CODE,
  CTL_ERR_NO_COMPILE,
} ctl_error_t;

typedef enum {
  CTL_MODE_HEADER,
  CTL_MODE_MODULE,
} ctl_mode_t;

void ctl_error_str(ctl_error_t err) {
  switch (err) {
    case CTL_ERR_OK:
      break;
    case CTL_ERR_NO_ARGS:
      fprintf(stderr, "Usage: ctl -h <templ_path> (HEADER), ctl -o <templ_path> (MODULE)\n");
      break;
    case CTL_ERR_NO_FILE:
      fprintf(stderr, "Error: No file found!\n");
      break;
    case CTL_ERR_NO_WRITE:
      fprintf(stderr, "Error: Can't write to file!\n");
      break;
    case CTL_ERR_UNCLOSED_CODE:
      fprintf(stderr, "Error: Unclosed code block!\n");
      break;
    case CTL_ERR_NO_COMPILE:
      fprintf(stderr, "Error: Failed to compile!\n");
      break;
  }
}

char *str_replace_all_len(char *buffer, char *str, char *replacement, int len) {
  char *ret = NULL;
  int str_len = strlen(str);
  for (int i = 0; i < len; i++) {
    char *ptr = &buffer[i];
    if (strncmp(ptr, str, str_len) == 0) {
      str_append(&ret, replacement);
      i += str_len - 1;
    }
    else {
      str_append_len(&ret, &buffer[i], 1);
    }
  }
  return ret;
}

char *str_hex_len(char *str, int len) {
  const int HEX = 4;
  char *ret = calloc(HEX * len + 1, sizeof(char));
  for (int i = 0; i < len; i++) {
    unsigned char hex = str[i] & 0xff;
    snprintf(ret + HEX * i, HEX * len, "\\x%02x", hex);
  }
  ret[HEX * len] = '\0';
  return ret;
}

ctl_error_t ctl_render(ctl_mode_t mode, char **template, char *content) {
  int len = strlen(content);
  int set = 0;
  int is_code = 0;
  int templ = 0;
  str_append(template, "#include <dlfcn.h>\n");
  str_append(template, "#include <so/ctl.h>\n");
  for (int i = 0; i < len; i++) {
    if (content[i] == '%') {
      // ignore escaped %
      if (i + 1 < len && content[i + 1] == '%') {
        i += 1;
        continue;
      }
      if (is_code) {
        for (int j = 0; j < i - set; j++) {
          if (strncmp(content + set + j, "TEMPLDEF", 8) == 0) {
            templ = 1;
          }
          if (strncmp(content + set + j, "ENDTEMPL", 8) == 0) {
            templ = 0;
          }
        }
        if (mode == CTL_MODE_MODULE) {
          str_append_len(template, content + set, i - set);
          str_append(template, "\n");
        }
        if (mode == CTL_MODE_HEADER && templ == 1) {
          templ = 0;
          char *templdef = NULL;
          str_append_len(&templdef, content + set, i - set);
          str_append(&templdef, "\n");
          templdef = str_replace_all(templdef, "CTL_TEMPLDEF", "__CTL_TEMPLDEF_HEADER");
          str_append(template, templdef);
          free(templdef);
        }
      }
      else {
        const int STRING_LIMIT = 4095;
        int str_len = i - set;
        int rem_len = str_len;
        int hex_len = 0;
        int idx = 0;
        while (rem_len > 0) {
          if (rem_len > STRING_LIMIT) {
            hex_len = STRING_LIMIT;
          }
          else {
            hex_len = str_len - idx;
          }
          rem_len -= hex_len;
          char *str = str_replace_all_len(content + set + idx, "%%", "%", hex_len);
          char *hex = str_hex_len(str, strlen(str));
          if (mode == CTL_MODE_MODULE && templ == 1) {
            str_append(template, "ctl->write(\"");
            str_append(template, hex);
            str_append(template, "\");\n");
          }
          free(str);
          free(hex);
          idx += hex_len;
        }
      }
      is_code = !is_code;
      set = i + 1;
    }
  }
  if (is_code) {
    return CTL_ERR_UNCLOSED_CODE;
  }
  else {
    const int STRING_LIMIT = 4095;
    int str_len = len - set;
    int rem_len = str_len;
    int hex_len = 0;
    int idx = 0;
    while (rem_len > 0) {
      if (rem_len > STRING_LIMIT) {
        hex_len = STRING_LIMIT;
      }
      else {
        hex_len = str_len - idx;
      }
      rem_len -= hex_len;
      char *str = str_replace_all_len(content + set + idx, "%%", "%", hex_len);
      char *hex = str_hex_len(str, strlen(str));
      if (mode == CTL_MODE_MODULE && templ == 1) {
        str_append(template, "ctl->write(\"");
        str_append(template, hex);
        str_append(template, "\");\n");
      }
      free(str);
      free(hex);
      idx += hex_len;
    }
  }
  *template = str_replace_all(*template, "%%", "%");
  return CTL_ERR_OK;
}

ctl_error_t ctl_file_read(char **content, const char *filename) {
  FILE *fp = fopen(filename, "r");
  if (fp == NULL) {
    return CTL_ERR_NO_FILE;
  }
  fseek(fp, 0, SEEK_END);
  int len = ftell(fp);
  fseek(fp, 0, SEEK_SET);
  *content = calloc(len + 1, sizeof(char));
  fread(*content, sizeof(char), len, fp);
  (*content)[len] = '\0';
  fclose(fp);
  return CTL_ERR_OK;
}

ctl_error_t ctl_file_read_binary(char **binary, size_t *len, const char *filename) {
  FILE *fp = fopen(filename, "rb");
  if (fp == NULL) {
    return CTL_ERR_NO_FILE;
  }
  fseek(fp, 0, SEEK_END);
  *len = ftell(fp);
  fseek(fp, 0, SEEK_SET);
  *binary = calloc(*len, sizeof(char));
  fread(*binary, sizeof(char), *len, fp);
  fclose(fp);
  return CTL_ERR_OK;
}

ctl_error_t ctl_file_write(const char *content, const char *filename) {
  FILE *fp = fopen(filename, "w");
  if (fp == NULL) {
    return CTL_ERR_NO_FILE;
  }
  if (fwrite(content, sizeof(char), strlen(content), fp) < 0) {
    return CTL_ERR_NO_WRITE;
  }
  fclose(fp);
  return CTL_ERR_OK;
}

int main(int argc, char **argv) {
  ctl_error_t err;
  opterr = 0;
  ctl_mode_t mode;
  char *path = NULL;
  int opt;
  while ((opt = getopt(argc, argv, "h:o:")) != -1) {
    switch (opt) {
      case 'h':
        mode = CTL_MODE_HEADER;
        path = optarg;
        break;
      case 'o':
        mode = CTL_MODE_MODULE;
        path = optarg;
        break;
      default:
        err = CTL_ERR_NO_ARGS;
        ctl_error_str(err);
        return err;
    }
  }
  if (path == NULL) {
    err = CTL_ERR_NO_ARGS;
    ctl_error_str(err);
    return err;
  }
  char *content = NULL;
  err = ctl_file_read(&content, path);
  if (err != CTL_ERR_OK) {
    ctl_error_str(err);
    return err;
  }
  char *template = NULL;
  err = ctl_render(mode, &template, content);
  free(content);
  if (err != CTL_ERR_OK) {
    if (template != NULL) {
      free(template);
    }
    ctl_error_str(err);
    return err;
  }
  if (mode == CTL_MODE_HEADER) {
    fprintf(stdout, "%s", template);
    free(template);
    return CTL_ERR_OK;
  }
  if (mode == CTL_MODE_MODULE) {
    err = ctl_file_write(template, CTL_TMP_C_PATH);
    free(template);
    char cmd[500];
    snprintf(cmd, sizeof(cmd), "cc -shared -fPIC -o %s %s", CTL_TMP_SO_PATH, CTL_TMP_C_PATH);
    int rc = system(cmd);
    remove(CTL_TMP_C_PATH);
    if (rc != 0) {
      err = CTL_ERR_NO_COMPILE;
      ctl_error_str(err);
      return err;
    }
    char *binary = NULL;
    size_t len = 0;
    err = ctl_file_read_binary(&binary, &len, CTL_TMP_SO_PATH);
    remove(CTL_TMP_SO_PATH);
    if (err != CTL_ERR_OK) {
      ctl_error_str(err);
      return err;
    }
    fwrite(binary, sizeof(char), len, stdout);
    free(binary);
    return CTL_ERR_OK;
  }
}
