#include <ui.h>

#include <SDL.h>
#include <cstring.h>
#include <tinydir.h>

#include <assert.h>
#include <path.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

static const char* WindowTitle   = "XPLORER";
static const int   DefaultWidth  = 1440;
static const int   DefaultHeight = 900;

// #define DEBUG_EVENT_LOOP 1

#ifdef DEBUG_EVENT_LOOP
#define EVENT_LOOP_PRINT printf
#else
#define EVENT_LOOP_PRINT(...)
#endif // DEBUG_EVENT_LOOP

typedef struct State {
  SDL_Window* window;
  uiFrame*    frame;
  uiTable*    table;
  path        current_dir;
} State;

uiMouseButton ToUiButton(Uint8 button);

void CreateUi(State* state) {
  assert(state);

  uiFrame* frame = uiMakeFrame();

  const char* header[] = {"Name", "Size", "Modified"};
  uiTable*    table    = uiMakeTable(0, sizeof(header) / sizeof(char*), header);
  assert(table);
  uiWidgetSetParent(uiMakeTablePtr(table), uiMakeFramePtr(frame));

  // uiLabel* label = uiMakeLabel("Hello world, what is going on!?");
  // uiWidgetSetParent(label, frame);

  state->frame = frame;
  state->table = table;
}

int compare_files(const void* _a, const void* _b) {
  assert(_a);
  assert(_b);

  const tinydir_file* a = _a;
  const tinydir_file* b = _b;

  for (size_t i = 0;
       (i < _TINYDIR_FILENAME_MAX) && (a->name[i] != 0) && (b->name[i] != 0);
       ++i) {
    if (a->name[i] < b->name[i]) {
      return -1;
    } else if (a->name[i] > b->name[i]) {
      return 1;
    }
  }

  return 0;
}

size_t GetFileCount(path directory) {
  size_t      count = 0;
  tinydir_dir dir;
  if (tinydir_open(&dir, path_cstr(directory)) == 0) {
    for (count = 0; dir.has_next; ++count) {
      tinydir_next(&dir);
    }
  }
  return count;
}

bool SetDirectory(State* state, path directory) {
  assert(state);

  bool directory_changed = false;

  tinydir_dir dir;
  if (tinydir_open(&dir, path_cstr(directory)) == 0) {
    const size_t count = GetFileCount(directory);

    tinydir_file* files = calloc(count, sizeof(tinydir_file));
    if (!files) {
      return false;
    }

    for (size_t i = 0; dir.has_next; ++i) {
      assert(i < count);
      tinydir_readfile(&dir, &files[i]);
      tinydir_next(&dir);
    }

    qsort(files, count, sizeof(files[0]), compare_files);

    uiTable* table = state->table;
    assert(table);

    uiTableClear(table);
    for (size_t i = 0; i < count; ++i) {
      tinydir_file file = files[i];

      const string file_size = string_format_size(file._s.st_size);

      const char* row[3] = {file.name, string_data(file_size), "<date>"};
      uiTableAddRow(table, row);
    }

    free(files);

    if (!path_empty(state->current_dir)) {
      path_del(&state->current_dir);
    }
    state->current_dir = directory;
    directory_changed  = true;
  }

  return directory_changed;
}

bool OnFileTableClick(
    State* state, uiTable* table, const uiTableClickEvent* event) {
  assert(state);
  assert(table);

  if (event->col == 0) { // Clicked the file/directory name.
    // TODO: Think more about uiPtr. Do we need uiConstPtr?
    //  Ideally: const uiLabel* label = uiGetPtr(uiTableGet(...));
    //  i.e., no checks on the client code; all checks in library code.
    const uiLabel* label =
        (const uiLabel*)uiTableGet(table, event->row, event->col);
    assert(uiWidgetGetType((const uiWidget*)label) == uiTypeLabel);

    printf("Click: %d,%d: %s\n", event->row, event->col, uiLabelGetText(label));

    // TODO: Handle '.' and '..' better. Define a path concatenation function.
    path       child_dir = path_new(uiLabelGetText(label));
    path       new_dir   = path_concat(state->current_dir, child_dir);
    const bool result    = SetDirectory(state, new_dir);
    if (!result) {
      path_del(&new_dir);
    }
    path_del(&child_dir);
    return result;
  }
  return false;
}

/// Handle widget events and return whether a redraw is needed.
bool HandleWidgetEvents(State* state) {
  assert(state);

  bool redraw = false;

  const uiWidgetEvent* events;
  const int            numWidgetEvents = uiGetEvents(&events);

  for (int i = 0; i < numWidgetEvents; ++i) {
    const uiWidgetEvent* ev = &events[i];

    // TODO: Set and check widget IDs.
    switch (ev->type) {
    case uiWidgetEventClick:
      if (ev->widget.type == uiTypeTable) {
        if (OnFileTableClick(
                state, uiGetTablePtr(ev->widget), &ev->table_click)) {
          redraw = true;
        }
      }
      break;
    default:
      break;
    }
  }

  return redraw;
}

static bool Render(State* state) {
  assert(state);
  assert(state->window);

  SDL_Surface* window_surface = SDL_GetWindowSurface(state->window);
  assert(window_surface);

#ifdef DEBUG_EVENT_LOOP
  const uiSize frame_size = uiGetFrameSize(state->frame);
  EVENT_LOOP_PRINT(
      "Render; surface: %dx%d; window surface; %dx%d\n", frame_size.width,
      frame_size.height, window_surface->w, window_surface->h);
#endif

  // Locking/unlocking SDL software surfaces is not necessary.
  //
  // Probably also best to avoid SDL_BlitSurface(); it does pixel format
  // conversion while blitting one pixel at a time. Instead, make the UI pixel
  // format match the SDL window's and write to SDL's back buffer directly.
  uiRender(
      state->frame, &(uiSurface){
                        .width  = window_surface->w,
                        .height = window_surface->h,
                        .pixels = window_surface->pixels,
                    });

  if (SDL_UpdateWindowSurface(state->window) != 0) {
    return false;
  }

  return true;
}

static bool Resize(State* state) {
  assert(state);

  // int width, height;
  // SDL_GetWindowSize(state->window, &width, &height);

  const SDL_Surface* window_surface = SDL_GetWindowSurface(state->window);
  if (!window_surface) {
    return false;
  }
  const int width  = window_surface->w;
  const int height = window_surface->h;

  EVENT_LOOP_PRINT("Resize: %dx%d\n", width, height);

  // TODO: Fix the white 1-pixel vertical/horizontal line that appears at odd
  //  sizes when resizing the window.
  //  https://github.com/libsdl-org/SDL/issues/9653
  uiResizeFrame(state->frame, width, height);

  return true;
}

bool Initialize(State* state) {
  assert(state);

  if ((state->window = SDL_CreateWindow(
           WindowTitle, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
           DefaultWidth, DefaultHeight,
           SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE)) == NULL) {
    return false;
  }

  CreateUi(state);

  path home = path_new(getenv("HOME"));
  SetDirectory(state, home);

  return true;
}

int main(
    __attribute__((unused)) int          argc,
    __attribute__((unused)) const char** argv) {
  bool success = true;

  State state = {0};

  if (SDL_Init(SDL_INIT_VIDEO) != 0) {
    return false;
  }

  if (!uiInit()) {
    return false;
  }

  if (!Initialize(&state)) {
    success = false;
    goto cleanup;
  }

  if (!Resize(&state)) {
    success = false;
    goto cleanup;
  }

  // TODO: All of the window and input handling could be moved to its own
  //  library so that different applications can re-use it.

  // Controls whether we should keep running.
  bool running = true;

  // Controls whether a redraw is required.
  // Initially true to perform an initial draw before the window is displayed.
  bool redraw = true;

  while (running) {
    EVENT_LOOP_PRINT("loop\n");

    // Draw if needed.
    if (redraw && !Render(&state)) {
      success = false;
      break;
    }
    redraw = false;

    // Handle events.
    SDL_Event event = {0};
    if (SDL_WaitEvent(&event) == 0) {
      success = false;
      break;
    } else if (event.type == SDL_QUIT) {
      break;
    } else {
      if (event.type == SDL_WINDOWEVENT) {
        // When the window is maximized, an SDL_WINDOWEVENT_MOVED comes in
        // before an SDL_WINDOWEVENT_SIZE_CHANGED with the window already
        // resized. This is unfortunate because we cannot rely on the latter
        // event alone to handle resizing.
        if ((event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) ||
            (event.window.event == SDL_WINDOWEVENT_RESIZED) ||
            (event.window.event == SDL_WINDOWEVENT_MOVED)) {
          if (!Resize(&state)) {
            success = false;
            break;
          }
          redraw = true;
        }
      } else if (event.type == SDL_KEYDOWN) {
        if (event.key.keysym.mod & KMOD_LCTRL) {
          switch (event.key.keysym.sym) {
          // Exit.
          case SDLK_c:
          case SDLK_d:
            running = false;
            break;
          default:
            break;
          }
        }
      } else if (event.type == SDL_MOUSEBUTTONDOWN) {
        const uiInputEvent ev = {
            .type         = uiEventMouseButton,
            .mouse_button = (uiMouseButtonEvent){
                                                 .button = ToUiButton(event.button.button),
                                                 .state  = uiMouseDown,
                                                 .mouse_position =
                    (uiPoint){.x = event.button.x, .y = event.button.y}}
        };
        redraw = uiSendEvent(state.frame, &ev);
      } else if (event.type == SDL_MOUSEBUTTONUP) {
        const uiInputEvent ev = {
            .type         = uiEventMouseButton,
            .mouse_button = (uiMouseButtonEvent){
                                                 .button = ToUiButton(event.button.button),
                                                 .state  = uiMouseUp,
                                                 .mouse_position =
                    (uiPoint){.x = event.button.x, .y = event.button.y}}
        };
        redraw = uiSendEvent(state.frame, &ev);
      } else if (event.type == SDL_MOUSEWHEEL) {
        const uiInputEvent ev = {
            .type         = uiEventMouseScroll,
            .mouse_scroll = (uiMouseScrollEvent){
                                                 .scroll_offset  = event.wheel.y,
                                                 .mouse_position = (uiPoint){
                    .x = event.wheel.mouseX, .y = event.wheel.mouseY}}
        };
        redraw = uiSendEvent(state.frame, &ev);
      } else {
        EVENT_LOOP_PRINT("event.window.event = %d\n", event.window.event);
      }

      if (HandleWidgetEvents(&state)) {
        Resize(&state); // Trigger a re-layout of widgets.
        redraw = true;
      }
    }
  }

cleanup:
  if (!success) {
    fprintf(stderr, "%s\n", SDL_GetError());
  }

  if (state.frame) {
    uiDestroyFrame(&state.frame);
  }
  if (state.window) {
    SDL_DestroyWindow(state.window);
    state.window = 0;
  }

  uiShutdown();
  SDL_Quit();

  return success ? 0 : 1;
}

// -----------------------------------------------------------------------------

uiMouseButton ToUiButton(Uint8 button) {
  // TODO: Buttons.
  return uiLMB;
}