#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>

#include "../include/chroma.h"

// Global logging level (using CHROMA_LOG_* constants)
static int log_level = CHROMA_LOG_WARN; // Default to WARN and ERROR only

// Initialize chroma state
int chroma_init(chroma_state_t *state) {
  if (!state) {
    return CHROMA_ERROR_INIT;
  }

  // Initialize all fields to zero
  memset(state, 0, sizeof(chroma_state_t));

  // Set initial state
  state->running = false;
  state->initialized = false;
  state->egl_display = EGL_NO_DISPLAY;
  state->egl_context = EGL_NO_CONTEXT;

  // Initialize stb_image
  chroma_image_init_stb();

  state->initialized = true;
  chroma_log("INFO", "Chroma state initialized");
  chroma_log_resource_allocation("chroma_state", sizeof(chroma_state_t),
                                 "main application state");

  return CHROMA_OK;
}

// Cleanup chroma state
void chroma_cleanup(chroma_state_t *state) {
  if (!state || !state->initialized) {
    return;
  }

  chroma_log("INFO", "Cleaning up chroma state");
  chroma_log_memory_stats("pre-cleanup");

  // Stop the main loop
  state->running = false;

  // Clean up all images
  chroma_images_cleanup(state);

  // Clean up EGL
  chroma_egl_cleanup(state);

  // Clean up Wayland
  chroma_wayland_disconnect(state);

  // Clean up configuration
  chroma_config_free(&state->config);

  state->initialized = false;
  chroma_log_resource_deallocation("chroma_state", sizeof(chroma_state_t),
                                   "main application state");
  chroma_log("INFO", "Chroma cleanup complete");
}

// Assign wallpaper to an output
static int assign_wallpaper_to_output(chroma_state_t *state,
                                      chroma_output_t *output) {
  if (!state || !output || !output->active) {
    return CHROMA_ERROR_INIT;
  }

  // Get image path for this output
  const char *image_path = chroma_config_get_image_for_output(
      &state->config, output->name ? output->name : "unknown",
      output->description);
  if (!image_path) {
    chroma_log("WARN", "No wallpaper configured for output %u (%s)", output->id,
               output->name ? output->name : "unknown");
    return CHROMA_ERROR_CONFIG;
  }

  // Load or get cached image
  chroma_image_t *image = chroma_image_get_or_load(state, image_path);
  if (!image) {
    chroma_log("ERROR", "Failed to load image for output %u: %s", output->id,
               image_path);
    return CHROMA_ERROR_IMAGE;
  }

  // Check if image changed and invalidate texture cache if neceessary
  bool image_changed = (output->image != image);
  if (image_changed && output->image) {
    chroma_output_invalidate_texture(output);
    output->vbo_dirty = true; // VBO needs update for new image
    chroma_log("DEBUG", "Image changed for output %u, invalidated texture",
               output->id);
  }

  // Assign image to output
  output->image = image;

  // Mark VBO as dirty if image changed
  if (image_changed) {
    output->vbo_dirty = true;
  }

  // Store old configuration values for comparison
  chroma_scale_mode_t old_scale_mode = output->scale_mode;
  chroma_filter_quality_t old_filter_quality = output->filter_quality;
  chroma_anchor_t old_anchor = output->anchor;
  float old_anchor_x = output->anchor_x;
  float old_anchor_y = output->anchor_y;
  bool had_config = output->config_loaded;

  // Load configuration for this output (scale mode, filter quality, anchor,
  // anchor coords)
  if (chroma_config_get_mapping_for_output(
          &state->config, output->name ? output->name : "unknown",
          output->description, &output->scale_mode, &output->filter_quality,
          &output->anchor, &output->anchor_x, &output->anchor_y) == CHROMA_OK) {
    output->config_loaded = true;
    chroma_log("DEBUG",
               "Loaded config for output %u: scale=%d, filter=%d, anchor=%d @ "
               "%.1f,%.1f",
               output->id, output->scale_mode, output->filter_quality,
               output->anchor, output->anchor_x, output->anchor_y);

    // Check if configuration changed and invalidate texture if needed
    if (had_config &&
        (old_scale_mode != output->scale_mode ||
         old_filter_quality != output->filter_quality ||
         old_anchor != output->anchor || old_anchor_x != output->anchor_x ||
         old_anchor_y != output->anchor_y)) {
      chroma_output_invalidate_texture(output);
      output->vbo_dirty = true; // VBO needs update for new scale mode
      chroma_log("DEBUG",
                 "Configuration changed for output %u, invalidated texture",
                 output->id);
    }
  } else {
    output->config_loaded = false;
    chroma_log("WARN", "Failed to load config for output %u", output->id);
  }

  // Create surface if it doesn't exist
  if (!output->surface) {
    int ret = chroma_surface_create(state, output);
    if (ret != CHROMA_OK) {
      chroma_log("ERROR", "Failed to create surface for output %u", output->id);
      return ret;
    }
  }

  // Render wallpaper
  int ret = chroma_render_wallpaper(state, output);
  if (ret != CHROMA_OK) {
    chroma_log("ERROR", "Failed to render wallpaper for output %u", output->id);
    return ret;
  }

  chroma_log("INFO", "Assigned wallpaper to output %u (%s): %s", output->id,
             output->name ? output->name : "unknown", image_path);
  chroma_log_memory_stats("post-wallpaper-assignment");

  return CHROMA_OK;
}

// Assign wallpapers to all active outputs
static int assign_wallpapers_to_all_outputs(chroma_state_t *state) {
  if (!state) {
    return CHROMA_ERROR_INIT;
  }

  int success_count = 0;
  int error_count = 0;

  for (int i = 0; i < state->output_count; i++) {
    chroma_output_t *output = &state->outputs[i];

    if (!output->active) {
      chroma_log("DEBUG", "Skipping inactive output %u", output->id);
      continue;
    }

    if (assign_wallpaper_to_output(state, output) == CHROMA_OK) {
      success_count++;
    } else {
      error_count++;
    }
  }

  chroma_log("INFO", "Wallpaper assignment complete: %d success, %d errors",
             success_count, error_count);

  return (success_count > 0) ? CHROMA_OK : CHROMA_ERROR_IMAGE;
}

// Handle output configuration complete event
void handle_output_done(chroma_state_t *state, chroma_output_t *output) {
  if (!state || !output) {
    return;
  }

  chroma_log("INFO",
             "Output %u (%s) configuration complete: %dx%d@%d, scale=%d",
             output->id, output->name ? output->name : "unknown", output->width,
             output->height, 0, output->scale);

  /* Assign wallpaper to this output */
  if (assign_wallpaper_to_output(state, output) != CHROMA_OK) {
    chroma_log("ERROR", "Failed to assign wallpaper to output %u", output->id);
  }
}

// Process Wayland events
static int process_wayland_events(chroma_state_t *state) {
  if (!state || !state->display) {
    return CHROMA_ERROR_WAYLAND;
  }

  // Dispatch pending events
  if (wl_display_dispatch_pending(state->display) == -1) {
    chroma_log("ERROR", "Failed to dispatch pending Wayland events: %s",
               strerror(errno));
    return CHROMA_ERROR_WAYLAND;
  }

  // Read events from the server
  if (wl_display_read_events(state->display) == -1) {
    chroma_log("ERROR", "Failed to read Wayland events: %s", strerror(errno));
    return CHROMA_ERROR_WAYLAND;
  }

  // Dispatch the read events
  if (wl_display_dispatch_pending(state->display) == -1) {
    chroma_log("ERROR", "Failed to dispatch read Wayland events: %s",
               strerror(errno));
    return CHROMA_ERROR_WAYLAND;
  }

  return CHROMA_OK;
}

// Main event loop
int chroma_run(chroma_state_t *state) {
  if (!state || !state->initialized) {
    return CHROMA_ERROR_INIT;
  }

  chroma_log("INFO", "Starting main event loop");
  chroma_log_memory_stats("main-loop-start");
  state->running = true;

  // Initial wallpaper assignment
  chroma_log("INFO", "Performing initial wallpaper assignment");
  assign_wallpapers_to_all_outputs(state);

  // Main event loop
  while (state->running && !chroma_should_quit) {
    // Dispatch any pending events first
    if (wl_display_dispatch_pending(state->display) == -1) {
      chroma_log("ERROR", "Failed to dispatch pending events: %s",
                 strerror(errno));
      break;
    }

    // Prepare to read events
    if (wl_display_prepare_read(state->display) == -1) {
      chroma_log("ERROR", "Failed to prepare Wayland display for reading");
      break;
    }

    // Flush outgoing requests
    if (wl_display_flush(state->display) == -1) {
      chroma_log("ERROR", "Failed to flush Wayland display: %s",
                 strerror(errno));
      wl_display_cancel_read(state->display);
      break;
    }

    // Get the display file descriptor
    int fd = wl_display_get_fd(state->display);
    if (fd == -1) {
      chroma_log("ERROR", "Failed to get Wayland display file descriptor");
      wl_display_cancel_read(state->display);
      break;
    }

    // Use `select()` to wait for events with longer timeout to reduce CPU usage
    fd_set readfds;
    struct timeval timeout;

    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);

    timeout.tv_sec = 10;
    timeout.tv_usec = 0;

    int select_result = select(fd + 1, &readfds, NULL, NULL, &timeout);

    if (select_result == -1) {
      if (errno == EINTR) {
        // Interrupted by signal, check if we should quit
        wl_display_cancel_read(state->display);
        continue;
      }
      chroma_log("ERROR", "select() failed: %s", strerror(errno));
      wl_display_cancel_read(state->display);
      break;
    }

    if (select_result == 0) {
      // Timeout - no events available
      wl_display_cancel_read(state->display);
      continue;
    }

    // Events are available
    if (FD_ISSET(fd, &readfds)) {
      if (process_wayland_events(state) != CHROMA_OK) {
        break;
      }
    } else {
      wl_display_cancel_read(state->display);
    }
  }

  state->running = false;
  chroma_log("INFO", "Main event loop ended");

  return CHROMA_OK;
}

// Error code to string conversion
const char *chroma_error_string(chroma_error_t error) {
  switch (error) {
  case CHROMA_OK:
    return "Success";
  case CHROMA_ERROR_INIT:
    return "Initialization error";
  case CHROMA_ERROR_WAYLAND:
    return "Wayland error";
  case CHROMA_ERROR_EGL:
    return "EGL error";
  case CHROMA_ERROR_IMAGE:
    return "Image loading error";
  case CHROMA_ERROR_CONFIG:
    return "Configuration error";
  case CHROMA_ERROR_MEMORY:
    return "Memory allocation error";
  default:
    return "Unknown error";
  }
}

// Convert log level string to numeric level
static int log_level_from_string(const char *level) {
  if (strcmp(level, "ERROR") == 0)
    return CHROMA_LOG_ERROR;
  if (strcmp(level, "WARN") == 0)
    return CHROMA_LOG_WARN;
  if (strcmp(level, "INFO") == 0)
    return CHROMA_LOG_INFO;
  if (strcmp(level, "DEBUG") == 0)
    return CHROMA_LOG_DEBUG;
  if (strcmp(level, "TRACE") == 0)
    return CHROMA_LOG_TRACE;
  return CHROMA_LOG_ERROR; // default to ERROR for unknown levels
}

// Logging function with level filtering
void chroma_log(const char *level, const char *format, ...) {
  int msg_level = log_level_from_string(level);
  if (msg_level > log_level) {
    return;
  }

  va_list args;
  char timestamp[32];
  int truncation_check = 0;
  struct timeval tv;
  struct tm *tm_info;

  gettimeofday(&tv, NULL);
  tm_info = localtime(&tv.tv_sec);

  truncation_check =
      snprintf(timestamp, sizeof(timestamp),
               "%04d-%02d-%02d %02d:%02d:%02d.%03d", tm_info->tm_year + 1900,
               tm_info->tm_mon + 1, tm_info->tm_mday, tm_info->tm_hour,
               tm_info->tm_min, tm_info->tm_sec, (int)(tv.tv_usec / 1000));

  if (truncation_check > 32 || truncation_check < 0) {
    // Something went seriously wrong with the snprintf, this is a fairly
    // serious error as the timestamp may be incomplete or corrupted, so print a
    // warning
    printf("Following timestamp may be incomplete, truncated or corrupted!\n");
  }
  printf("[%s] %s: ", timestamp, level);

  va_start(args, format);
  vprintf(format, args);
  va_end(args);

  printf("\n");
  fflush(stdout);
}

// Set log level
void chroma_set_log_level(int level) { log_level = level; }

// Get log level
int chroma_get_log_level(void) { return log_level; }

// Handle configuration reload (SIGHUP)
int chroma_reload_config(chroma_state_t *state, const char *config_file) {
  if (!state) {
    return CHROMA_ERROR_INIT;
  }

  chroma_log("INFO", "Reloading configuration");

  // Free current configuration
  chroma_config_free(&state->config);

  // Load new configuration
  int ret = chroma_config_load(&state->config, config_file);
  if (ret != CHROMA_OK) {
    chroma_log("ERROR", "Failed to reload configuration: %s",
               chroma_error_string(ret));
    return ret;
  }

  // Reassign wallpapers with new configuration
  ret = assign_wallpapers_to_all_outputs(state);
  if (ret != CHROMA_OK) {
    chroma_log("ERROR", "Failed to reassign wallpapers after config reload");
    return ret;
  }

  chroma_log("INFO", "Configuration reloaded successfully");
  return CHROMA_OK;
}

// Check if an output needs wallpaper update
static bool output_needs_update(chroma_output_t *output) {
  if (!output || !output->active) {
    return false;
  }

  // Check if output has no surface or image assigned
  if (!output->surface || !output->image) {
    return true;
  }

  // Check if image is no longer loaded
  if (!output->image->loaded) {
    return true;
  }

  return false;
}

// Update outputs that need wallpaper refresh
int chroma_update_outputs(chroma_state_t *state) {
  if (!state) {
    return CHROMA_ERROR_INIT;
  }

  int updated_count = 0;

  for (int i = 0; i < state->output_count; i++) {
    chroma_output_t *output = &state->outputs[i];

    if (output_needs_update(output)) {
      if (assign_wallpaper_to_output(state, output) == CHROMA_OK) {
        updated_count++;
      }
    }
  }

  if (updated_count > 0) {
    chroma_log("INFO", "Updated wallpapers for %d outputs", updated_count);
  }

  return CHROMA_OK;
}

// Get statistics
void chroma_get_stats(chroma_state_t *state, int *active_outputs,
                      int *loaded_images) {
  if (!state) {
    if (active_outputs)
      *active_outputs = 0;
    if (loaded_images)
      *loaded_images = 0;
    return;
  }

  int active = 0;
  for (int i = 0; i < state->output_count; i++) {
    if (state->outputs[i].active) {
      active++;
    }
  }

  int loaded = 0;
  for (int i = 0; i < state->image_count; i++) {
    if (state->images[i].loaded) {
      loaded++;
    }
  }

  if (active_outputs)
    *active_outputs = active;
  if (loaded_images)
    *loaded_images = loaded;
}

// Get current memory usage from /proc/self/status
// FIXME: some users may choose to confine /proc, we need a cleaner
// way of getting this data in the future
size_t chroma_get_memory_usage(void) {
  FILE *file = fopen("/proc/self/status", "r");
  if (!file) {
    return 0;
  }

  size_t vm_rss = 0;
  char line[256];

  while (fgets(line, sizeof(line), file)) {
    if (strncmp(line, "VmRSS:", 6) == 0) {
      // Parse VmRSS line: "VmRSS:    1234 kB"
      char *ptr = line + 6;
      while (*ptr == ' ' || *ptr == '\t')
        ptr++;                                        // Skip whitespace
      vm_rss = (size_t)strtoul(ptr, NULL, 10) * 1024; // Convert kB to bytes
      break;
    }
  }

  fclose(file);
  return vm_rss;
}

// Log memory statistics with context
void chroma_log_memory_stats(const char *context) {
  size_t memory_usage = chroma_get_memory_usage();
  char mem_str[64];

  chroma_format_memory_size(memory_usage, mem_str, sizeof(mem_str));
  chroma_log("INFO", "Memory usage [%s]: %s", context, mem_str);
}

// Log resource allocation
void chroma_log_resource_allocation(const char *resource_type, size_t size,
                                    const char *description) {
  if (size > 0) {
    char size_str[64];
    chroma_format_memory_size(size, size_str, sizeof(size_str));
    chroma_log("DEBUG", "Allocated %s: %s (%s)", resource_type, size_str,
               description);
  } else {
    chroma_log("DEBUG", "Allocated %s: %s", resource_type, description);
  }

  // Log memory stats after significant allocations (>1MB)
  if (size > 1024 * 1024) {
    chroma_log_memory_stats("post-allocation");
  }
}

// Log resource deallocation
void chroma_log_resource_deallocation(const char *resource_type, size_t size,
                                      const char *description) {
  if (size > 0) {
    char size_str[64];
    chroma_format_memory_size(size, size_str, sizeof(size_str));
    chroma_log("DEBUG", "Deallocated %s: %s (%s)", resource_type, size_str,
               description);
  } else {
    chroma_log("DEBUG", "Deallocated %s: %s", resource_type, description);
  }

  // Log memory stats after significant deallocations (>1MB)
  if (size > 1024 * 1024) {
    chroma_log_memory_stats("post-deallocation");
  }
}
