#define DN_WITH_OS 1
#if defined(USE_SINGLE_HEADER)
#include "Single-Header/dn_single_header.h"
#else
#include "Source/dn.h"
#endif

#if defined(USE_SINGLE_HEADER)
#include "Single-Header/dn_single_header.cpp"
#else
#include "Source/dn.cpp"
#endif

enum FileType
{
  FileType_Header,
  FileType_Impl,
  FileType_Count
};

struct File
{
  FileType type;
  DN_Str8  file_name;
};

static void AppendCppFileLineByLine(DN_Str8Builder *dest, DN_Str8 cpp_path)
{
  DN_TcScratch scratch = DN_TcScratchBeginArena(&dest->arena, 1);
  DN_ErrSink  *err     = DN_TcErrSinkBeginDefault();
  DN_Str8      buffer  = DN_OS_FileReadAllArena(&scratch.arena, cpp_path, err);
  DN_ErrSinkEndExitIfErrorF(err, -1, "Failed to load file from '%S' for appending", cpp_path);

  bool inside_clangd_preprocessor_block = false;
  for (DN_Str8 walker = buffer; walker.count;) {
    // NOTE: Trim the whitespace, mainly for windows, the file we read will have \r\n whereas we just want to emit \n
    DN_Str8BSplitResult split = DN_Str8BSplit(walker, DN_Str8Lit("\n"));
    DN_Str8             line  = DN_Str8TrimTailWhitespace(split.lhs);
    walker                    = split.rhs;

    // NOTE: Detect if we're inside a clangd preprocessor block
    // TODO: This breaks if there's any nested #if's in the block (we naiively match on #endif)
    bool commented_out = false;
    if (!inside_clangd_preprocessor_block) {
      DN_Str8FindResult find = DN_Str8FindStr8(line, DN_Str8Lit("#if defined(_CLANGD)"), DN_Str8EqCase_Sensitive);
      if (find.found) {
        line                             = DN_Str8FmtArena(&scratch.arena, "%S// DN: Single header generator commented out => %S", find.start_to_before_match, DN_Str8TrimWhitespaceAround(find.match_to_end_of_buffer));
        commented_out                    = true;
        inside_clangd_preprocessor_block = true;
      }
    }

    // NOTE: Inline relative includes as long as we're not inside a CLANDG block
    DN_Str8 extra_include_path = {};
    if (!inside_clangd_preprocessor_block) {
      DN_Str8 ignore_patterns[] = {
        DN_Str8Lit("#include \"dn.h\""),
        DN_Str8Lit("#include \"dn.cpp\""),
      };

      DN_Str8FindResult ignore = DN_Str8FindStr8Array(line, ignore_patterns, DN_ArrayCountU(ignore_patterns), DN_Str8EqCase_Sensitive);
      if (!ignore.found) {
        DN_Str8FindResult find = DN_Str8FindStr8(line, DN_Str8Lit("#include \""), DN_Str8EqCase_Sensitive);
        if (find.found) {
          line                     = DN_Str8FmtArena(&scratch.arena, "%S// DN: Single header generator commented out => %S", find.start_to_before_match, DN_Str8TrimWhitespaceAround(find.match_to_end_of_buffer));
          DN_Str8 rel_include_path = DN_Str8TrimWhitespaceAround(find.after_match_to_end_of_buffer);
          DN_Str8 root_dir         = DN_Str8FileDirectoryFromPath(cpp_path);
          extra_include_path       = DN_Str8FmtOsPathArena(&scratch.arena, "%S/%S", root_dir, DN_Str8TrimSuffixSensitive(rel_include_path, DN_Str8Lit("\"")));
        }
      }
    }

    // NOTE: Detect if we're at the end of the CLAND block
    if (inside_clangd_preprocessor_block) {
      if (!commented_out)
        line = DN_Str8FmtArena(&scratch.arena, "// %S", line);
      if (DN_Str8FindStr8(line, DN_Str8Lit("#endif"), DN_Str8EqCase_Sensitive).found)
        inside_clangd_preprocessor_block = false;
    }

    DN_Str8BuilderAppendCopy(dest, line);
    DN_Str8BuilderAppendRef(dest, DN_Str8Lit("\n"));

    if (extra_include_path.count)
      AppendCppFileLineByLine(dest, extra_include_path);
  }
  DN_TcScratchEnd(&scratch);
}

int main(int argc, char **argv)
{
  DN_Core dn = {};
  DN_Init(&dn, DN_InitFlags_OS, DN_TcInitArgsDefault());

  if (argc != 3) {
    DN_OS_PrintErrF("USAGE: %s <path/to/dn/Source> <output_dir>", argv[0]);
    return -1;
  }

  DN_Str8 dn_root_dir = DN_Str8FromCStr8(argv[1]);
  DN_Str8 output_dir  = DN_Str8FromCStr8(argv[2]);
  if (!DN_OS_PathMakeDir(output_dir)) {
    DN_OS_PrintErrF("Failed to make requested output directory: %S", output_dir);
    return -1;
  }

  DN_Str8 const REL_FILE_PATHS[] = {
      DN_Str8Lit("dn"),
  };

  for (DN_ForIndexU(type, FileType_Count)) {
    DN_TcScratch   scratch = DN_TcScratchBeginArena(nullptr, 0);
    DN_Str8Builder builder = DN_Str8BuilderFromArena(&scratch.arena);
    DN_Str8        suffix  = type == FileType_Header ? DN_Str8Lit("h") : DN_Str8Lit("cpp");
    for (DN_ForItCArray(it, DN_Str8 const, REL_FILE_PATHS)) {
      DN_Str8 path = DN_Str8FmtOsPathArena(&scratch.arena, "%S/%S.%S", dn_root_dir, *it.data, suffix);
      AppendCppFileLineByLine(&builder, path);
    }

    DN_Date date = DN_OS_DateLocalTimeNow();
    DN_Str8BuilderPrependF(&builder, "// Generated by the DN single header generator %04u-%02u-%02u %02u:%02u:%02u\n\n", date.year, date.month, date.day, date.hour, date.minutes, date.seconds);

    DN_Str8     buffer             = DN_Str8TrimWhitespaceAround(DN_Str8FromStr8BuilderArena(&builder, &scratch.arena));
    DN_Str8     single_header_path = DN_Str8FmtOsPathArena(&scratch.arena, "%S/dn_single_header.%S", output_dir, suffix);
    DN_ErrSink *err                = DN_TcErrSinkBeginDefault();
    DN_OS_FileWriteAllSafe(single_header_path, buffer, err);
    DN_ErrSinkEndExitIfErrorF(err, -1, "Failed to write Single header file '%S'", single_header_path);
  }
}
