/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

// 16-bit color masks and shifts, default is 5-5-5
static uint32_t masks[3]  = { 0x7c00, 0x3e0, 0x1f };
static uint32_t shifts[3] = { 10, 5, 0 };
static uint32_t ncolors[3] = { (1 << 5) - 1, (1 << 5) - 1, (1 << 5) - 1 };

int
image_bmp_read_header(image *im)
{
  int offset, palette_colors;

  buffer_consume(im->buf, 10);

  offset = buffer_get_int_le(im->buf);
  buffer_consume(im->buf, 4);
  im->width  = buffer_get_int_le(im->buf);
  im->height = buffer_get_int_le(im->buf);
  buffer_consume(im->buf, 2);
  im->bpp = buffer_get_short_le(im->buf);
  im->compression = buffer_get_int_le(im->buf);

  DEBUG_TRACE("BMP offset %d, width %d, height %d, bpp %d, compression %d\n",
    offset, im->width, im->height, im->bpp, im->compression);

  if (im->compression > 3) { // JPEG/PNG
    warn("Image::Scale unsupported BMP compression type: %d (%s)\n", im->compression, SvPVX(im->path));
    return 0;
  }

  // Negative height indicates a flipped image
  if (im->height < 0) {
    croak("flipped\n");
    im->flipped = 1;
    im->height = abs(im->height);
  }

  // Not used during reading, but lets output PNG be correct
  im->channels = 4;

  // Skip BMP size, resolution
  buffer_consume(im->buf, 12);

  palette_colors = buffer_get_int_le(im->buf);

  // Skip number of important colors
  buffer_consume(im->buf, 4);

  // < 16-bit always has a palette
  if (!palette_colors && im->bpp < 16) {
    switch (im->bpp) {
      case 8:
        palette_colors = 256;
        break;
      case 4:
        palette_colors = 16;
        break;
      case 1:
        palette_colors = 2;
        break;
    }
  }

  DEBUG_TRACE("palette_colors %d\n", palette_colors);
  if (palette_colors) {
    // Read palette
    int i;

    if (palette_colors > 256) {
      warn("Image::Scale cannot read BMP with palette > 256 colors (%s)\n", SvPVX(im->path));
      return 0;
    }

    New(0, im->palette, 1, palette);

    for (i = 0; i < palette_colors; i++) {
      int b = buffer_get_char(im->buf);
      int g = buffer_get_char(im->buf);
      int r = buffer_get_char(im->buf);
      buffer_consume(im->buf, 1);

      im->palette->colors[i] = COL(r, g, b);
      DEBUG_TRACE("palette %d = %08x\n", i, im->palette->colors[i]);
    }
  }
  else if (im->compression == BMP_BI_BITFIELDS) {
    int pos, bit, i;

    if (im->bpp == 16) {
      // Read 16-bit bitfield masks
      for (i = 0; i < 3; i++) {
        masks[i] = buffer_get_int_le(im->buf);

        // Determine shift value
        pos = 0;
        bit = masks[i] & -masks[i];
        while (bit) {
          pos++;
          bit >>= 1;
        }
        shifts[i] = pos - 1;

        // green can be 6 bits
        if (i == 1) {
          if (masks[1] == 0x7e0)
            ncolors[1] = (1 << 6) - 1;
          else
            ncolors[1] = (1 << 5) - 1;
        }

        DEBUG_TRACE("16bpp mask %d: %08x >> %d, ncolors %d\n", i, masks[i], shifts[i], ncolors[i]);
      }
    }
    else { // 32-bit bitfields
      // Read 32-bit bitfield masks
      for (i = 0; i < 3; i++) {
        masks[i] = buffer_get_int_le(im->buf);

        // Determine shift value
        pos = 0;
        bit = masks[i] & -masks[i];
        while (bit) {
          pos++;
          bit >>= 1;
        }
        shifts[i] = pos - 1;

        DEBUG_TRACE("32bpp mask %d: %08x >> %d\n", i, masks[i], shifts[i]);
      }
    }
  }

  // XXX make sure to skip to offset

  return 1;
}

int
image_bmp_load(image *im)
{
  int offset = 0;
  int paddingbits = 0;
  int mask = 0;
  int i, x, y, blen;
  int starty, lasty, incy, linebytes;
  unsigned char *bptr;

  // If reusing the object a second time, reset buffer
  if (im->used) {
    DEBUG_TRACE("Resetting BMP state\n");
    image_bmp_finish(im);

    buffer_clear(im->buf);

    if (im->fh != NULL) {
      // reset file to begining of image
      PerlIO_seek(im->fh, im->image_offset, SEEK_SET);

      if ( !_check_buf(im->fh, im->buf, 8, BUFFER_SIZE) ) {
        warn("Image::Scale unable to read BMP header (%s)\n", SvPVX(im->path));
        image_bmp_finish(im);
        return 0;
      }
    }
    else {
      // reset SV read
      im->sv_offset = MIN(sv_len(im->sv_data) - im->image_offset, BUFFER_SIZE);
      buffer_append(im->buf, SvPVX(im->sv_data) + im->image_offset, im->sv_offset);
    }

    image_bmp_read_header(im);
  }

  // Calculate bits of padding per line
  paddingbits = 32 - (im->width * im->bpp) % 32;
  if (paddingbits == 32)
    paddingbits = 0;

  // Bytes per line
  linebytes = ((im->width * im->bpp) + paddingbits) / 8;

  // No padding if RLE compressed
  if (paddingbits && (im->compression == BMP_BI_RLE4 || im->compression == BMP_BI_RLE8))
    paddingbits = 0;

  // XXX Don't worry about RLE support yet
  if (im->compression == BMP_BI_RLE4 || im->compression == BMP_BI_RLE8) {
    warn("Image::Scale does not support BMP RLE compression yet\n");
    image_bmp_finish(im);
    return 0;
  }

  DEBUG_TRACE("linebits %d, paddingbits %d, linebytes %d\n", im->width * im->bpp, paddingbits, linebytes);

  bptr = buffer_ptr(im->buf);
  blen = buffer_len(im->buf);

  // Allocate storage for decompressed image
	image_alloc(im, im->width, im->height);

	if (im->flipped) {
    starty = 0;
    lasty  = im->height;
    incy   = 1;
  }
  else {
    starty = im->height - 1;
    lasty  = -1;
    incy   = -1;
  }

  y = starty;

  if (im->bpp == 1)
    mask = 0x80;
  else if (im->bpp == 4)
    mask = 0xF0;

  while (y != lasty) {
    for (x = 0; x < im->width; x++) {
      if (blen <= 0 || blen < im->bpp / 8) {
        // Load more from the buffer
        if (blen < 0)
          blen = 0;

        buffer_consume(im->buf, buffer_len(im->buf) - blen);

        if (im->fh != NULL) {
          // Read from file
          if ( !_check_buf(im->fh, im->buf, im->channels, 8192) ) {
            image_bmp_finish(im);
            warn("Image::Scale unable to read entire BMP file (%s)\n", SvPVX(im->path));
            return 0;
          }
        }
        else {
          // Read from scalar
          int svbuflen = MIN(sv_len(im->sv_data) - im->sv_offset, 8192);

          if (!svbuflen) {
            image_bmp_finish(im);
            warn("Image::Scale unable to read entire BMP file (%s)\n", SvPVX(im->path));
            return 0;
          }
          buffer_append(im->buf, SvPVX(im->sv_data) + im->sv_offset, svbuflen);
          im->sv_offset += svbuflen;
        }

        bptr = buffer_ptr(im->buf);
        blen = buffer_len(im->buf);
        offset = 0;
      }

      i = x + (y * im->width);

      switch (im->bpp) {
        case 32: // XXX how to detect alpha channel?
          //im->pixbuf[i] = COL_FULL(bptr[offset+2], bptr[offset+1], bptr[offset], bptr[offset+3]);
          im->pixbuf[i] = COL(bptr[offset+2], bptr[offset+1], bptr[offset]);
          offset += 4;
          blen -= 4;
          linebytes -= 4;
          break;

        case 24: // 24-bit BGR
          im->pixbuf[i] = COL(bptr[offset+2], bptr[offset+1], bptr[offset]);
          offset += 3;
          blen -= 3;
          linebytes -= 3;
          break;

        case 16:
        {
          int p = (bptr[offset+1] << 8) | bptr[offset];
          DEBUG_TRACE("p %x (r %02x g %02x b %02x)\n", p,
              ((p & masks[0]) >> shifts[0]) * 255 / ncolors[0],
              ((p & masks[1]) >> shifts[1]) * 255 / ncolors[1],
              ((p & masks[2]) >> shifts[2]) * 255 / ncolors[2]);

          im->pixbuf[i] = COL(
            ((p & masks[0]) >> shifts[0]) * 255 / ncolors[0],
            ((p & masks[1]) >> shifts[1]) * 255 / ncolors[1],
            ((p & masks[2]) >> shifts[2]) * 255 / ncolors[2]
          );

          offset += 2;
          blen -= 2;
          linebytes -= 2;
          break;
        }

        case 8:
          im->pixbuf[i] = im->palette->colors[ bptr[offset] ];
          offset++;
          blen--;
          linebytes--;
          break;

        case 4:
          // uncompressed
          if (mask == 0xF0) {
            im->pixbuf[i] = im->palette->colors[ (bptr[offset] & mask) >> 4 ];
            mask = 0xF;
          }
          else {
            im->pixbuf[i] = im->palette->colors[ (bptr[offset] & mask) ];
            offset++;
            blen--;
            linebytes--;
            mask = 0xF0;
          }
          break;

        case 1:
          im->pixbuf[i] = im->palette->colors[ (bptr[offset] & mask) ? 1 : 0 ];
          mask >>= 1;
          if (!mask) {
            offset++;
            blen--;
            linebytes--;
            mask = 0x80;
          }
          break;
      }

      DEBUG_TRACE("x %d / y %d, linebytes left %d, pix %08x\n", x, y, linebytes, im->pixbuf[i]);
    }

    if (linebytes) {
      DEBUG_TRACE("Consuming %d bytes of padding\n", linebytes);

      if (blen < linebytes) {
        // Load more from the buffer
        buffer_consume(im->buf, buffer_len(im->buf) - blen);
        if ( !_check_buf(im->fh, im->buf, im->channels, 8192) ) {
          image_bmp_finish(im);
          warn("Image::Scale unable to read entire BMP file (%s)\n", SvPVX(im->path));
          return 0;
        }
        bptr = buffer_ptr(im->buf);
        blen = buffer_len(im->buf);
        offset = 0;
      }

      offset += linebytes;
      blen -= linebytes;

      // Reset mask for next line
      if (im->bpp == 4)
        mask = 0xF0;
      else if (im->bpp == 1)
        mask = 0x80;
    }

    linebytes = ((im->width * im->bpp) + paddingbits) / 8;

    y += incy;
  }

  // Set channels to 4 so we write a color PNG, unless bpp is 1
  if (im->bpp > 1)
    im->channels = 4;

  return 1;
}

void
image_bmp_finish(image *im)
{
  if (im->palette != NULL) {
    Safefree(im->palette);
    im->palette = NULL;
  }
}