// Copyright (C) 2026 Sophie Sitzler
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// @file main.c
// @desc The ductape compiler.

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

#define BUF_IMPL
// @include <buf.h>
#include <buf.h>

#define DUC_BUILDPATH "DUC_BUILDPATH"
#define DUC_HEADERPATH "DUC_HEADERPATH"
#define DUC_LIBPATH "DUC_LIBPATH"

#define DUC_SYM_SIG_HASH "__DUC_SIG_HASH_"
#define DUC_SYM_UID "__DUC_UID_"
#define DUC_SYM_SPLIT "%"
#define DUC_SYM_SPLIT_ESCAPE DUC_SYM_SPLIT DUC_SYM_SPLIT
#define DUC_SYM_WRITE "DUC_WRITE"
#define DUC_SYM_TEMPLDEF "DUC_TEMPLDEF"
#define DUC_SYM_ENDTEMPL "DUC_ENDTEMPL"
#define DUC_SYM_TEMPLDEF_HEADER "__DUC_TEMPLDEF_HEADER"

#define DUC_TMP_C_PATH ".duc_tmp.c"
#define DUC_TMP_SO_PATH ".duc_tmp.so"

#define DUC_CMD_COMPILE "cc -shared -fPIC -o %s %s %s"

// @enum duc_err_t
typedef enum {
  DUC_ERR_OK,
  DUC_ERR_NO_ARGS,
  DUC_ERR_NO_FILE,
  DUC_ERR_NO_WRITE,
  DUC_ERR_UNCLOSED_CODE,
  DUC_ERR_NO_COMPILE,
  __DUC_ERR__COUNT,
} duc_err_t;

// @enum duc_mode_t
typedef enum {
  DUC_MODE_HEADER,
  DUC_MODE_MODULE,
  __DUC_MODE__COUNT,
} duc_mode_t;

// @func duc_error_str
void duc_error_str(duc_err_t err) {
  switch (err) {
    case DUC_ERR_OK:
      break;
    case DUC_ERR_NO_ARGS:
      fprintf(stderr, "Usage: ductape <files...>\n");
      break;
    case DUC_ERR_NO_FILE:
      fprintf(stderr, "Error: No file found!\n");
      break;
    case DUC_ERR_NO_WRITE:
      fprintf(stderr, "Error: Can't write to file!\n");
      break;
    case DUC_ERR_UNCLOSED_CODE:
      fprintf(stderr, "Error: Unclosed code block!\n");
      break;
    case DUC_ERR_NO_COMPILE:
      fprintf(stderr, "Error: Failed to compile!\n");
      break;
    case __DUC_ERR__COUNT:
      break;
  }
}

// @func duc_sig_hash
size_t duc_sig_hash(char *str, size_t len) {
  size_t hash = 5381;
  size_t i = 0;
  while (i < len) {
    unsigned char c = (unsigned) str[i];
    if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
      i++;
      continue;
    }
    hash = ((hash << 5) + hash) + c;
    i++;
  }
  return hash;
}

// @func duc_render
duc_err_t duc_render(duc_mode_t mode, buf_t *out, char *content, char **uid) {
  size_t len = strlen(content);
  size_t set = 0;
  size_t is_code = 0;
  size_t is_templ = 0;
  size_t is_templ_begin = 0;
  size_t hash = 0;
  for (size_t i = 0; i < len; i++) {
    if (content[i] == *DUC_SYM_SPLIT) {
      // ignore escaped DUC_SYM_SPLIT
      if (i + 1 < len && content[i + 1] == *DUC_SYM_SPLIT) {
        i += 1;
        continue;
      }
      if (is_code) {
        for (size_t j = 0; j < i - set; j++) {
          if (strncmp(content + set + j, DUC_SYM_TEMPLDEF, strlen(DUC_SYM_TEMPLDEF)) == 0) {
            is_templ = 1;
            is_templ_begin = 1;
            hash = duc_sig_hash(content + set, i - set);
            bool is_arg = false;
            bool is_uid = false;
            size_t uid_set = 0;
            for (size_t k = j; k < i - set; k++) {
              char c = content[set + k];
              if (c == '(') {
                is_arg = true;
              }
              if (is_arg && isalpha(c)) {
                is_uid = true;
                is_arg = false;
                uid_set = k;
              }
              if (is_uid && (!isalpha(c) && !isdigit(c) && c != '_')) {
                is_uid = false;
                *uid = strndup(content + set + uid_set, k - uid_set);
                break;
              }
            }
          }
          if (strncmp(content + set + j, DUC_SYM_ENDTEMPL, strlen(DUC_SYM_ENDTEMPL)) == 0) {
            is_templ = 0;
          }
        }
        if (mode == DUC_MODE_MODULE) {
          buf_write_len(out, content + set, i - set);
          buf_write(out, "\n");
          if (is_templ_begin) {
            is_templ_begin = 0;
            buf_write(out, "(void) ctx;");
          }
        }
        if (mode == DUC_MODE_HEADER && is_templ == 1) {
          is_templ = 0;
          buf_t templdef = BUF_NULL;
          buf_write_len(&templdef, content + set, i - set);
          buf_write(&templdef, "\n");
          buf_repall(&templdef, DUC_SYM_TEMPLDEF, DUC_SYM_TEMPLDEF_HEADER);
          buf_concat(out, templdef);
          buf_free(&templdef);
        }
      }
      else {
        const size_t STRING_LIMIT = 4095;
        size_t str_len = i - set;
        size_t rem_len = str_len;
        size_t hex_len = 0;
        size_t 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;
          buf_t hex = BUF_NULL;
          buf_write_len(&hex, content + set + idx, hex_len);
          buf_repall(&hex, DUC_SYM_SPLIT_ESCAPE, DUC_SYM_SPLIT);
          buf_to_hex(&hex);
          if (mode == DUC_MODE_MODULE && is_templ == 1) {
            buf_write(out, DUC_SYM_WRITE "(\"");
            buf_concat(out, hex);
            buf_write(out, "\")\n");
          }
          buf_free(&hex);
          idx += hex_len;
        }
      }
      is_code = !is_code;
      set = i + 1;
    }
  }
  buf_t meta = BUF_NULL;
  buf_write(&meta, "#include <dlfcn.h>\n");
  buf_write(&meta, "#include <string.h>\n");
  buf_write(&meta, "#include <stdlib.h>\n");
  buf_write(&meta, "#include <ductape.h>\n");
  buf_writef(&meta, "const char " DUC_SYM_UID "%s[] = \"%s\";\n", *uid, *uid);
  buf_writef(&meta, "const size_t " DUC_SYM_SIG_HASH "%s = %ld;\n", *uid, hash);
  buf_concat(&meta, *out);
  buf_free(out);
  *out = BUF_NULL;
  buf_concat(out, meta);
  buf_free(&meta);
  if (is_code) {
    return DUC_ERR_UNCLOSED_CODE;
  }
  else {
    const size_t STRING_LIMIT = 4095;
    size_t str_len = len - set;
    size_t rem_len = str_len;
    size_t hex_len = 0;
    size_t 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;
      buf_t hex = BUF_NULL;
      buf_write_len(&hex, content + set + idx, hex_len);
      buf_repall(&hex, DUC_SYM_SPLIT_ESCAPE, DUC_SYM_SPLIT);
      buf_to_hex(&hex);
      if (mode == DUC_MODE_MODULE && is_templ == 1) {
        buf_write(out, DUC_SYM_WRITE "(\"");
        buf_concat(out, hex);
        buf_write(out, "\")\n");
      }
      buf_free(&hex);
      idx += hex_len;
    }
  }
  buf_repall(out, DUC_SYM_SPLIT_ESCAPE, DUC_SYM_SPLIT);
  return DUC_ERR_OK;
}

// @func duc_file_read
duc_err_t duc_file_read(char **content, const char *filename) {
  FILE *fp = fopen(filename, "r");
  if (fp == NULL) {
    return DUC_ERR_NO_FILE;
  }
  fseek(fp, 0, SEEK_END);
  size_t 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 DUC_ERR_OK;
}

// @func duc_file_read_binary
duc_err_t duc_file_read_binary(char **binary, size_t *len, const char *filename) {
  FILE *fp = fopen(filename, "rb");
  if (fp == NULL) {
    return DUC_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 DUC_ERR_OK;
}

// @func duc_file_write
duc_err_t duc_file_write(const char *content, size_t len, const char *filename) {
  FILE *fp = fopen(filename, "w");
  if (fp == NULL) {
    return DUC_ERR_NO_FILE;
  }
  if (fwrite(content, sizeof(char), len, fp) != len) {
    return DUC_ERR_NO_WRITE;
  }
  fclose(fp);
  return DUC_ERR_OK;
}

// @func duc_compile_file
duc_err_t duc_compile_file(duc_mode_t mode, char *filepath, char **uid) {
  duc_err_t err;
  char *content = NULL;
  err = duc_file_read(&content, filepath);
  if (err != DUC_ERR_OK) {
    duc_error_str(err);
    return err;
  }
  buf_t template = BUF_NULL;
  err = duc_render(mode, &template, content, uid);
  free(content);
  if (err != DUC_ERR_OK || *uid == NULL) {
    buf_free(&template);
    duc_error_str(err);
    return err;
  }
  switch (mode) {
    case DUC_MODE_HEADER: {
      char *path = getenv(DUC_HEADERPATH);
      if (path == NULL) {
        path = "./";
      }
      buf_t filename = BUF_NULL;
      buf_writef(&filename, "%s%s.h", path, *uid);
      buf_terminate(&filename);
      duc_file_write((char *) template.bytes, template.len, (char *) filename.bytes);
      buf_free(&filename);
      buf_free(&template);
      return DUC_ERR_OK;
    }
    case DUC_MODE_MODULE: {
      err = duc_file_write((char *) template.bytes, template.len, DUC_TMP_C_PATH);
      buf_free(&template);
      char *lib_path = getenv(DUC_LIBPATH);
      if (lib_path == NULL) {
        lib_path = "";
      }
      char cmd[500];
      snprintf(cmd, sizeof(cmd), DUC_CMD_COMPILE, DUC_TMP_SO_PATH, lib_path, DUC_TMP_C_PATH);
      int rc = system(cmd);
      remove(DUC_TMP_C_PATH);
      if (rc != 0) {
        err = DUC_ERR_NO_COMPILE;
        duc_error_str(err);
        return err;
      }
      char *binary = NULL;
      size_t len = 0;
      err = duc_file_read_binary(&binary, &len, DUC_TMP_SO_PATH);
      remove(DUC_TMP_SO_PATH);
      if (err != DUC_ERR_OK) {
        duc_error_str(err);
        return err;
      }
      char *path = getenv(DUC_BUILDPATH);
      if (path == NULL) {
        path = "./";
      }
      buf_t filename = BUF_NULL;
      buf_writef(&filename, "%s%s.so", path, *uid);
      buf_terminate(&filename);
      duc_file_write(binary, len, (char *) filename.bytes);
      buf_free(&filename);
      free(binary);
      return DUC_ERR_OK;
    }
    case __DUC_MODE__COUNT:
      break;
  }
  return DUC_ERR_OK;
}

// @func main
int main(int argc, char **argv) {
  if (argc <= 1) {
    return DUC_ERR_NO_ARGS;
  }
  char *headerpath = getenv(DUC_HEADERPATH);
  char *buildpath = getenv(DUC_BUILDPATH);
  char *libpath = getenv(DUC_LIBPATH);
  fprintf(stdout, "[ductape] Compiling templates...\n");
  fprintf(stdout, " ." DUC_HEADERPATH " = '%s'\n", headerpath ? headerpath : "./");
  fprintf(stdout, " ." DUC_BUILDPATH " = '%s'\n", buildpath ? buildpath : "./");
  if (libpath) {
    fprintf(stdout, " ." DUC_LIBPATH " = '%s'\n", libpath);
  }
  duc_err_t err = DUC_ERR_OK;
  for (size_t i = 1; i < (size_t) argc; i++) {
    char *filepath = argv[i];
    duc_err_t header_err = DUC_ERR_OK;
    char *uid = NULL;
    if ((header_err = duc_compile_file(DUC_MODE_HEADER, filepath, &uid)) != DUC_ERR_OK) {
      err = header_err;
    }
    if (uid) {
      free(uid);
    }
    uid = NULL;
    duc_err_t module_err = DUC_ERR_OK;
    if ((module_err = duc_compile_file(DUC_MODE_MODULE, filepath, &uid)) != DUC_ERR_OK) {
      err = module_err;
    }
    fprintf(stdout, "-- compile '%s' | %s [%s]\n", uid, filepath, header_err == DUC_ERR_OK && module_err == DUC_ERR_OK ? "OK" : "FAILED");
    if (uid) {
      free(uid);
    }
  }
  return err;
}
