#include "core/core.c"

#define CL_Arena MA_Arena
#define CL_PushSize MA_PushSizeNonZeroed
#define CL_ASSERT IO_Assert
#define CL_VSNPRINTF stbsp_vsnprintf
#define CL_SNPRINTF stbsp_snprintf
#include "clexer.h"

MA_Arena G__Arena;
MA_Arena *G_Arena = &G__Arena;

S8_List BLD_DefaultMSVCCompileFlags;
S8_List BLD_DefaultMSVCDebugCompileFlags;
S8_List BLD_DefaultMSVCReleaseCompileFlags;
S8_List BLD_DefaultMSVCReleaseCompileFlagsWithoutCRT;
S8_List BLD_DefaultMSVCCompileFlagsWithSTD;
S8_List BLD_DefaultMSVCDebugCompileFlagsWithSTD;
S8_List BLD_DefaultMSVCReleaseCompileFlagsWithSTD;
S8_List BLD_BundledMSVCIncludeSearchPaths;
S8_List BLD_BundledMSVCLibrarySearchPaths;
S8_List BLD_DefaultMSVCLinkerFlags;
S8_List BLD_DefaultMSVCDebugLinkerFlags;
S8_List BLD_DefaultMSVCLinkerFlagsWithoutCRT;
S8_List BLD_CoreLibraryIncludeSearchPaths;

bool SRC_WasModified(S8_String file);

static char *BLD_CoreLibraryIncludes[] = {
    BASE_PATH,
    VENDOR_PATH "imgui",
};

CL_SearchPaths SRC_SearchPaths = {
    BLD_CoreLibraryIncludes,
    lengthof(BLD_CoreLibraryIncludes),
};

void BLD_Init() {
    // COMPILER FLAGS
    {
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-WX");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-W3");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-wd4200");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-diagnostics:column");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-nologo");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-FC");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-Z7");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-GF");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-Gm-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-Oi");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-Zo");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-D_CRT_SECURE_NO_WARNINGS");

        BLD_DefaultMSVCDebugCompileFlags = S8_CopyList(G_Arena, BLD_DefaultMSVCCompileFlags);
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugCompileFlags, "-Od");
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugCompileFlags, "-D_DEBUG");
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugCompileFlags, "-MTd"); // Statically link the C library with debug symbols // required because std C++ calls some debug stuff

        BLD_DefaultMSVCReleaseCompileFlags = S8_CopyList(G_Arena, BLD_DefaultMSVCCompileFlags);
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlags, "-O2");
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlags, "-MT");

        BLD_DefaultMSVCReleaseCompileFlagsWithoutCRT = S8_CopyList(G_Arena, BLD_DefaultMSVCCompileFlags);
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlagsWithoutCRT, "-GS-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlagsWithoutCRT, "-Gs9999999");

        BLD_DefaultMSVCCompileFlagsWithSTD = S8_CopyList(G_Arena, BLD_DefaultMSVCCompileFlags);
        BLD_DefaultMSVCDebugCompileFlagsWithSTD = S8_CopyList(G_Arena, BLD_DefaultMSVCDebugCompileFlags);
        BLD_DefaultMSVCReleaseCompileFlagsWithSTD = S8_CopyList(G_Arena, BLD_DefaultMSVCReleaseCompileFlags);
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlagsWithSTD, "-EHsc");
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugCompileFlagsWithSTD, "-EHsc");
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlagsWithSTD, "-EHsc");

        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-EHa-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCCompileFlags, "-GR-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugCompileFlags, "-EHa-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugCompileFlags, "-GR-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlags, "-EHa-");
        S8_AddF(G_Arena, &BLD_DefaultMSVCReleaseCompileFlags, "-GR-");
    }

    // SEARCH PATHS
    {
        S8_AddArrayWithPrefix(G_Arena, &BLD_CoreLibraryIncludeSearchPaths, "-I", BLD_CoreLibraryIncludes, lengthof(BLD_CoreLibraryIncludes));

        S8_AddF(G_Arena, &BLD_BundledMSVCIncludeSearchPaths, "-I" MISC_PATH "MSVC/14.34.31933/include");
        S8_AddF(G_Arena, &BLD_BundledMSVCIncludeSearchPaths, "-I" MISC_PATH "WindowsKits/Include/10.0.22000.0/ucrt");
        S8_AddF(G_Arena, &BLD_BundledMSVCIncludeSearchPaths, "-I" MISC_PATH "WindowsKits/Include/10.0.22000.0/shared");
        S8_AddF(G_Arena, &BLD_BundledMSVCIncludeSearchPaths, "-I" MISC_PATH "WindowsKits/Include/10.0.22000.0/um");
        S8_AddArrayWithPrefix(G_Arena, &BLD_BundledMSVCIncludeSearchPaths, "-I", BLD_CoreLibraryIncludes, lengthof(BLD_CoreLibraryIncludes));

        S8_AddF(G_Arena, &BLD_BundledMSVCLibrarySearchPaths, "-LIBPATH:" MISC_PATH "MSVC/14.34.31933/lib/x64");
        S8_AddF(G_Arena, &BLD_BundledMSVCLibrarySearchPaths, "-LIBPATH:" MISC_PATH "WindowsKits/Lib/10.0.22000.0/um/x64");
        S8_AddF(G_Arena, &BLD_BundledMSVCLibrarySearchPaths, "-LIBPATH:" MISC_PATH "WindowsKits/Lib/10.0.22000.0/ucrt/x64");
    }

    // LINKER
    {
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlags, "-incremental:no");
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlags, "-opt:ref");

        BLD_DefaultMSVCDebugLinkerFlags = S8_CopyList(G_Arena, BLD_DefaultMSVCLinkerFlags);
        S8_AddF(G_Arena, &BLD_DefaultMSVCDebugLinkerFlags, "-NODEFAULTLIB:LIBCMT");

        BLD_DefaultMSVCLinkerFlagsWithoutCRT = S8_CopyList(G_Arena, BLD_DefaultMSVCLinkerFlags);
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlagsWithoutCRT, "-STACK:0x100000,0x100000");
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlagsWithoutCRT, "-subsystem:windows");
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlagsWithoutCRT, "gdi32.lib");
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlagsWithoutCRT, "user32.lib");
        S8_AddF(G_Arena, &BLD_DefaultMSVCLinkerFlagsWithoutCRT, "kernel32.lib");
    }
}

typedef struct BLD_CompilerArgs BLD_CompilerArgs;
struct BLD_CompilerArgs {
    bool compile_to_objects;
    S8_String compiler_path;
    S8_List include_paths;
    S8_List library_paths;

    S8_List files_to_compile_to_objects;
    S8_List object_files_to_link;
    S8_List libraries_to_link;

    S8_List link_flags;
    S8_List compile_flags;

    S8_String output_filename;
};

BLD_CompilerArgs BLD_DefaultCompilerArgs() {
    BLD_CompilerArgs args = {0};
    args.compiler_path = S8_Lit(CL_PATH);
    args.include_paths = BLD_BundledMSVCIncludeSearchPaths;
    args.library_paths = BLD_BundledMSVCLibrarySearchPaths;
    args.compile_flags = BLD_DefaultMSVCDebugCompileFlags;
    args.link_flags = BLD_DefaultMSVCDebugLinkerFlags;
    return args;
}

int BLD_Compile(BLD_CompilerArgs a) {
    S8_String include_search_paths = S8_MergeWithSeparator(G_Arena, a.include_paths, S8_Lit(" "));
    S8_String library_paths = S8_MergeWithSeparator(G_Arena, a.library_paths, S8_Lit(" "));
    S8_String files_to_compile_to_objects = S8_MergeWithSeparator(G_Arena, a.files_to_compile_to_objects, S8_Lit(" "));
    S8_String object_files_to_link = S8_MergeWithSeparator(G_Arena, a.object_files_to_link, S8_Lit(" "));
    S8_String libraries_to_link = S8_MergeWithSeparator(G_Arena, a.libraries_to_link, S8_Lit(" "));

    S8_String link_flags = S8_MergeWithSeparator(G_Arena, a.link_flags, S8_Lit(" "));
    S8_String compiler_flags = S8_MergeWithSeparator(G_Arena, a.compile_flags, S8_Lit(" "));

    char *_c = "-c";
    if (a.compile_to_objects == false) {
        _c = "";
    }

    S8_String output_filename = a.output_filename;
    if (output_filename.len != 0) {
        output_filename = S8_Format(G_Arena, "-Fe:%.*s", S8_Expand(output_filename));
    }

    int result = OS_SystemF("%.*s %s %.*s %.*s %.*s %.*s %.*s -link %.*s %.*s %.*s",
                            S8_Expand(a.compiler_path), _c, S8_Expand(output_filename), S8_Expand(files_to_compile_to_objects),
                            S8_Expand(object_files_to_link), S8_Expand(compiler_flags), S8_Expand(include_search_paths),
                            S8_Expand(library_paths), S8_Expand(link_flags), S8_Expand(libraries_to_link));
    return result;
}

void BLD_AddDynamicSourceFile(S8_List *object_source_files, S8_List *object_files, S8_String source_filename, S8_String object_filename) {
    if (SRC_WasModified(source_filename)) {
        IO_Printf("RECOMPILING %.*s\n", S8_Expand(source_filename));
        S8_AddNode(G_Arena, object_source_files, source_filename);
    }
    S8_AddNode(G_Arena, object_files, object_filename);
}

void BLD_AddStaticSourceFile(S8_List *object_source_files, S8_List *object_files, S8_String source_filename, S8_String object_filename) {
    if (OS_FileExists(object_filename) == false) {
        S8_AddNode(G_Arena, object_source_files, source_filename);
        IO_Printf("RECOMPILING %.*s\n", S8_Expand(source_filename));
    }
    S8_AddNode(G_Arena, object_files, object_filename);
}

void BLD_AddRaylib(S8_List *libraries_to_link) {
    S8_AddF(G_Arena, libraries_to_link, VENDOR_PATH "raylib/windows/raylibdll.lib");
    OS_CopyFile(S8_Lit(VENDOR_PATH "raylib/windows/raylib.dll"), S8_Lit("raylib.dll"), false);
    IO_Printf("cp %s %s\n", VENDOR_PATH "raylib/windows/raylib.dll", "raylib.dll");
}

const int BLD_Lib_Glad = 1;
const int BLD_Lib_StbImage = 2;
const int BLD_Lib_StbTruetype = 4;
const int BLD_Lib_ImGui = 8;
const int BLD_Lib_StbDs = 16;
const int BLD_Lib_RemoveCore = 32;

const int BLD_Lib_Raylib = 32; // Arg to build project

void BLD_AddLibsToBuild(S8_List *object_source_files, S8_List *object_files, int flags) {
    if ((flags & BLD_Lib_RemoveCore) == 0) {
        BLD_AddDynamicSourceFile(object_source_files, object_files, S8_Lit(BASE_PATH "core/core.c"), S8_Lit("encore.obj"));
    }
    if (flags & BLD_Lib_Glad) {
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "glad/glad.c"), S8_Lit("glad.obj"));
    }
    if (flags & BLD_Lib_StbImage) {
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "stb_image.c"), S8_Lit("stb_image.obj"));
    }
    if (flags & BLD_Lib_StbTruetype) {
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "stb_truetype.c"), S8_Lit("stb_truetype.obj"));
    }
    if (flags & BLD_Lib_ImGui) {
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/backends/imgui_impl_win32.cpp"), S8_Lit("imgui_impl_win32.obj"));
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/backends/imgui_impl_opengl3.cpp"), S8_Lit("imgui_impl_opengl3.obj"));
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/imgui.cpp"), S8_Lit("imgui.obj"));
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/imgui_widgets.cpp"), S8_Lit("imgui_widgets.obj"));
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/imgui_draw.cpp"), S8_Lit("imgui_draw.obj"));
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/imgui_tables.cpp"), S8_Lit("imgui_tables.obj"));
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "imgui/imgui_demo.cpp"), S8_Lit("imgui_demo.obj"));
    }
    if (flags & BLD_Lib_StbDs) {
        BLD_AddStaticSourceFile(object_source_files, object_files, S8_Lit(VENDOR_PATH "stb_ds.c"), S8_Lit("stb_ds.obj"));
    }
}

int BLD_PresetUnity(S8_String cfile, int libs_to_compile) {
    S8_List object_files = {0};

    // Incremental compile of libraries
    {
        S8_List object_source_files = {0};
        BLD_AddLibsToBuild(&object_source_files, &object_files, libs_to_compile);
        if (object_source_files.node_count) {
            BLD_CompilerArgs args = BLD_DefaultCompilerArgs();
            args.compile_to_objects = true;
            args.files_to_compile_to_objects = object_source_files;
            int result = BLD_Compile(args);
            if (result != 0) {
                IO_Printf("FAILED to compile libraries\n");
                return result;
            }
        }
    }

    BLD_CompilerArgs args = BLD_DefaultCompilerArgs();
    args.files_to_compile_to_objects = S8_MakeList(G_Arena, cfile);

    S8_List libraries_to_link = {0};
    if (libs_to_compile & BLD_Lib_Raylib) {
        BLD_AddRaylib(&libraries_to_link);
    }
    args.libraries_to_link = libraries_to_link;
    args.object_files_to_link = object_files;
    int result = BLD_Compile(args);
    return result;
}

typedef struct TokenReplacement TokenReplacement;
struct TokenReplacement {
    S8_String replace;
    S8_String with;
};

S8_String TokenReplace(MA_Arena *arena, CL_ArenaTuple *arena_tuple, S8_String content, TokenReplacement *replace, int replace_count) {
    IO_Assert(content.str[content.len] == 0);
    CL_LexResult *lex_result = CL_LexString(arena_tuple, "TokenReplace", content.str);
    CL_Tokens tokens = lex_result->tokens;

    S8_List out = {0};
    S8_String c = content;

    for (int i = 0; i < tokens.count; i += 1) {
        CL_Token *it = tokens.data + i;
        S8_String s = S8_Make(it->str, it->len);
        S8_String replacement = {0};

        bool match = false;
        for (int ii = 0; ii < replace_count; ii += 1) {
            if (S8_AreEqual(s, replace[ii].replace, 0)) {
                match = true;
                replacement = replace[ii].with;
                break;
            }
        }

        if (match) {
            S8_String prefix = S8_GetPrefix(c, s.str - c.str);
            S8_AddNode(arena_tuple->other, &out, prefix);
            S8_AddNode(arena_tuple->other, &out, replacement);
            c = S8_Skip(c, s.len + prefix.len);
        }
    }
    S8_AddNode(arena_tuple->other, &out, c);

    S8_String result = S8_Merge(arena, out);
    return result;
}

//
// Cache
//

double SRC_Time;

typedef struct SRC_CacheEntry SRC_CacheEntry;
struct SRC_CacheEntry {
    uint64_t filepath_hash;
    uint64_t file_hash;
    uint64_t includes_hash;
    uint64_t total_hash;
};

#define SRC_CACHE_ENTRY_COUNT 1024
typedef struct SRC_Cache SRC_Cache;
struct SRC_Cache {
    int entry_cap;
    int entry_len;
    SRC_CacheEntry entries[SRC_CACHE_ENTRY_COUNT];
};
SRC_Cache *SRC_InMemoryCache;
SRC_Cache *SRC_FromFileCache;
CL_ArenaTuple SRC_ArenaTuple;
S8_String SRC_CacheFilename;

void SRC_InitCache(MA_Arena *arena, S8_String cachefilename) {
    SRC_CacheFilename = cachefilename;
    CL_InitDefaultTuple(&SRC_ArenaTuple);

    SRC_InMemoryCache = MA_PushStruct(arena, SRC_Cache);
    SRC_InMemoryCache->entry_cap = SRC_CACHE_ENTRY_COUNT;

    SRC_FromFileCache = MA_PushStruct(arena, SRC_Cache);
    SRC_FromFileCache->entry_cap = SRC_CACHE_ENTRY_COUNT;

    S8_String cache = OS_ReadFile(arena, SRC_CacheFilename);
    if (cache.len) MA_MemoryCopy(SRC_FromFileCache, cache.str, cache.len);
}

void SRC_SaveCache() {
    S8_String dump = S8_Make((char *)SRC_InMemoryCache, sizeof(SRC_Cache));
    OS_WriteFile(SRC_CacheFilename, dump);
}

SRC_CacheEntry *SRC_AddHash(uint64_t filepath, uint64_t file, uint64_t includes) {
    IO_Assert(SRC_InMemoryCache->entry_len + 1 < SRC_InMemoryCache->entry_cap);
    SRC_CacheEntry *result = SRC_InMemoryCache->entries + SRC_InMemoryCache->entry_len++;
    result->filepath_hash = filepath;
    result->file_hash = file;
    result->includes_hash = includes;
    result->total_hash = HashBytes(result, sizeof(uint64_t) * 3);
    return result;
}

SRC_CacheEntry *SRC_FindCache(SRC_Cache *cache, uint64_t filepath_hash) {
    for (int cache_i = 0; cache_i < cache->entry_len; cache_i += 1) {
        SRC_CacheEntry *it = cache->entries + cache_i;
        if (it->filepath_hash == filepath_hash) {
            return it;
        }
    }
    return 0;
}

SRC_CacheEntry *SRC_HashFile(S8_String file, char *parent_file) {
    char *resolved_file = CL_ResolveFilepath(G_Arena, &SRC_SearchPaths, file.str, parent_file, false);
    if (!resolved_file) {
        IO_Printf("Failed to resolve file: %s\n", file.str);
        return 0;
    }

    uint64_t filepath_hash = HashBytes(resolved_file, S8_Length(resolved_file));
    SRC_CacheEntry *entry = SRC_FindCache(SRC_InMemoryCache, filepath_hash);
    if (entry) return entry;

    CL_LexResult *first_lex = CL_LexFile(&SRC_ArenaTuple, resolved_file);
    IO_Assert(first_lex);
    uint64_t file_hash = HashBytes(first_lex->stream_begin, first_lex->stream - first_lex->stream_begin);
    uint64_t includes_hash = 13;

    CL_LexList list = CL_MakeLexList(first_lex);
    for (CL_IncludeIter iter = CL_IterateIncludes(&list); iter.filename; CL_GetNextInclude(&iter)) {
        if (iter.is_system_include) continue;

        S8_String file_it = S8_MakeFromChar(iter.filename);
        SRC_CacheEntry *cache = SRC_HashFile(file_it, resolved_file);
        if (!cache) {
            IO_Printf("Missing cache for: %s\n", file_it.str);
            continue;
        }

        includes_hash = HashMix(includes_hash, cache->total_hash);
    }

    SRC_CacheEntry *result = SRC_AddHash(filepath_hash, file_hash, includes_hash);
    return result;
}

bool SRC_WasModified(S8_String file) {
    double time_start = OS_GetTime();

    if (OS_FileExists(file) == false) {
        IO_Printf("FAILED File doesnt exist: %.*s\n", S8_Expand(file));
        exit(0);
    }
    if (OS_IsAbsolute(file) == false) {
        file = OS_GetAbsolutePath(G_Arena, file);
    }

    S8_String without_ext = S8_ChopLastPeriod(file);
    S8_String name_only = S8_SkipToLastSlash(without_ext);
    S8_String obj = S8_Format(G_Arena, "%.*s.obj", S8_Expand(name_only));
    bool modified = OS_FileExists(obj) == false;

    SRC_CacheEntry *in_memory = SRC_HashFile(file, 0);
    IO_Assert(in_memory);

    if (modified == false) {
        SRC_CacheEntry *from_file = SRC_FindCache(SRC_FromFileCache, in_memory->filepath_hash);
        if (from_file == 0 || (in_memory->total_hash != from_file->total_hash)) {
            modified = true;
        }
    }

    SRC_Time = SRC_Time + (OS_GetTime() - time_start);

    return modified;
}

#ifdef BLD_METAPROGRAM
int BLD_Main();
int main() {
    BLD_Init();
    SRC_InitCache(G_Arena, S8_Lit("main.cache"));
    int result = BLD_Main();
    if (result == 0) SRC_SaveCache();
}
#endif