/*  Part of XPCE --- The SWI-Prolog GUI toolkit

    Author:        Jan Wielemaker
    E-mail:        jan@swi-prolog.org
    WWW:           https://www.swi-prolog.org
    Copyright (c)  2025-2026, SWI-Prolog Solutions b.v.
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in
       the documentation and/or other materials provided with the
       distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
*/

#include <h/kernel.h>
#include <h/graphics.h>
#include <h/unix.h>
#include "sdlimage.h"
#include "sdlcolour.h"
#include "sdlstream.h"
#include <SDL3_image/SDL_image.h>
#include <math.h>

static status sdl_surface_to_image(Image image, SDL_Surface *surf0);

cairo_surface_t *
pceImage2CairoSurface(Image image)
{ if ( !image->ws_ref )
  { if ( !XopenImage(image, CurrentDisplay(NIL)) )
      return NULL;
  }
  return image->ws_ref;
}

/**
 * Free resources associated with an Image object.
 *
 * @param image Pointer to the Image object.
 */
void
ws_destroy_image(Image image)
{ cairo_surface_t *s = image->ws_ref;
  if ( s )
  { image->ws_ref = NULL;
    cairo_surface_destroy(s);
  }
}

/**
 * Add image to a file for creating a saved object.  Note this is _not_
 * for saving images to a file.  ws_save_image_file() does that.
 *
 * @param image Pointer to the Image object.
 * @param file File object representing the target file.  This file
 * is open for writing through `file->fd`.
 */
status
ws_store_image(Image image, FileObj file)
{ SDL_Surface *surf = pceImage2SDL_Surface(image);
  status rc = FAIL;

  if ( surf )
  { SDL_IOStream *io = IOSTREAM_to_SDL_IOStream(file->fd);

    if ( io )
    { Sputc('P', file->fd);
      rc = IMG_SavePNG_IO(surf, io, true);
      if ( !rc )
	errorPce(image, NAME_SDL_Error, CtoString(SDL_GetError()));
    } else
    { Sputc('O', file->fd);
    }

    SDL_DestroySurface(surf);
  } else
  { Sputc('O', file->fd);
  }

  return rc;
}

/**
 * Load an image in PNM format.
 *
 * @param image Pointer to the Image object.
 * @param fd IOSTREAM to read from.
 * @return SUCCEED if loading succeeds; otherwise, FAIL.
 */
status
loadPNMImage(Image image, IOSTREAM *fd)
{ assert(image->ws_ref == NULL);

  SDL_IOStream *io = IOSTREAM_to_SDL_IOStream(fd);
  if ( !io )
    fail;

  SDL_Surface *surf0 = IMG_Load_IO(io, true);  /* closes io */
  if ( !surf0 )
  { Cprintf("loadPNMImage(%s): %s\n", pp(image), SDL_GetError());
    fail;
  }

  return sdl_surface_to_image(image, surf0);
}

static void
premultiply_alpha(SDL_Surface *surface, bool *pisbitmap)
{ if ( !SDL_LockSurface(surface) )
    return;

  Uint8 *pixels = surface->pixels;
  int pitch = surface->pitch;
  int width = surface->w;
  int height = surface->h;
  const SDL_PixelFormatDetails *format_details =
    SDL_GetPixelFormatDetails(surface->format);
  bool isbitmap = true;

  for (int y = 0; y < height; y++)
  { Uint32 *row = (Uint32 *)(pixels + y * pitch);
    for (int x = 0; x < width; x++)
    { Uint8 r, g, b, a;
      SDL_GetRGBA(row[x], format_details, NULL, &r, &g, &b, &a);
      if ( isbitmap )
      { if ( !( ((r|g|b) == 0   && a == 255) ||
		((r&g&b) == 255 && a == 0) ) )
	  isbitmap = false;
      }

      r = (r * a) / 255;
      g = (g * a) / 255;
      b = (b * a) / 255;
      row[x] = SDL_MapRGBA(format_details, NULL, r, g, b, a);
    }
  }

  *pisbitmap = isbitmap;
  SDL_UnlockSurface(surface);
}


/**
 * Copy a Cairo surface such that the data is always owned
 * by Cairo.
 */

static cairo_surface_t *
my_cairo_copy_surface(cairo_surface_t *src)
{ int width  = cairo_image_surface_get_width(src);
  int height = cairo_image_surface_get_height(src);
  cairo_surface_t *dst = cairo_image_surface_create(
    CAIRO_FORMAT_ARGB32, width, height);
  if ( dst )
  { cairo_t *cr = cairo_create(dst);
    cairo_set_source_surface(cr, src, 0, 0);
    cairo_paint(cr);
    cairo_destroy(cr);
  }

  return dst;
}

static status
my_cairo_check_surface(Any ctx, cairo_surface_t *s)
{ if ( cairo_surface_status(s) != CAIRO_STATUS_SUCCESS )
  { Cprintf("%s: cairo surface error: %s\n",
	    pp(ctx), cairo_status_to_string(cairo_surface_status(s)));
    fail;
  }
  succeed;
}

/**
 * Convert an SDL_Surface to a cairo-backed Image, consuming surf0.
 *
 * Converts to ARGB8888, premultiplies alpha, wraps in a cairo surface,
 * copies to a cairo-owned buffer, then sets image->kind, size, ws_ref.
 */
static status
sdl_surface_to_image(Image image, SDL_Surface *surf0)
{ SDL_Surface *surf1 = SDL_ConvertSurface(surf0, SDL_PIXELFORMAT_ARGB8888);
  SDL_DestroySurface(surf0);
  if ( !surf1 )
  { Cprintf("Failed to convert %s: %s\n", pp(image), SDL_GetError());
    fail;
  }

  bool isbitmap = false;
  premultiply_alpha(surf1, &isbitmap);
  cairo_surface_t *surf = cairo_image_surface_create_for_data(
    (unsigned char *)surf1->pixels,
    CAIRO_FORMAT_ARGB32,
    surf1->w,
    surf1->h,
    surf1->pitch);
  if ( !my_cairo_check_surface(image, surf) )
  { SDL_DestroySurface(surf1);
    fail;
  }
  cairo_surface_t *final = my_cairo_copy_surface(surf);
  cairo_surface_destroy(surf);
  SDL_DestroySurface(surf1);
  if ( !my_cairo_check_surface(image, final) )
    fail;

  if ( isbitmap )
    DEBUG(NAME_bitmap, Cprintf("%s: bitmap\n", pp(image)));
  assign(image, kind, isbitmap ? NAME_bitmap : NAME_pixmap);
  assign(image->size, w, toInt(cairo_image_surface_get_width(final)));
  assign(image->size, h, toInt(cairo_image_surface_get_height(final)));
  image->ws_ref = final;
  succeed;
}

/**
 * Load an image from its associated file path.
 *
 * @param image Pointer to the Image object.
 * @return SUCCEED if loading succeeds; otherwise, FAIL.
 */
status
ws_load_image_file(Image image)
{ IOSTREAM *fd;

  assert(image->ws_ref == NULL);

  int req_w = valInt(image->size->w);
  int req_h = valInt(image->size->h);

  if ( instanceOfObject(image->file, ClassFile) )
  { FileObj f = (FileObj)image->file;
    char *fname = charArrayToFN((CharArray)getOsNameFile(f));
    /* Detect SVG by extension and use the size-aware loader so that
     * aspect ratio is preserved when only one dimension is specified.
     * SDL auto-computes the missing dimension when it receives 0.
     */
    size_t len = strlen(fname);
    bool is_svg = ( len > 4 &&
		    strcasecmp(fname + len - 4, ".svg") == 0 );
    if ( is_svg )
    { SDL_IOStream *io = SDL_IOFromFile(fname, "rb");
      if ( !io )
      { Cprintf("Failed to open %s: %s\n", fname, SDL_GetError());
	fail;
      }
      SDL_Surface *surf0 = IMG_LoadSizedSVG_IO(io, req_w, req_h);
      SDL_CloseIO(io);
      if ( !surf0 )
      { Cprintf("Failed to load SVG %s: %s\n", fname, SDL_GetError());
	fail;
      }
      return sdl_surface_to_image(image, surf0);
    }
    SDL_Surface *surf0 = IMG_Load(fname);
    if ( !surf0 )
    { Cprintf("Failed to load %s: %s\n", fname, SDL_GetError());
      fail;
    }
    return sdl_surface_to_image(image, surf0);
  } else if ( (fd = Sopen_object(image->file, "rbr")) )
  { SDL_IOStream *io = IOSTREAM_to_SDL_IOStream(fd);
    status rc;
    if ( io && IMG_isSVG(io) )
    { SDL_Surface *surf0 = IMG_LoadSizedSVG_IO(io, req_w, req_h);
      SDL_CloseIO(io);
      Sclose(fd);
      if ( !surf0 )
      { Cprintf("Failed to load SVG %s: %s\n", pp(image), SDL_GetError());
	fail;
      }
      return sdl_surface_to_image(image, surf0);
    }
    if ( io )
      SDL_CloseIO(io);
    Sseek(fd, 0, SIO_SEEK_SET);
    rc = loadPNMImage(image, fd);
    Sclose(fd);
    return rc;
  } else
  { fail;
  }
}

/**
 * Create an image from XPM (X PixMap) data.
 *
 * @param image Pointer to the Image object.
 * @param xpm Array of XPM strings.
 * @param d Display on which to create the image.
 * @return SUCCEED if creation succeeds; otherwise, FAIL.
 */

static uint32_t *
parse_xpm_1_data(int width, int height, int ncolors, char **xpm)
{ uint32_t palette[256] = {0};

  for (int i = 0; i < ncolors; i++)
  { char symbol;
    char color[32];
    sscanf(xpm[1 + i], "%c c %s", &symbol, color);

    uint32_t rgb = 0xFF000000;  // default alpha

    if (strcmp(color, "None") == 0)
    { rgb = 0x00000000;
    } else if (color[0] == '#')
    { unsigned int r, g, b;
      sscanf(color + 1, "%02x%02x%02x", &r, &g, &b);
      rgb |= (r << 16) | (g << 8) | b;
    } else
    { Int Rgb = getNamedRGB(CtoName(color));
      if ( Rgb )
	rgb |= valInt(Rgb);
      else
	Cprintf("XPM: Unknown colour name: %s\n", color);
    }
    palette[(unsigned char)symbol] = rgb;
  }

  // Allocate pixel buffer
  uint32_t *pixels = calloc(width * height, sizeof(uint32_t));

  for (int y = 0; y < height; y++)
  { const char *row = xpm[1 + ncolors + y];
    for (int x = 0; x < width; x++) {
      pixels[y * width + x] = palette[(unsigned char)row[x]];
    }
  }

  return pixels;
}

static uint32_t *
parse_xpm_2_data(int width, int height, int ncolors, char **xpm)
{ uint32_t *palette[256] = {0};

  for (int i = 0; i < ncolors; i++)
  { unsigned char c1 = (unsigned char)xpm[1 + i][0];
    unsigned char c2 = (unsigned char)xpm[1 + i][1];
    char color[32];
    sscanf(xpm[1 + i]+2, " c %s", color);

    uint32_t rgb = 0xFF000000;  // default alpha

    if (strcmp(color, "None") == 0)
    { rgb = 0x00000000;
    } else if (color[0] == '#')
    { unsigned int r, g, b;
      sscanf(color + 1, "%02x%02x%02x", &r, &g, &b);
      rgb |= (r << 16) | (g << 8) | b;
    } else
    { Int Rgb = getNamedRGB(CtoName(color));
      if ( Rgb )
	rgb |= valInt(Rgb);
      else
	Cprintf("XPM: Unknown colour name: %s\n", color);
    }
    if ( !palette[c2] )
      palette[c2] = calloc(256, sizeof(uint32_t));

    palette[c2][c1] = rgb;
  }

  uint32_t *pixels = calloc(width * height, sizeof(uint32_t));
  for (int y = 0; y < height; y++)
  { const char *row = xpm[1 + ncolors + y];
    for (int x = 0; x < width; x++)
    { unsigned char c1 = (unsigned char)row[x*2];
      unsigned char c2 = (unsigned char)row[x*2+1];

      pixels[y * width + x] = palette[c2][c1];
    }
  }

  for(int i=0; i<256; i++)
  { if ( palette[i] )
      free(palette[i]);
  }

  return pixels;
}


status
ws_create_image_from_xpm_data(Image image, char **xpm, DisplayObj d)
{ int width, height, ncolors, cpp;
  sscanf(xpm[0], "%d %d %d %d", &width, &height, &ncolors, &cpp);

  assert(image->ws_ref == NULL);

  DEBUG(NAME_xpm,
	Cprintf("Parsing XPM image of %dx%d, %d colors, cpp=%d\n",
		width, height, ncolors, cpp));

  uint32_t *pixels = NULL;
  if ( cpp == 1 )
    pixels = parse_xpm_1_data(width, height, ncolors, xpm);
  else if ( cpp == 2 )
    pixels =parse_xpm_2_data(width, height, ncolors, xpm);
  else
    Cprintf("XPM: No support for %d cpp images\n", cpp);

  if ( pixels )
  { int stride = width * 4;
    cairo_surface_t *surf = cairo_image_surface_create_for_data(
      (unsigned char *)pixels, CAIRO_FORMAT_ARGB32, width, height, stride);

    image->ws_ref = surf;
    succeed;
  } else
    fail;
}

/**
 * Convert an XPCE image to an SDL surface.
 */

SDL_Surface *
pceImage2SDL_Surface(Image image)
{ cairo_surface_t *cairo_surf = pceImage2CairoSurface(image);

  if ( cairo_surf )
  { int width = cairo_image_surface_get_width(cairo_surf);
    int height = cairo_image_surface_get_height(cairo_surf);
    int stride = cairo_image_surface_get_stride(cairo_surf);
    unsigned char *data = cairo_image_surface_get_data(cairo_surf);

    SDL_Surface *sdl_surf = SDL_CreateSurface(width, height,
					      SDL_PIXELFORMAT_ARGB8888);
    if ( !sdl_surf )
    { Cprintf("Failed to create SDL surface %dx%x: %s\n",
	      width, height, SDL_GetError());
      return NULL;
    }

    SDL_LockSurface(sdl_surf);
    for (int y = 0; y < height; ++y)
    { uint32_t *dst_row = (uint32_t *)((uint8_t *)sdl_surf->pixels +
				       y * sdl_surf->pitch);
      uint32_t *src_row = (uint32_t *)(data + y * stride);
      for (int x = 0; x < width; ++x)
      { uint32_t pixel = src_row[x];
	// Cairo's ARGB premultiplied -> SDL ARGB
	// (which expects straight alpha)
	// Optionally unpremultiply here if you need accuracy
	dst_row[x] = pixel; // naive copy; may look wrong with
			    // transparency on some platforms
      }
    }
    SDL_UnlockSurface(sdl_surf);

    return sdl_surf;
  }

  return NULL;
}

/**
 * Save an image to a file in a specified format.
 *
 * @param image Pointer to the Image object.
 * @param into Target source/sink.
 * @param fmt Format name (e.g., "png").
 * @return SUCCEED if saving succeeds; otherwise, FAIL.
 */
status
ws_save_image_file(Image image, SourceSink into, Name fmt)
{ SDL_Surface *surf = pceImage2SDL_Surface(image);
  status rc = FAIL;

  if ( surf )
  { if ( instanceOfObject(into, ClassFile) )
    { FileObj file = (FileObj)into;
      if ( file->fd == NULL )
      { if ( fmt == NAME_png )
	{ rc = IMG_SavePNG(surf, nameToFN(file->name));
	} else
	{ Cprintf("Cannot save %s: format %s is not supported\n",
		  pp(image), pp(fmt));
	}
      } else
      { Cprintf("Cannot save %s to %s: file is open\n",
		pp(image), pp(file));
      }
    } else
    { Cprintf("Cannot save %s to %s: Can only save to a file\n",
	      pp(image), pp(into));
    }

    SDL_DestroySurface(surf);
    return rc;
  }

  fail;
}

/**
 * Open (realize) the image in the context of a display, possibly scaling.
 *
 * @param image Pointer to the Image object.
 * @param d Target display.
 * @param scale Scaling factor.
 * @return SUCCEED on success; otherwise, FAIL.
 */
status
ws_open_image(Image image)
{ if ( !image->ws_ref )
  { int w = valInt(image->size->w);
    int h = valInt(image->size->h);

    cairo_surface_t *surf =
      cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);

    image->ws_ref = surf;
    d_image(image, 0, 0, w, h);
    r_clear(0, 0, w, h);
    d_done();
  }

  succeed;
}

/**
 * Close (unrealize) the image from the display context.
 *
 * @param image Pointer to the Image object.
 * @param d Target display.
 */
void
ws_close_image(Image image)
{ if ( image->ws_ref )
  { cairo_surface_destroy(image->ws_ref);
    image->ws_ref = NULL;
  }
}

/**
 * Create a new scaled version of the image.
 *
 * @param image Pointer to the source Image.
 * @param w Desired width.
 * @param h Desired height.
 * @return A new Image object; NULL on failure.
 */
Image
ws_scale_image(Image image, int w, int h)
{ if ( !image->ws_ref && !XopenImage(image, CurrentDisplay(NIL)) )
    return NULL;

  int src_w = cairo_image_surface_get_width(image->ws_ref);
  int src_h = cairo_image_surface_get_height(image->ws_ref);

  cairo_surface_t *dst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
  cairo_t *cr = cairo_create(dst);
  cairo_scale(cr, (double)w / src_w, (double)h / src_h);
  cairo_set_source_surface(cr, image->ws_ref, 0, 0);
  cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
  cairo_paint(cr);
  cairo_destroy(cr);

  Image scaled = answerObject(ClassImage, NIL, toInt(w), toInt(h),
			      NAME_pixmap, EAV);
  scaled->ws_ref = dst;
  answer(scaled);
}

/**
 * Create a rotated version of the image.
 *
 * @param image Pointer to the source Image.
 * @param angle Rotation angle in degrees.
 * @return A new Image object; NULL on failure.
 */
Image
ws_rotate_image(Image image, float angle)
{ if ( !image->ws_ref && !XopenImage(image, CurrentDisplay(NIL)) )
    return NULL;

  double rad = angle * M_PI / 180.0;
  double cosA = fabs(cos(rad));
  double sinA = fabs(sin(rad));
  int src_w = cairo_image_surface_get_width(image->ws_ref);
  int src_h = cairo_image_surface_get_height(image->ws_ref);
  int dst_w = (int)ceil(src_w * cosA + src_h * sinA);
  int dst_h = (int)ceil(src_w * sinA + src_h * cosA);

  cairo_surface_t *dst =
    cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dst_w, dst_h);
  cairo_t *cr = cairo_create(dst);

  /* Transparent background */
  cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
  cairo_paint(cr);
  cairo_set_operator(cr, CAIRO_OPERATOR_OVER);

  /* Rotate around the centre of the destination, painting the source
   * centred there.
   */
  cairo_translate(cr, dst_w / 2.0, dst_h / 2.0);
  cairo_rotate(cr, rad);
  cairo_set_source_surface(cr, image->ws_ref, -src_w / 2.0, -src_h / 2.0);
  cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
  cairo_paint(cr);
  cairo_destroy(cr);

  Image rotated = answerObject(ClassImage, NIL, toInt(dst_w), toInt(dst_h),
			       NAME_pixmap, EAV);
  rotated->ws_ref = dst;
  answer(rotated);
}

/**
 * Create a greyscale version of the image.
 *
 * @param image Pointer to the source Image.
 * @return A new monochrome Image; NULL on failure.
 */
Image
ws_grayscale_image(Image image)
{ if ( !image->ws_ref && !XopenImage(image, CurrentDisplay(NIL)) )
    fail;

  Int w = image->size->w;
  Int h = image->size->h;
  Image img_gray = answerObject(ClassImage, NIL, w, h, NAME_pixmap, EAV);
  cairo_surface_t *gray =
    cairo_image_surface_create(CAIRO_FORMAT_ARGB32, valInt(w), valInt(h));
  cairo_surface_t *orig = image->ws_ref;

  int stride = cairo_image_surface_get_stride(orig);
  unsigned char *src = cairo_image_surface_get_data(orig);
  unsigned char *dst = cairo_image_surface_get_data(gray);

  assert(valInt(w) == cairo_image_surface_get_width(orig));
  assert(valInt(h) == cairo_image_surface_get_height(orig));

  for(int y = 0; y < valInt(h); y++)
  { for(int x = 0; x < valInt(w); x++)
    { unsigned char *s = src + y * stride + x * 4;
      unsigned char *d = dst + y * stride + x * 4;

      unsigned char a = s[3];
      if (a == 0)
      { // Fully transparent, keep transparent
	d[0] = d[1] = d[2] = 0;
	d[3] = 0;
      } else
      { // Convert to grayscale using luminosity method
	uint8_t r = s[2], g = s[1], b = s[0]; // BGRA
	uint8_t G = (uint8_t)(0.3 * r + 0.59 * g + 0.11 * b);

	d[0] = d[1] = d[2] = G;  // B = G = R
	d[3] = a;
      }
    }
  }

  cairo_surface_mark_dirty(gray);
  img_gray->ws_ref = gray;

  answer(img_gray);
}

status
ws_has_alpha_image(Image image)
{ if ( !image->ws_ref && !XopenImage(image, CurrentDisplay(NIL)) )
    fail;

  cairo_surface_t *surf = image->ws_ref;
  if ( cairo_image_surface_get_format(surf) != CAIRO_FORMAT_ARGB32 )
    fail;

  int w      = cairo_image_surface_get_width(surf);
  int h      = cairo_image_surface_get_height(surf);
  int stride = cairo_image_surface_get_stride(surf);
  unsigned char *data = cairo_image_surface_get_data(surf);

  for(int y = 0; y < h; y++)
  { unsigned char *row = data + y * stride;
    for(int x = 0; x < w; x++)
    { if ( row[x*4 + 3] != 255 )
	succeed;
    }
  }

  fail;
}
