view postdog/main.c @ 196:83f16548ba41

[AI] Adding s3 bucket uploader code using Seobeo.
author MrJuneJune <me@mrjunejune.com>
date Sat, 14 Feb 2026 16:08:15 -0800
parents 0face9898d04
children
line wrap: on
line source

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <uv.h>

#ifdef _WIN32
  #include <direct.h>
  #include <io.h>
  #define mkdir(path, mode) _mkdir(path)
  #define access _access
  #define F_OK 0
#else
  #include <sys/stat.h>
  #include <dirent.h>
  #include <unistd.h>
#endif


#include "dowa/dowa.h"
#include "seobeo/seobeo.h"
#include "third_party/raylib/include/raylib.h"
#include "third_party/raylib/include/raygui.h"
#include "third_party/raylib/custom.h"

#ifndef POSTDOG_PATHS
  #define POSTDOG_PATHS "/home/june/zenbu/postdog/history"
#endif

#define SCREEN_WIDTH 1280
#define SCREEN_HEIGHT 780
#define MAX_SCROLL_HEIGHT 10000

#define HEADER_BUFFER_LENGTH 1024 * 4
#define DEFAULT_TEXT_BUFFER_LENGTH 1024 * 4
#define URL_TEXT_BUFFER_LENGTH 1024 * 10
#define BODY_BUFFER_LENGTH 1024 * 1024 * 5
#define RESULT_BUFFER_LENGTH 1024 * 1024 * 5

// #define URL_TEXT_DEFAULT "https://httpbin.org/get"
#define URL_TEXT_DEFAULT "wss://mrjunejune.com/echo"
#define HEADER_TEXT_DEFAULT "Content-Type: application/json"
#define BODY_TEXT_DEFAULT ""
#define GET_PARAM_TEXT_DEFAULT "foo bar"

// ============================================================================
// TextArea Component
// ============================================================================

#define TEXT_SIZE_DEFAULT 20 // used to calcualte spacing
#define TEXT_AREA_LINE_HEIGHT GuiGetStyle(DEFAULT, TEXT_SIZE)
#define TEXT_AREA_PADDING 30
#define TEXT_AREA_CURSOR_WIDTH 2
#define TEXT_AREA_MAX_UNDO_STATES 64
#define TEXT_AREA_MAX_INSTANCES 8

typedef struct {
  char *text;
  int cursor_pos;
  int selection_start;
  int selection_end;
} TextAreaUndoEntry;

// Cached line info for fast rendering
typedef struct {
  int start_pos;      // Character position where this line starts
  int end_pos;        // Character position where this line ends (exclusive)
  int char_count;     // Number of characters in this line
} LineInfo;

typedef struct {
  int id;             // Unique ID for this text area
  int cursor_pos;         // Current cursor position in text
  int selection_start;      // Selection start (-1 if no selection)
  int selection_end;      // Selection end (-1 if no selection)
  float scroll_offset_y;    // Vertical scroll offset
  float scroll_offset_x;    // Horizontal scroll offset (for non-wrap mode)
  boolean is_selecting;     // Currently dragging to select
  boolean is_initialized;     // State has been initialized

  // Undo history
  TextAreaUndoEntry *undo_stack;  // Dowa array of undo entries
  int undo_index;         // Current position in undo stack

  // Internal tracking
  double last_blink_time;
  boolean cursor_visible;

  // Line cache for fast rendering
  LineInfo *line_cache;     // Dowa array of line info
  int cached_text_len;      // Text length when cache was built
  int cached_text_hash;     // Simple hash to detect text changes
  float cached_content_width; // Content width when cache was built
  boolean cache_valid;      // Whether cache is valid
} TextAreaState;

static TextAreaState g_text_area_states[TEXT_AREA_MAX_INSTANCES] = {0};
static int g_text_area_state_count = 0;
static char *g_clipboard_text = NULL;
static Dowa_Arena *g_text_area_arena = NULL;

// Helper functions
static int TA_Min_Int(int a, int b) { return a < b ? a : b; }
static int TA_Max_Int(int a, int b) { return a > b ? a : b; }
static float TA_Min_Float(float a, float b) { return a < b ? a : b; }
static float TA_Max_Float(float a, float b) { return a > b ? a : b; }

// Sanitize text for display - replace tabs with spaces, remove other non-printable chars
static void SanitizeTextForDisplay(char *text) {
  if (!text) return;
  char *read = text;
  char *write = text;
  while (*read) {
    if (*read == '\t') {
      // Replace tab with 2 spaces
      *write++ = ' ';
      *write++ = ' ';
    } else if (*read == '\n' || *read == '\r') {
      // Keep newlines and carriage returns
      *write++ = *read;
    } else if ((unsigned char)*read >= 32 && (unsigned char)*read < 127) {
      // Keep printable ASCII
      *write++ = *read;
    } else if ((unsigned char)*read >= 128) {
      // Keep UTF-8 characters (high bit set)
      *write++ = *read;
    }
    // Skip other non-printable characters
    read++;
  }
  *write = '\0';
}

static TextAreaState* GetTextAreaState(int id) {
  for (int i = 0; i < g_text_area_state_count; i++) {
    if (g_text_area_states[i].id == id && g_text_area_states[i].is_initialized) {
      return &g_text_area_states[i];
    }
  }
  return NULL;
}

static TextAreaState* CreateTextAreaState(int id) {
  // First, check if there's an existing slot with this ID (possibly reset)
  for (int i = 0; i < g_text_area_state_count; i++) {
    if (g_text_area_states[i].id == id) {
      TextAreaState *state = &g_text_area_states[i];
      // Reuse this slot - clear it but keep the id
      memset(state, 0, sizeof(TextAreaState));
      state->id = id;
      state->selection_start = -1;
      state->selection_end = -1;
      state->undo_index = -1;
      state->cursor_visible = TRUE;
      state->is_initialized = TRUE;
      return state;
    }
  }

  // No existing slot, create a new one
  if (g_text_area_state_count >= TEXT_AREA_MAX_INSTANCES) {
    return &g_text_area_states[0]; // Reuse first slot as fallback
  }

  TextAreaState *state = &g_text_area_states[g_text_area_state_count++];
  memset(state, 0, sizeof(TextAreaState));
  state->id = id;
  state->selection_start = -1;
  state->selection_end = -1;
  state->undo_index = -1;
  state->cursor_visible = TRUE;
  state->is_initialized = TRUE;
  return state;
}

static void GetLineAndColumn(const char *text, int pos, int *out_line, int *out_column) {
  int line = 0;
  int column = 0;
  for (int i = 0; i < pos && text[i] != '\0'; i++) {
    if (text[i] == '\n') {
      line++;
      column = 0;
    } else {
      column++;
    }
  }
  *out_line = line;
  *out_column = column;
}

static int GetPosFromLineColumn(const char *text, int line, int column) {
  int current_line = 0;
  int current_col = 0;
  int i = 0;

  while (text[i] != '\0') {
    if (current_line == line && current_col == column) {
      return i;
    }
    if (text[i] == '\n') {
      if (current_line == line) {
        return i;
      }
      current_line++;
      current_col = 0;
    } else {
      current_col++;
    }
    i++;
  }
  return i;
}

static int GetLineStart(const char *text, int pos) {
  int i = pos;
  while (i > 0 && text[i - 1] != '\n') {
    i--;
  }
  return i;
}

static int GetLineEnd(const char *text, int pos) {
  int i = pos;
  int len = strlen(text);
  while (i < len && text[i] != '\n') {
    i++;
  }
  return i;
}

static int CountLines(const char *text) {
  int count = 1;
  for (int i = 0; text[i] != '\0'; i++) {
    if (text[i] == '\n') count++;
  }
  return count;
}

// Simple hash to detect text changes (djb2)
static int SimpleTextHash(const char *text, int len) {
  int hash = 5381;
  int sample_count = len < 100 ? len : 100;  // Sample first 100 chars
  for (int i = 0; i < sample_count; i++) {
    hash = ((hash << 5) + hash) + text[i];
  }
  // Also sample some chars from middle and end
  if (len > 200) {
    for (int i = len/2; i < len/2 + 50 && i < len; i++) {
      hash = ((hash << 5) + hash) + text[i];
    }
  }
  return hash;
}

// Build line cache - O(n) once, then O(1) for rendering
static void BuildLineCache(TextAreaState *state, const char *text, float content_width,
                           int font_size, boolean wrap, Dowa_Arena *arena) {
  int text_len = strlen(text);

  // Check if cache is still valid
  int new_hash = SimpleTextHash(text, text_len);
  if (state->cache_valid &&
      state->cached_text_len == text_len &&
      state->cached_text_hash == new_hash &&
      state->cached_content_width == content_width) {
    return;  // Cache is valid, no need to rebuild
  }

  // Clear old cache
  if (state->line_cache) {
    Dowa_Array_Free(state->line_cache);
    state->line_cache = NULL;
  }

  // Approximate character width (for non-monospace, use average)
  float avg_char_width = font_size * 0.6f;  // Reasonable approximation for most fonts
  int max_chars_per_line = (int)(content_width / avg_char_width);
  if (max_chars_per_line < 1) max_chars_per_line = 1;

  Dowa_Array_Reserve(state->line_cache, text_len / max_chars_per_line + 10);

  int line_start = 0;
  int i = 0;

  while (i <= text_len) {
    boolean is_end = (i == text_len);
    boolean is_newline = (!is_end && text[i] == '\n');
    int chars_in_line = i - line_start;

    // Check if we need to wrap (based on character count approximation)
    boolean should_wrap = wrap && !is_end && !is_newline &&
                          chars_in_line >= max_chars_per_line;

    if (is_end || is_newline || should_wrap) {
      LineInfo line = {
        .start_pos = line_start,
        .end_pos = i,
        .char_count = chars_in_line
      };
      Dowa_Array_Push(state->line_cache, line);

      if (is_newline) {
        line_start = i + 1;
      } else if (should_wrap) {
        // Try to find a space to wrap at
        int wrap_pos = i;
        for (int j = i - 1; j > line_start && j > i - 20; j--) {
          if (text[j] == ' ') {
            wrap_pos = j;
            break;
          }
        }
        // Update the last line entry with correct end
        state->line_cache[Dowa_Array_Length(state->line_cache) - 1].end_pos = wrap_pos;
        state->line_cache[Dowa_Array_Length(state->line_cache) - 1].char_count = wrap_pos - line_start;
        line_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos;
        i = line_start - 1;  // Will be incremented at end of loop
      } else {
        line_start = i + 1;
      }
    }
    i++;
  }

  // Ensure at least one line exists
  if (Dowa_Array_Length(state->line_cache) == 0) {
    LineInfo empty_line = { .start_pos = 0, .end_pos = 0, .char_count = 0 };
    Dowa_Array_Push(state->line_cache, empty_line);
  }

  // Update cache metadata
  state->cached_text_len = text_len;
  state->cached_text_hash = new_hash;
  state->cached_content_width = content_width;
  state->cache_valid = TRUE;
}

static int MeasureTextRange(const char *text, int start, int end, int font_size) {
  if (start >= end) return 0;

  char temp[1024];
  int len = TA_Min_Int(end - start, 1023);
  strncpy(temp, text + start, len);
  temp[len] = '\0';

  return MeasureTextEx(GuiGetFont(), temp, font_size, TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE)).x;
}

static int GetCharIndexFromPos(const char *text, Rectangle bounds, Vector2 pos,
                boolean wrap, float scroll_y, int font_size, int line_height) {
  if (!text || strlen(text) == 0) return 0;

  float content_x = bounds.x + TEXT_AREA_PADDING;
  float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y;
  float content_width = bounds.width - TEXT_AREA_PADDING * 2;

  int text_len = strlen(text);

  float click_line_y = (pos.y - content_y) / line_height;
  int target_visual_line = (int)click_line_y;
  if (target_visual_line < 0) target_visual_line = 0;

  int current_visual_line = 0;
  int i = 0;
  int line_char_start = 0;

  while (i <= text_len) {
    boolean is_newline = (i < text_len && text[i] == '\n');

    if (wrap && i > line_char_start) {
      int line_width = MeasureTextRange(text, line_char_start, i, font_size);
      if (line_width > content_width && i > line_char_start + 1) {
        int wrap_pos = i - 1;
        for (int j = i - 1; j > line_char_start; j--) {
          if (text[j] == ' ') {
            wrap_pos = j;
            break;
          }
        }

        if (current_visual_line == target_visual_line) {
          float click_x = pos.x - content_x;
          int best_pos = line_char_start;
          float best_dist = 99999;

          for (int k = line_char_start; k <= wrap_pos; k++) {
            int char_x = MeasureTextRange(text, line_char_start, k, font_size);
            float dist = (float)(click_x - char_x);
            if (dist < 0) dist = -dist;
            if (dist < best_dist) {
              best_dist = dist;
              best_pos = k;
            }
          }
          return best_pos;
        }

        current_visual_line++;
        line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos;
        i = line_char_start;
        continue;
      }
    }

    if (is_newline || i == text_len) {
      if (current_visual_line == target_visual_line || i == text_len) {
        float click_x = pos.x - content_x;
        int line_end = i;
        int best_pos = line_char_start;
        float best_dist = 99999;

        for (int k = line_char_start; k <= line_end; k++) {
          int char_x = MeasureTextRange(text, line_char_start, k, font_size);
          float dist = (float)(click_x - char_x);
          if (dist < 0) dist = -dist;
          if (dist < best_dist) {
            best_dist = dist;
            best_pos = k;
          }
        }
        return TA_Min_Int(best_pos, text_len);
      }

      current_visual_line++;
      line_char_start = i + 1;
    }

    i++;
  }

  return text_len;
}

// Get character index from mouse position using line cache
static int GetCharIndexFromPosWithCache(const char *text, Rectangle bounds, Vector2 pos,
                       float scroll_y, int font_size, int line_height,
                       LineInfo *line_cache) {
  if (!text || strlen(text) == 0) return 0;
  if (!line_cache || Dowa_Array_Length(line_cache) == 0) return 0;

  float content_x = bounds.x + TEXT_AREA_PADDING;
  float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y;

  int text_len = strlen(text);
  int line_count = Dowa_Array_Length(line_cache);

  // Find which visual line was clicked
  float click_line_y = (pos.y - content_y) / line_height;
  int target_line = (int)click_line_y;
  if (target_line < 0) target_line = 0;
  if (target_line >= line_count) target_line = line_count - 1;
  if (target_line < 0) return 0;  // Safety check

  LineInfo *line = &line_cache[target_line];

  // Find character position within the line
  float click_x = pos.x - content_x;
  if (click_x < 0) return line->start_pos;

  // Linear search for the character closest to click position
  int best_pos = line->start_pos;
  float best_dist = 99999;

  for (int k = line->start_pos; k <= line->end_pos; k++) {
    int char_x = MeasureTextRange(text, line->start_pos, k, font_size);
    float dist = click_x - char_x;
    if (dist < 0) dist = -dist;

    if (dist < best_dist) {
      best_dist = dist;
      best_pos = k;
    }
  }

  return TA_Min_Int(best_pos, text_len);
}

// Get cursor position using line cache (fast O(log n) or O(n) worst case)
static Vector2 GetCursorScreenPosFromCache(const char *text, int cursor_pos, Rectangle bounds,
                       float scroll_y, int font_size, int line_height,
                       LineInfo *line_cache) {
  float content_x = bounds.x + TEXT_AREA_PADDING;
  float content_y = bounds.y + TEXT_AREA_PADDING - scroll_y;

  if (!text || cursor_pos == 0) {
    return (Vector2){content_x, content_y};
  }
  if (!line_cache || Dowa_Array_Length(line_cache) == 0) {
    return (Vector2){content_x, content_y};
  }

  int text_len = strlen(text);
  cursor_pos = TA_Min_Int(cursor_pos, text_len);

  // Find which line contains the cursor
  int line_count = Dowa_Array_Length(line_cache);
  for (int i = 0; i < line_count; i++) {
    LineInfo *line = &line_cache[i];

    // Cursor is within this line or at the end of this line
    if (cursor_pos >= line->start_pos && cursor_pos <= line->end_pos) {
      float x = content_x + MeasureTextRange(text, line->start_pos, cursor_pos, font_size);
      float y = content_y + i * line_height;
      return (Vector2){x, y};
    }
  }

  // Fallback: cursor at the end
  int last_line = line_count > 0 ? line_count - 1 : 0;
  LineInfo *line = &line_cache[last_line];
  float x = content_x + MeasureTextRange(text, line->start_pos, cursor_pos, font_size);
  float y = content_y + last_line * line_height;
  return (Vector2){x, y};
}

static float GetContentHeight(const char *text, Rectangle bounds, boolean wrap,
                 int font_size, int line_height) {
  if (!text || strlen(text) == 0) return line_height;

  float content_width = bounds.width - TEXT_AREA_PADDING * 2;
  int visual_lines = 1;
  int line_char_start = 0;
  int text_len = strlen(text);

  for (int i = 0; i <= text_len; i++) {
    if (i == text_len || text[i] == '\n') {
      visual_lines++;
      line_char_start = i + 1;
    } else if (wrap) {
      int line_width = MeasureTextRange(text, line_char_start, i + 1, font_size);
      if (line_width > content_width && i > line_char_start) {
        int wrap_pos = i;
        for (int j = i; j > line_char_start; j--) {
          if (text[j] == ' ') {
            wrap_pos = j;
            break;
          }
        }
        visual_lines++;
        line_char_start = (text[wrap_pos] == ' ') ? wrap_pos + 1 : wrap_pos;
      }
    }
  }

  return visual_lines * line_height;
}

static void PushUndoState(TextAreaState *state, const char *text, Dowa_Arena *arena) {
  TextAreaUndoEntry entry;
  entry.text = Dowa_String_Copy_Arena((char*)text, arena);
  entry.cursor_pos = state->cursor_pos;
  entry.selection_start = state->selection_start;
  entry.selection_end = state->selection_end;

  if (state->undo_index < (int)Dowa_Array_Length(state->undo_stack) - 1) {
    dowa__header(state->undo_stack)->length = state->undo_index + 1;
  }

  Dowa_Array_Push_Arena(state->undo_stack, entry, arena);
  state->undo_index = Dowa_Array_Length(state->undo_stack) - 1;

  if (Dowa_Array_Length(state->undo_stack) > TEXT_AREA_MAX_UNDO_STATES) {
    for (int i = 0; i < (int)Dowa_Array_Length(state->undo_stack) - 1; i++) {
      state->undo_stack[i] = state->undo_stack[i + 1];
    }
    dowa__header(state->undo_stack)->length--;
    state->undo_index--;
  }
}

static boolean PerformUndo(TextAreaState *state, char *text, int text_size) {
  if (state->undo_index <= 0) return FALSE;

  state->undo_index--;
  TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index];

  strncpy(text, entry->text, text_size - 1);
  text[text_size - 1] = '\0';
  state->cursor_pos = entry->cursor_pos;
  state->selection_start = entry->selection_start;
  state->selection_end = entry->selection_end;

  return TRUE;
}

static boolean PerformRedo(TextAreaState *state, char *text, int text_size) {
  if (state->undo_index >= (int)Dowa_Array_Length(state->undo_stack) - 1) return FALSE;

  state->undo_index++;
  TextAreaUndoEntry *entry = &state->undo_stack[state->undo_index];

  strncpy(text, entry->text, text_size - 1);
  text[text_size - 1] = '\0';
  state->cursor_pos = entry->cursor_pos;
  state->selection_start = entry->selection_start;
  state->selection_end = entry->selection_end;

  return TRUE;
}

static void InsertTextAtCursor(char *text, int text_size, int cursor_pos,
                const char *insert_text, int *new_cursor_pos) {
  int text_len = strlen(text);
  int insert_len = strlen(insert_text);

  if (text_len + insert_len >= text_size - 1) {
    insert_len = text_size - 1 - text_len;
    if (insert_len <= 0) return;
  }

  memmove(text + cursor_pos + insert_len,
      text + cursor_pos,
      text_len - cursor_pos + 1);

  memcpy(text + cursor_pos, insert_text, insert_len);

  *new_cursor_pos = cursor_pos + insert_len;
}

static void DeleteTextRange(char *text, int start, int end) {
  if (start >= end) return;
  int text_len = strlen(text);
  if (start < 0) start = 0;
  if (end > text_len) end = text_len;

  memmove(text + start, text + end, text_len - end + 1);
}

static char* GetSelectedText(const char *text, int sel_start, int sel_end, Dowa_Arena *arena) {
  if (sel_start < 0 || sel_end < 0 || sel_start >= sel_end) return NULL;

  int len = sel_end - sel_start;
  char *result = Dowa_Arena_Allocate(arena, len + 1);
  strncpy(result, text + sel_start, len);
  result[len] = '\0';
  return result;
}

boolean GuiTextArea(int id, Rectangle bounds, char *text, int text_size, boolean is_edit_mode,
          boolean should_text_wrap, boolean readonly, Dowa_Arena *arena) {
  boolean should_toggle_edit_mode = FALSE;

  // Get or create state for this text area
  TextAreaState *state = GetTextAreaState(id);
  if (!state) {
    state = CreateTextAreaState(id);
    state->cursor_pos = readonly ? 0 : strlen(text);  // Readonly starts at beginning
    state->last_blink_time = GetTime();
    if (!readonly) PushUndoState(state, text, arena);
  }

  int text_len = strlen(text);
  Vector2 mouse_pos = GetMousePosition();
  boolean mouse_in_bounds = CheckCollisionPointRec(mouse_pos, bounds);

  // Handle click to enter/exit edit mode (readonly still allows selection mode)
  if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
    if (mouse_in_bounds && !is_edit_mode) {
      should_toggle_edit_mode = TRUE;
    } else if (!mouse_in_bounds && is_edit_mode) {
      should_toggle_edit_mode = TRUE;
    }
  }

  // Content area - build/update line cache
  float content_width = bounds.width - TEXT_AREA_PADDING * 2;
  BuildLineCache(state, text, content_width, GuiGetStyle(DEFAULT, TEXT_SIZE), should_text_wrap, arena);

  // Calculate content height from cache (O(1))
  int total_lines = Dowa_Array_Length(state->line_cache);
  float content_height = total_lines * TEXT_AREA_LINE_HEIGHT;
  float visible_height = bounds.height - TEXT_AREA_PADDING * 2;
  float max_scroll = TA_Max_Float(0, content_height - visible_height);

  // Handle scrolling
  float wheel = GetMouseWheelMove();
  if (mouse_in_bounds && wheel != 0) {
    state->scroll_offset_y -= wheel * TEXT_AREA_LINE_HEIGHT * 3;
    state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll));
  }

  if (is_edit_mode) {
    boolean ctrl_pressed = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL) ||
                 IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER);
    boolean shift_pressed = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
    boolean text_changed = FALSE;

    double current_time = GetTime();
    if (current_time - state->last_blink_time > 0.5) {
      state->cursor_visible = !state->cursor_visible;
      state->last_blink_time = current_time;
    }

    // Mouse Selection (use line cache for accurate position)
    if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && mouse_in_bounds) {
      int click_pos = GetCharIndexFromPosWithCache(text, bounds, mouse_pos,
                         state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE),
                         TEXT_AREA_LINE_HEIGHT, state->line_cache);
      state->cursor_pos = click_pos;
      state->selection_start = -1;
      state->selection_end = -1;
      state->is_selecting = TRUE;
      state->cursor_visible = TRUE;
      state->last_blink_time = current_time;
    }

    if (state->is_selecting && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
      int drag_pos = GetCharIndexFromPosWithCache(text, bounds, mouse_pos,
                        state->scroll_offset_y, GuiGetStyle(DEFAULT, TEXT_SIZE),
                        TEXT_AREA_LINE_HEIGHT, state->line_cache);
      if (drag_pos != state->cursor_pos) {
        if (state->selection_start < 0) {
          state->selection_start = state->cursor_pos;
        }
        state->selection_end = drag_pos;
      }
    }

    if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
      state->is_selecting = FALSE;
      if (state->selection_start >= 0 && state->selection_end >= 0) {
        if (state->selection_start > state->selection_end) {
          int temp = state->selection_start;
          state->selection_start = state->selection_end;
          state->selection_end = temp;
        }
        if (state->selection_start == state->selection_end) {
          state->selection_start = -1;
          state->selection_end = -1;
        }
      }
    }

    // Ctrl+A: Select All
    if (ctrl_pressed && IsKeyPressed(KEY_A)) {
      state->selection_start = 0;
      state->selection_end = text_len;
      state->cursor_pos = text_len;
    }

    // Ctrl+C: Copy
    if (ctrl_pressed && IsKeyPressed(KEY_C)) {
      if (state->selection_start >= 0 && state->selection_end >= 0 &&
        state->selection_start != state->selection_end) {
        int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
        int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
        char *selected = GetSelectedText(text, sel_min, sel_max, arena);
        if (selected) {
          if (g_clipboard_text) free(g_clipboard_text);
          g_clipboard_text = strdup(selected);
          SetClipboardText(g_clipboard_text);
        }
      }
    }

    // Ctrl+X: Cut (not allowed in readonly mode)
    if (!readonly && ctrl_pressed && IsKeyPressed(KEY_X)) {
      if (state->selection_start >= 0 && state->selection_end >= 0 &&
        state->selection_start != state->selection_end) {
        int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
        int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
        char *selected = GetSelectedText(text, sel_min, sel_max, arena);
        if (selected) {
          if (g_clipboard_text) free(g_clipboard_text);
          g_clipboard_text = strdup(selected);
          SetClipboardText(g_clipboard_text);
        }

        PushUndoState(state, text, arena);
        DeleteTextRange(text, sel_min, sel_max);
        state->cursor_pos = sel_min;
        state->selection_start = -1;
        state->selection_end = -1;
        text_changed = TRUE;
      }
    }

    // Ctrl+V: Paste (not allowed in readonly mode)
    if (!readonly && ctrl_pressed && IsKeyPressed(KEY_V)) {
      const char *clipboard = GetClipboardText();
      if (clipboard && strlen(clipboard) > 0) {
        // Skip leading whitespace (spaces, tabs, newlines)
        while (*clipboard && (*clipboard == ' ' || *clipboard == '\t' ||
               *clipboard == '\n' || *clipboard == '\r')) {
          clipboard++;
        }

        if (strlen(clipboard) > 0) {
          if (state->selection_start >= 0 && state->selection_end >= 0 &&
            state->selection_start != state->selection_end) {
            int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
            int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
            PushUndoState(state, text, arena);
            DeleteTextRange(text, sel_min, sel_max);
            state->cursor_pos = sel_min;
            state->selection_start = -1;
            state->selection_end = -1;
          } else {
            PushUndoState(state, text, arena);
          }

          int new_cursor;
          InsertTextAtCursor(text, text_size, state->cursor_pos, clipboard, &new_cursor);
          state->cursor_pos = new_cursor;
          text_changed = TRUE;
        }
      }
    }

    // Ctrl+Z: Undo / Ctrl+Shift+Z: Redo (not allowed in readonly mode)
    if (!readonly && ctrl_pressed && IsKeyPressed(KEY_Z)) {
      if (shift_pressed) {
        PerformRedo(state, text, text_size);
      } else {
        PerformUndo(state, text, text_size);
      }
    }

    // Arrow Keys Navigation
    if (IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) {
      if (state->selection_start >= 0 && !shift_pressed) {
        state->cursor_pos = TA_Min_Int(state->selection_start, state->selection_end);
        state->selection_start = -1;
        state->selection_end = -1;
      } else if (state->cursor_pos > 0) {
        if (shift_pressed) {
          if (state->selection_start < 0) {
            state->selection_start = state->cursor_pos;
            state->selection_end = state->cursor_pos;
          }
          state->cursor_pos--;
          state->selection_end = state->cursor_pos;
        } else {
          state->cursor_pos--;
        }
      }
      state->cursor_visible = TRUE;
      state->last_blink_time = current_time;
    }

    if (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) {
      if (state->selection_start >= 0 && !shift_pressed) {
        state->cursor_pos = TA_Max_Int(state->selection_start, state->selection_end);
        state->selection_start = -1;
        state->selection_end = -1;
      } else if (state->cursor_pos < text_len) {
        if (shift_pressed) {
          if (state->selection_start < 0) {
            state->selection_start = state->cursor_pos;
            state->selection_end = state->cursor_pos;
          }
          state->cursor_pos++;
          state->selection_end = state->cursor_pos;
        } else {
          state->cursor_pos++;
        }
      }
      state->cursor_visible = TRUE;
      state->last_blink_time = current_time;
    }

    if (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP)) {
      int line, col;
      GetLineAndColumn(text, state->cursor_pos, &line, &col);
      if (line > 0) {
        int new_pos = GetPosFromLineColumn(text, line - 1, col);
        if (shift_pressed) {
          if (state->selection_start < 0) {
            state->selection_start = state->cursor_pos;
            state->selection_end = state->cursor_pos;
          }
          state->selection_end = new_pos;
        } else {
          state->selection_start = -1;
          state->selection_end = -1;
        }
        state->cursor_pos = new_pos;
      }
      state->cursor_visible = TRUE;
      state->last_blink_time = current_time;
    }

    if (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN)) {
      int line, col;
      GetLineAndColumn(text, state->cursor_pos, &line, &col);
      int total_lines = CountLines(text);
      if (line < total_lines - 1) {
        int new_pos = GetPosFromLineColumn(text, line + 1, col);
        if (shift_pressed) {
          if (state->selection_start < 0) {
            state->selection_start = state->cursor_pos;
            state->selection_end = state->cursor_pos;
          }
          state->selection_end = new_pos;
        } else {
          state->selection_start = -1;
          state->selection_end = -1;
        }
        state->cursor_pos = new_pos;
      }
      state->cursor_visible = TRUE;
      state->last_blink_time = current_time;
    }

    // Home/End keys
    if (IsKeyPressed(KEY_HOME)) {
      int line_start = GetLineStart(text, state->cursor_pos);
      if (shift_pressed) {
        if (state->selection_start < 0) {
          state->selection_start = state->cursor_pos;
          state->selection_end = state->cursor_pos;
        }
        state->selection_end = line_start;
      } else {
        state->selection_start = -1;
        state->selection_end = -1;
      }
      state->cursor_pos = line_start;
    }

    if (IsKeyPressed(KEY_END)) {
      int line_end = GetLineEnd(text, state->cursor_pos);
      if (shift_pressed) {
        if (state->selection_start < 0) {
          state->selection_start = state->cursor_pos;
          state->selection_end = state->cursor_pos;
        }
        state->selection_end = line_end;
      } else {
        state->selection_start = -1;
        state->selection_end = -1;
      }
      state->cursor_pos = line_end;
    }

    // Text Input (not allowed in readonly mode)
    if (!readonly && !ctrl_pressed) {
      int key = GetCharPressed();
      while (key > 0) {
        if (key >= 32 && key <= 126) {
          if (state->selection_start >= 0 && state->selection_end >= 0 &&
            state->selection_start != state->selection_end) {
            int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
            int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
            PushUndoState(state, text, arena);
            DeleteTextRange(text, sel_min, sel_max);
            state->cursor_pos = sel_min;
            state->selection_start = -1;
            state->selection_end = -1;
            text_changed = TRUE;
          } else if (!text_changed) {
            PushUndoState(state, text, arena);
          }

          char insert_str[2] = {(char)key, '\0'};
          int new_cursor;
          InsertTextAtCursor(text, text_size, state->cursor_pos, insert_str, &new_cursor);
          state->cursor_pos = new_cursor;
          text_changed = TRUE;
        }
        key = GetCharPressed();
      }
    } else if (readonly) {
      // Consume key presses to prevent them from being handled elsewhere
      while (GetCharPressed() > 0) {}
    }

    // Enter key (not allowed in readonly mode)
    if (!readonly && (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER))) {
      if (state->selection_start >= 0 && state->selection_end >= 0 &&
        state->selection_start != state->selection_end) {
        int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
        int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
        PushUndoState(state, text, arena);
        DeleteTextRange(text, sel_min, sel_max);
        state->cursor_pos = sel_min;
        state->selection_start = -1;
        state->selection_end = -1;
      } else {
        PushUndoState(state, text, arena);
      }

      int new_cursor;
      InsertTextAtCursor(text, text_size, state->cursor_pos, "\n", &new_cursor);
      state->cursor_pos = new_cursor;
      text_changed = TRUE;
    }

    // Backspace (not allowed in readonly mode)
    if (!readonly && (IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE))) {
      if (state->selection_start >= 0 && state->selection_end >= 0 &&
        state->selection_start != state->selection_end) {
        int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
        int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
        PushUndoState(state, text, arena);
        DeleteTextRange(text, sel_min, sel_max);
        state->cursor_pos = sel_min;
        state->selection_start = -1;
        state->selection_end = -1;
        text_changed = TRUE;
      } else if (state->cursor_pos > 0) {
        PushUndoState(state, text, arena);
        DeleteTextRange(text, state->cursor_pos - 1, state->cursor_pos);
        state->cursor_pos--;
        text_changed = TRUE;
      }
    }

    // Delete key (not allowed in readonly mode)
    if (!readonly && (IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE))) {
      text_len = strlen(text);
      if (state->selection_start >= 0 && state->selection_end >= 0 &&
        state->selection_start != state->selection_end) {
        int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
        int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
        PushUndoState(state, text, arena);
        DeleteTextRange(text, sel_min, sel_max);
        state->cursor_pos = sel_min;
        state->selection_start = -1;
        state->selection_end = -1;
        text_changed = TRUE;
      } else if (state->cursor_pos < text_len) {
        PushUndoState(state, text, arena);
        DeleteTextRange(text, state->cursor_pos, state->cursor_pos + 1);
        text_changed = TRUE;
      }
    }

    // Tab key (not allowed in readonly mode)
    if (!readonly && IsKeyPressed(KEY_TAB)) {
      if (state->selection_start >= 0 && state->selection_end >= 0 &&
        state->selection_start != state->selection_end) {
        int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
        int sel_max = TA_Max_Int(state->selection_start, state->selection_end);
        PushUndoState(state, text, arena);
        DeleteTextRange(text, sel_min, sel_max);
        state->cursor_pos = sel_min;
        state->selection_start = -1;
        state->selection_end = -1;
      } else {
        PushUndoState(state, text, arena);
      }

      int new_cursor;
      InsertTextAtCursor(text, text_size, state->cursor_pos, "  ", &new_cursor);
      state->cursor_pos = new_cursor;
      text_changed = TRUE;
    }

    // Auto-scroll to keep cursor visible
    // Rebuild cache if text changed during editing
    if (text_changed) {
      state->cache_valid = FALSE;
      BuildLineCache(state, text, content_width, GuiGetStyle(DEFAULT, TEXT_SIZE), should_text_wrap, arena);
    }
    Vector2 cursor_screen = GetCursorScreenPosFromCache(text, state->cursor_pos, bounds,
                          state->scroll_offset_y,
                          GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT,
                          state->line_cache);

    float visible_top = bounds.y + TEXT_AREA_PADDING;
    float visible_bottom = bounds.y + bounds.height - TEXT_AREA_PADDING - TEXT_AREA_LINE_HEIGHT;

    if (cursor_screen.y < visible_top) {
      state->scroll_offset_y -= visible_top - cursor_screen.y;
    } else if (cursor_screen.y > visible_bottom) {
      state->scroll_offset_y += cursor_screen.y - visible_bottom;
    }

    state->scroll_offset_y = TA_Max_Float(0, TA_Min_Float(state->scroll_offset_y, max_scroll));
  }

  // Drawing
  DrawRectangleSelectiveRounded(bounds, 0.2, 1, g_colors.secondary, FALSE, FALSE, TRUE, TRUE);
  DrawRectangleRec(AddPadding(bounds, 10), WHITE);
  // DrawRectangleRec(bounds, is_edit_mode ? DARKGRAY : (Color){40, 40, 40, 255});
  // DrawRectangleLinesEx(bounds, 1, is_edit_mode ? WHITE : GRAY);
  // DrawRectangleRoundedLines(bounds, 0.2, 1, is_edit_mode ? BLACK : GRAY);

  BeginScissorMode((int)bounds.x, (int)bounds.y, (int)bounds.width, (int)bounds.height);

  float content_x = bounds.x + TEXT_AREA_PADDING;
  float content_y = bounds.y + TEXT_AREA_PADDING - state->scroll_offset_y;

  text_len = strlen(text);

  // Calculate visible line range (only render what's visible)
  int first_visible_line = (int)(state->scroll_offset_y / TEXT_AREA_LINE_HEIGHT);
  int last_visible_line = first_visible_line + (int)(visible_height / TEXT_AREA_LINE_HEIGHT) + 2;
  int line_count = Dowa_Array_Length(state->line_cache);
  if (first_visible_line < 0) first_visible_line = 0;
  if (last_visible_line > line_count) last_visible_line = line_count;

  // Draw selection highlight (only for visible lines)
  if (state->selection_start >= 0 && state->selection_end >= 0 &&
    state->selection_start != state->selection_end) {
    int sel_min = TA_Min_Int(state->selection_start, state->selection_end);
    int sel_max = TA_Max_Int(state->selection_start, state->selection_end);

    for (int line_idx = first_visible_line; line_idx < last_visible_line; line_idx++) {
      LineInfo *line = &state->line_cache[line_idx];

      // Check if selection intersects this line
      if (sel_min < line->end_pos && sel_max > line->start_pos) {
        int highlight_start = TA_Max_Int(sel_min, line->start_pos);
        int highlight_end = TA_Min_Int(sel_max, line->end_pos);

        float x1 = content_x + MeasureTextRange(text, line->start_pos, highlight_start, GuiGetStyle(DEFAULT, TEXT_SIZE));
        float x2 = content_x + MeasureTextRange(text, line->start_pos, highlight_end, GuiGetStyle(DEFAULT, TEXT_SIZE));
        float y = content_y + line_idx * TEXT_AREA_LINE_HEIGHT;

        DrawRectangle((int)x1, (int)y, (int)(x2 - x1), TEXT_AREA_LINE_HEIGHT,
                Fade(SKYBLUE, 0.5f));
      }
    }
  }

  // Draw text using line cache (only visible lines)
  Color text_color = should_text_wrap ? BLACK : WHITE;
  for (int line_idx = first_visible_line; line_idx < last_visible_line; line_idx++) {
    LineInfo *line = &state->line_cache[line_idx];

    if (line->char_count > 0) {
      char line_buffer[1024];
      int line_len = TA_Min_Int(line->end_pos - line->start_pos, 1023);
      strncpy(line_buffer, text + line->start_pos, line_len);
      line_buffer[line_len] = '\0';

      Vector2 draw_text_vector = {
        .x = content_x,
        .y = content_y + line_idx * TEXT_AREA_LINE_HEIGHT
      };
      DrawTextEx(GuiGetFont(), line_buffer, draw_text_vector,
          GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), text_color);
    }
  }

  // Draw cursor
  if (is_edit_mode && state->cursor_visible) {
    Vector2 cursor_pos = GetCursorScreenPosFromCache(text, state->cursor_pos, bounds,
                         state->scroll_offset_y,
                         GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_AREA_LINE_HEIGHT,
                         state->line_cache);

    DrawRectangle((int)cursor_pos.x, (int)cursor_pos.y,
            TEXT_AREA_CURSOR_WIDTH, TEXT_AREA_LINE_HEIGHT, BLACK);
  }

  EndScissorMode();

  // Draw scrollbar if needed
  if (max_scroll > 0) {
    float scrollbar_height = (visible_height / content_height) * visible_height;
    float scrollbar_y = bounds.y + TEXT_AREA_PADDING +
               (state->scroll_offset_y / max_scroll) * (visible_height - scrollbar_height);

    DrawRectangle((int)(bounds.x + bounds.width - 8), (int)scrollbar_y,
            6, (int)scrollbar_height, Fade(WHITE, 0.3f));
  }

  return should_toggle_edit_mode;
}

void GuiTextAreaResetState(int id) {
  TextAreaState *state = GetTextAreaState(id);
  if (state) {
    if (state->undo_stack) {
      Dowa_Array_Free(state->undo_stack);
      state->undo_stack = NULL;
    }
    if (state->line_cache) {
      Dowa_Array_Free(state->line_cache);
      state->line_cache = NULL;
    }
    state->cache_valid = FALSE;
    state->is_initialized = FALSE;
  }
}

void GuiTextAreaResetAllStates(void) {
  for (int i = 0; i < g_text_area_state_count; i++) {
    if (g_text_area_states[i].undo_stack) {
      Dowa_Array_Free(g_text_area_states[i].undo_stack);
      g_text_area_states[i].undo_stack = NULL;
    }
    if (g_text_area_states[i].line_cache) {
      Dowa_Array_Free(g_text_area_states[i].line_cache);
      g_text_area_states[i].line_cache = NULL;
    }
    g_text_area_states[i].cache_valid = FALSE;
    g_text_area_states[i].is_initialized = FALSE;
  }
  g_text_area_state_count = 0;
}

// ============================================================================
// End TextArea Component
// ============================================================================

typedef Dowa_KV(char*, char*) INPUT_HASHMAP;

typedef struct {
  char *data;
  size_t size;
} ResponseBuffer;

typedef struct {
  char *filename;
  char *title;
  Rectangle rect;
  long time_modified;
  boolean deleted;
} HistoryItem;

typedef struct {
  Rectangle rectangle;
  char *label;
  bool active;
} TabItem;

typedef enum {
  TAB_HEADER = 0,
  TAB_BODY,
  TAB_GET_PARAMS,
  TAB_WEBSOCKET,
  TAB_LENGTH
} PostDog_Tab_Enum;

typedef enum {
  RESULT_TAB_BODY = 0,
  RESULT_TAB_HEADERS,
  RESULT_TAB_LENGTH
} PostDog_ResultTab_Enum;

// Text area IDs
#define TEXT_AREA_ID_INPUT_HEADER   1
#define TEXT_AREA_ID_INPUT_BODY   2
#define TEXT_AREA_ID_INPUT_PARAMS   3
#define TEXT_AREA_ID_INPUT_WS     4
#define TEXT_AREA_ID_RESULT_BODY    5
#define TEXT_AREA_ID_RESULT_HEADERS 6

static uint32 counter = 0;
static uv_mutex_t history_mutex;
static uv_loop_t *main_loop = NULL;
HistoryItem *history_items = NULL;
HistoryItem *new_history_items = NULL;

// Global UI state
char *url_input_text = NULL;
char **result_body_array = NULL;  // [RESULT_TAB_BODY, RESULT_TAB_HEADERS]
char **input_body_array = NULL;
int active_method_dropdown = 0;
int active_input_tab = 0;
int active_result_tab = 0;
Seobeo_WebSocket *ws = NULL;
boolean WS_BREAK = FALSE;
uv_thread_t websocket_thread_id;
Color TEXT_COLOR = BLACK;
boolean LOADING = FALSE;

int CompareHistoryItemsByDate(const void *a, const void *b) {
  HistoryItem *itemA = (HistoryItem *)a;
  HistoryItem *itemB = (HistoryItem *)b;
  return (itemB->time_modified - itemA->time_modified);
}

char *PostDog_Extract_Title(const char *filename)
{
  char full_file_path[512] = {0};
  snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename);
  FILE *file = fopen(full_file_path, "r");
  if (!file)
  return strdup(filename);

  char *title = malloc(sizeof(char) * 512);
  if (!fgets(title, 512, file)) {
  fclose(file);
  free(title);
  return strdup(filename);
  }
  fclose(file);

  // Strip trailing newline
  title[strcspn(title, "\n")] = '\0';

  return title;
}

// TODO: Make this into generic fucntion so I can use it across different thing.
void PostDog_List_Directory(const char *path, HistoryItem **p_file_arr)
{
  HistoryItem *file_arr = *p_file_arr;
#ifdef _WIN32
  struct _finddata_t fileinfo;
  intptr_t handle;
  char search_path[256];
  sprintf(search_path, "%s\\*", path);

  if ((handle = _findfirst(search_path, &fileinfo)) == -1L) {
    printf("Directory is empty or cannot be read.\n");
  } else {
    do {
      HistoryItem item = {0};
      item.filename = strdup(fileinfo.name);
      item.title = PostDog_Extract_Title(fileinfo.name);
      item.time_modified = fileinfo.time_write;
      item.deleted = FALSE;
      Dowa_Array_Push(file_arr, item);
    } while (_findnext(handle, &fileinfo) == 0);
    _findclose(handle);
  }
#else
  struct dirent *entry;
  struct stat file_stat;
  DIR *dp = opendir(path);
  if (dp == NULL) return;

  char full_path[256];
  while ((entry = readdir(dp)))
  {
  if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
    continue;
  snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
  if (stat(full_path, &file_stat) == 0)
  {
    HistoryItem item = {0};
    item.filename = strdup(entry->d_name);
    item.title = PostDog_Extract_Title(entry->d_name);
    item.time_modified = file_stat.st_mtime;
    item.deleted = FALSE;
    Dowa_Array_Push(file_arr, item);
  }
  }
  closedir(dp);
#endif

  // Update the caller's pointer in case array was reallocated
  *p_file_arr = file_arr;

  int count = Dowa_Array_Length(file_arr);
  if (count > 1) {
  qsort(file_arr, count, sizeof(HistoryItem), CompareHistoryItemsByDate);
  }
}

int PostDog_History_Load(HistoryItem **p_history_files)
{
  if (access(POSTDOG_PATHS, F_OK) == -1)
  {
    printf("Directory '%s' not found. Creating it...\n", POSTDOG_PATHS);
    if (mkdir(POSTDOG_PATHS, 0777) != 0)
      return -1;
    return 0;
  }

  printf("Directory '%s' already exists.\n", POSTDOG_PATHS);
  PostDog_List_Directory(POSTDOG_PATHS, p_history_files);
  return 0;
}

bool InArea(Vector2 mouse_position, Rectangle area)
{
  return (
  mouse_position.x >= area.x &&
  mouse_position.x < area.x + area.width &&
  mouse_position.y >= area.y &&
  mouse_position.y < area.y + area.height
  );
}

bool Clicked(Vector2 mouse_position, Rectangle area)
{
  return (InArea(mouse_position, area) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT));
}

// -------- END of UI ---- //

char *PostDog_Enum_To_String(int active_enum)
{
  switch(active_enum)
  {
  case 0: return "GET";
  case 1: return "POST";
  case 2: return "PUT";
  case 3: return "DELETE";
  }
  return 0;
}

char *PostDog_Construct_URL(char *filename, char *out_buffer, size_t buffer_size)
{
  snprintf(out_buffer, buffer_size, "%s/%s", POSTDOG_PATHS, filename);
  return out_buffer;
}

boolean PostDog_History_CreateFile(char *filename, char* values)
{
  char full_file_path[512] = {0};
  snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename);
  FILE *file = fopen(full_file_path, "w");
  if (!file)
  {
  fprintf(stderr, "Failed to create a file: %s\n", full_file_path);
  return FALSE;
  }
  fwrite(values, 1, strlen(values), file);
  fclose(file);
  return TRUE;
}

void PostDog_Request_SaveFile(void)
{
  const char *method = PostDog_Enum_To_String(active_method_dropdown);
  size_t new_file_size = 1024 * 1024;
  Dowa_Arena *arena = Dowa_Arena_Create(1024 * 1024 * 2);
  char *new_file = Dowa_Arena_Allocate(arena, 1024 * 1024);
  char *title = Dowa_Arena_Allocate(arena, strlen(method) + strlen(url_input_text) + 2);
  sprintf(title, "%s %s", method, url_input_text);
  snprintf(
    new_file,
    new_file_size,
    "%s\n"
    "---\n"
    "%s\n"
    "---\n"
    "%s\n"
    "---\n"
    "%s\n"
    "---\n"
    "%s\n"
    "---\n"
    "%s\n"
    "---\n"
    "%s\n"
    "---\n"
    "%s\n",
    title,
    url_input_text,
    method,
    input_body_array[TAB_HEADER],
    input_body_array[TAB_BODY],
    input_body_array[TAB_GET_PARAMS],
    input_body_array[TAB_WEBSOCKET],
    result_body_array[RESULT_TAB_BODY]
  );
  char *filename = Dowa_Arena_Allocate(arena, 1024);
  if (!filename)
  {
  perror("Error opening file");
  exit(EXIT_FAILURE);
  }
  char *uuid4 = (char *)Dowa_Arena_Allocate(arena, 37);
  if (!uuid4)
  {
  perror("Error uuid");
  exit(EXIT_FAILURE);
  }

  uv_mutex_lock(&history_mutex);

  int32 seed = (uint32)time(NULL) ^ counter++;
  Dowa_String_UUID(seed, uuid4);
  snprintf(filename, 1024, "%s.txt", uuid4);

  if (PostDog_History_CreateFile(filename, new_file))
  {
  HistoryItem item = {0};
  item.filename = strdup(filename);
  item.title = strdup(title);
  item.deleted = FALSE;

  Dowa_Array_Push(new_history_items, item);
  }

  uv_mutex_unlock(&history_mutex);
  Dowa_Arena_Free(arena);
}

void PostDog_Websocket_Listen(void)
{
  while (TRUE)
  {
    if (WS_BREAK) break;

    Seobeo_WebSocket_Message *p_msg = Seobeo_WebSocket_Receive(ws);
    if (p_msg)
    {
      if (p_msg->opcode == SEOBEO_WS_OPCODE_TEXT)
      {
        printf("Response: %.*s\n", (int)p_msg->length, (char*)p_msg->data);
        char *body = result_body_array[RESULT_TAB_BODY];
        size_t current_len = strlen(body);
        snprintf(body + current_len, RESULT_BUFFER_LENGTH - current_len,
          "\n%s", (char*)p_msg->data);
      }
      Seobeo_WebSocket_Message_Destroy(p_msg);
    }
    usleep(10000);
  }
  return;
}

void PostDog_Websocket_Connect(void)
{
  // Pass headers from the Headers tab when connecting
  const char *headers = input_body_array[TAB_HEADER];
  if (headers && strlen(headers) > 0)
    ws = Seobeo_WebSocket_Connect_With_Headers(url_input_text, headers);
  else
    ws = Seobeo_WebSocket_Connect(url_input_text);

  result_body_array[RESULT_TAB_BODY][0] = '\0';
  result_body_array[RESULT_TAB_HEADERS][0] = '\0';

  // Reset result text area states
  GuiTextAreaResetState(TEXT_AREA_ID_RESULT_BODY);
  GuiTextAreaResetState(TEXT_AREA_ID_RESULT_HEADERS);
}

int PostDog_Websocket_Send(void)
{
  char *body = result_body_array[RESULT_TAB_BODY];
  if (Seobeo_WebSocket_Send_Text(ws, input_body_array[active_input_tab]) < 0)
  {
    snprintf(body + strlen(body), RESULT_BUFFER_LENGTH - strlen(body),
      "Failed to send message\n");
    return -1;
  }
  snprintf(body + strlen(body), RESULT_BUFFER_LENGTH - strlen(body),
    "\n%s", input_body_array[active_input_tab]);
  return 0;
}

void PostDog_Websocket_Destroy(uv_thread_t thread_id)
{
  Seobeo_WebSocket_Destroy(ws);
  uv_thread_join(&thread_id);
}

void PostDog_Websocket_Start(void *arg)
{
  PostDog_Websocket_Connect();
  PostDog_Websocket_Listen();
}

uv_thread_t PostDog_Websocket_Start_Thread()
{
  uv_thread_t thread_id;

  if (uv_thread_create(&thread_id, PostDog_Websocket_Start, NULL) != 0)
  {
    perror("Failed to create thread");
    memset(&thread_id, 0, sizeof(thread_id));
    return thread_id;
  }

  return thread_id;
}

int PostDog_Http_Request(void)
{
  Seobeo_Client_Request *req = Seobeo_Client_Request_Create(url_input_text);
  printf("URL: %s\n", url_input_text);
  Seobeo_Client_Response *res;
  switch (active_method_dropdown)
  {
    case 0:
    {
      Seobeo_Client_Request_Set_Method(req, "GET");
      break;
    }
    case 1:
    {
      Seobeo_Client_Request_Set_Method(req, "POST");
      break;
    }
    case 2:
    {
      Seobeo_Client_Request_Set_Method(req, "PUT");
      break;
    }
    case 3:
    {
      Seobeo_Client_Request_Set_Method(req, "DELETE");
      break;
    }
  }

  if (input_body_array[TAB_HEADER] && strlen(input_body_array[TAB_HEADER]) > 0)
  {
    char *headersCopy = strdup(input_body_array[TAB_HEADER]);
    char *line = strtok(headersCopy, "\n");
    while (line != NULL)
    {
      while (*line == ' ' || *line == '\t') line++;
      if (strlen(line) > 0)
        Seobeo_Client_Request_Add_Header_Array(req, line);
      line = strtok(NULL, "\n");
    }

  }
  Seobeo_Client_Request_Set_Follow_Redirects(req, TRUE, 5); // TODO: remove magic number;
  res = Seobeo_Client_Request_Execute(req);

  if (res == NULL) {
    snprintf(result_body_array[RESULT_TAB_BODY], RESULT_BUFFER_LENGTH, "Error: Failed to send the request\n");
    result_body_array[RESULT_TAB_HEADERS][0] = '\0';
  } else {
    // Store response body
    snprintf(result_body_array[RESULT_TAB_BODY], RESULT_BUFFER_LENGTH, "%s",
         res->body ? res->body : "");
    // Sanitize body for display (replace tabs, remove non-printable chars)
    SanitizeTextForDisplay(result_body_array[RESULT_TAB_BODY]);

    // Store response headers
    int offset = snprintf(result_body_array[RESULT_TAB_HEADERS], RESULT_BUFFER_LENGTH,
                          "HTTP Status: %d %s\n\n",
                          res->status_code, res->status_text ? res->status_text : "");
    if (res->headers) {
      size_t header_count = Dowa_Array_Length(res->headers);
      for (size_t i = 0; i < header_count && offset < RESULT_BUFFER_LENGTH - 100; i++) {
        offset += snprintf(result_body_array[RESULT_TAB_HEADERS] + offset,
                          RESULT_BUFFER_LENGTH - offset,
                          "%s: %s\n", res->headers[i].key, res->headers[i].value);
      }
    }
  }
  printf("Body: %s\n", res ? res->body : "NULL");
  Seobeo_Client_Request_Destroy(req);
  Seobeo_Client_Response_Destroy(res);
  PostDog_Request_SaveFile();

  // Reset result text area states so scroll/cursor don't carry over
  GuiTextAreaResetState(TEXT_AREA_ID_RESULT_BODY);
  GuiTextAreaResetState(TEXT_AREA_ID_RESULT_HEADERS);

  return 0;
}

void PostDog_Http_Work(uv_work_t *req)
{
  PostDog_Http_Request();
  printf("HTTP request finished.\n");
}

void PostDog_Http_Work_Done(uv_work_t *req, int status)
{
  LOADING = FALSE;
  free(req);
}

void PostDog_Http_Thread_Request()
{
  uv_work_t *work_req = malloc(sizeof(uv_work_t));
  if (!work_req)
  {
  perror("Failed to allocate work request");
  return;
  }
  LOADING = TRUE;
  if (uv_queue_work(main_loop, work_req, PostDog_Http_Work, PostDog_Http_Work_Done) != 0)
  {
  perror("Failed to queue work");
  free(work_req);
  LOADING = FALSE;
  }
}

void PostDog_Update_URL(boolean is_url_updated)
{
  char *params_start = strchr(url_input_text, '?');
  if (is_url_updated)
  {
    if (!params_start)
      return;
    params_start++;
    Dowa_Arena *arena = Dowa_Arena_Create(1024*1024);

    char buffer[4096] = "";
    char **lines = Dowa_String_Split(params_start, "&", URL_TEXT_BUFFER_LENGTH, 1, arena);
    for (int i = 0; i < Dowa_Array_Length(lines); i++)
    {
      char *line = lines[i];
      char **key_value = Dowa_String_Split(line, "=", (int)strlen(line), 1, arena);

      if (Dowa_Array_Length(key_value) < 2)
        break;
      snprintf(buffer + strlen(buffer), 4096 - strlen(buffer), "%s %s\n", key_value[0], key_value[1]);
    }
    snprintf(input_body_array[TAB_GET_PARAMS], URL_TEXT_BUFFER_LENGTH, "%s", buffer);
    int length = strlen(input_body_array[TAB_GET_PARAMS]);
    input_body_array[TAB_GET_PARAMS][length--] = '\0';
    Dowa_Arena_Free(arena);
  }
  else
  {
    if (params_start)
    {
      size_t length = params_start - url_input_text;
      url_input_text[length] = '\0';
    }
    int get_params_length = (int)strlen(input_body_array[TAB_GET_PARAMS]);
    if (get_params_length == 0) 
      return;

    char *separator = "?";
    Dowa_Arena *arena = Dowa_Arena_Create(1024*1024);
    char **lines = Dowa_String_Split(input_body_array[TAB_GET_PARAMS], "\n", get_params_length, 1, arena);
    for (int i = 0; i < Dowa_Array_Length(lines); i++)
    {
      char *line = lines[i];
      char **key_value = Dowa_String_Split(line, " ", (int)strlen(line), 1, arena);

      if (Dowa_Array_Length(key_value) < 2)
        break;

      strcat(url_input_text, separator);
      strcat(url_input_text, key_value[0]);
      strcat(url_input_text, "=");
      for (int i = 1; i < Dowa_Array_Length(key_value); i++)
      {
        if (!key_value[i] || key_value[i][0] == '\0')
        break;
        if (i > 1) strcat(url_input_text, "%20");
        strcat(url_input_text, key_value[i]);
      }
      separator = "&";
    }

    Dowa_Arena_Free(arena);
  }
}

int PostDog_String_To_MethodEnum(char *value)
{
  if (strstr(value, "GET"))
  return 0;
  if (strstr(value, "POST"))
  return 1;
  if (strstr(value, "PUT"))
  return 2;
  if (strstr(value, "DELETE"))
  return 3;
  return 0;
}

void PostDog_Params_Reset(void)
{
  url_input_text[0] = '\0';
  active_method_dropdown = 0;
  active_result_tab = 0;

  for (int i = 0; i < Dowa_Array_Length(input_body_array); i++)
    input_body_array[i][0] = '\0';

  for (int i = 0; i < Dowa_Array_Length(result_body_array); i++)
    result_body_array[i][0] = '\0';

  // Reset text area states when clearing
  GuiTextAreaResetAllStates();
}

void PostDog_Load_File(const char *filename)
{
  char full_file_path[512] = {0};
  snprintf(full_file_path, 512, "%s/%s", POSTDOG_PATHS, filename);
  FILE *file = fopen(full_file_path, "r");
  if (!file)
  return;

  fseek(file, 0, SEEK_END);
  size_t file_size = ftell(file);
  fseek(file, 0, SEEK_SET);

  Dowa_Arena *init_arena = Dowa_Arena_Create(file_size + 2);
  Dowa_Arena *split_arena = Dowa_Arena_Create(file_size * 2);
  char *file_buffer = Dowa_Arena_Allocate(init_arena, file_size+1);
  fread(file_buffer, 1, file_size, file);
  fclose(file);

  char **values = Dowa_String_Split(file_buffer, "---\n", file_size, 4, split_arena);

  for (int i = 0; i < Dowa_Array_Length(values); i++)
  {
    switch (i)
    {
      case 0:  // Title - skip
      break;

      case 1:  // URL
      snprintf(url_input_text, strlen(values[i]) + 1, "%s", values[i]);
      url_input_text[strcspn(url_input_text, "\n")] = '\0';
      break;

      case 2:  // Method
      active_method_dropdown = PostDog_String_To_MethodEnum(values[i]);
      break;

      case 3:  // Headers (TAB_HEADER)
      case 4:  // Body (TAB_BODY)
      case 5:  // Get Params (TAB_GET_PARAMS)
      case 6:  // Websocket (TAB_WEBSOCKET)
      {
      int map_index = i - 3;  // 3->0, 4->1, 5->2, 6->3
      snprintf(input_body_array[map_index], strlen(values[i]) + 1, "%s", values[i]);
      // Trim trailing newlines
      for (int j = strlen(values[i]); j > 0; j--)
      {
        if (input_body_array[map_index][j] == '\n')
        {
        input_body_array[map_index][j] = '\0';
        break;
        }
      }
      break;
      }

      default:  // Response (index 7+) - load into body tab for backward compatibility
      snprintf(result_body_array[RESULT_TAB_BODY], strlen(values[i]) + 1, "%s", values[i]);
      break;
    }
  }

  // Reset result tab to body
  active_result_tab = RESULT_TAB_BODY;

  // Reset text area states when loading new file
  GuiTextAreaResetAllStates();

  Dowa_Arena_Free(init_arena);
  Dowa_Arena_Free(split_arena);
}

// ============================================================================
// UI LAYOUT STRUCTURE - All rectangles for the UI
// ============================================================================
typedef struct {
  // Main areas
  Rectangle screen;
  Rectangle sidebar;
  Rectangle content;

  // Sidebar sections
  Rectangle logo_area;
  Rectangle history_list;

  // URL bar section
  Rectangle url_bar;
  Rectangle method_dropdown;
  Rectangle url_input;
  Rectangle send_button;

  // Body section (input + result split)
  Rectangle body_area;
  Rectangle input_panel;
  Rectangle result_panel;

  // Input panel internals
  Rectangle input_tabs;
  Rectangle input_body;

  // Result panel internals
  Rectangle result_tabs;
  Rectangle result_body;
} UILayout;

// ============================================================================
// UI STATE - All interactive state
// ============================================================================
typedef struct {
  boolean url_edit_mode;
  boolean method_edit_mode;
  boolean input_body_edit_mode;
  boolean result_body_edit_mode;
  int prev_input_tab;
  float history_scroll_offset;
} UIState;

// ============================================================================
// LAYOUT CALCULATION FUNCTIONS
// ============================================================================
static UILayout CalculateLayout(int screen_width, int screen_height, float padding)
{
  UILayout layout = {0};

  // Screen
  layout.screen = (Rectangle){0, 0, screen_width, screen_height};

  // Main split: sidebar (20%) | content (80%)
  layout.sidebar = LeftColumn(layout.screen, 0.20f, 0);
  layout.content = RightColumn(layout.screen, layout.sidebar, 0);

  // Sidebar sections
  layout.logo_area = (Rectangle){
    .x = layout.sidebar.x,
    .y = layout.sidebar.y,
    .width = layout.sidebar.width,
    .height = 120
  };

  layout.history_list = (Rectangle){
    .x = layout.sidebar.x + padding,
    .y = layout.logo_area.y + layout.logo_area.height + padding,
    .width = layout.sidebar.width - (2 * padding),
    .height = layout.sidebar.height - layout.logo_area.height - (2 * padding)
  };

  // URL bar section (top of content area)
  float url_bar_height = 60;
  layout.url_bar = (Rectangle){
    .x = layout.content.x,
    .y = layout.content.y + padding,
    .width = layout.content.width,
    .height = url_bar_height
  };

  // URL bar components (method dropdown + URL input + send button)
  float dropdown_width = 100;
  float button_width = 80;
  float url_bar_inner_padding = 8;
  float control_height = 36;
  float control_y = layout.url_bar.y + (layout.url_bar.height - control_height) / 2;

  layout.method_dropdown = (Rectangle){
    .x = layout.url_bar.x + padding,
    .y = control_y,
    .width = dropdown_width,
    .height = control_height
  };

  layout.url_input = (Rectangle){
    .x = layout.method_dropdown.x + layout.method_dropdown.width + url_bar_inner_padding,
    .y = control_y,
    .width = layout.url_bar.width - dropdown_width - button_width - (4 * padding) - (2 * url_bar_inner_padding),
    .height = control_height
  };

  layout.send_button = (Rectangle){
    .x = layout.url_input.x + layout.url_input.width + url_bar_inner_padding,
    .y = control_y,
    .width = button_width,
    .height = control_height
  };

  // Body area (below URL bar)
  layout.body_area = (Rectangle){
    .x = layout.content.x,
    .y = layout.url_bar.y + layout.url_bar.height + padding,
    .width = layout.content.width,
    .height = layout.content.height - layout.url_bar.height - (3 * padding)
  };

  // Split body into input (left) and result (right) panels
  float split_ratio = 0.5f;
  layout.input_panel = (Rectangle){
    .x = layout.body_area.x,
    .y = layout.body_area.y,
    .width = layout.body_area.width * split_ratio,
    .height = layout.body_area.height
  };

  layout.result_panel = (Rectangle){
    .x = layout.body_area.x + layout.input_panel.width,
    .y = layout.body_area.y,
    .width = layout.body_area.width - layout.input_panel.width,
    .height = layout.body_area.height
  };

  // Input panel internals
  float tab_height = 40;
  layout.input_tabs = (Rectangle){
    .x = layout.input_panel.x + padding,
    .y = layout.input_panel.y + padding,
    .width = layout.input_panel.width - (2 * padding),
    .height = tab_height
  };

  layout.input_body = (Rectangle){
    .x = layout.input_panel.x + padding,
    .y = layout.input_tabs.y + layout.input_tabs.height,
    .width = layout.input_panel.width - (2 * padding),
    .height = layout.input_panel.height - tab_height - (2 * padding)
  };

  // Result panel internals
  layout.result_tabs = (Rectangle){
    .x = layout.result_panel.x + padding,
    .y = layout.result_panel.y + padding,
    .width = layout.result_panel.width - (2 * padding),
    .height = tab_height
  };

  layout.result_body = (Rectangle){
    .x = layout.result_panel.x + padding,
    .y = layout.result_tabs.y + layout.result_tabs.height,
    .width = layout.result_panel.width - (2 * padding),
    .height = layout.result_panel.height - tab_height - (2 * padding)
  };

  return layout;
}

// ============================================================================
// DRAWING FUNCTIONS - Separated for clarity
// ============================================================================

static void DrawSidebar(UILayout *layout, Texture2D logo_texture, float padding)
{
  // Sidebar background with rounded left corners
  DrawRectangleRec(layout->sidebar, g_colors.primary);

  // Logo
  Rectangle logo_inner = AddPadding(layout->logo_area, padding);
  float logo_size = logo_inner.height < logo_inner.width ? logo_inner.height : logo_inner.width;
  Rectangle dest_rect = {
    .x = logo_inner.x + (logo_inner.width - logo_size) / 2,
    .y = logo_inner.y,
    .width = logo_size,
    .height = logo_size
  };
  Rectangle source_rect = {0, 0, logo_texture.width, logo_texture.height};
  DrawTexturePro(logo_texture, source_rect, dest_rect, (Vector2){0, 0}, 0.0f, WHITE);
}

static void DrawHistoryList(UILayout *layout, Vector2 mouse_pos, float padding, float scroll_offset)
{
  int32 new_len = Dowa_Array_Length(new_history_items);
  int32 history_len = Dowa_Array_Length(history_items);
  int32 total = new_len + history_len;
  float item_height = 70;

  BeginScissorMode(layout->history_list.x, layout->history_list.y,
           layout->history_list.width, layout->history_list.height);

  int32 visible_index = 0;
  for (int i = 0; i < total; i++)
  {
    HistoryItem *item = i < new_len ?
      &new_history_items[new_len - i - 1] : &history_items[i - new_len];

    if (item->deleted) continue;

    // Calculate item rectangle
    Rectangle item_rect = {
      .x = layout->history_list.x,
      .y = layout->history_list.y + (padding + item_height) * visible_index + scroll_offset,
      .width = layout->history_list.width,
      .height = item_height
    };
    item->rect = item_rect;
    visible_index++;

    // Skip if not visible
    if (item_rect.y + item_rect.height < layout->history_list.y ||
      item_rect.y > layout->history_list.y + layout->history_list.height)
      continue;

    // Draw item background
    DrawRectangleRounded(item_rect, 0.3f, 8, g_colors.secondary);

    // Title area (top 60%)
    Rectangle title_rect = item_rect;
    title_rect.height = item_rect.height * 0.55f;

    // Draw title text
    Rectangle title_text_rect = AddPadding(title_rect, 8);
    if (item->title)
    {
      DrawTextEx(GuiGetFont(), item->title, (Vector2){ .x=title_text_rect.x, .y=title_text_rect.y + 4 },
           GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text);
    }

    // Button area (bottom 40%)
    Rectangle button_area = {
      .x = item_rect.x,
      .y = title_rect.y + title_rect.height,
      .width = item_rect.width,
      .height = item_rect.height - title_rect.height
    };

    Rectangle btn_view = {
      .x = button_area.x + padding,
      .y = button_area.y,
      .width = (button_area.width - 3 * padding) / 2,
      .height = button_area.height - padding
    };

    Rectangle btn_delete = {
      .x = btn_view.x + btn_view.width + padding,
      .y = btn_view.y,
      .width = btn_view.width,
      .height = btn_view.height
    };

    // Hover cursor for buttons
    if (CheckCollisionPointRec(mouse_pos, btn_view) ||
      CheckCollisionPointRec(mouse_pos, btn_delete))
      SetMouseCursor(MOUSE_CURSOR_POINTING_HAND);

    if (GuiButton(btn_view, "View"))
      PostDog_Load_File(item->filename);

    if (GuiButton(btn_delete, "Delete"))
    {
      char delete_path[512];
      if (!remove(PostDog_Construct_URL(item->filename, delete_path, sizeof(delete_path))))
        item->deleted = TRUE;
      else
        fprintf(stderr, "Couldn't delete file: %s\n", item->filename);
    }
  }

  EndScissorMode();

  // Scrollbar
  float content_height = visible_index * (item_height + padding);
  if (content_height > layout->history_list.height)
  {
    float scrollbar_height = (layout->history_list.height / content_height) * layout->history_list.height;
    float scrollbar_y = layout->history_list.y - (scroll_offset / content_height) * layout->history_list.height;
    Rectangle scrollbar_rect = {
      layout->history_list.x + layout->history_list.width - 6,
      scrollbar_y,
      4,
      scrollbar_height
    };
    DrawRectangleRounded(scrollbar_rect, 0.5f, 4, Fade(g_colors.text_light, 0.5f));
  }
}

static void DrawURLBar(UILayout *layout, UIState *state, float padding)
{
  // URL bar background
  Rectangle url_bar_bg = AddPadding(layout->url_bar, padding / 2);
  DrawRectangleRounded(url_bar_bg, 0.3f, 8, g_colors.secondary);

  // Combined method dropdown + URL input with rounded corners
  Rectangle combined_input = {
    .x = layout->method_dropdown.x,
    .y = layout->method_dropdown.y,
    .width = layout->url_input.x + layout->url_input.width - layout->method_dropdown.x,
    .height = layout->method_dropdown.height
  };

  // Method colors: GET=Green, POST=Blue, PUT=Orange, DELETE=Red
  Color method_colors[] = {
    (Color){76, 175, 80, 255},   // GET - Green
    (Color){33, 150, 243, 255},  // POST - Blue
    (Color){255, 152, 0, 255},   // PUT - Orange
    (Color){244, 67, 54, 255}    // DELETE - Red
  };

  // Method dropdown + URL input combined component
  DropdownTextBoxConfig url_config = {
    .dropdown_items = "GET;POST;PUT;DELETE",
    .dropdown_active = &active_method_dropdown,
    .dropdown_edit_mode = &state->method_edit_mode,
    .dropdown_width = layout->method_dropdown.width,
    .text_buffer = url_input_text,
    .text_buffer_size = URL_TEXT_BUFFER_LENGTH,
    .text_edit_mode = &state->url_edit_mode,
    .corner_radius = 0.2f,
    .background_color = g_colors.background,
    .border_color = g_colors.border,
    .text_color = g_colors.text,
    .item_colors = method_colors,
    .item_count = 4
  };
  PostDog_DropdownTextBox(combined_input, url_config);

  // Handle Enter key in URL input
  if (state->url_edit_mode && IsKeyPressed(KEY_ENTER))
  {
    PostDog_Http_Thread_Request();
    state->url_edit_mode = FALSE;
  }

  // Send button
  DrawRectangleRounded(layout->send_button, 0.3f, 8, g_colors.primary);
  Rectangle btn_text_rect = layout->send_button;
  char *btn_text = "Send";
  int text_width = MeasureText(btn_text, GuiGetStyle(DEFAULT, TEXT_SIZE));
  DrawTextEx(GuiGetFont(),
    btn_text,
    (Vector2){ .x=btn_text_rect.x + (btn_text_rect.width - text_width) / 2, .y=btn_text_rect.y + (btn_text_rect.height - GuiGetStyle(DEFAULT, TEXT_SIZE)) / 2 },
    GuiGetStyle(DEFAULT, TEXT_SIZE), TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE), g_colors.text_light);

  if (CheckCollisionPointRec(GetMousePosition(), layout->send_button) &&
    IsMouseButtonPressed(MOUSE_BUTTON_LEFT))
    PostDog_Http_Thread_Request();
}

static void DrawBodyPanels(UILayout *layout, UIState *state, float padding)
{
  // Draw split panel background (input left, result right)
  DrawRectangleSelectiveRounded(layout->input_panel, 12, 8, g_colors.background,
                  TRUE, FALSE, FALSE, TRUE);
  DrawRectangleSelectiveRounded(layout->result_panel, 12, 8,
                  LOADING ? Fade(g_colors.error, 0.3f) : g_colors.secondary,
                  FALSE, TRUE, TRUE, FALSE);

  // Tab labels
  const char *tab_labels[] = {"Headers", "Body", "Params", "WebSocket"};
  int tab_count = 4;

  // Draw custom tabs
  PostDog_TabBarSimple(layout->input_tabs, tab_labels, tab_count, &active_input_tab);

  // Input body text area
  int text_area_id = TEXT_AREA_ID_INPUT_HEADER + active_input_tab;
  if (GuiTextArea(text_area_id, layout->input_body, input_body_array[active_input_tab],
          BODY_BUFFER_LENGTH, state->input_body_edit_mode, TRUE, FALSE, g_text_area_arena))
    state->input_body_edit_mode = !state->input_body_edit_mode;

  // WebSocket send button (only on WebSocket tab)
  if (active_input_tab == TAB_WEBSOCKET)
  {
    WS_BREAK = FALSE;
    Rectangle ws_send_btn = {
      .x = layout->input_body.x + layout->input_body.width - 90 - padding,
      .y = layout->input_body.y + layout->input_body.height - 35 - padding,
      .width = 90,
      .height = 35
    };

    DrawRectangleRounded(ws_send_btn, 0.3f, 8, g_colors.highlight);
    char *ws_text = !ws ? "Connect" : "Send";
    int ws_text_width = MeasureText(ws_text, GuiGetStyle(DEFAULT, TEXT_SIZE));
    DrawTextEx(GuiGetFont(),
      ws_text,
      (Vector2) { .x=ws_send_btn.x + (ws_send_btn.width - ws_text_width) / 2, .y=ws_send_btn.y + (ws_send_btn.height - GuiGetStyle(DEFAULT, TEXT_SIZE)) / 2 },
      GuiGetStyle(DEFAULT, TEXT_SIZE), 
      TEXT_SIZE_DEFAULT/GuiGetStyle(DEFAULT, TEXT_SIZE),
      g_colors.text);

    if ((CheckCollisionPointRec(GetMousePosition(), ws_send_btn) &&
       IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) ||
      (state->input_body_edit_mode && IsKeyDown(KEY_LEFT_SHIFT) && IsKeyPressed(KEY_ENTER)))
    {
      if (!ws)
        websocket_thread_id = PostDog_Websocket_Start_Thread();
      usleep(10000);
      PostDog_Websocket_Send();
    }
  }
  else
  {
    WS_BREAK = TRUE;
  }

  // Result tabs
  const char *result_tab_labels[] = {"Body", "Headers"};
  int result_tab_count = 2;
  PostDog_TabBarSimple(layout->result_tabs, result_tab_labels, result_tab_count, &active_result_tab);

  // Result body text area
  int result_text_area_id = TEXT_AREA_ID_RESULT_BODY + active_result_tab;
  // Result text area is readonly (selectable/copyable but not editable)
  if (GuiTextArea(result_text_area_id, layout->result_body, result_body_array[active_result_tab],
          RESULT_BUFFER_LENGTH, state->result_body_edit_mode, TRUE, TRUE, g_text_area_arena))
    state->result_body_edit_mode = !state->result_body_edit_mode;

  PostDog_Update_URL(state->url_edit_mode);
}

// ============================================================================
// MAIN FUNCTION
// ============================================================================
int main()
{
  // ========================================================================
  // INITIALIZATION
  // ========================================================================
  SetConfigFlags(FLAG_WINDOW_UNDECORATED);

  // Initialize libuv
  main_loop = uv_default_loop();
  uv_mutex_init(&history_mutex);

  // Initialize color scheme
  PostDog_InitColorScheme();

  // Initialize window
  InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "PostDog");
  SetWindowState(FLAG_WINDOW_RESIZABLE);
  SetTargetFPS(60);

  // Load resources
  Font customFont = LoadFontEx("postdog/Roboto-Regular.ttf", 20, 0, 0);
  GuiSetFont(customFont);
  GuiSetStyle(DEFAULT, TEXT_SIZE, TEXT_SIZE_DEFAULT);

  Image logo_original = LoadImage("postdog/logo_bigger.png");
  ImageResize(&logo_original, 200, 200);
  SetWindowIcon(logo_original);
  Texture2D logo_texture = LoadTextureFromImage(logo_original);
  // ToggleBorderlessWindowed();
  UnloadImage(logo_original);

  // Initialize text area arena
  g_text_area_arena = Dowa_Arena_Create(1024 * 1024 * 4);

  // Initialize history
  Dowa_Array_Reserve(history_items, 10);
  Dowa_Array_Reserve(new_history_items, 10);
  PostDog_History_Load(&history_items);

  // Initialize text buffers
  url_input_text = (char *)malloc(sizeof(char) * URL_TEXT_BUFFER_LENGTH);
  snprintf(url_input_text, URL_TEXT_BUFFER_LENGTH, URL_TEXT_DEFAULT);

  Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * HEADER_BUFFER_LENGTH));
  Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * BODY_BUFFER_LENGTH));
  Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH));
  Dowa_Array_Push(input_body_array, (char *)malloc(sizeof(char) * DEFAULT_TEXT_BUFFER_LENGTH));

  snprintf(input_body_array[TAB_HEADER], HEADER_BUFFER_LENGTH, HEADER_TEXT_DEFAULT);
  snprintf(input_body_array[TAB_BODY], BODY_BUFFER_LENGTH, BODY_TEXT_DEFAULT);
  snprintf(input_body_array[TAB_GET_PARAMS], DEFAULT_TEXT_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT);
  snprintf(input_body_array[TAB_WEBSOCKET], DEFAULT_TEXT_BUFFER_LENGTH, GET_PARAM_TEXT_DEFAULT);

  // Initialize result buffers (body and headers tabs)
  Dowa_Array_Push(result_body_array, (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH));
  Dowa_Array_Push(result_body_array, (char *)malloc(sizeof(char) * RESULT_BUFFER_LENGTH));
  result_body_array[RESULT_TAB_BODY][0] = '\0';
  result_body_array[RESULT_TAB_HEADERS][0] = '\0';

  // Initialize UI state
  UIState ui_state = {0};
  float padding = 10.0f;

  // ========================================================================
  // MAIN LOOP
  // ========================================================================
  while (!WindowShouldClose())
  {
    // Process libuv events (non-blocking)
    uv_run(main_loop, UV_RUN_NOWAIT);

    // Handle keyboard shortcuts
    DefaultBehaviours();

    // ====================================================================
    // LAYOUT CALCULATION
    // ====================================================================
    int screen_width = GetScreenWidth();
    int screen_height = GetScreenHeight();
    UILayout layout = CalculateLayout(screen_width, screen_height, padding);

    // ====================================================================
    // INPUT HANDLING
    // ====================================================================
    Vector2 mouse_pos = GetMousePosition();
    float mouse_wheel = GetMouseWheelMove();

    // Reset cursor
    SetMouseCursor(MOUSE_CURSOR_DEFAULT);

    // History scroll
    if (CheckCollisionPointRec(mouse_pos, layout.history_list) && mouse_wheel != 0)
    {
      ui_state.history_scroll_offset += mouse_wheel * 30;
      int32 total = Dowa_Array_Length(new_history_items) + Dowa_Array_Length(history_items);
      float content_height = total * 80;
      float max_scroll = content_height - layout.history_list.height;
      if (ui_state.history_scroll_offset > 0) ui_state.history_scroll_offset = 0;
      if (max_scroll > 0 && ui_state.history_scroll_offset < -max_scroll)
        ui_state.history_scroll_offset = -max_scroll;
    }

    // Cursor changes for interactive areas
    if (CheckCollisionPointRec(mouse_pos, layout.send_button) ||
      CheckCollisionPointRec(mouse_pos, layout.input_tabs) ||
      CheckCollisionPointRec(mouse_pos, layout.logo_area))
      SetMouseCursor(MOUSE_CURSOR_POINTING_HAND);

    // Logo click resets
    if (CheckCollisionPointRec(mouse_pos, layout.logo_area) &&
      IsMouseButtonPressed(MOUSE_BUTTON_LEFT))
      PostDog_Params_Reset();

    // ====================================================================
    // DRAWING
    // ====================================================================
    BeginDrawing();
      ClearBackground(g_colors.background);
      DrawRectangleSelectiveRounded(layout.content, 12, 8, g_colors.secondary,
                      FALSE, TRUE, TRUE, FALSE);
      DrawSidebar(&layout, logo_texture, padding);
      DrawHistoryList(&layout, mouse_pos, padding, ui_state.history_scroll_offset);
      DrawBodyPanels(&layout, &ui_state, padding);
      DrawURLBar(&layout, &ui_state, padding);
    EndDrawing();
  }

  // ========================================================================
  // CLEANUP
  // ========================================================================
  GuiTextAreaResetAllStates();
  Dowa_Arena_Free(g_text_area_arena);

  uv_mutex_destroy(&history_mutex);
  uv_loop_close(main_loop);

  UnloadTexture(logo_texture);
  CloseWindow();

  return 0;
}