/* SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup imbuf
 * Radiance High Dynamic Range image file IO
 * For description and code for reading/writing of radiance hdr files
 * by Greg Ward, refer to:
 * http://radsite.lbl.gov/radiance/refer/Notes/picture_format.html
 */

#include "MEM_guardedalloc.h"

#include "BLI_fileops.h"
#include "BLI_utildefines.h"

#include "imbuf.h"

#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"

#include "IMB_allocimbuf.h"
#include "IMB_filetype.h"

#include "IMB_colormanagement.h"
#include "IMB_colormanagement_intern.h"

/* needed constants */
#define MINELEN 8
#define MAXELEN 0x7fff
#define MINRUN 4 /* minimum run length */
#define RED 0
#define GRN 1
#define BLU 2
#define EXP 3
#define COLXS 128
typedef unsigned char RGBE[4];
typedef float fCOLOR[3];

/* copy source -> dest */
#define COPY_RGBE(c1, c2) \
  (c2[RED] = c1[RED], c2[GRN] = c1[GRN], c2[BLU] = c1[BLU], c2[EXP] = c1[EXP])

/* read routines */
static const unsigned char *oldreadcolrs(RGBE *scan,
                                         const unsigned char *mem,
                                         int xmax,
                                         const unsigned char *mem_eof)
{
  size_t i, rshift = 0, len = xmax;
  while (len > 0) {
    if (UNLIKELY(mem_eof - mem < 4)) {
      return NULL;
    }
    scan[0][RED] = *mem++;
    scan[0][GRN] = *mem++;
    scan[0][BLU] = *mem++;
    scan[0][EXP] = *mem++;
    if (scan[0][RED] == 1 && scan[0][GRN] == 1 && scan[0][BLU] == 1) {
      for (i = scan[0][EXP] << rshift; i > 0 && len > 0; i--) {
        COPY_RGBE(scan[-1], scan[0]);
        scan++;
        len--;
      }
      rshift += 8;
    }
    else {
      scan++;
      len--;
      rshift = 0;
    }
  }
  return mem;
}

static const unsigned char *freadcolrs(RGBE *scan,
                                       const unsigned char *mem,
                                       int xmax,
                                       const unsigned char *mem_eof)
{
  if (UNLIKELY(mem_eof - mem < 4)) {
    return NULL;
  }

  if (UNLIKELY((xmax < MINELEN) | (xmax > MAXELEN))) {
    return oldreadcolrs(scan, mem, xmax, mem_eof);
  }

  int val = *mem++;
  if (val != 2) {
    return oldreadcolrs(scan, mem - 1, xmax, mem_eof);
  }

  scan[0][GRN] = *mem++;
  scan[0][BLU] = *mem++;

  val = *mem++;

  if (scan[0][GRN] != 2 || scan[0][BLU] & 128) {
    scan[0][RED] = 2;
    scan[0][EXP] = val;
    return oldreadcolrs(scan + 1, mem, xmax - 1, mem_eof);
  }

  if (UNLIKELY(((scan[0][BLU] << 8) | val) != xmax)) {
    return NULL;
  }

  for (size_t i = 0; i < 4; i++) {
    if (UNLIKELY(mem_eof - mem < 2)) {
      return NULL;
    }
    for (size_t j = 0; j < xmax;) {
      int code = *mem++;
      if (code > 128) {
        code &= 127;
        if (UNLIKELY(code + j > xmax)) {
          return NULL;
        }
        val = *mem++;
        while (code--) {
          scan[j++][i] = (unsigned char)val;
        }
      }
      else {
        if (UNLIKELY(mem_eof - mem < code)) {
          return NULL;
        }
        if (UNLIKELY(code + j > xmax)) {
          return NULL;
        }
        while (code--) {
          scan[j++][i] = *mem++;
        }
      }
    }
  }

  return mem;
}

/* helper functions */

/* rgbe -> float color */
static void RGBE2FLOAT(RGBE rgbe, fCOLOR fcol)
{
  if (rgbe[EXP] == 0) {
    fcol[RED] = fcol[GRN] = fcol[BLU] = 0;
  }
  else {
    float f = ldexp(1.0, rgbe[EXP] - (COLXS + 8));
    fcol[RED] = f * (rgbe[RED] + 0.5f);
    fcol[GRN] = f * (rgbe[GRN] + 0.5f);
    fcol[BLU] = f * (rgbe[BLU] + 0.5f);
  }
}

/* float color -> rgbe */
static void FLOAT2RGBE(const fCOLOR fcol, RGBE rgbe)
{
  int e;
  float d = (fcol[RED] > fcol[GRN]) ? fcol[RED] : fcol[GRN];
  if (fcol[BLU] > d) {
    d = fcol[BLU];
  }
  if (d <= 1e-32f) {
    rgbe[RED] = rgbe[GRN] = rgbe[BLU] = rgbe[EXP] = 0;
  }
  else {
    d = (float)frexp(d, &e) * 256.0f / d;
    rgbe[RED] = (unsigned char)(fcol[RED] * d);
    rgbe[GRN] = (unsigned char)(fcol[GRN] * d);
    rgbe[BLU] = (unsigned char)(fcol[BLU] * d);
    rgbe[EXP] = (unsigned char)(e + COLXS);
  }
}

/* ImBuf read */

bool imb_is_a_hdr(const unsigned char *buf, const size_t size)
{
  /* NOTE: `#?RADIANCE` is used by other programs such as `ImageMagik`,
   * Although there are some files in the wild that only use `#?` (from looking online).
   * If this is ever a problem we could check for the longer header since this is part of the spec.
   *
   * We could check `32-bit_rle_rgbe` or `32-bit_rle_xyze` too since this is part of the format.
   * Currently this isn't needed.
   *
   * See: http://paulbourke.net/dataformats/pic/
   */
  const unsigned char magic[2] = {'#', '?'};
  if (size < sizeof(magic)) {
    return false;
  }
  return memcmp(buf, magic, sizeof(magic)) == 0;
}

struct ImBuf *imb_loadhdr(const unsigned char *mem,
                          size_t size,
                          int flags,
                          char colorspace[IM_MAX_SPACE])
{
  struct ImBuf *ibuf;
  RGBE *sline;
  fCOLOR fcol;
  float *rect_float;
  int found = 0;
  int width = 0, height = 0;
  const unsigned char *ptr, *mem_eof = mem + size;
  char oriY[3], oriX[3];

  if (!imb_is_a_hdr(mem, size)) {
    return NULL;
  }

  colorspace_set_default_role(colorspace, IM_MAX_SPACE, COLOR_ROLE_DEFAULT_FLOAT);

  /* find empty line, next line is resolution info */
  size_t x;
  for (x = 1; x < size; x++) {
    if ((mem[x - 1] == '\n') && (mem[x] == '\n')) {
      found = 1;
      break;
    }
  }

  if ((found && (x < (size - 1))) == 0) {
    /* Data not found! */
    return NULL;
  }

  x++;

  /* sscanf requires a null-terminated buffer argument */
  char buf[32] = {0};
  memcpy(buf, &mem[x], MIN2(sizeof(buf) - 1, size - x));

  if (sscanf(buf, "%2s %d %2s %d", (char *)&oriY, &height, (char *)&oriX, &width) != 4) {
    return NULL;
  }

  if (width < 1 || height < 1) {
    return NULL;
  }

  /* Checking that width x height does not extend past mem_eof is not easily possible
   * since the format uses RLE compression. Can cause excessive memory allocation to occur. */

  /* find end of this line, data right behind it */
  ptr = (const unsigned char *)strchr((const char *)&mem[x], '\n');
  if (ptr == NULL || ptr >= mem_eof) {
    return NULL;
  }
  ptr++;

  if (flags & IB_test) {
    ibuf = IMB_allocImBuf(width, height, 32, 0);
  }
  else {
    ibuf = IMB_allocImBuf(width, height, 32, (flags & IB_rect) | IB_rectfloat);
  }

  if (UNLIKELY(ibuf == NULL)) {
    return NULL;
  }

  ibuf->ftype = IMB_FTYPE_RADHDR;

  if (flags & IB_alphamode_detect) {
    ibuf->flags |= IB_alphamode_premul;
  }

  if (flags & IB_test) {
    return ibuf;
  }

  /* read in and decode the actual data */
  sline = (RGBE *)MEM_mallocN(sizeof(*sline) * width, __func__);
  rect_float = ibuf->rect_float;

  for (size_t y = 0; y < height; y++) {
    ptr = freadcolrs(sline, ptr, width, mem_eof);
    if (ptr == NULL) {
      printf("WARNING! HDR decode error, image may be just truncated, or completely wrong...\n");
      break;
    }
    for (x = 0; x < width; x++) {
      /* Convert to LDR. */
      RGBE2FLOAT(sline[x], fcol);
      *rect_float++ = fcol[RED];
      *rect_float++ = fcol[GRN];
      *rect_float++ = fcol[BLU];
      *rect_float++ = 1.0f;
    }
  }
  MEM_freeN(sline);
  if (oriY[0] == '-') {
    IMB_flipy(ibuf);
  }

  if (flags & IB_rect) {
    IMB_rect_from_float(ibuf);
  }

  return ibuf;
}

/* ImBuf write */
static int fwritecolrs(
    FILE *file, int width, int channels, const unsigned char *ibufscan, const float *fpscan)
{
  int beg, c2, count = 0;
  fCOLOR fcol;
  RGBE rgbe, *rgbe_scan;

  if (UNLIKELY((ibufscan == NULL) && (fpscan == NULL))) {
    return 0;
  }

  rgbe_scan = (RGBE *)MEM_mallocN(sizeof(RGBE) * width, "radhdr_write_tmpscan");

  /* Convert scan-line. */
  for (size_t i = 0, j = 0; i < width; i++) {
    if (fpscan) {
      fcol[RED] = fpscan[j];
      fcol[GRN] = (channels >= 2) ? fpscan[j + 1] : fpscan[j];
      fcol[BLU] = (channels >= 3) ? fpscan[j + 2] : fpscan[j];
    }
    else {
      fcol[RED] = (float)ibufscan[j] / 255.0f;
      fcol[GRN] = (float)((channels >= 2) ? ibufscan[j + 1] : ibufscan[j]) / 255.0f;
      fcol[BLU] = (float)((channels >= 3) ? ibufscan[j + 2] : ibufscan[j]) / 255.0f;
    }
    FLOAT2RGBE(fcol, rgbe);
    COPY_RGBE(rgbe, rgbe_scan[i]);
    j += channels;
  }

  if ((width < MINELEN) | (width > MAXELEN)) { /* OOBs, write out flat */
    int x = fwrite((char *)rgbe_scan, sizeof(RGBE), width, file) - width;
    MEM_freeN(rgbe_scan);
    return x;
  }
  /* put magic header */
  putc(2, file);
  putc(2, file);
  putc((unsigned char)(width >> 8), file);
  putc((unsigned char)(width & 255), file);
  /* put components separately */
  for (size_t i = 0; i < 4; i++) {
    for (size_t j = 0; j < width; j += count) { /* find next run */
      for (beg = j; beg < width; beg += count) {
        for (count = 1; (count < 127) && ((beg + count) < width) &&
                        (rgbe_scan[beg + count][i] == rgbe_scan[beg][i]);
             count++) {
          /* pass */
        }
        if (count >= MINRUN) {
          break; /* long enough */
        }
      }
      if (((beg - j) > 1) && ((beg - j) < MINRUN)) {
        c2 = j + 1;
        while (rgbe_scan[c2++][i] == rgbe_scan[j][i]) {
          if (c2 == beg) { /* short run */
            putc((unsigned char)(128 + beg - j), file);
            putc((unsigned char)(rgbe_scan[j][i]), file);
            j = beg;
            break;
          }
        }
      }
      while (j < beg) { /* write out non-run */
        if ((c2 = beg - j) > 128) {
          c2 = 128;
        }
        putc((unsigned char)(c2), file);
        while (c2--) {
          putc(rgbe_scan[j++][i], file);
        }
      }
      if (count >= MINRUN) { /* write out run */
        putc((unsigned char)(128 + count), file);
        putc(rgbe_scan[beg][i], file);
      }
      else {
        count = 0;
      }
    }
  }
  MEM_freeN(rgbe_scan);
  return (ferror(file) ? -1 : 0);
}

static void writeHeader(FILE *file, int width, int height)
{
  fprintf(file, "#?RADIANCE");
  fputc(10, file);
  fprintf(file, "# %s", "Created with Blender");
  fputc(10, file);
  fprintf(file, "EXPOSURE=%25.13f", 1.0);
  fputc(10, file);
  fprintf(file, "FORMAT=32-bit_rle_rgbe");
  fputc(10, file);
  fputc(10, file);
  fprintf(file, "-Y %d +X %d", height, width);
  fputc(10, file);
}

bool imb_savehdr(struct ImBuf *ibuf, const char *filepath, int flags)
{
  FILE *file = BLI_fopen(filepath, "wb");
  float *fp = NULL;
  size_t width = ibuf->x, height = ibuf->y;
  unsigned char *cp = NULL;

  (void)flags; /* unused */

  if (file == NULL) {
    return 0;
  }

  writeHeader(file, width, height);

  if (ibuf->rect) {
    cp = (unsigned char *)ibuf->rect + ibuf->channels * (height - 1) * width;
  }
  if (ibuf->rect_float) {
    fp = ibuf->rect_float + ibuf->channels * (height - 1) * width;
  }

  for (size_t y = 0; y < height; y++) {
    if (fwritecolrs(file, width, ibuf->channels, cp, fp) < 0) {
      fclose(file);
      printf("HDR write error\n");
      return 0;
    }
    if (cp) {
      cp -= ibuf->channels * width;
    }
    if (fp) {
      fp -= ibuf->channels * width;
    }
  }

  fclose(file);
  return 1;
}
