// #Timing API:
#define System_Timed_Block_Print(name) \
  system_timed_block_print Concat(_sys_timed_block_print_guard, __LINE__)(name)
#define Timed_Block_Print(name) \
  timed_block_print Concat(_timed_block_print_guard, __LINE__)(name)
// This is for timing stuff that happens where context is not available.
// memory will leak from this operation. #MemoryLeak #NoContext
#define Timed_Block_Print_No_Context(name) \
  timed_block_print_no_context Concat(_timed_block_print_guard, __LINE__)(name)


force_inline u64 rdtsc();

#if OS_WINDOWS
#include <intrin.h>

#pragma intrinsic(__rdtsc)

force_inline u64 rdtsc() {
  return __rdtsc();
}
#endif

global u32 g_cpu_base_frequency_megahertz;

void set_cpu_base_frequency (u32 frequency_megahertz) {
  g_cpu_base_frequency_megahertz = frequency_megahertz;
}

string format_time_seconds_no_context (f64 time) { // #MemoryLeak #NoContext
  if (time < 1.0e-6) { return format_string_no_context("%1.2f ns", time * 1.0e9); }
  if (time < 1.0e-3) { return format_string_no_context("%1.3f us", time * 1.0e6); }
  if (time < 1)      { return format_string_no_context("%1.3f ms", time * 1.0e3); }
  
  return format_string_no_context("%1.3f s", time);
}

string format_cycles_no_context (u64 ticks) { // #MemoryLeak #NoContext
  string units[5] = { "cycles", "K cycles", "M cycles", "B cycles", "T cycles" };
  
  f64 count_f64 = (f64)ticks;
  s64 unit_index = 0;
  while (count_f64 >= 1000 && unit_index < 4) { // 4 is from (units.count-1)
    count_f64 /= 1000.0;
    unit_index += 1;
  }
  
  return format_string_no_context("%1.2f %s", count_f64, units[unit_index].data);
}

string format_time_seconds (f64 time) {
  if (time < 1.0e-6) { return format_string("%1.2f ns", time * 1.0e9); }
  if (time < 1.0e-3) { return format_string("%1.3f us", time * 1.0e6); }
  if (time < 1)      { return format_string("%1.3f ms", time * 1.0e3); }
  
  return format_string("%1.3f s", time);
}

string format_cycles (u64 ticks) {
  string units[5] = { "cycles", "K cycles", "M cycles", "B cycles", "T cycles" };
  
  f64 count_f64 = (f64)ticks;
  s64 unit_index = 0;
  while (count_f64 >= 1000 && unit_index < 4) { // 4 is from (units.count-1)
    count_f64 /= 1000.0;
    unit_index += 1;
  }
  
  return format_string("%1.2f %s", count_f64, units[unit_index].data);
}

string format_bytes (s64 bytes, s32 trailing_width = 3) {
  string units[6] = { "B", "KB", "MB", "GB", "TB", "PB" };
  f64 count_f64 = (f64)bytes;
  s32 unit_index = 0;
  
  while (count_f64 >= 1024 && unit_index < (5)) {
    count_f64 /= 1024.0;
    unit_index += 1;
  }
  
  // This makes the trailing width param kinda pointless... idk.
  if (unit_index == 0) trailing_width = 0;
  // if (unit_index == 1) trailing_width = 2;
  
  switch (trailing_width) {
    case 0: return format_string("%.0f %s", count_f64, units[unit_index].data);
    case 1: return format_string("%.1f %s", count_f64, units[unit_index].data);
    case 2: return format_string("%.2f %s", count_f64, units[unit_index].data);
    case 3: return format_string("%.3f %s", count_f64, units[unit_index].data);
    default: break;
  }
  Assert(trailing_width >= 0 && trailing_width <= 3);
  
  return ""; 
}

struct timed_block_print {
  string block_name;
  u64 start_tick;
  
  timed_block_print(string _block_name) {
    Assert(g_cpu_base_frequency_megahertz != 0);
    Assert(thread_local_context != nullptr); // we need temp allocator initialized!
    block_name = _block_name;
    start_tick = rdtsc();
  }
  
  ~timed_block_print() {
    u64 end_tick = rdtsc();
    u64 tick_difference = end_tick - start_tick;
    f64 ticks_f64 = (f64)tick_difference;
    f64 elapsed_time_seconds = ticks_f64 / (f64)((s64)g_cpu_base_frequency_megahertz * 1000000);
    push_allocator(temp());
    log("[Timed_Block %s]: %s (%s)", block_name.data, format_time_seconds(elapsed_time_seconds).data, format_cycles(tick_difference).data);
  }
};

struct timed_block_print_no_context {
  string block_name;
  u64 start_tick;
  
  timed_block_print_no_context(string _block_name) {
    Assert(g_cpu_base_frequency_megahertz != 0);
    block_name = _block_name;
    start_tick = rdtsc();
  }
  
  ~timed_block_print_no_context() {
    u64 end_tick = rdtsc();
    u64 tick_difference = end_tick - start_tick;
    f64 ticks_f64 = (f64)tick_difference;
    f64 elapsed_time_seconds = ticks_f64 / (f64)((s64)g_cpu_base_frequency_megahertz * 1000000);
    printf("[Timed_Block(No Context) %s]: %s (%s)\n", 
          block_name.data, 
          format_time_seconds_no_context(elapsed_time_seconds).data, 
          format_cycles_no_context(tick_difference).data);
  }
};

struct system_timed_block_print {
  string block_name;
  f64 start_time;
  
  system_timed_block_print(string _block_name) {
    block_name = _block_name;
    start_time = GetUnixTimestamp();
  }
  
  ~system_timed_block_print() {
    f64 end_time = GetUnixTimestamp();
    f64 elapsed_time_seconds = end_time - start_time;
    push_allocator(temp());
    log("[Timed_Block %s]: %s", block_name.data, format_time_seconds(elapsed_time_seconds).data);
  }
};
