#include <Windows.h>
#include <shellscalingapi.h>
#include <GL/Gl.h>
#include <intrin.h>

#pragma comment(linker, "/subsystem:windows")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "Shcore.lib")
#pragma comment(lib, "opengl32.lib")

// Symbols taken from GLFW
//
// Executables (but not DLLs) exporting this symbol with this value will be
// automatically directed to the high-performance GPU on Nvidia Optimus systems
// with up-to-date drivers
//
__declspec(dllexport) DWORD NvOptimusEnablement = 1;

// Executables (but not DLLs) exporting this symbol with this value will be
// automatically directed to the high-performance GPU on AMD PowerXpress systems
// with up-to-date drivers
//
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;



OS os;
#include "math.h"

typedef HRESULT tSetProcessDpiAwareness(PROCESS_DPI_AWARENESS);
typedef MMRESULT TimeBeginPeriod(MMRESULT);
constexpr DWORD window_style_simplified = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU;
constexpr DWORD window_style_resizable = WS_OVERLAPPEDWINDOW;
constexpr DWORD window_style_borderless = WS_POPUP;

struct Win32Bitmap {
  Bitmap bitmap;
  HDC dc;
  HBITMAP dib;
};

struct Win32FontCtx {
  HFONT font;
  Win32Bitmap bitmap;
  TEXTMETRIC text_metric;
  Font result;
};

struct OS_Win32 {
  HWND window;
  HDC window_dc;
  Win32Bitmap screen;
  HINSTANCE instance;
  int show_cmd;
  char *cmd_line;
  bool good_scheduling;
  void *main_fiber;
  void *msg_fiber;
  U8 text_buff[32];
  Vec2I prev_window_size;
  Vec2I prev_mouse_pos;
};
static_assert(sizeof(OS_Win32) < 256, "Too big");
#define w32(a) (*(OS_Win32 *)(a).platform)

api Bitmap bitmap(U32 *pixels, Vec2I size, Vec2 align_in_pixels = {}) {
  Bitmap result;
  result.pixels = pixels;
  result.size = size;
  result.align = align_in_pixels;
  return result;
}

function Win32Bitmap win32_create_bitmap(Vec2I size) {
  Win32Bitmap result;
  result.bitmap.size = size;
  if (result.bitmap.size.y < 0)
    result.bitmap.size.y = -result.bitmap.size.y;
  
  HDC hdc = GetDC(0);
  BITMAPINFO bminfo = {};
  bminfo.bmiHeader.biSize = sizeof(bminfo.bmiHeader);
  bminfo.bmiHeader.biWidth = (LONG)size.x;
  bminfo.bmiHeader.biHeight = (LONG)-size.y;
  bminfo.bmiHeader.biPlanes = 1;
  bminfo.bmiHeader.biBitCount = 32;
  bminfo.bmiHeader.biCompression = BI_RGB; // AA RR GG BB
  bminfo.bmiHeader.biXPelsPerMeter = 1;
  bminfo.bmiHeader.biYPelsPerMeter = 1;
  
  void *mem = 0;
  result.dib = CreateDIBSection(hdc, &bminfo, DIB_RGB_COLORS, (void **)&mem, 0, 0);
  assert_msg(mem != 0, "Failed to create win32 bitmap");
  result.dc = CreateCompatibleDC(hdc);
  result.bitmap.pixels = (U32 *)mem;
  return result;
}

function void win32_destroy_bitmap(Win32Bitmap *b) {
  if (b->bitmap.pixels) {
    b->bitmap.pixels = 0;
    DeleteDC(b->dc);
    DeleteObject(b->dib);
  }
}

function LRESULT 
CALLBACK win32_window_proc(HWND window, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  EventKind kind = EventKind_None;
  Key key = Key_None;
  LRESULT result = 0;
  switch (uMsg) {
    case WM_CLOSE:
    DestroyWindow(window);
    os.quit = 1;
    break;
    case WM_DESTROY:
    PostQuitMessage(0);
    os.quit = 1;
    break;
    case WM_SYSKEYDOWN:
    case WM_KEYDOWN: {
      kind = EventKind_KeyDown;
      switch (wParam) {
        
#define X(button, win)                                                                                       \
case win:                                                                                                  \
key = Key_##button;                                                                                      \
break;
        
        KEY_MAPPING
      }
    } break;
    case WM_SYSKEYUP:
    case WM_KEYUP: {
      kind = EventKind_KeyUp;
      switch (wParam) {
        KEY_MAPPING
#undef X
      }
    } break;
    case WM_TIMER: {
      SwitchToFiber(w32(os).main_fiber);
    } break;
    case WM_ENTERMENULOOP:
    case WM_ENTERSIZEMOVE: {
      SetTimer(w32(os).window, 0, 1, 0);
    } break;
    case WM_EXITMENULOOP:
    case WM_EXITSIZEMOVE: {
      KillTimer(w32(os).window, 0);
    } break;
    case WM_MOUSEMOVE: {
      POINT p;
      GetCursorPos(&p);
      ScreenToClient(w32(os).window, &p);
      os.mouse_pos = vec2i(p.x, p.y);
      os.mouse_pos.y = os.window_size.y - os.mouse_pos.y;
      kind = EventKind_MouseMove;
    } break;
    case WM_LBUTTONDOWN: {
      kind = EventKind_KeyDown;
      key = Key_MouseLeft;
    } break;
    case WM_LBUTTONUP: {
      kind = EventKind_KeyUp;
      key = Key_MouseLeft;
    } break;
    case WM_RBUTTONDOWN: {
      kind = EventKind_KeyDown;
      key = Key_MouseRight;
    } break;
    case WM_RBUTTONUP: {
      kind = EventKind_KeyUp;
      key = Key_MouseRight;
    } break;
    case WM_MBUTTONDOWN: {
      kind = EventKind_KeyDown;
      key = Key_MouseMiddle;
    } break;
    case WM_MBUTTONUP: {
      kind = EventKind_KeyUp;
      key = Key_MouseMiddle;
    } break;
    case WM_MOUSEWHEEL: {
      if ((int)wParam > 0)
        os.mouse_wheel = 1;
      else
        os.mouse_wheel = -1;
      kind = EventKind_MouseWheel;
    } break;
    case WM_CHAR: {
      // @Todo: Can this overflow?
      //kind = EventKind_KeyboardText;
      //U32 codepoint = '?';
      //utf16_to_utf32((U16 *)&wParam, &codepoint);
      //os.text.len += utf32_to_utf8(codepoint, w32(os).text_buff + os.text.len);
    } break;
    default:
    result = DefWindowProcW(window, uMsg, wParam, lParam);
    break;
  }
  
  if (kind) {
    if (kind == EventKind_KeyDown) {
      if (os.key[key].down == 0)
        os.key[key].pressed = 1;
      os.key[key].down = 1;
    } else if (kind == EventKind_KeyUp) {
      os.key[key].released = 1;
      os.key[key].down = 0;
    }
  }
  
  return result;
}

function void 
CALLBACK _os_fiber_event_proc(void *data) {
  unused(data);
  for (;;) {
    MSG msg;
    while (PeekMessageW(&msg, w32(os).window, 0, 0, PM_REMOVE)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    SwitchToFiber(w32(os).main_fiber);
  }
}

function Vec2I 
get_window_size(HWND window) {
  Vec2I result;
  RECT window_rect;
  GetClientRect(window, &window_rect);
  result.x = window_rect.right - window_rect.left;
  result.y = window_rect.bottom - window_rect.top;
  return result;
}

function Vec2I 
get_window_size_with_border(HWND window) {
  RECT ClientRect;
  GetWindowRect(window, &ClientRect);
  Vec2I draw_area;
  draw_area.x = (ClientRect.right - ClientRect.left);
  draw_area.y = (ClientRect.bottom - ClientRect.top);
  return draw_area;
}

function Vec2I 
get_border_size(HWND window) {
  Vec2I client = get_window_size(window);
  Vec2I wind_size = get_window_size_with_border(window);
  Vec2I result = vec2i(wind_size.x - client.x, wind_size.y - client.y);
  return result;
}

api void os_quit() { os.quit = 1; }
api void os_set_fps(F64 fps) { os.ms_per_frame = 1 / fps; }
api void os_set_ms_per_frame(F64 ms) { os.ms_per_frame = ms; }

api void os_show_cursor(B32 status) {
  ShowCursor(status);
  os.cursor_visible = status;
}

api void os_set_window_title(String title) {
  Scratch scratch;
  BOOL result = SetWindowTextA(w32(os).window, (char *)title.str);
  assert_msg(result != 0, "Failed to set window title");
  os.window_title = title;
}

api void os_set_window_size(S32 x, S32 y) {
  Vec2I border = get_border_size(w32(os).window);
  int actual_width = (int)(x + border.x);
  int actual_height = (int)(y + border.y);
  bool result =
    SetWindowPos(w32(os).window, 0, 0, 0, actual_width, actual_height, SWP_NOMOVE | SWP_NOOWNERZORDER);
  assert_msg(result, "SetWindowPos returned invalid value");
  os.window_size = get_window_size(w32(os).window);
}

api void os_pull_state() {
  os.window_was_resized = false;
  os.window_size = get_window_size(w32(os).window);
  if (os.window_size != w32(os).prev_window_size) {
    os.window_was_resized = true;
    w32(os).prev_window_size = os.window_size;
  }
  os.monitor_size = vec2i(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
  
  // @Note: Client relative
  POINT point = {0, 0};
  ClientToScreen(w32(os).window, &point);
  os.window_pos.x = point.x;
  os.window_pos.y = point.y;
  
  // @Note: Get DPI scale
  UINT dpi = GetDpiForWindow(w32(os).window);
  assert_msg(dpi != 0, "Failed to get dpi for window");
  os.dpi_scale = (F32)dpi / 96.f;
  
  // @Note: Reset text
  os.text.len = 0;
  // @Note: Reset keys
  for (int i = 0; i < Key_Count; i++) {
    os.key[i].released = 0;
    os.key[i].pressed = 0;
  }
  os.mouse_wheel = 0;
  SwitchToFiber(w32(os).msg_fiber);
  if (!os.quit) {
    os.delta_mouse_pos = w32(os).prev_mouse_pos - os.mouse_pos;
    w32(os).prev_mouse_pos = os.mouse_pos;
    
    // @Note: Resize
    if (os.render_backend == RenderBackend_Software) {
      if (os.window_size != w32(os).screen.bitmap.size && os.window_size.x != 0 && os.window_size.y != 0) {
        win32_destroy_bitmap(&w32(os).screen);
        w32(os).screen = win32_create_bitmap(vec2i(os.window_size.x, -(os.window_size.y)));
        os.screen = &w32(os).screen.bitmap;
      }
    }
  }
}

api void os_init_software_render() { os.render_backend = RenderBackend_Software; }

api B32 os_init_opengl() {
  PIXELFORMATDESCRIPTOR p = {};
  p.nSize = sizeof(p);
  p.nVersion = 1;
  p.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
  p.iPixelType = PFD_TYPE_RGBA;
  p.cColorBits = 32;
  p.cDepthBits = 24;
  p.cStencilBits = 8;
  p.iLayerType = PFD_MAIN_PLANE;
  
  S32 pixel_format = ChoosePixelFormat(w32(os).window_dc, &p);
  if (pixel_format != 0) {
    if (SetPixelFormat(w32(os).window_dc, pixel_format, &p)) {
      HGLRC gl_ctx = wglCreateContext(w32(os).window_dc);
      if (gl_ctx != NULL) {
        if (wglMakeCurrent(w32(os).window_dc, gl_ctx)) {
          // Success
        } 
        else {
          log_error("Failed on wglMakeCurrent!");
          return false;
        }
      } 
      else {
        log_error("Failed on wglCreateContext!");
        return false;
      }
    } 
    else {
      log_error("Failed on SetPixelFormat!");
      return false;
    }
  } 
  else {
    log_error("Failed on ChoosePixelFormat!");
    return false;
  }
  
  os.opengl.vendor = (char *)glGetString(GL_VENDOR);
  os.opengl.renderer = (char *)glGetString(GL_RENDERER);
  os.opengl.version = (char *)glGetString(GL_VERSION);
  os.opengl.extensions = (char *)glGetString(GL_EXTENSIONS);
  os.render_backend = RenderBackend_OpenGL1;
  
  return true;
}

global S64 Global_counts_per_second;
api F64 os_time() {
  if (Global_counts_per_second == 0) {
    LARGE_INTEGER freq;
    QueryPerformanceFrequency(&freq);
    Global_counts_per_second = freq.QuadPart;
  }
  
  LARGE_INTEGER time;
  QueryPerformanceCounter(&time);
  F64 result = (F64)time.QuadPart / (F64)Global_counts_per_second;
  return result;
}

api B32 os_init() {
  HMODULE shcore = LoadLibraryA("Shcore.dll");
  if (shcore) {
    tSetProcessDpiAwareness *set_dpi_awr =
    (tSetProcessDpiAwareness *)GetProcAddress(shcore, "SetProcessDpiAwareness");
    if (set_dpi_awr) {
      HRESULT hr = set_dpi_awr(PROCESS_PER_MONITOR_DPI_AWARE);
      assert_msg(SUCCEEDED(hr), "Failed to set dpi awareness");
    }
  }
  
  HMODULE winmm = LoadLibraryA("winmm.dll");
  if (winmm) {
    TimeBeginPeriod *timeBeginPeriod = (TimeBeginPeriod *)GetProcAddress(winmm, "timeBeginPeriod");
    if (timeBeginPeriod) {
      if (timeBeginPeriod(1) == TIMERR_NOERROR) {
        w32(os).good_scheduling = true;
      }
    }
  }
  
  DWORD window_style_chosen = window_style_resizable;
  if (!os.window_resizable)
    window_style_chosen = window_style_simplified;
  
  os.app_start_time = os_time();
  os.frame_start_time = os.app_start_time;
  os.update_begin_cycles = __rdtsc();
  
  WNDCLASSW wc = {}; // @Todo(Krzosa): UTF
  wc.lpfnWndProc = win32_window_proc;
  wc.hInstance = w32(os).instance;
  // @Todo(Krzosa): UTF conversions
  wc.lpszClassName = L"Have a good day"; // @Todo(Krzosa): Cant set window title
  wc.hCursor = LoadCursor(0, IDC_ARROW);
  wc.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
  if (!RegisterClassW(&wc)) {  // @Todo(Krzosa): UTF
    log_error("Failed to create window class!");
    return false;
  }
  
  RECT window_rect;
  window_rect.left = (LONG)os.window_pos.x;
  window_rect.top = (LONG)os.window_pos.y;
  window_rect.right = (LONG)os.window_size.x + window_rect.left;
  window_rect.bottom = (LONG)os.window_size.y + window_rect.top;
  AdjustWindowRectEx(&window_rect, window_style_chosen, false, 0);
  
  // @Todo(Krzosa): UTF
  w32(os).window = CreateWindowW(wc.lpszClassName, wc.lpszClassName, window_style_chosen, window_rect.left, window_rect.top, window_rect.right - window_rect.left,window_rect.bottom - window_rect.top, NULL, NULL, w32(os).instance, NULL);
  if (w32(os).window == 0) {
    log_error("Failed to create window!");
    return false;
  }
  
  ShowWindow(w32(os).window, w32(os).show_cmd);
  UpdateWindow(w32(os).window);
  w32(os).window_dc = GetDC(w32(os).window);
  
  w32(os).main_fiber = ConvertThreadToFiber(0);
  assert_msg(w32(os).main_fiber, "Failed to create main fiber");
  w32(os).msg_fiber = CreateFiber(0, _os_fiber_event_proc, 0);
  assert_msg(w32(os).msg_fiber, "Failed to create message fiber");
  
  if (os.cursor_visible == false)
    os_show_cursor(false);
  
  switch (os.render_backend) {
    case RenderBackend_Software: {
      os_init_software_render();
    } break;
    case RenderBackend_OpenGL1: {
      os_init_opengl();
    } break;
    default: assert_msg(0, "Invalid value for render backend");
    break;
  }
  
  os_pull_state();
  os.initialized = true;
  return true;
}

api bool os_game_loop() {
  assert_msg(os.initialized, "Platform is not initialized! Please call os_init");
  switch (os.render_backend) {
    case RenderBackend_Software: {
      if (os.screen) { // @Note: Draw screen
        U32 *p = os.screen->pixels;
        for (int y = 0; y < os.screen->y; y++) {
          for (int x = 0; x < os.screen->x; x++) {
            *p = ((*p & 0xff000000)) | ((*p & 0x00ff0000) >> 16) | ((*p & 0x0000ff00)) |
            ((*p & 0x000000ff) << 16);
            p += 1;
          }
        }
        
        HDC hdc = w32(os).window_dc;
        SelectObject(w32(os).screen.dc, w32(os).screen.dib);
        BitBlt(hdc, 0, 0, (LONG)os.screen->size.x, (LONG)os.screen->size.y, w32(os).screen.dc, 0, 0, SRCCOPY);
      }
    } break;
    case RenderBackend_OpenGL1: {
      SwapBuffers(w32(os).window_dc);
    } break;
    default:
    assert_msg(0, "Please select a rendering backend!");
    break;
  }
  arena_clear(os.frame_arena);
  os.update_time = os_time() - os.frame_start_time;
  os.update_end_cycles = __rdtsc();
  F64 frame_time = os.update_time;
  F64 ms_per_frame = os.ms_per_frame;
  if (frame_time < ms_per_frame) {
    if (w32(os).good_scheduling) {
      // @Todo: I have no idea if letting over sleep is bad or not
      // Busy waiting is chugging cpu alot more, not sure what to do
      F64 time_to_sleep = (ms_per_frame - frame_time) * 1000;
      if (time_to_sleep > 0) {
        Sleep((DWORD)time_to_sleep);
      }
    }
    
    do {
      frame_time = os_time() - os.frame_start_time;
    } while (frame_time < ms_per_frame);
  }
  os.frame++;
  os.delta_time = frame_time;
  os.fps = 1 / os.delta_time;
  os.time += os.delta_time;
  os.frame_start_time = os_time();
  os.update_begin_cycles = __rdtsc();
  os_pull_state();
  return !os.quit;
}

int main(int argc, char **argv);
int WINAPI WinMain(HINSTANCE instance, HINSTANCE, LPSTR cmd_line, int show_cmd) {
  thread_ctx_init();
  w32(os).instance = instance;
  w32(os).show_cmd = show_cmd;
  w32(os).cmd_line = cmd_line;
  
  Arena frame_arena = {};
  arena_init(&frame_arena, "Frame arena"_s);
  os.perm_arena = &pernament_arena;
  os.frame_arena = &frame_arena;
  
  os.dpi_scale = 1;
  os.text.str = w32(os).text_buff;
  os.window_title = "Have a good day!"_s;
  os.window_pos.x = 0;
  os.window_pos.y = 50;
  os.window_size.x = 1280;
  os.window_size.y = 720;
  os.ms_per_frame = 1.f / 60.f;
  os.cursor_visible = true;
  
  if(AttachConsole(-1)) {
    freopen("CONIN$", "r",stdin);
    freopen("CONOUT$", "w",stdout);
    freopen("CONOUT$", "w",stderr);
  }
  
  return main(__argc, __argv);
}

//////////////
// @Note: Font API
function FontGlyph extract_glyph(Allocator *arena, Win32FontCtx *ctx, wchar_t glyph) {
  SIZE size;
  GetTextExtentPoint32W(ctx->bitmap.dc, &glyph, 1, &size);
  TextOutW(ctx->bitmap.dc, 0, 0, &glyph, 1);
  
  // @Note: Find bitmap edges
  int minx = 100000;
  int miny = 100000;
  int maxx = -100000;
  int maxy = -100000;
  for (int y = 499; y >= 500 - size.cy; y--) {
    for (int x = 0; x < size.cx; x++) {
      if (ctx->bitmap.bitmap.pixels[x + y * (int)ctx->bitmap.bitmap.size.x] != 0) {
        if (minx > x)
          minx = x;
        if (miny > y)
          miny = y;
        if (maxx < x)
          maxx = x;
        if (maxy < y)
          maxy = y;
      }
    }
  }
  
  assert(minx >= 0 && miny >= 0);
  int bwidth = maxx - minx + 1;
  int bheight = maxy - miny + 1;
  U32 *cropped =
  (U32 *)exp_alloc(arena, sizeof(U32) * (U32)(bwidth) * (U32)(bheight));
  for (int y = miny; y <= maxy; y++) {
    for (int x = minx; x <= maxx; x++) {
      U32 value = ctx->bitmap.bitmap.pixels[x + y * (int)ctx->bitmap.bitmap.size.x];
      U32 *dst = cropped + ((size_t)(x - minx) + (size_t)(y - miny) * bwidth);
#if 1 // Premultiplied alpha
      F32 alpha = (F32)((value & 0x000000ff) >> 0);
      F32 rgb  = (F32)0xff*(alpha/255.f); 
      U8 val    = (U8)(rgb + 0.5f);
      *dst = (((U32)(alpha+0.5f) << 24) | val << 16 | val << 8 | val);
#else
      U8 grey = (value & 0x000000ff);
      *dst = (grey << 24 | 0xff << 16 | 0xff << 8 | 0xff);
#endif
    }
  }
  exp_alloc(arena, sizeof(U32) * (U32)bwidth);
  
  // @Note: Calculate char metrics
  int glyph_descent = (499 - size.cy) - miny;
  
  FontGlyph result;
  INT width;
  GetCharWidth32W(ctx->bitmap.dc, glyph, glyph, &width);
  result.xadvance = (F32)width;
  result.bitmap = bitmap(cropped, vec2i(bwidth, bheight),
                         vec2((F32)-minx, (F32)ctx->text_metric.tmDescent + (F32)glyph_descent));
  return result;
}

function Win32FontCtx begin_font_extraction(char *filename, char *font_name, S64 height) {
  assert_msg(height < 500, "Height of font over 500");
  Win32FontCtx ctx = {};
  if (filename) {
    int fonts_added = AddFontResourceExA(filename, FR_PRIVATE, 0);
    assert_msg(fonts_added != 0, "AddFontResourceEx added 0 fonts");
  }
  
  ctx.bitmap = win32_create_bitmap(vec2i(500, -500));
  ctx.font =
    CreateFontA((S32)height, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
                CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE, font_name);
  assert_msg(ctx.font != NULL, "CreateFont returned a 0 pointer");
  SelectObject(ctx.bitmap.dc, ctx.bitmap.dib);
  SelectObject(ctx.bitmap.dc, ctx.font);
  GetTextMetrics(ctx.bitmap.dc, &ctx.text_metric);
  ctx.result.height = (F32)height;
  ctx.result.descent = (F32)ctx.text_metric.tmDescent;
  ctx.result.ascent = (F32)ctx.text_metric.tmAscent;
  ctx.result.line_advance = (F32)(height + ctx.text_metric.tmExternalLeading);
  SetBkColor(ctx.bitmap.dc, RGB(0, 0, 0));
  SetTextColor(ctx.bitmap.dc, RGB(255, 255, 255));
  return ctx;
}

function Font end_font_extraction(Win32FontCtx *ctx) {
  win32_destroy_bitmap(&ctx->bitmap);
  DeleteObject(ctx->font);
  return ctx->result;
}

api Font os_load_font(Allocator *arena, S32 height, const char *font_name, const char *filename) {
  FontGlyph *glyphs = exp_alloc_array(arena, FontGlyph, 96, AF_ZeroMemory);
  Win32FontCtx font_ctx = begin_font_extraction((char *)filename, (char *)font_name, height);
  for (U32 i = '!'; i <= '~'; i++) {
    glyphs[i - '!'] = extract_glyph(arena, &font_ctx, (wchar_t)i);
  }
  Font font = end_font_extraction(&font_ctx);
  font.glyphs = glyphs;
  font.glyphs_len = '~' - '!';
  return font;
}

function void set_window_style(HWND window, DWORD style) {
  // @Todo: Need to readup on this
  SetWindowLongPtrW(window, GWL_STYLE, style);
  SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE);
  ShowWindow(window, SW_SHOW);
}

#undef w32
///////////////////////////////////////
// @Section Audio
#include <objbase.h>
#include <audioclient.h>
#include <audiopolicy.h>
#include <mmdeviceapi.h>

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);

// NOTE: typedefines for the functions which are goint to be loaded
typedef HRESULT CoCreateInstanceFunction(REFCLSID rclsid, LPUNKNOWN *pUnkOuter, DWORD dwClsContext,
                                         REFIID riid, LPVOID *ppv);
typedef HRESULT CoInitializeExFunction(LPVOID pvReserved, DWORD dwCoInit);

// NOTE: empty functions(stubs) which are used when library fails to load
HRESULT CoCreateInstanceStub(REFCLSID rclsid, LPUNKNOWN *pUnkOuter, DWORD dwClsContext, REFIID riid,
                             LPVOID *ppv) {
  unused(rclsid);
  unused(pUnkOuter);
  unused(dwClsContext);
  unused(riid);
  unused(ppv);
  return S_FALSE;
}

HRESULT CoInitializeExStub(LPVOID pvReserved, DWORD dwCoInit) {
  unused(pvReserved);
  unused(dwCoInit);
  return S_FALSE;
}

// NOTE: pointers to the functions from the dll
CoCreateInstanceFunction *CoCreateInstanceFunctionPointer = CoCreateInstanceStub;
CoInitializeExFunction *CoInitializeExFunctionPointer = CoInitializeExStub;

// NOTE: Number of REFERENCE_TIME units per second
// One unit is equal to 100 nano seconds
#define REF_TIMES_PER_SECOND 10000000
#define REF_TIMES_PER_MSECOND 10000
#define w32(a) (*(Audio_Win32 *)(a)->platform)

struct Audio_Win32 {
  IMMDevice *device;
  IAudioClient *audio_client;
  
  IMMDeviceEnumerator *device_enum;
  IAudioRenderClient *audio_render_client;
  IAudioCaptureClient *audio_capture_client;
};
static_assert(sizeof(Audio::platform) > sizeof(Audio_Win32),
              "Audio::platform is too small to hold Audio_Win32 struct");

// Load COM Library functions dynamically,
// this way sound is not necessary to run the game
function B32 win32_load_com() {
  B32 result = true;
  HMODULE ole32Library = LoadLibraryA("ole32.dll");
  if (ole32Library) {
    CoCreateInstanceFunctionPointer =
    (CoCreateInstanceFunction *)GetProcAddress(ole32Library, "CoCreateInstance");
    if (!CoCreateInstanceFunctionPointer) {
      CoCreateInstanceFunctionPointer = CoCreateInstanceStub;
      log_error("CoCreateInstance failed to load");
      result = false;
    }
    CoInitializeExFunctionPointer = (CoInitializeExFunction *)GetProcAddress(ole32Library, "CoInitializeEx");
    if (!CoInitializeExFunctionPointer) {
      CoInitializeExFunctionPointer = CoInitializeExStub;
      log_error("CoInitializeEx failed to load");
      result = false;
    }
  } else {
    CoCreateInstanceFunctionPointer = CoCreateInstanceStub;
    CoInitializeExFunctionPointer = CoInitializeExStub;
    log_error("Failed to load OLE32.dll");
    result = false;
  }
  return result;
}

api void os_clean_audio(Audio *audio) {
  if (w32(audio).audio_client)
    w32(audio).audio_client->Stop();
  if (w32(audio).device_enum)
    w32(audio).device_enum->Release();
  if (w32(audio).device)
    w32(audio).device->Release();
  if (w32(audio).audio_client)
    w32(audio).audio_client->Release();
  if (w32(audio).audio_render_client)
    w32(audio).audio_render_client->Release();
  audio->initialized = false;
}

function DWORD win32_audio_thread(void *parameter) {
  Audio *audio = (Audio *)parameter;
  SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
  HANDLE buffer_ready_event = CreateEvent(0, 0, 0, 0);
  if (!buffer_ready_event) {
    return -1;
  }
  if (FAILED(w32(audio).audio_client->SetEventHandle(buffer_ready_event))) {
    return -1;
  }
  U32 buffer_frame_count;
  if (FAILED(w32(audio).audio_client->GetBufferSize(&buffer_frame_count))) {
    return -1;
  }
  // U32 buffer_sample_count = buffer_frame_count * audio->number_of_channels;
  if (FAILED(w32(audio).audio_client->Start())) {
    w32(audio).audio_client->Stop();
    return -1;
  }
  for (;;) {
    if (WaitForSingleObject(buffer_ready_event, INFINITE) != WAIT_OBJECT_0) {
      w32(audio).audio_client->Stop();
      return -1;
    }
    U32 padding_frame_count;
    if (FAILED(w32(audio).audio_client->GetCurrentPadding(&padding_frame_count))) {
      w32(audio).audio_client->Stop();
      return -1;
    }
    U32 *samples;
    U32 fill_frame_count = buffer_frame_count - padding_frame_count;
    if (FAILED(w32(audio).audio_render_client->GetBuffer(fill_frame_count, (BYTE **)&samples))) {
      w32(audio).audio_client->Stop();
      return -1;
    }
    audio->callback(audio, samples, fill_frame_count);
    if (FAILED(w32(audio).audio_render_client->ReleaseBuffer(fill_frame_count, 0))) {
      w32(audio).audio_client->Stop();
      return -1;
    }
  }
  return 0;
}

function AUDIO_CALLBACK(default_audio_callback) {
  memory_zero(buffer, (U64)frames_to_fill * (U64)audio->samples_per_second);
}

api B32 os_init_audio(Audio *audio) {
  audio->bits_per_sample = 16;
  if (audio->number_of_channels == 0)
    audio->number_of_channels = 2;
  if (audio->samples_per_second == 0)
    audio->samples_per_second = 44100;
  if (audio->callback == 0)
    audio->callback = default_audio_callback;
  
  B32 success = win32_load_com();
  if (!success) {
    return false;
  }
  if (FAILED(CoInitializeExFunctionPointer(0, COINITBASE_MULTITHREADED))) {
    log_error("Failed to initialize COM, CoInitializeEx");
    return false;
  }
  if (FAILED(CoCreateInstanceFunctionPointer(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
                                             IID_IMMDeviceEnumerator, (LPVOID *)&w32(audio).device_enum))) {
    log_error("Failed to initialize COM, CoCreateInstance");
    return false;
  }
  if (FAILED(w32(audio).device_enum->GetDefaultAudioEndpoint(eRender, eConsole, &w32(audio).device))) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, GetDefaultAudioEndpoint");
    return false;
  }
  if (FAILED(
             w32(audio).device->Activate(IID_IAudioClient, CLSCTX_ALL, 0, (void **)&w32(audio).audio_client))) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, "
              "w32(audio).device->Activate(IID_IAudioClient,");
    return false;
  }
  
  WAVEFORMATEX waveFormat = {};
  waveFormat.wFormatTag = WAVE_FORMAT_PCM;
  waveFormat.nChannels = audio->number_of_channels;
  waveFormat.nSamplesPerSec = audio->samples_per_second;
  waveFormat.wBitsPerSample = audio->bits_per_sample;
  waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8;
  waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
  REFERENCE_TIME requestedBufferDuration = REF_TIMES_PER_MSECOND * 40;
  if (FAILED(w32(audio).audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED,
                                                 AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_RATEADJUST |
                                                 AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY,
                                                 requestedBufferDuration, 0, &waveFormat, 0))) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, w32(audio).audio_client->Initialize");
    return false;
  }
  if (FAILED(w32(audio).audio_client->GetService(IID_IAudioRenderClient,
                                                 (void **)&w32(audio).audio_render_client))) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, "
              "w32(audio).audio_client->GetService(IID_IAudioRenderClient");
    return false;
  }
  if (FAILED(w32(audio).audio_client->GetBufferSize(&audio->buffer_frame_count))) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, w32(audio).audio_client->GetBufferSize");
    return false;
  }
  HANDLE thread_handle = CreateThread(0, 0, win32_audio_thread, audio, 0, 0);
  if (!thread_handle) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, CreateThread returned 0 in handle");
    return false;
  }
  if (thread_handle == INVALID_HANDLE_VALUE) {
    os_clean_audio(audio);
    log_error("Failed to initialize WASAPI, CreateThread for "
              "audio failed with INVALID HANDLE VALUE");
    return false;
  }
  CloseHandle(thread_handle);
  audio->initialized = true;
#undef w32
  return true;
}

