#include <cstdio>
#include <cstring>
#include <vector>
#include <string>
#include <list>
#include <filesystem>
#include <fstream>
#include <unordered_set>

// #define EXT_ARGS
// #define EXT_LINK
// #define EXT_INC
// #define EXT_OBJ
// #define NO_Werror

std::filesystem::path OBJ_DIR = "obj";
std::filesystem::path INC_DIR = "include";
std::filesystem::path SRC_DIR = "source";

#ifdef _WIN32
std::filesystem::path BUILD_FILE = "main.exe";
std::string cpp_compiler = "zig c++";
std::string c_compiler = "zig cc";
#else
std::filesystem::path BUILD_FILE = "main";
std::string cpp_compiler = "g++";
std::string c_compiler = "gcc";
#endif

std::string opt_flags = "-ggdb";
std::vector<std::string> command;

enum class Color
{
  black,
  red,
  green,
  yellow,
  blue,
  purple,
  cyan,
  white,
  reset,
};

void pick_color(Color color)
{
  switch (color)
  {
  case Color::black:
    printf("\033[0;30m");
    break;
  case Color::red:
    printf("\033[0;31m");
    break;
  case Color::green:
    printf("\033[0;32m");
    break;
  case Color::yellow:
    printf("\033[0;33m");
    break;
  case Color::blue:
    printf("\033[0;34m");
    break;
  case Color::purple:
    printf("\033[0;35m");
    break;
  case Color::cyan:
    printf("\033[0;36m");
    break;
  case Color::white:
    printf("\033[0;37m");
    break;
  case Color::reset:
    printf("\033[0m");
    break;
  default:
    break;
  }
}

void print_command(std::vector<std::string> &arguments)
{
  pick_color(Color::cyan);
  for (auto &&i : arguments)
  {
    printf("%s ", i.c_str());
  }
  pick_color(Color::white);
  printf("\n");
}

bool run_command(std::vector<std::string> &arguments)
{
  print_command(arguments);
  std::string command = "";

  for (auto &&i : arguments)
  {
    command += " " + i;
  }

  return std::system(command.c_str()) == 0;
}

bool check_if_rebuild(const std::filesystem::path &org_path, const std::filesystem::path &new_path)
{
  if (!std::filesystem::exists(org_path))
    return false;

  if (!std::filesystem::exists(new_path))
    return true;

  auto file_time_one = std::filesystem::last_write_time(org_path);
  auto file_time_two = std::filesystem::last_write_time(new_path);
  return file_time_one > file_time_two;
}

bool rebuild_my_self(std::filesystem::path src_path, int argc, const char **exec_path)
{
  std::filesystem::path exec = exec_path[0];

  if (!check_if_rebuild(src_path, exec) && !check_if_rebuild(__FILE__, exec))
    return false;

#ifndef _WIN32
  std::vector<std::string> input;

  for (int i = 0; i < argc; ++i)
    input.push_back(exec_path[i]);

  std::vector<std::string> comand = {cpp_compiler, src_path.string(), "-o", exec_path[0], "-g"};
  if (!run_command(comand))
    return false;

  if (!run_command(input))
    return false;

  printf("rebuild\n");
#else
  pick_color(Color::red);
  printf("rebuild me\n");
  pick_color(Color::white);
#endif
  return true;
}

struct all_in_directory
{
  std::vector<std::filesystem::path> files;
  std::vector<std::filesystem::path> dirs;
};

all_in_directory get_all_files_in_dir(std::filesystem::path directory_path)
{
  namespace fs = std::filesystem;

  all_in_directory dir;
  if (!fs::is_directory(directory_path))
    return dir;

  std::list<fs::path> dirs;
  dirs.push_back(directory_path);
  dir.dirs.push_back(directory_path);

  while (!dirs.empty())
  {
    for (const auto &entry : fs::directory_iterator(dirs.front()))
    {
      // Check if the entry is a regular file
      if (fs::is_regular_file(entry))
      {
        dir.files.push_back(entry.path());
      }
      // Check if the entry is a directory
      else if (fs::is_directory(entry))
      {
        dirs.push_back(entry.path());
        dir.dirs.push_back(entry.path());
      }
    }
    dirs.pop_front();
  }

  return dir;
}

std::vector<std::string> get_includes(const std::filesystem::path &path)
{
  std::vector<std::string> ret;

  std::ifstream ifs(path);
  std::string line;

  while (std::getline(ifs, line))
  {
    if (line.empty())
      continue;

    if (line[0] != '#')
      break;

    // if include ends with " its my .h
    if (line[line.size() - 1] == '\"')
    {
      // magic num 10 is lenOf(#include ") so start at 10 until end of
      // line witch is len of line - 10 - 1 of cahar "
      ret.push_back(line.substr(10, line.length() - 10 - 1));
    }
  }

  return ret;
}

void clear_all_build()
{
  std::filesystem::remove_all(OBJ_DIR);
  std::filesystem::remove_all(BUILD_FILE);
}

#ifdef EXT_INC
int compile_src_dir(std::vector<std::string> ext_inc)
#else
int compile_src_dir()
#endif
{
  all_in_directory src_dir = get_all_files_in_dir(SRC_DIR);
  all_in_directory obj_dir;

  for (auto &&i : src_dir.dirs)
  {
    std::filesystem::create_directory(OBJ_DIR / i);
  }

  std::unordered_set<std::string> modified_heders;
  all_in_directory inc_dir = get_all_files_in_dir(INC_DIR);

  for (size_t i = 0; i < inc_dir.files.size(); i++)
  {
    if (check_if_rebuild(inc_dir.files[i], BUILD_FILE))
    {
      std::string heder = inc_dir.files[i].string().substr(4);
      modified_heders.insert(heder);
    }
  }

  command.clear();
  command.push_back(cpp_compiler);
  command.push_back("-c");
  int src_loc = command.size();
  command.push_back("");
  command.push_back("-o");
  int obj_loc = command.size();
  command.push_back("");
  command.push_back(opt_flags);

#ifdef EXT_INC
  command.insert(command.end(), ext_inc.begin(), ext_inc.end());
#endif

  command.push_back("-I" + INC_DIR.string());
  command.push_back("-std=c++23");
  command.push_back("-Wall");
#ifndef NO_Werror
  command.push_back("-Werror");
#endif

  int build = 0;
  for (auto &&i : src_dir.files)
  {
    std::filesystem::path tmp = i;
    std::filesystem::path out = OBJ_DIR / tmp.replace_extension(".o");
    bool reb = false;
    // check if .cpp changed
    if (check_if_rebuild(i, out))
      reb = true;

    // if .cpp didnt change check include files changed
    if (!reb)
    {
      std::vector<std::string> incudes = get_includes(i);
      for (auto &&j : incudes)
      {
        if (modified_heders.find(j) != modified_heders.end())
        {
          reb = true;
          break;
        }
      }
    }

    if (reb)
    {
      command[src_loc] = i.string();
      command[obj_loc] = out.string();
      if (!run_command(command))
        return -1;

      build = 1;
    }
  }
  return build;
}

#if defined(EXT_LINK)
void compile_obj_dir(std::vector<std::string> ext_link)
#else
void compile_obj_dir()
#endif
{
  all_in_directory obj_dir = get_all_files_in_dir(OBJ_DIR);

  command.clear();
  command.push_back(cpp_compiler);

  for (auto &&i : obj_dir.files)
  {
    command.push_back(i.string());
  }

  command.push_back("-o");
  command.push_back(BUILD_FILE.string());

#if defined(EXT_LINK)
  command.insert(command.end(), ext_link.begin(), ext_link.end());
#endif

  run_command(command);
}

#if defined(EXT_OBJ) && defined(EXT_INC)
void build_as_one(std::vector<std::string> ext_obj, std::vector<std::string> ext_inc)
#elif defined(EXT_OBJ)
void build_as_one(std::vector<std::string> ext_obj)
#elif defined(EXT_INC)
void build_as_one(std::vector<std::string> ext_inc)
#else
void build_as_one()
#endif
{
  all_in_directory src_dir = get_all_files_in_dir(SRC_DIR);

  command.clear();
  command.push_back(cpp_compiler);
  command.push_back("-o");
  command.push_back(BUILD_FILE.string());
  command.push_back(opt_flags);

#ifdef EXT_INC
  command.insert(command.end(), ext_inc.begin(), ext_inc.end());
#endif

  command.push_back("-I" + INC_DIR.string());
  command.push_back("-std=c++23");
  command.push_back("-Wall");
#ifndef NO_Werror
  command.push_back("-Werror");
#endif
  for (auto &&i : src_dir.files)
  {
    command.push_back(i.string());
  }

#ifdef EXT_OBJ
  command.insert(command.end(), ext_obj.begin(), ext_obj.end());
#endif
  run_command(command);
}

#ifdef EXT_ARGS
bool run_main(std::vector<std::string> ext_args)
#else
bool run_main()
#endif
{
  command.clear();
#ifndef _WIN32
  command.push_back("./");
#endif
  command.push_back(BUILD_FILE.string());

#ifdef EXT_ARGS
  command.insert(command.end(), ext_args.begin(), ext_args.end());
#endif

  return run_command(command);
}