/* SPDX-License-Identifier: GPL-2.0-or-later
 * Copyright 2001-2002 NaN Holding BV. All rights reserved. */

/** \file
 * \ingroup imbuf
 */

#include <math.h>

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

#include "imbuf.h"

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

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

/* Some code copied from article on microsoft.com,
 * copied here for enhanced BMP support in the future:
 * http://www.microsoft.com/msj/defaultframe.asp?page=/msj/0197/mfcp1/mfcp1.htm&nav=/msj/0197/newnav.htm
 */

typedef struct BMPINFOHEADER {
  uint biSize;
  uint biWidth;
  uint biHeight;
  ushort biPlanes;
  ushort biBitCount;
  uint biCompression;
  uint biSizeImage;
  uint biXPelsPerMeter;
  uint biYPelsPerMeter;
  uint biClrUsed;
  uint biClrImportant;
} BMPINFOHEADER;

#if 0
typedef struct BMPHEADER {
  ushort biType;
  uint biSize;
  ushort biRes1;
  ushort biRes2;
  uint biOffBits;
} BMPHEADER;
#endif

#define BMP_FILEHEADER_SIZE 14

#define CHECK_HEADER_FIELD(_mem, _field) ((_mem[0] == _field[0]) && (_mem[1] == _field[1]))
#define CHECK_HEADER_FIELD_BMP(_mem) \
  (CHECK_HEADER_FIELD(_mem, "BM") || CHECK_HEADER_FIELD(_mem, "BA") || \
   CHECK_HEADER_FIELD(_mem, "CI") || CHECK_HEADER_FIELD(_mem, "CP") || \
   CHECK_HEADER_FIELD(_mem, "IC") || CHECK_HEADER_FIELD(_mem, "PT"))

static bool checkbmp(const uchar *mem, const size_t size)
{
  if (size < (BMP_FILEHEADER_SIZE + sizeof(BMPINFOHEADER))) {
    return false;
  }

  if (!CHECK_HEADER_FIELD_BMP(mem)) {
    return false;
  }

  bool ok = false;
  BMPINFOHEADER bmi;
  uint u;

  /* Skip file-header. */
  mem += BMP_FILEHEADER_SIZE;

  /* for systems where an int needs to be 4 bytes aligned */
  memcpy(&bmi, mem, sizeof(bmi));

  u = LITTLE_LONG(bmi.biSize);
  /* we only support uncompressed images for now. */
  if (u >= sizeof(BMPINFOHEADER)) {
    if (bmi.biCompression == 0) {
      u = LITTLE_SHORT(bmi.biBitCount);
      if (u > 0 && u <= 32) {
        ok = true;
      }
    }
  }

  return ok;
}

bool imb_is_a_bmp(const uchar *buf, size_t size)
{
  return checkbmp(buf, size);
}

static size_t imb_bmp_calc_row_size_in_bytes(size_t x, size_t depth)
{
  /* https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader#calculating-surface-stride
   */
  return (((x * depth) + 31) & ~31) >> 3;
}

ImBuf *imb_bmp_decode(const uchar *mem, size_t size, int flags, char colorspace[IM_MAX_SPACE])
{
  ImBuf *ibuf = NULL;
  BMPINFOHEADER bmi;
  int ibuf_depth;
  const uchar *bmp;
  uchar *rect;
  ushort col;
  bool top_to_bottom = false;

  if (checkbmp(mem, size) == 0) {
    return NULL;
  }

  colorspace_set_default_role(colorspace, IM_MAX_SPACE, COLOR_ROLE_DEFAULT_BYTE);

  /* For systems where an int needs to be 4 bytes aligned. */
  memcpy(&bmi, mem + BMP_FILEHEADER_SIZE, sizeof(bmi));

  const size_t palette_offset = (size_t)BMP_FILEHEADER_SIZE + LITTLE_LONG(bmi.biSize);
  const int depth = LITTLE_SHORT(bmi.biBitCount);
  const int xppm = LITTLE_LONG(bmi.biXPelsPerMeter);
  const int yppm = LITTLE_LONG(bmi.biYPelsPerMeter);
  const int x = LITTLE_LONG(bmi.biWidth);
  int y = LITTLE_LONG(bmi.biHeight);

  /* Negative height means bitmap is stored top-to-bottom. */
  if (y < 0) {
    y = -y;
    top_to_bottom = true;
  }

  /* Validate and cross-check offsets and sizes. */
  if (x < 1 || !(ELEM(depth, 1, 4, 8, 16, 24, 32))) {
    return NULL;
  }

  const size_t pixel_data_offset = (size_t)LITTLE_LONG(*(int *)(mem + 10));
  const size_t header_bytes = BMP_FILEHEADER_SIZE + sizeof(BMPINFOHEADER);
  const size_t num_actual_data_bytes = size - pixel_data_offset;
  const size_t row_size_in_bytes = imb_bmp_calc_row_size_in_bytes(x, depth);
  const size_t num_expected_data_bytes = row_size_in_bytes * y;
  if (num_actual_data_bytes < num_expected_data_bytes || num_actual_data_bytes > size ||
      pixel_data_offset < header_bytes || pixel_data_offset > (size - num_expected_data_bytes) ||
      palette_offset < header_bytes || palette_offset > pixel_data_offset) {
    return NULL;
  }

  if (depth <= 8) {
    ibuf_depth = 24;
  }
  else {
    ibuf_depth = depth;
  }

  bmp = mem + pixel_data_offset;

#if 0
  printf("palette_offset: %d, x: %d y: %d, depth: %d\n", palette_offset, x, y, depth);
#endif

  if (flags & IB_test) {
    ibuf = IMB_allocImBuf(x, y, ibuf_depth, 0);
  }
  else {
    ibuf = IMB_allocImBuf(x, y, ibuf_depth, IB_rect);
    if (!ibuf) {
      return NULL;
    }

    rect = (uchar *)ibuf->rect;

    if (depth <= 8) {
      const char(*palette)[4] = (const char(*)[4])(mem + palette_offset);
      const int startmask = ((1 << depth) - 1) << 8;
      for (size_t i = y; i > 0; i--) {
        int index;
        int bitoffs = 8;
        int bitmask = startmask;
        int nbytes = 0;
        const char *pcol;
        if (top_to_bottom) {
          rect = (uchar *)&ibuf->rect[(i - 1) * x];
        }
        for (size_t j = x; j > 0; j--) {
          bitoffs -= depth;
          bitmask >>= depth;
          index = (bmp[0] & bitmask) >> bitoffs;
          pcol = palette[index];
          /* intentionally BGR -> RGB */
          rect[0] = pcol[2];
          rect[1] = pcol[1];
          rect[2] = pcol[0];

          rect[3] = 255;
          rect += 4;
          if (bitoffs == 0) {
            /* Advance to the next byte */
            bitoffs = 8;
            bitmask = startmask;
            nbytes += 1;
            bmp += 1;
          }
        }
        /* Advance to the next row */
        bmp += (row_size_in_bytes - nbytes);
      }
    }
    else if (depth == 16) {
      for (size_t i = y; i > 0; i--) {
        if (top_to_bottom) {
          rect = (uchar *)&ibuf->rect[(i - 1) * x];
        }
        for (size_t j = x; j > 0; j--) {
          col = bmp[0] + (bmp[1] << 8);
          rect[0] = ((col >> 10) & 0x1f) << 3;
          rect[1] = ((col >> 5) & 0x1f) << 3;
          rect[2] = ((col >> 0) & 0x1f) << 3;

          rect[3] = 255;
          rect += 4;
          bmp += 2;
        }
      }
    }
    else if (depth == 24) {
      const int x_pad = x % 4;
      for (size_t i = y; i > 0; i--) {
        if (top_to_bottom) {
          rect = (uchar *)&ibuf->rect[(i - 1) * x];
        }
        for (size_t j = x; j > 0; j--) {
          rect[0] = bmp[2];
          rect[1] = bmp[1];
          rect[2] = bmp[0];

          rect[3] = 255;
          rect += 4;
          bmp += 3;
        }
        /* for 24-bit images, rows are padded to multiples of 4 */
        bmp += x_pad;
      }
    }
    else if (depth == 32) {
      for (size_t i = y; i > 0; i--) {
        if (top_to_bottom) {
          rect = (uchar *)&ibuf->rect[(i - 1) * x];
        }
        for (size_t j = x; j > 0; j--) {
          rect[0] = bmp[2];
          rect[1] = bmp[1];
          rect[2] = bmp[0];
          rect[3] = bmp[3];
          rect += 4;
          bmp += 4;
        }
      }
    }
  }

  if (ibuf) {
    ibuf->ppm[0] = xppm;
    ibuf->ppm[1] = yppm;
    ibuf->ftype = IMB_FTYPE_BMP;
  }

  return ibuf;
}

#undef CHECK_HEADER_FIELD_BMP
#undef CHECK_HEADER_FIELD

/* Couple of helper functions for writing our data */
static int putIntLSB(uint ui, FILE *ofile)
{
  putc((ui >> 0) & 0xFF, ofile);
  putc((ui >> 8) & 0xFF, ofile);
  putc((ui >> 16) & 0xFF, ofile);
  return putc((ui >> 24) & 0xFF, ofile);
}

static int putShortLSB(ushort us, FILE *ofile)
{
  putc((us >> 0) & 0xFF, ofile);
  return putc((us >> 8) & 0xFF, ofile);
}

bool imb_savebmp(ImBuf *ibuf, const char *filepath, int UNUSED(flags))
{
  BMPINFOHEADER infoheader;

  const size_t bytes_per_pixel = (ibuf->planes + 7) >> 3;
  BLI_assert(ELEM(bytes_per_pixel, 1, 3));

  const size_t pad_bytes_per_scanline = (4 - ibuf->x * bytes_per_pixel % 4) % 4;
  const size_t bytesize = (ibuf->x * bytes_per_pixel + pad_bytes_per_scanline) * ibuf->y;

  const uchar *data = (const uchar *)ibuf->rect;
  FILE *ofile = BLI_fopen(filepath, "wb");
  if (ofile == NULL) {
    return 0;
  }

  const bool is_grayscale = bytes_per_pixel == 1;
  const size_t palette_size = is_grayscale ? 255 * 4 : 0; /* RGBA32 */
  const size_t pixel_array_start = BMP_FILEHEADER_SIZE + sizeof(infoheader) + palette_size;

  putShortLSB(19778, ofile);                      /* "BM" */
  putIntLSB(bytesize + pixel_array_start, ofile); /* Total file size */
  putShortLSB(0, ofile);                          /* Res1 */
  putShortLSB(0, ofile);                          /* Res2 */
  putIntLSB(pixel_array_start, ofile);            /* offset to start of pixel array */

  putIntLSB(sizeof(infoheader), ofile);
  putIntLSB(ibuf->x, ofile);
  putIntLSB(ibuf->y, ofile);
  putShortLSB(1, ofile);
  putShortLSB(is_grayscale ? 8 : 24, ofile);
  putIntLSB(0, ofile);
  putIntLSB(bytesize, ofile);
  putIntLSB(round(ibuf->ppm[0]), ofile);
  putIntLSB(round(ibuf->ppm[1]), ofile);
  putIntLSB(0, ofile);
  putIntLSB(0, ofile);

  /* color palette table, which is just every grayscale color, full alpha */
  if (is_grayscale) {
    for (char i = 0; i < 255; i++) {
      putc(i, ofile);
      putc(i, ofile);
      putc(i, ofile);
      putc(0xFF, ofile);
    }
  }

  if (is_grayscale) {
    for (size_t y = 0; y < ibuf->y; y++) {
      for (size_t x = 0; x < ibuf->x; x++) {
        const size_t ptr = (x + y * ibuf->x) * 4;
        if (putc(data[ptr], ofile) == EOF) {
          return 0;
        }
      }
      /* Add padding here. */
      for (size_t t = 0; t < pad_bytes_per_scanline; t++) {
        if (putc(0, ofile) == EOF) {
          return 0;
        }
      }
    }
  }
  else {
    /* Need to write out padded image data in BGR format. */
    for (size_t y = 0; y < ibuf->y; y++) {
      for (size_t x = 0; x < ibuf->x; x++) {
        const size_t ptr = (x + y * ibuf->x) * 4;
        if (putc(data[ptr + 2], ofile) == EOF) {
          return 0;
        }
        if (putc(data[ptr + 1], ofile) == EOF) {
          return 0;
        }
        if (putc(data[ptr], ofile) == EOF) {
          return 0;
        }
      }
      /* Add padding here. */
      for (size_t t = 0; t < pad_bytes_per_scanline; t++) {
        if (putc(0, ofile) == EOF) {
          return 0;
        }
      }
    }
  }

  fflush(ofile);
  fclose(ofile);
  return 1;
}
