/****************************************************************************
 * This file is part of the project AqHome.
 * AqHome (c) by 2025 Martin Preuss, all rights reserved.
 *
 * The license for this file can be found in the file COPYING which you
 * should have received along with this file.
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include "./readbmp.h"

#include <gwenhywfar/i18n.h>
#include <gwenhywfar/debug.h>
#include <gwenhywfar/text.h>
#include <gwenhywfar/args.h>



/* ------------------------------------------------------------------------------------------------
 * defs
 * ------------------------------------------------------------------------------------------------
 */

#define I18S(msg) msg
#define I18N(msg) GWEN_I18N_Translate(PACKAGE, msg)

#define A_ARG GWEN_ARGS_FLAGS_HAS_ARGUMENT
#define A_END (GWEN_ARGS_FLAGS_HELP | GWEN_ARGS_FLAGS_LAST)
#define A_CHAR GWEN_ArgsType_Char
#define A_INT GWEN_ArgsType_Int


#define BMP_FILE_OFFS_FILEHEADER       0
#define BMP_FILE_OFFS_IMAGEHEADER      14

#define BMP_FILEHEADER_OFFS_TYPE       0   /* 2 bytes ("BM") */
#define BMP_FILEHEADER_OFFS_FSIZE      2   /* 4 bytes */
#define BMP_FILEHEADER_OFFS_RESERVED1  6   /* 2 bytes */
#define BMP_FILEHEADER_OFFS_RESERVED2  8   /* 2 bytes */
#define BMP_FILEHEADER_OFFS_PIXELOFFS 10   /* 4 bytes offset to begin of pixel data */
#define BMP_FILEHEADER_SIZE           14


#define BMP_IMAGEHEADER_OFFS_HSIZE            0   /* 4 bytes header size */
#define BMP_IMAGEHEADER_OFFS_WIDTH            4   /* 4 bytes  */
#define BMP_IMAGEHEADER_OFFS_HEIGHT           8   /* 4 bytes  */
#define BMP_IMAGEHEADER_OFFS_PLANES          12   /* 2 bytes  (1) */
#define BMP_IMAGEHEADER_OFFS_BPP             14   /* 2 bytes  bit per pixel */
#define BMP_IMAGEHEADER_OFFS_COMPR           16   /* 4 bytes  (0=uncompressed) */
#define BMP_IMAGEHEADER_OFFS_IMGSIZE         20   /* 4 bytes  (may be 0 for uncompressed data) */
#define BMP_IMAGEHEADER_OFFS_XPPM            24   /* 4 bytes  X pixel per meter */
#define BMP_IMAGEHEADER_OFFS_YPPM            28   /* 4 bytes  Y pixel per meter */
#define BMP_IMAGEHEADER_OFFS_COLORMAPENTRIES 32   /* 4 bytes  number of color map entries actually used */
#define BMP_IMAGEHEADER_OFFS_IMPORTANT       36   /* 4 bytes  number of significant colors */



/* ------------------------------------------------------------------------------------------------
 * forward declarations
 * ------------------------------------------------------------------------------------------------
 */

static int AQH_Tool_ReadAndDumpBmpFile(const char *fname);
static int AQH_Tool_ExportBmpFile(const char *fname);

static int _exportBmp_1bpp(const BMP_FILE *bf);
static int _exportBmp_gray8bpp(const BMP_FILE *bf);

static GWEN_BUFFER *_extractPixels_gray8bpp(const uint8_t *ptrPixels, int imageWidth, int imageHeight);
static void _printBytes_ASM(const char *sName, const uint8_t *ptrPixels, int lenPixels, int imageWidth, int imageHeight);
static GWEN_BUFFER *_rleEncode(const uint8_t *ptrPixels, int lenPixels);
static int _countRepeats(const uint8_t *ptrPixels, int lenPixels);


static BMP_FILEHEADER *_fileHeader_new();
static void _fileHeader_free(BMP_FILEHEADER *fh);

static BMP_IMAGEHEADER *_imageHeader_new();
static void _imageHeader_free(BMP_IMAGEHEADER *ih);

static void _dumpBmpFileHeader(const BMP_FILEHEADER *fh);
static void _dumpBmpImageHeader(const BMP_IMAGEHEADER *ih);
static BMP_FILEHEADER *_readFileHeaderAt(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset);
static BMP_IMAGEHEADER *_readImageHeaderAt(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset);

static uint16_t _readUint16At(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset, uint16_t defaultValue);
static uint32_t _readUint32At(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset, uint32_t defaultValue);
static int32_t _readInt32At(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset, int32_t defaultValue);

static GWEN_BUFFER *_readFileIntoBuffer(const char *fname);




/* ------------------------------------------------------------------------------------------------
 * code
 * ------------------------------------------------------------------------------------------------
 */



int AQH_Tool_DumpBmpFile(GWEN_DB_NODE *dbGlobalArgs, int argc, char **argv)
{
  GWEN_DB_NODE *dbLocalArgs;
  int rv;
  const char *sFilename;
  const GWEN_ARGS args[]= {
    /* flags type    name              min max s    long           short_descr, long_descr */
    { A_ARG, A_CHAR, "bmpFile",        1,  1,  "i", "bmpFile",     I18S("BMP file to read"), NULL},
    { A_END, A_INT,  "help",           0,  0,  "h", "help",        I18S("Show this help screen"), NULL}
  };

  dbLocalArgs=GWEN_DB_GetGroup(dbGlobalArgs, GWEN_DB_FLAGS_DEFAULT, "local");
  rv=GWEN_Args_Check(argc, argv, 1, GWEN_ARGS_MODE_ALLOW_FREEPARAM, args, dbLocalArgs);
  if (rv==GWEN_ARGS_RESULT_ERROR) {
    fprintf(stderr, "ERROR: Could not parse arguments\n");
    return 1;
  }
  else if (rv==GWEN_ARGS_RESULT_HELP) {
    GWEN_BUFFER *ubuf;

    ubuf=GWEN_Buffer_new(0, 1024, 0, 1);
    if (GWEN_Args_Usage(args, ubuf, GWEN_ArgsOutType_Txt)) {
      fprintf(stderr, "ERROR: Could not create help string\n");
      return 1;
    }
    fprintf(stderr, "%s\n", GWEN_Buffer_GetStart(ubuf));
    GWEN_Buffer_free(ubuf);
    return 1;
  }

  sFilename=GWEN_DB_GetCharValue(dbLocalArgs, "bmpFile", 0, NULL);
  if (!(sFilename && *sFilename)) {
    fprintf(stderr, "Missing file name\n");
    return 1;
  }

  //return AQH_Tool_ReadAndDumpBmpFile(sFilename);
  return AQH_Tool_ExportBmpFile(sFilename);
}





int AQH_Tool_ReadAndDumpBmpFile(const char *fname)
{
  BMP_FILE *bf;

  bf=BMP_File_fromFile(fname);
  if (bf) {
    _dumpBmpFileHeader(bf->fileHeader);
    _dumpBmpImageHeader(bf->imageHeader);
    BMP_File_free(bf);
  }
  return 0;
}



int AQH_Tool_ExportBmpFile(const char *fname)
{
  BMP_FILE *bf;

  bf=BMP_File_fromFile(fname);
  if (bf) {
    int rv;

    _dumpBmpFileHeader(bf->fileHeader);
    _dumpBmpImageHeader(bf->imageHeader);
    if (bf->imageHeader->bitsPerPixel==1)
      rv=_exportBmp_1bpp(bf);
    else if (bf->imageHeader->bitsPerPixel==8)
      rv=_exportBmp_gray8bpp(bf);
    else {
      fprintf(stderr, "Invalid bits per pixel (%d)", bf->imageHeader->bitsPerPixel);
      rv=2;
    }

    BMP_File_free(bf);
    return rv;
  }
  return 2;;
}



int _exportBmp_1bpp(const BMP_FILE *bf)
{
  const uint8_t *ptrBuffer;
  uint32_t offsPixels;
  const uint8_t *ptrPixels;
  int imageWidth;
  int imageHeight;
  int rowWidthInBytes;
  int columns;
  int y;

  ptrBuffer=(const uint8_t *) GWEN_Buffer_GetStart(bf->buffer);
  offsPixels=bf->fileHeader->offsPixels;
  ptrPixels=ptrBuffer+offsPixels;
  imageWidth=bf->imageHeader->imgWidth;
  imageHeight=bf->imageHeader->imgHeight;
  columns=imageWidth/8;
  rowWidthInBytes=4*((columns+3)/4);  /* BMPs have multiple of 4 bytes per row! */
  fprintf(stdout, "imgData: \n");
  fprintf(stdout, "  .dw %d, %d\n", imageWidth, imageHeight);

  for (y=imageHeight-1; y>=0; y--) {
    const uint8_t *rowPtr;
    int x;

    fprintf(stdout, "  .db ");
    rowPtr=ptrPixels+(y*rowWidthInBytes);
    for (x=0; x<columns; x++) {
      if (x)
        fprintf(stdout, ", ");
      fprintf(stdout, "0x%02x", rowPtr[x]^0xff);
    }
    fprintf(stdout, "\n");
  }
  return 0;
}



int _exportBmp_gray8bpp(const BMP_FILE *bf)
{
  const uint8_t *ptrBuffer;
  uint32_t offsPixels;
  const uint8_t *ptrPixels;
  int imageWidth;
  int imageHeight;
  GWEN_BUFFER *pixelBuf;
  GWEN_BUFFER *asmBuf;

  ptrBuffer=(const uint8_t *) GWEN_Buffer_GetStart(bf->buffer);
  offsPixels=bf->fileHeader->offsPixels;
  ptrPixels=ptrBuffer+offsPixels;
  imageWidth=bf->imageHeader->imgWidth;
  imageHeight=bf->imageHeader->imgHeight;

  pixelBuf=_extractPixels_gray8bpp(ptrPixels, imageWidth, imageHeight);
  asmBuf=_rleEncode((const uint8_t*) GWEN_Buffer_GetStart(pixelBuf), GWEN_Buffer_GetUsedBytes(pixelBuf));
  _printBytes_ASM("imageData",
		  (const uint8_t*) GWEN_Buffer_GetStart(asmBuf), GWEN_Buffer_GetUsedBytes(asmBuf),
		  imageWidth, imageHeight);

  fprintf(stderr, "Compression: %d -> %d bytes\n", GWEN_Buffer_GetUsedBytes(pixelBuf), GWEN_Buffer_GetUsedBytes(asmBuf));
  GWEN_Buffer_free(asmBuf);
  GWEN_Buffer_free(pixelBuf);

  return 0;
}



GWEN_BUFFER *_extractPixels_gray8bpp(const uint8_t *ptrPixels, int imageWidth, int imageHeight)
{
  int rowWidthInBytes;
  int columns;
  int y;
  GWEN_BUFFER *destBuf;

  destBuf=GWEN_Buffer_new(0, 256, 0, 1);
  columns=imageWidth;
  rowWidthInBytes=4*((columns+3)/4);  /* BMPs have multiple of 4 bytes per row! */

  for (y=imageHeight-1; y>=0; y--) {
    const uint8_t *rowPtr;
    int x;
    uint8_t currentByte=0;
    int packedPixels=0;

    rowPtr=ptrPixels+(y*rowWidthInBytes);
    for (x=0; x<columns; x++) {
      uint8_t pixel;
      uint8_t newPix;

      pixel=rowPtr[x];
      switch(pixel) {
        case 0x00: newPix=0b01; break;  /* outline color */
        case 0x60: newPix=0b10; break;  /* color 1 */
        case 0xc0: newPix=0b11; break;  /* color 2 */
        case 0xff: newPix=0b00; break;  /* background color */
        default:   newPix=0b00; break;  /* background color */
      }
      currentByte<<=2;
      currentByte|=newPix;
      packedPixels++;
      if (packedPixels==4) {
	GWEN_Buffer_AppendByte(destBuf, currentByte);
	packedPixels=0;
	currentByte=0;
      }
    }
  }
  if (GWEN_Buffer_GetUsedBytes(destBuf)==0) {
    GWEN_Buffer_free(destBuf);
    return NULL;
  }

  return destBuf;
}



void _printBytes_ASM(const char *sName, const uint8_t *ptrPixels, int lenPixels, int imageWidth, int imageHeight)
{
  int i;

  fprintf(stdout, "%s: \n", sName);
  fprintf(stdout, "  .dw %d, %d ; width, height\n", imageWidth, imageHeight);
  for (i=0; i<lenPixels; i++) {
    uint8_t currentByte;

    currentByte=ptrPixels[i];
    if ((i & 15)==0) {
      if (i)
	fprintf(stdout, "\n");
      fprintf(stdout, "  .db 0x%02x", currentByte);
    }
    else {
      fprintf(stdout, ", 0x%02x", currentByte);
    }
  }
  fprintf(stdout, "\n");
}



GWEN_BUFFER *_rleEncode(const uint8_t *ptrPixels, int lenPixels)
{
  GWEN_BUFFER *resultBuf;
  GWEN_BUFFER *currentBuf;
  int numCurrentBuf;

  resultBuf=GWEN_Buffer_new(0, 256, 0, 1);
  currentBuf=GWEN_Buffer_new(0, 128, 0, 1);

  while(ptrPixels && lenPixels>0) {
    int countRepeats;

    countRepeats=_countRepeats(ptrPixels, lenPixels);
    if (countRepeats<4) {
      GWEN_Buffer_AppendByte(currentBuf, *ptrPixels);
      numCurrentBuf=GWEN_Buffer_GetUsedBytes(currentBuf);
      if (numCurrentBuf==127) {
	GWEN_Buffer_AppendByte(resultBuf, numCurrentBuf); /* bit 7 =0 */
	GWEN_Buffer_AppendBytes(resultBuf, GWEN_Buffer_GetStart(currentBuf), numCurrentBuf);
	GWEN_Buffer_Reset(currentBuf);
      }
      ptrPixels++;
      lenPixels--;
    }
    else {
      numCurrentBuf=GWEN_Buffer_GetUsedBytes(currentBuf);
      if (numCurrentBuf>0) {
	GWEN_Buffer_AppendByte(resultBuf, numCurrentBuf); /* bit 7 =0 */
	GWEN_Buffer_AppendBytes(resultBuf, GWEN_Buffer_GetStart(currentBuf), numCurrentBuf);
	GWEN_Buffer_Reset(currentBuf);
      }
      GWEN_Buffer_AppendByte(resultBuf, countRepeats | 128);
      GWEN_Buffer_AppendByte(resultBuf, *ptrPixels);
      ptrPixels+=countRepeats;
      lenPixels-=countRepeats;
    }
  }

  numCurrentBuf=GWEN_Buffer_GetUsedBytes(currentBuf);
  if (numCurrentBuf>0) {
    GWEN_Buffer_AppendByte(resultBuf, numCurrentBuf); /* bit 7 =0 */
    GWEN_Buffer_AppendBytes(resultBuf, GWEN_Buffer_GetStart(currentBuf), numCurrentBuf);
  }
  GWEN_Buffer_free(currentBuf);

  if (GWEN_Buffer_GetUsedBytes(resultBuf)==0) {
    GWEN_Buffer_free(resultBuf);
    return NULL;
  }

  return resultBuf;
}



int _countRepeats(const uint8_t *ptrPixels, int lenPixels)
{
  if (ptrPixels && lenPixels) {
    int currentByte;
    int count=1;

    currentByte=*(ptrPixels++);
    lenPixels--;
    while(ptrPixels && lenPixels) {
      if (*ptrPixels==currentByte) {
	count++;
	if (count==127)
	  return count;
      }
      else
	return count;

      ptrPixels++;
      lenPixels--;
    }
    return count;
  }
  return 0;
}






BMP_FILE *BMP_File_new(const char *fname)
{
  BMP_FILE *bf;

  GWEN_NEW_OBJECT(BMP_FILE, bf);
  bf->filename=fname?strdup(fname):NULL;
  return bf;
}



void BMP_File_free(BMP_FILE *bf)
{
  if (bf) {
    free(bf->filename);
    _imageHeader_free(bf->imageHeader);
    _fileHeader_free(bf->fileHeader);
    GWEN_Buffer_free(bf->buffer);
    GWEN_FREE_OBJECT(bf);
  }
}



BMP_FILE *BMP_File_fromFile(const char *fname)
{
  BMP_FILE *bf;
  const uint8_t *ptrBuffer;
  uint32_t lenBuffer;

  bf=BMP_File_new(fname);
  bf->buffer=_readFileIntoBuffer(fname);
  if (bf->buffer==NULL) {
    fprintf(stderr, "Error reading bmp file \"%s\"\n", fname);
    BMP_File_free(bf);
    return NULL;
  }
  ptrBuffer=(const uint8_t *) GWEN_Buffer_GetStart(bf->buffer);
  lenBuffer=GWEN_Buffer_GetUsedBytes(bf->buffer);

  bf->fileHeader=_readFileHeaderAt(ptrBuffer, lenBuffer, 0);
  if (bf->fileHeader==NULL) {
    fprintf(stderr, "Error reading bmp file header from \"%s\"\n", fname);
    BMP_File_free(bf);
    return NULL;
  }

  bf->imageHeader=_readImageHeaderAt(ptrBuffer, lenBuffer, BMP_FILE_OFFS_IMAGEHEADER);
  if (bf->imageHeader==NULL) {
    fprintf(stderr, "Error reading bmp image header from \"%s\"\n", fname);
    BMP_File_free(bf);
    return NULL;
  }

  return bf;
}





BMP_FILEHEADER *_fileHeader_new()
{
  BMP_FILEHEADER *fh;

  GWEN_NEW_OBJECT(BMP_FILEHEADER, fh);
  return fh;
}



void _fileHeader_free(BMP_FILEHEADER *fh)
{
  if (fh) {
    GWEN_FREE_OBJECT(fh);
  }
}



BMP_IMAGEHEADER *_imageHeader_new()
{
  BMP_IMAGEHEADER *ih;

  GWEN_NEW_OBJECT(BMP_IMAGEHEADER, ih);
  return ih;
}



void _imageHeader_free(BMP_IMAGEHEADER *ih)
{
  if (ih) {
    GWEN_FREE_OBJECT(ih);
  }
}



void _dumpBmpFileHeader(const BMP_FILEHEADER *fh)
{
  fprintf(stderr, "BMP File Header:\n");
  fprintf(stderr, "- file type: %d\n", fh->fileType);
  fprintf(stderr, "- file size: %d\n", fh->fileSize);
  fprintf(stderr, "- Pixels at: %08x\n", fh->offsPixels);
}



void _dumpBmpImageHeader(const BMP_IMAGEHEADER *ih)
{
  fprintf(stderr, "BMP Image Header:\n");
  fprintf(stderr, "- dims         : %d x %d (%d planes, %d bpp)\n", ih->imgWidth, ih->imgHeight, ih->imgPlanes, ih->bitsPerPixel);
  fprintf(stderr, "- compression  : %d\n", ih->compression);
  fprintf(stderr, "- image size   : %d\n", ih->imgSize);
  fprintf(stderr, "- resolution   : %d x %d per meter\n", ih->imgXPixelsPerMeter, ih->imgYPixelsPerMeter);
  fprintf(stderr, "- used colormap: %d entries (%d important)\n", ih->colorMapUsedEntries, ih->colorMapImportantColors);
}





BMP_FILEHEADER *_readFileHeaderAt(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset)
{
  BMP_FILEHEADER *fh;

  if (lenBuffer<(offset+BMP_FILEHEADER_SIZE)) {
    DBG_ERROR(NULL, "Offset 0x%08x out of boundary", offset);
    return NULL;
  }

  fh=_fileHeader_new();
  fh->fileType=_readUint16At(ptrBuffer, lenBuffer, offset+BMP_FILEHEADER_OFFS_TYPE, 0);
  fh->fileSize=_readUint32At(ptrBuffer, lenBuffer, offset+BMP_FILEHEADER_OFFS_FSIZE, 0);
  fh->offsPixels=_readUint32At(ptrBuffer, lenBuffer, offset+BMP_FILEHEADER_OFFS_PIXELOFFS, 0xffffffff);

  return fh;
}



BMP_IMAGEHEADER *_readImageHeaderAt(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset)
{
  BMP_IMAGEHEADER *ih;
  uint32_t hsize;

  hsize=_readUint32At(ptrBuffer, lenBuffer, BMP_FILE_OFFS_IMAGEHEADER, 0xffffffff);
  if (lenBuffer<(offset+hsize)) {
    DBG_ERROR(NULL, "Offset 0x%08x out of boundary", offset);
    return NULL;
  }

  ih=_imageHeader_new();
  ih->imgWidth=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_WIDTH, 0);
  ih->imgHeight=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_HEIGHT, 0);
  ih->imgPlanes=_readUint16At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_PLANES, 0);
  ih->bitsPerPixel=_readUint16At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_BPP, 0);
  ih->compression=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_COMPR, 0);
  ih->imgSize=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_IMGSIZE, 0);
  ih->imgXPixelsPerMeter=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_XPPM, 0);
  ih->imgYPixelsPerMeter=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_YPPM, 0);
  ih->colorMapUsedEntries=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_COLORMAPENTRIES, 0);
  ih->colorMapImportantColors=_readInt32At(ptrBuffer, lenBuffer, offset+BMP_IMAGEHEADER_OFFS_IMPORTANT, 0);

  return ih;
}



uint16_t _readUint16At(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset, uint16_t defaultValue)
{
  uint16_t v;

  if (lenBuffer<(offset+2)) {
    DBG_ERROR(NULL, "Offset 0x%08x out of boundary", offset);
    return defaultValue;
  }
  v=ptrBuffer[offset]+(ptrBuffer[offset+1]<<8);
  return v;
}



uint32_t _readUint32At(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset, uint32_t defaultValue)
{
  uint32_t v;

  if (lenBuffer<(offset+4)) {
    DBG_ERROR(NULL, "Offset 0x%08x out of boundary", offset);
    return defaultValue;
  }
  v=ptrBuffer[offset]+(ptrBuffer[offset+1]<<8)+(ptrBuffer[offset+2]<<16)+(ptrBuffer[offset+2]<<16);
  return v;
}



int32_t _readInt32At(const uint8_t *ptrBuffer, uint32_t lenBuffer, uint32_t offset, int32_t defaultValue)
{
  uint32_t v;

  if (lenBuffer<(offset+4)) {
    DBG_ERROR(NULL, "Offset 0x%08x out of boundary", offset);
    return defaultValue;
  }
  v=ptrBuffer[offset]+(ptrBuffer[offset+1]<<8)+(ptrBuffer[offset+2]<<16)+(ptrBuffer[offset+2]<<16);
  return (int32_t) v;
}




GWEN_BUFFER *_readFileIntoBuffer(const char *fname)
{
  GWEN_BUFFER *buf;
  int rv;

  buf=GWEN_Buffer_new(0, 1024, 0, 1);
  rv=GWEN_SyncIo_Helper_ReadFile(fname, buf);
  if (rv<0) {
    DBG_ERROR(NULL, "Error reading file \"%s\": %d", fname?fname:"<empty>", rv);
    GWEN_Buffer_free(buf);
    return NULL;
  }

  return buf;
}





