#define NO_ERROR nullptr

enum class ErrorClass: s32 {
  NONE      = 0, // should not be used, just to avoid a default value being assigned.
  WARNING   = 1,
  ERROR     = 2,
  FATAL     = 3,
  TODO      = 4,
};

// #downcasts to string
struct Error {
  s64 count;
  u8* data;
  ErrorClass severity = ErrorClass::NONE;
  s32 thread_id;
  s32 source_line;
  string file_path;
  string function_name;
  string thread_name;
  f64 timestamp;
  
  // Linked list to errors
  Error* previous_error; // if we're passing errors up the callstack.
  Error* next_error;
};

char* error_severity (ErrorClass severity) {
  switch (severity) {
    case ErrorClass::NONE:    {
      return "[NONE]";
    } break;
    case ErrorClass::WARNING: {
      return "[WARNING]";
    } break;
    case ErrorClass::ERROR:   {
      return "[ERROR]";
    } break;
    case ErrorClass::FATAL:   {
      return "[FATAL ERROR]";
    } break;
    case ErrorClass::TODO:   {
      return "[TODO]";
    } break;
    
  }
  return "";
}

void push_error (Thread_Context* tctx, Error* new_error);

string to_string (Error* error) {
  return { error->count, error->data };
}

#define log_todo(fmt, ...) \
    log_error_internal(__FILE__, __FUNCTION__, __LINE__, ErrorClass::TODO, fmt, ##__VA_ARGS__)

#define log_fatal_error(fmt, ...) \
    log_error_internal(__FILE__, __FUNCTION__, __LINE__, ErrorClass::FATAL, fmt, ##__VA_ARGS__)

#define log_error(fmt, ...) \
    log_error_internal(__FILE__, __FUNCTION__, __LINE__, ErrorClass::ERROR, fmt, ##__VA_ARGS__)

#define log_warning(fmt, ...) \
    log_error_internal(__FILE__, __FUNCTION__, __LINE__, ErrorClass::WARNING, fmt, ##__VA_ARGS__)
  
#define log_none(fmt, ...) \
    log_error_internal(__FILE__, __FUNCTION__, __LINE__, ErrorClass::NONE, fmt, ##__VA_ARGS__)

Error* new_error (ErrorClass severity, string error_string) {
  Error* error = New<Error>();
  error->count = error_string.count;
  error->data  = error_string.data;
  error->severity = severity;
  
  return error;
}

void log_error_internal (string file_path, string function_name, s32 line_number, ErrorClass severity, string fmt, ...) {
  auto tctx = thread_context();
  Assert(tctx != nullptr);
  push_arena(tctx->error_arena);
  
  String_Builder* sb = thread_context()->log_builder;

  print_to_builder(sb, "%s ", error_severity(severity));
  
  va_list args;
  va_start(args, fmt);
  print_to_builder_internal(sb, fmt, args);
  va_end(args);
  
  append(sb, "\n");
  
  string error_string = copy_string(string_view(sb));
  
  reset_string_builder(sb);
  
  Error* error = new_error(severity, error_string);  
  // Additional information
  error->thread_id     = tctx->thread_idx;
  error->source_line   = line_number;
  error->file_path     = copy_string(file_path);
  error->function_name = copy_string(function_name);
  // Note: we don't need to assign previous_error or next_error, as that is done by the thread_context when we #push_error
  error->previous_error = nullptr;
  error->next_error     = nullptr;
  error->thread_name    = copy_string(tctx->thread_name);
  error->timestamp      = GetUnixTimestamp();
  
  push_error(tctx, error);
}

Error* copy_error (Thread_Context* tctx, Error* old_error) {
  push_arena(tctx->error_arena);
  
  string error_string_copy = copy_string(to_string(old_error));
  
  Error* error = new_error(old_error->severity, error_string_copy);
  
  error->thread_id     = old_error->thread_id;
  error->source_line   = old_error->source_line;
  error->file_path     = copy_string(old_error->file_path);
  error->function_name = copy_string(old_error->function_name);
  // Note: we don't need to assign previous_error or next_error, as that is done by the thread_context when we #push_error
  error->previous_error = nullptr;
  error->next_error     = nullptr;
  error->thread_name    = copy_string(old_error->thread_name);
  error->timestamp      = old_error->timestamp;
  
  return error;
}

void push_error (Thread_Context* tctx, Error* new_error) {
  Assert(tctx == thread_context()); // Not a real assert, just wondering if we'll ever call this with a non-local context?
  Assert(new_error != nullptr);
  if (new_error == nullptr) return;
  Error* current_error = tctx->current_error;
  if (current_error) {
    current_error->next_error = new_error;
    new_error->previous_error = current_error;
  } else {
    tctx->first_error = new_error;
    new_error->previous_error = nullptr;
  }
  tctx->current_error = new_error;
  
  switch (new_error->severity) {
    case ErrorClass::TODO:
    case ErrorClass::NONE:
    case ErrorClass::WARNING: {
      print(to_string(new_error));
    } break;
    case ErrorClass::ERROR:
    case ErrorClass::FATAL:   {
      print_error(to_string(new_error));
    } break;
  }
}

// push error to a context without back-linking to previous context 
void push_error_no_context (Thread_Context* tctx, Error* new_error) {
  Assert(new_error != nullptr);
  if (new_error == nullptr) return;
  Error* current_error = tctx->current_error;
  if (current_error) {
    new_error->previous_error = current_error;
    current_error->next_error = new_error;
  } else {
    tctx->first_error = new_error;
  }
  tctx->current_error = new_error;
}

void clear_errors (Thread_Context* tctx) { // Reset pointers and reset error_arena
  arena_reset(tctx->error_arena); // maybe overwrite memory?
  tctx->first_error = nullptr;
  tctx->current_error = nullptr;
}

void clear_error (Thread_Context* tctx, Error* error) {
  // If we want to clear a specific error (simply remove from the list)
  Assert(error != nullptr);
  bool is_current_error = (tctx->current_error == error);
  bool is_first_error   = (tctx->first_error == error);
  
  Error* current_error = tctx->first_error;
  if (current_error == nullptr) return;  // no errors in linked list.
  
  while (current_error != error && current_error != nullptr) {
    current_error = current_error->next_error;
  }
  Assert(current_error != nullptr);
  if (current_error == nullptr) return; // shouldn't happen
  Error* the_previous_error = current_error->previous_error;
  Error* the_next_error     = current_error->next_error;
  // Remove current_node from linked list:
  if (the_previous_error) {
    the_previous_error->next_error = the_next_error;
  }
  if (the_next_error) {
    the_next_error->previous_error = the_previous_error;
  }
  if (is_first_error && is_current_error) { // It matches the only item in the list, just empty the list:
    clear_errors(tctx);
    return;
  }
  if (is_first_error) {
    // the_next_error becomes new first error.
    tctx->first_error = the_next_error;
  }
  if (is_current_error) {
    // the current_error becomes the previous error
    tctx->current_error = the_previous_error;
  }
}

void push_errors_to_parent_thread (Thread_Context* tctx) {
  if (!tctx->first_error) return;
  Assert(tctx->parent_thread_context);

  while (tctx->first_error) {
    Error* error_copy = copy_error(tctx->parent_thread_context, tctx->first_error);
    push_error_no_context(tctx->parent_thread_context, error_copy);
    clear_error(tctx, tctx->first_error);
  }
}

ArrayView<Error*> get_all_errors (Thread_Context* tctx) {
  Array<Error*> error_array = {};
  // call with temp() recommended.
  Error* current_error = tctx->first_error;
  while (current_error) {
    array_add(error_array, current_error); 
    current_error = current_error->next_error;
  }
  
  // #TODO(Low priority): also recurse through child threads?
  // NOTE: I don't think we actually want this, because we merge
  // our errors on the main thread when we thread_deinit.
  // for_each(t, tctx->child_threads) {  }
  
  return error_array;
}

// Will need to use __FILE__ and __LINE__ macros

// Error* new_error (string error_message, ErrorClass severity, Error* previous_error=nullptr);
// Error* append_error (Error* old_error, Error* new_error);
// void context_report_error (Error* error);
// void cleanup_error (Error* error);
