/*
 * 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 cdoc.h
// @desc Lazy documentation parser for C

#pragma once

#include <stdlib.h>

// @enum cdoc_type_t
// @groupdef cdoc_type_t
typedef enum {
  CDOC_TYPE_NONE     =  0,
  CDOC_TYPE_FILE     =  1,
  CDOC_TYPE_IMPL     =  2,
  CDOC_TYPE_TYPE     =  3,
  CDOC_TYPE_INCLUDE  =  4,
  CDOC_TYPE_MACRO    =  5,
  CDOC_TYPE_VAR      =  6,
  CDOC_TYPE_ENUM     =  7,
  CDOC_TYPE_STRUCT   =  8,
  CDOC_TYPE_UNION    =  9,
  CDOC_TYPE_FUNCTION = 10,
  __CDOC_TYPE_COUNT  = 11,
} cdoc_type_t;

// @struct cdoc_ref_t
// @groupdef cdoc_ref_t
typedef struct {
  cdoc_type_t type;
  char *symbol;
  char *description;
  char *signature;
  char *obsolete;
  char *note;
  char *ret;
  size_t line;
  bool is_internal;
  char **param_list;
  size_t param_list_len;
  char *groupdef;
  char *group;
} cdoc_ref_t;

// @struct cdoc_t
// @groupdef cdoc_t
typedef struct {
  cdoc_ref_t *ref_list;
  size_t ref_list_len;
} cdoc_t;

// @macro CDOC_REF_INIT
// @group cdoc_ref_t
#define CDOC_REF_INIT (cdoc_ref_t) { CDOC_TYPE_NONE, NULL, NULL, NULL, NULL, NULL, NULL, 0,  false, NULL, 0, NULL, NULL }

// @macro CDOC_INIT
// @group cdoc_t
#define CDOC_INIT (cdoc_t) { NULL, 0 }

// @func cdoc_type_str
// @group cdoc_type_t
const char* cdoc_type_str(cdoc_type_t type);

// @func cdoc_type_name
// @group cdoc_type_t
const char* cdoc_type_name(cdoc_type_t type);

// @func cdoc_is_doc
bool cdoc_is_doc(char *line);

// @func cdoc_symbol_get
char *cdoc_symbol_get(char *line);

// @func cdoc_parse_desc
char *cdoc_parse_desc(char *value);

// @func cdoc_parse
// @group cdoc_t
cdoc_t cdoc_parse(char *content);

// @func cdoc_free
// @group cdoc_t
void cdoc_free(cdoc_t *self);

#ifdef CDOC_IMPL

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

// @impl cdoc_type_str
const char* cdoc_type_str(cdoc_type_t type) {
  switch (type) {
    case __CDOC_TYPE_COUNT:
    case CDOC_TYPE_NONE:
      return NULL;
    case CDOC_TYPE_FILE:
      return "file";
    case CDOC_TYPE_IMPL:
      return "impl";
    case CDOC_TYPE_TYPE:
      return "type";
    case CDOC_TYPE_INCLUDE:
      return "include";
    case CDOC_TYPE_MACRO:
      return "macro";
    case CDOC_TYPE_VAR:
      return "var";
    case CDOC_TYPE_ENUM:
      return "enum";
    case CDOC_TYPE_STRUCT:
      return "struct";
    case CDOC_TYPE_UNION:
      return "union";
    case CDOC_TYPE_FUNCTION:
      return "func";
  }
  return NULL;
}

// @impl cdoc_type_name
const char* cdoc_type_name(cdoc_type_t type) {
  switch (type) {
    case __CDOC_TYPE_COUNT:
    case CDOC_TYPE_NONE:
      return NULL;
    case CDOC_TYPE_FILE:
      return "file";
    case CDOC_TYPE_IMPL:
      return "impl";
    case CDOC_TYPE_TYPE:
      return "type";
    case CDOC_TYPE_INCLUDE:
      return "include";
    case CDOC_TYPE_MACRO:
      return "macro";
    case CDOC_TYPE_VAR:
      return "variable";
    case CDOC_TYPE_ENUM:
      return "enum";
    case CDOC_TYPE_STRUCT:
      return "struct";
    case CDOC_TYPE_UNION:
      return "union";
    case CDOC_TYPE_FUNCTION:
      return "function";
  }
  return NULL;
}

// @impl cdoc_escape_str
char *cdoc_escape_str(char *value) {
  if (value == NULL) return NULL;
  buf_t buf= BUF_NULL;
  buf_write(&buf, value);
  buf_repall(&buf, "&", "&amp;");
  buf_repall(&buf, "<", "&lt;");
  buf_repall(&buf, ">", "&gt;");
  buf_repall(&buf, "\"", "&quot;");
  buf_repall(&buf, "'", "&apos;");
  buf_write_len(&buf, "\0", 1);
  return (char *) buf.bytes;
}

// @impl cdoc_parse
char *cdoc_parse_desc(char *value) {
  if (value == NULL) return NULL;
  buf_t buf = BUF_NULL;
  size_t len = strlen(value);
  size_t set = 0;
  for (size_t i = 0; i < len; i++) {
    char c = value[i];
    if (c == '{') {
      buf_write_len(&buf, value + set, i - set);
      set = i + 1;
    }
    if (c == '}') {
      buf_write(&buf, "<a href=\"#");
      buf_write_len(&buf, value + set, i - set);
      buf_write(&buf, "\">");
      buf_write_len(&buf, value + set, i - set);
      buf_write(&buf, "</a>");
      set = i + 1;
    }
  }
  if (set < len) {
      buf_write_len(&buf, value + set, len - set);
  }
  buf_write_len(&buf, "\0", 1);
  return (char *) buf.bytes;
}

// @impl cdoc_is_doc
bool cdoc_is_doc(char *line) {
  return strncmp(line, "// @", 4) == 0;
}

// @impl cdoc_symbol_get
char *cdoc_symbol_get(char *line) {
  if (cdoc_is_doc(line)) {
    char *op = line + 4;
    for (size_t i = 0; i < __CDOC_TYPE_COUNT; i++) {
      cdoc_type_t type = (cdoc_type_t) i;
      if (type == CDOC_TYPE_NONE) {
        continue;
      }
      if (type == CDOC_TYPE_FILE) {
        continue;
      }
      const char *str = cdoc_type_str(type);
      if (str && strncmp(op, str, strlen(str)) == 0) {
        char *value = strstr(op, " ") + 1;
        if (value) {
          size_t offset = value - op;
          size_t value_len = strlen(line) - offset - 4;
          return strndup(value, value_len);
        }
      }
    }
  }
  return NULL;
}

// @impl cdoc_parse
cdoc_t cdoc_parse(char *content) {
  cdoc_t doc = CDOC_INIT;
  size_t i = 0;
  size_t ln = 0;
  size_t sol = 0;
  size_t ln_sig = 0;
  bool is_doc = false;
  bool is_signature = false;
  cdoc_ref_t ref = CDOC_REF_INIT;
  size_t content_len = strlen(content);
  while (i < content_len) {
    char c = content[i];
    if (c == '\n') {
      size_t ln_len = i - sol;
      char *line = content + sol;
      if (strncmp(line, "// @", 4) == 0) {
        is_doc = true;
        bool is_op = false;
        char *op = line + 4;
        for (size_t i = 0; i < __CDOC_TYPE_COUNT; i++) {
          cdoc_type_t type = (cdoc_type_t) i;
          const char *str = cdoc_type_str(type);
          if (str && strncmp(op, str, strlen(str)) == 0) {
            ref.type = type;
            is_op = true;
            break;
          }
        }
        if (is_op) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.symbol = strndup(value, value_len);
          }
        }
        if (strncmp(op, "desc", 4) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.description = strndup(value, value_len);
          }
        }
        if (strncmp(op, "obsolete", 8) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.obsolete = strndup(value, value_len);
          }
        }
        if (strncmp(op, "note", 4) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.note = strndup(value, value_len);
          }
        }
        if (strncmp(op, "return", 6) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.ret = strndup(value, value_len);
          }
        }
        if (strncmp(op, "internal", 8) == 0) {
          ref.is_internal = true;
        }
        if (strncmp(op, "param", 5) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            ref.param_list = realloc(ref.param_list, sizeof(char *) * (ref.param_list_len + 1));
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.param_list[ref.param_list_len] = strndup(value, value_len);
            ref.param_list_len++;
          }
        }
        if (strncmp(op, "groupdef", 8) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.groupdef = strndup(value, value_len);
          }
        }
        else if (strncmp(op, "group", 5) == 0) {
          char *value = strstr(op, " ") + 1;
          if (value) {
            size_t offset = value - op;
            size_t value_len = ln_len - offset - 4;
            ref.group = strndup(value, value_len);
          }
        }
      }
      else if (is_doc) {
        is_signature = true;
        is_doc = false;
        ref.line = ln + 1;
      }
      if (is_signature) {
        char *value = line;
        size_t value_len = ln_len;
        size_t signature_len = 0;
        if (value) {
          if (ref.signature) {
            signature_len = strlen(ref.signature);
          }
          ref.signature = realloc(ref.signature, sizeof(char) * (signature_len + value_len + 2));
          memcpy(ref.signature + signature_len, value, value_len);
          ref.signature[signature_len + value_len] = '\n';
          ref.signature[signature_len + value_len + 1] = '\0';
        }
        bool is_end = true;
        if (ref.type == CDOC_TYPE_MACRO) {
          if (ref.signature[strlen(ref.signature) - 2] == '\\') {
            is_end = false;
          }
        }
        if (ref.type == CDOC_TYPE_VAR) {
          if (ln_sig == 0 && ref.signature[strlen(ref.signature) - 2] == '{') {
            is_end = false;
          }
          if (ln_sig > 0 && ref.signature[signature_len] != '}') {
            is_end = false;
          }
        }
        if (ref.type == CDOC_TYPE_ENUM) {
          if (ref.signature[signature_len] != '}') {
            is_end = false;
          }
        }
        if (ref.type == CDOC_TYPE_STRUCT) {
          if (ln_sig == 0 && ref.signature[strlen(ref.signature) - 2] == '{') {
            is_end = false;
          }
          if (ln_sig > 0 && ref.signature[signature_len] != '}') {
            is_end = false;
          }
        }
        if (ref.type == CDOC_TYPE_UNION) {
          if (ref.signature[signature_len] != '}') {
            is_end = false;
          }
        }
        if (ref.type == CDOC_TYPE_FUNCTION) {
          if (ln_sig == 0 && ref.signature[strlen(ref.signature) - 2] == '{') {
            buf_t buf = BUF_NULL;
            buf_write(&buf, ref.signature);
            free(ref.signature);
            buf_repall(&buf, "{", "{ ... }");
            buf_write_len(&buf, "\0", 1);
            ref.signature = (char *) buf.bytes;
          }
        }
        ln_sig++;
        if (is_end) {
          is_signature = false;
          ln_sig = 0;
          if (ref.type != CDOC_TYPE_NONE) {
            doc.ref_list = realloc(doc.ref_list, sizeof(cdoc_ref_t) * (doc.ref_list_len + 1));
            memcpy(&doc.ref_list[doc.ref_list_len], &ref, sizeof(cdoc_ref_t));
            doc.ref_list_len++;
            ref = CDOC_REF_INIT;
          }
        }
      }
      sol = i + 1;
      ln++;
    }
    i++;
  }
  return doc;
}

// @impl cdoc_free
void cdoc_free(cdoc_t *self) {
  for (size_t i = 0; i < self->ref_list_len; i++) {
    cdoc_ref_t *ref = &self->ref_list[i];
    if (ref->symbol) free(ref->symbol);
    if (ref->description) free(ref->description);
    if (ref->obsolete) free(ref->obsolete);
    if (ref->note) free(ref->note);
    if (ref->ret) free(ref->ret);
    if (ref->signature) free(ref->signature);
    if (ref->param_list) {
      for (size_t i = 0; i < ref->param_list_len; i++) {
        free(ref->param_list[i]);
      }
      free(ref->param_list);
    }
    if (ref->groupdef) free(ref->groupdef);
    if (ref->group) free(ref->group);
  }
}

#endif // CDOC_IMPL
