// Image format loaders for formats not supported by stb_image.
// Each loader converts its format to RGBA8 and fills a chroma_image_t.
// Loaders requiring optional libraries are guarded by #ifdef CHROMA_HAS_*;
// when absent the function returns CHROMA_ERROR_IMAGE with a build hint.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

static uint32_t read_be32(const unsigned char *buf) {
  return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
         ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];
}

static uint16_t read_be16(const unsigned char *buf) {
  return (uint16_t)(((uint16_t)buf[0] << 8) | (uint16_t)buf[1]);
}

// Farbfeld is a lossless image format: 16-byte magic + RGBA16 big-endian.
// Spec: https://tools.suckless.org/farbfeld/
int chroma_image_load_farbfeld(chroma_image_t *image, const char *path) {
  if (!image || !path) {
    return CHROMA_ERROR_INIT;
  }

  FILE *fp = fopen(path, "rb");
  if (!fp) {
    chroma_log("ERROR", "Cannot open farbfeld file: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  unsigned char header[16];
  if (fread(header, 1, 16, fp) != 16) {
    chroma_log("ERROR", "Failed to read farbfeld header: %s", path);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }

  if (memcmp(header, "farbfeld", 8) != 0) {
    chroma_log("ERROR", "Invalid farbfeld signature in: %s", path);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }

  int width = (int)read_be32(header + 8);
  int height = (int)read_be32(header + 12);

  if (width <= 0 || height <= 0 || width > 16384 || height > 16384) {
    chroma_log("ERROR", "Invalid farbfeld dimensions: %dx%d", width, height);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }

  size_t pixel_count = (size_t)width * (size_t)height;
  size_t src_size = pixel_count * 8;
  size_t dst_size = pixel_count * 4;

  unsigned char *src_data = malloc(src_size);
  if (!src_data) {
    fclose(fp);
    return CHROMA_ERROR_MEMORY;
  }

  if (fread(src_data, 1, src_size, fp) != src_size) {
    chroma_log("ERROR", "Truncated farbfeld data: %s", path);
    free(src_data);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }
  fclose(fp);

  unsigned char *dst_data = malloc(dst_size);
  if (!dst_data) {
    free(src_data);
    return CHROMA_ERROR_MEMORY;
  }

  for (size_t i = 0; i < pixel_count; i++) {
    size_t s = i * 8;
    size_t d = i * 4;
    dst_data[d + 0] = (unsigned char)(read_be16(src_data + s + 0) >> 8);
    dst_data[d + 1] = (unsigned char)(read_be16(src_data + s + 2) >> 8);
    dst_data[d + 2] = (unsigned char)(read_be16(src_data + s + 4) >> 8);
    dst_data[d + 3] = (unsigned char)(read_be16(src_data + s + 6) >> 8);
  }

  free(src_data);

  memset(image, 0, sizeof(chroma_image_t));
  strncpy(image->path, path, MAX_PATH_LEN - 1);
  image->data = dst_data;
  image->width = width;
  image->height = height;
  image->channels = 4;
  image->loaded = true;
  image->data_from_stbi = false;
  image->ref_count = 1;

  chroma_log("INFO", "Loaded farbfeld image: %s (%dx%d)", path, width, height);
  return CHROMA_OK;
}

// SVG rasterization via nanosvg (header-only). Enable with
// -DCHROMA_HAS_NANOSVG. Renders at output resolution if provided, otherwise
// uses the SVG viewport size.
#ifdef CHROMA_HAS_NANOSVG

#define NANOSVG_IMPLEMENTATION
#define NANOSVGRAST_IMPLEMENTATION
#include "../include/vendor/nanosvg.h"
#include "../include/vendor/nanosvgrast.h"

int chroma_image_load_svg(chroma_image_t *image, const char *path,
                          int out_width, int out_height) {
  if (!image || !path) {
    return CHROMA_ERROR_INIT;
  }

  FILE *fp = fopen(path, "rb");
  if (!fp) {
    chroma_log("ERROR", "Cannot open SVG file: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  fseek(fp, 0, SEEK_END);
  long size = ftell(fp);
  fseek(fp, 0, SEEK_SET);

  char *svg_data = malloc((size_t)size + 1);
  if (!svg_data) {
    fclose(fp);
    return CHROMA_ERROR_MEMORY;
  }
  if (fread(svg_data, 1, (size_t)size, fp) != (size_t)size) {
    chroma_log("ERROR", "Failed to read SVG file: %s", path);
    free(svg_data);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }
  svg_data[size] = '\0';
  fclose(fp);

  struct NSVGimage *nsvg = nsvgParse(svg_data, "px", 96);
  free(svg_data);

  if (!nsvg) {
    chroma_log("ERROR", "Failed to parse SVG: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  int w = out_width > 0 ? out_width : (int)nsvg->width;
  int h = out_height > 0 ? out_height : (int)nsvg->height;
  if (w <= 0)
    w = 800;
  if (h <= 0)
    h = 600;

  struct NSVGrasterizer *rast = nsvgCreateRasterizer();
  if (!rast) {
    nsvgDelete(nsvg);
    return CHROMA_ERROR_MEMORY;
  }

  unsigned char *data = malloc((size_t)w * (size_t)h * 4);
  if (!data) {
    nsvgDeleteRasterizer(rast);
    nsvgDelete(nsvg);
    return CHROMA_ERROR_MEMORY;
  }

  float scale_x = (float)w / nsvg->width;
  float scale_y = (float)h / nsvg->height;
  float scale = (scale_x < scale_y) ? scale_x : scale_y;
  nsvgRasterize(rast, nsvg, 0, 0, scale, data, w, h, w * 4);

  nsvgDeleteRasterizer(rast);
  nsvgDelete(nsvg);

  memset(image, 0, sizeof(chroma_image_t));
  strncpy(image->path, path, MAX_PATH_LEN - 1);
  image->data = data;
  image->width = w;
  image->height = h;
  image->channels = 4;
  image->loaded = true;
  image->data_from_stbi = false;
  image->ref_count = 1;

  chroma_log("INFO", "Loaded SVG image: %s (%dx%d)", path, w, h);
  return CHROMA_OK;
}

#else

int chroma_image_load_svg(chroma_image_t *image, const char *path,
                          int out_width, int out_height) {
  (void)image;
  (void)path;
  (void)out_width;
  (void)out_height;
  chroma_log("ERROR", "SVG support requires nanosvg. Rebuild with: "
                      "make CHROMA_FEATURES=svg");
  return CHROMA_ERROR_IMAGE;
}

#endif

// WebP decoding via libwebp. Enable with -DCHROMA_HAS_WEBP -lwebp.
#ifdef CHROMA_HAS_WEBP

#include <webp/decode.h>

int chroma_image_load_webp(chroma_image_t *image, const char *path) {
  if (!image || !path) {
    return CHROMA_ERROR_INIT;
  }

  FILE *fp = fopen(path, "rb");
  if (!fp) {
    chroma_log("ERROR", "Cannot open WebP file: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  fseek(fp, 0, SEEK_END);
  long size = ftell(fp);
  fseek(fp, 0, SEEK_SET);

  if (size <= 0 || size > 256 * 1024 * 1024) {
    chroma_log("ERROR", "WebP file too large: %s", path);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }

  unsigned char *webp_data = malloc((size_t)size);
  if (!webp_data) {
    fclose(fp);
    return CHROMA_ERROR_MEMORY;
  }
  if (fread(webp_data, 1, (size_t)size, fp) != (size_t)size) {
    chroma_log("ERROR", "Failed to read WebP file: %s", path);
    free(webp_data);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }
  fclose(fp);

  int w, h;
  if (!WebPGetInfo(webp_data, (size_t)size, &w, &h)) {
    chroma_log("ERROR", "Invalid WebP data: %s", path);
    free(webp_data);
    return CHROMA_ERROR_IMAGE;
  }

  unsigned char *data = WebPDecodeRGBA(webp_data, (size_t)size, &w, &h);
  free(webp_data);

  if (!data) {
    chroma_log("ERROR", "Failed to decode WebP: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  memset(image, 0, sizeof(chroma_image_t));
  strncpy(image->path, path, MAX_PATH_LEN - 1);
  image->data = data;
  image->width = w;
  image->height = h;
  image->channels = 4;
  image->loaded = true;
  image->data_from_stbi = false;
  image->ref_count = 1;

  chroma_log("INFO", "Loaded WebP image: %s (%dx%d)", path, w, h);
  return CHROMA_OK;
}

#else

int chroma_image_load_webp(chroma_image_t *image, const char *path) {
  (void)image;
  (void)path;
  chroma_log("ERROR", "WebP support requires libwebp. Rebuild with: "
                      "make CHROMA_FEATURES=webp");
  return CHROMA_ERROR_IMAGE;
}

#endif

// TIFF decoding via libtiff. Enable with -DCHROMA_HAS_TIFF -ltiff.
#ifdef CHROMA_HAS_TIFF

#include <tiffio.h>

int chroma_image_load_tiff(chroma_image_t *image, const char *path) {
  if (!image || !path) {
    return CHROMA_ERROR_INIT;
  }

  TIFF *tif = TIFFOpen(path, "r");
  if (!tif) {
    chroma_log("ERROR", "Cannot open TIFF file: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  uint32_t w, h;
  TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w);
  TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h);

  if (w == 0 || h == 0 || w > 32768 || h > 32768) {
    chroma_log("ERROR", "Invalid TIFF dimensions: %ux%u", w, h);
    TIFFClose(tif);
    return CHROMA_ERROR_IMAGE;
  }

  size_t pixel_count = (size_t)w * (size_t)h;
  size_t buf_size = pixel_count * 4;
  unsigned char *data = malloc(buf_size);
  if (!data) {
    TIFFClose(tif);
    return CHROMA_ERROR_MEMORY;
  }

  if (!TIFFReadRGBAImageOriented(tif, w, h, (uint32_t *)data,
                                 ORIENTATION_TOPLEFT, 0)) {
    chroma_log("ERROR", "Failed to decode TIFF: %s", path);
    free(data);
    TIFFClose(tif);
    return CHROMA_ERROR_IMAGE;
  }

  TIFFClose(tif);

  memset(image, 0, sizeof(chroma_image_t));
  strncpy(image->path, path, MAX_PATH_LEN - 1);
  image->data = data;
  image->width = (int)w;
  image->height = (int)h;
  image->channels = 4;
  image->loaded = true;
  image->data_from_stbi = false;
  image->ref_count = 1;

  chroma_log("INFO", "Loaded TIFF image: %s (%dx%d)", path, (int)w, (int)h);
  return CHROMA_OK;
}

#else

int chroma_image_load_tiff(chroma_image_t *image, const char *path) {
  (void)image;
  (void)path;
  chroma_log("ERROR", "TIFF support requires libtiff. Rebuild with: "
                      "make CHROMA_FEATURES=tiff");
  return CHROMA_ERROR_IMAGE;
}

#endif

// AVIF decoding via libavif. Enable with -DCHROMA_HAS_AVIF -lavif.
#ifdef CHROMA_HAS_AVIF

#include <avif/avif.h>

int chroma_image_load_avif(chroma_image_t *image, const char *path) {
  if (!image || !path) {
    return CHROMA_ERROR_INIT;
  }

  FILE *fp = fopen(path, "rb");
  if (!fp) {
    chroma_log("ERROR", "Cannot open AVIF file: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  fseek(fp, 0, SEEK_END);
  long size = ftell(fp);
  fseek(fp, 0, SEEK_SET);

  if (size <= 0 || size > 256 * 1024 * 1024) {
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }

  avifRWData raw = AVIF_DATA_EMPTY;
  avifRWDataRealloc(&raw, (size_t)size);
  if (!raw.data) {
    fclose(fp);
    return CHROMA_ERROR_MEMORY;
  }
  if (fread(raw.data, 1, (size_t)size, fp) != (size_t)size) {
    chroma_log("ERROR", "Failed to read AVIF file: %s", path);
    avifRWDataFree(&raw);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }
  fclose(fp);

  avifDecoder *decoder = avifDecoderCreate();
  avifResult result = avifDecoderSetIOMemory(decoder, raw.data, raw.size);
  if (result != AVIF_RESULT_OK) {
    chroma_log("ERROR", "Failed to set AVIF IO: %s", path);
    avifDecoderDestroy(decoder);
    avifRWDataFree(&raw);
    return CHROMA_ERROR_IMAGE;
  }

  result = avifDecoderParse(decoder);
  if (result != AVIF_RESULT_OK) {
    chroma_log("ERROR", "Failed to parse AVIF: %s", path);
    avifDecoderDestroy(decoder);
    avifRWDataFree(&raw);
    return CHROMA_ERROR_IMAGE;
  }

  result = avifDecoderNextImage(decoder);
  if (result != AVIF_RESULT_OK) {
    chroma_log("ERROR", "Failed to decode AVIF: %s", path);
    avifDecoderDestroy(decoder);
    avifRWDataFree(&raw);
    return CHROMA_ERROR_IMAGE;
  }

  int w = (int)decoder->image->width;
  int h = (int)decoder->image->height;

  avifRGBImage rgb;
  avifRGBImageSetDefaults(&rgb, decoder->image);
  rgb.format = AVIF_RGB_FORMAT_RGBA;
  rgb.depth = 8;

  avifRGBImageAllocatePixels(&rgb);
  avifImageYUVToRGB(decoder->image, &rgb);

  unsigned char *data = malloc(rgb.rowBytes * (size_t)h);
  if (!data) {
    avifRGBImageFreePixels(&rgb);
    avifDecoderDestroy(decoder);
    avifRWDataFree(&raw);
    return CHROMA_ERROR_MEMORY;
  }
  memcpy(data, rgb.pixels, rgb.rowBytes * (size_t)h);

  avifRGBImageFreePixels(&rgb);
  avifDecoderDestroy(decoder);
  avifRWDataFree(&raw);

  memset(image, 0, sizeof(chroma_image_t));
  strncpy(image->path, path, MAX_PATH_LEN - 1);
  image->data = data;
  image->width = w;
  image->height = h;
  image->channels = 4;
  image->loaded = true;
  image->data_from_stbi = false;
  image->ref_count = 1;

  chroma_log("INFO", "Loaded AVIF image: %s (%dx%d)", path, w, h);
  return CHROMA_OK;
}

#else

int chroma_image_load_avif(chroma_image_t *image, const char *path) {
  (void)image;
  (void)path;
  chroma_log("ERROR", "AVIF support requires libavif. Rebuild with: "
                      "make CHROMA_FEATURES=avif");
  return CHROMA_ERROR_IMAGE;
}

#endif

// JPEG XL decoding via libjxl. Enable with -DCHROMA_HAS_JPEGXL -ljxl.
#ifdef CHROMA_HAS_JPEGXL

#include <jxl/decode.h>
#include <jxl/decode_cxx.h>

int chroma_image_load_jpegxl(chroma_image_t *image, const char *path) {
  if (!image || !path) {
    return CHROMA_ERROR_INIT;
  }

  FILE *fp = fopen(path, "rb");
  if (!fp) {
    chroma_log("ERROR", "Cannot open JPEG XL file: %s", path);
    return CHROMA_ERROR_IMAGE;
  }

  fseek(fp, 0, SEEK_END);
  long size = ftell(fp);
  fseek(fp, 0, SEEK_SET);

  if (size <= 0 || size > 256 * 1024 * 1024) {
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }

  unsigned char *jxl_data = malloc((size_t)size);
  if (!jxl_data) {
    fclose(fp);
    return CHROMA_ERROR_MEMORY;
  }
  if (fread(jxl_data, 1, (size_t)size, fp) != (size_t)size) {
    chroma_log("ERROR", "Failed to read JPEG XL file: %s", path);
    free(jxl_data);
    fclose(fp);
    return CHROMA_ERROR_IMAGE;
  }
  fclose(fp);

  JxlDecoder *dec = JxlDecoderCreate(NULL);
  if (!dec) {
    free(jxl_data);
    return CHROMA_ERROR_MEMORY;
  }

  JxlDecoderStatus status = JxlDecoderSetInput(dec, jxl_data, (size_t)size);
  if (status != JXL_DEC_SUCCESS) {
    chroma_log("ERROR", "Failed to set JPEG XL input: %s", path);
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  status =
      JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE);
  if (status != JXL_DEC_SUCCESS) {
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  JxlBasicInfo info;
  memset(&info, 0, sizeof(info));
  status = JxlDecoderProcessInput(dec);
  if (status != JXL_DEC_BASIC_INFO) {
    chroma_log("ERROR", "Failed to get JPEG XL basic info: %s", path);
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  status = JxlDecoderGetBasicInfo(dec, &info);
  if (status != JXL_DEC_SUCCESS) {
    chroma_log("ERROR", "Failed to parse JPEG XL info: %s", path);
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  int w = (int)info.xsize;
  int h = (int)info.ysize;

  size_t buffer_size = (size_t)w * (size_t)h * 4;
  unsigned char *data = malloc(buffer_size);
  if (!data) {
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_MEMORY;
  }

  JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0};

  status = JxlDecoderProcessInput(dec);
  if (status != JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
    chroma_log("ERROR", "Unexpected JPEG XL decoder state: %s", path);
    free(data);
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  status = JxlDecoderSetImageOutBuffer(dec, &format, data, buffer_size);
  if (status != JXL_DEC_SUCCESS) {
    chroma_log("ERROR", "Failed to set JPEG XL output buffer: %s", path);
    free(data);
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  status = JxlDecoderProcessInput(dec);
  if (status != JXL_DEC_FULL_IMAGE) {
    chroma_log("ERROR", "Failed to decode JPEG XL image: %s", path);
    free(data);
    JxlDecoderDestroy(dec);
    free(jxl_data);
    return CHROMA_ERROR_IMAGE;
  }

  JxlDecoderDestroy(dec);
  free(jxl_data);

  memset(image, 0, sizeof(chroma_image_t));
  strncpy(image->path, path, MAX_PATH_LEN - 1);
  image->data = data;
  image->width = w;
  image->height = h;
  image->channels = 4;
  image->loaded = true;
  image->data_from_stbi = false;
  image->ref_count = 1;

  chroma_log("INFO", "Loaded JPEG XL image: %s (%dx%d)", path, w, h);
  return CHROMA_OK;
}

#else

int chroma_image_load_jpegxl(chroma_image_t *image, const char *path) {
  (void)image;
  (void)path;
  chroma_log("ERROR", "JPEG XL support requires libjxl. Rebuild with: "
                      "make CHROMA_FEATURES=jpegxl");
  return CHROMA_ERROR_IMAGE;
}

#endif
