/*
   uPNG -- derived from LodePNG version 20100808

   Copyright (c) 2005-2010 Lode Vandevenne
   Copyright (c) 2010 Sean Middleditch
   Copyright (c) 2013-2014 Matthew Hungerford
   Copyright (c) 2015 by Pebble Inc.

   This software is provided 'as-is', without any express or implied
   warranty. In no event will the authors be held liable for any damages
   arising from the use of this software.

   Permission is granted to anyone to use this software for any purpose,
   including commercial applications, and to alter it and redistribute it
   freely, subject to the following restrictions:

   1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software
   in a product, an acknowledgment in the product documentation would be
   appreciated but is not required.

   2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

   3. This notice may not be removed or altered from any source
   distribution.
   */

/* Version history:
   0.0   8 Aug 2010  LodePNG 20100808 release
   1.0  19 Oct 2010  Initial uPNG based on LodePNG 20100808, huffman decoder, SDL/GL viewer
   1.1  11 Dec 2013  Reduced huffman data overhead and moved huffman tables to heap
   1.2  10 Mar 2014  Support non-byte-aligned images (fixes 1,2,4 bit PNG8 support)
   1.3  11 Feb 2015  Add PNG8 alpha_palette support.  Add APNG support (iterative frame decoding)
   1.4  14 Dec 2015  Replace built-in huffman inflate with tinflate (tiny inflate)
 */

#include "upng.h"

#include "kernel/pbl_malloc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <stdint.h>
#include <stdbool.h>

#include "tinflate.h"

// Used to allow direct character buffer access with offsets and byteswap in place
#define MAKE_WORD_PTR(p) MAKE_WORD((p)[0], (p)[1], (p)[2], (p)[3])
#define MAKE_SHORT_PTR(p) (uint16_t)((MAKE_BYTE((p)[0]) << 8) | MAKE_BYTE((p)[1]))

#define FIRST_LENGTH_CODE_INDEX 257
#define LAST_LENGTH_CODE_INDEX 285

/*256 literals, the end code, some length codes, and 2 unused codes */
#define NUM_DEFLATE_CODE_SYMBOLS 288
/*the distance codes have their own symbols, 30 used, 2 unused */
#define NUM_DISTANCE_SYMBOLS 32
/* the code length codes. 0-15: code lengths, 16: copy previous 3-6 times,
 * 17: 3-10 zeros, 18: 11-138 zeros */
#define NUM_CODE_LENGTH_CODES 19
/* largest number of symbols used by any tree type */
#define MAX_SYMBOLS 288

#define DEFLATE_CODE_BITLEN 15
#define DISTANCE_BITLEN 15
#define CODE_LENGTH_BITLEN 7
#define MAX_BIT_LENGTH 15 // bug? 15 /* largest bitlen used by any tree type */

#define DEFLATE_CODE_BUFFER_SIZE (NUM_DEFLATE_CODE_SYMBOLS * 2)
#define DISTANCE_BUFFER_SIZE (NUM_DISTANCE_SYMBOLS * 2)
#define CODE_LENGTH_BUFFER_SIZE (NUM_DISTANCE_SYMBOLS * 2)

#define SET_ERROR(upng, code) do {(upng)->error = (code); (upng)->error_line = __LINE__;} while (0)

#define upng_chunk_data_length(chunk) MAKE_WORD_PTR(chunk)
#define upng_chunk_type(chunk) MAKE_WORD_PTR((chunk) + 4)
#define upng_chunk_data(chunk) ((chunk) + 8)

#define upng_chunk_type_critical(chunk_type) (((chunk_type) & 0x20000000) == 0)

typedef enum upng_state {
  UPNG_ERROR   = -1,
  UPNG_DECODED = 0,
  UPNG_LOADED  = 1, // Global data loaded (Palette) (APNG control data)
  UPNG_HEADER  = 2,
  UPNG_NEW     = 3
} upng_state;

typedef enum upng_color {
  UPNG_LUM  = 0,
  UPNG_RGB  = 2,
  UPNG_PLT  = 3,
  UPNG_LUMA = 4,
  UPNG_RGBA = 6
} upng_color;

typedef struct upng_source {
  const uint8_t* buffer;
  uint32_t   size;
  char     owning;
} upng_source;

struct upng_t {
  uint32_t  width;
  uint32_t  height;

  rgb *palette;
  uint16_t palette_entries;

  uint8_t *alpha_palette;
  uint16_t alpha_palette_entries;

  upng_color  color_type;
  uint32_t  color_depth;
  upng_format  format;

  const uint8_t* cursor; // data cursor for parsing linearly
  uint8_t* buffer;
  uint32_t size;

  // APNG information for image at current frame
  bool is_apng;
  apng_fctl* apng_frame_control;
  uint32_t apng_num_frames;
  uint32_t apng_num_plays;  // 0 indicates infinite looping
  uint32_t apng_duration_ms;

  upng_error  error;
  uint32_t  error_line;

  upng_state  state;
  upng_source  source;
};

static uint8_t read_bit(uint32_t *bitpointer, const uint8_t *bitstream) {
  uint8_t result =
    (uint8_t)((bitstream[(*bitpointer) >> 3] >> ((*bitpointer) & 0x7)) & 1);
  (*bitpointer)++;
  return result;
}

static void inflate_uncompressed(upng_t* upng, uint8_t* out, uint32_t outsize,
    const uint8_t *in, uint32_t *bp, uint32_t *pos, uint32_t inlength) {
  uint32_t p;
  uint16_t len, nlen, n;

  /* go to first boundary of byte */
  while (((*bp) & 0x7) != 0) {
    (*bp)++;
  }
  p = (*bp) / 8;  /*byte position */

  /* read len (2 bytes) and nlen (2 bytes) */
  if (p >= inlength - 4) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return;
  }

  len = in[p] + 256 * in[p + 1];
  p += 2;
  nlen = in[p] + 256 * in[p + 1];
  p += 2;

  /* check if 16-bit nlen is really the one's complement of len */
  if (len + nlen != UINT16_MAX) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return;
  }

  if ((*pos) + len > outsize) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return;
  }

  /* read the literal data: len bytes are now stored in the out buffer */
  if (p + len > inlength) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return;
  }

  for (n = 0; n < len; n++) {
    out[(*pos)++] = in[p++];
  }

  (*bp) = p * 8;
}

/*inflate the deflated data (cfr. deflate spec); return value is the error*/
static upng_error uz_inflate_data(upng_t* upng, uint8_t* out, uint32_t outsize,
    const uint8_t *in, uint32_t insize, uint32_t inpos) {
  /*bit pointer in the "in" data, current byte is bp >> 3,
   * current bit is bp & 0x7 (from lsb to msb of the byte) */
  uint32_t bp = 0;
  uint32_t pos = 0; /*byte position in the out buffer */

  uint16_t done = 0;

  while (done == 0) {
    uint16_t btype;

    /* ensure next bit doesn't point past the end of the buffer */
    if ((bp >> 3) >= insize) {
      SET_ERROR(upng, UPNG_EMALFORMED);
      return upng->error;
    }

    /* read block control bits */
    done = read_bit(&bp, &in[inpos]);
    btype = read_bit(&bp, &in[inpos]) | (read_bit(&bp, &in[inpos]) << 1);

    /* process control type appropriateyly */
    if (btype == 3) {
      SET_ERROR(upng, UPNG_EMALFORMED);
      return upng->error;
    } else if (btype == 0) {
      inflate_uncompressed(upng, out, outsize, &in[inpos], &bp, &pos, insize); /*no compression */
    } else {
      /*compression, btype 01 or 10 */
      int tinflate_status = tinflate_uncompress(out, (unsigned int *)&outsize, &in[inpos], insize);
      if (tinflate_status < 0) {
        SET_ERROR(upng, UPNG_EMALFORMED);
        return upng->error;
      }
      done = 1;  // No need to increment bp, tinflate handles end-of-bounds marker
    }

    /* stop if an error has occured */
    if (upng->error != UPNG_EOK) {
      return upng->error;
    }
  }

  return upng->error;
}

static upng_error uz_inflate(upng_t* upng, uint8_t *out, uint32_t outsize,
    const uint8_t *in, uint32_t insize) {
  /* we require two bytes for the zlib data header */
  if (insize < 2) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /* 256 * in[0] + in[1] must be a multiple of 31,
   * the FCHECK value is supposed to be made that way */
  if ((in[0] * 256 + in[1]) % 31 != 0) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /*error: only compression method 8: inflate with sliding window of 32k
   * is supported by the PNG spec */
  if ((in[0] & 15) != 8 || ((in[0] >> 4) & 15) > 7) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /* the specification of PNG says about the zlib stream:
   * "The additional flags shall not specify a preset dictionary." */
  if (((in[1] >> 5) & 1) != 0) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /* create output buffer */
  uz_inflate_data(upng, out, outsize, in, insize, 2);

  return upng->error;
}

/*Paeth predicter, used by PNG filter type 4*/
static int32_t paeth_predictor(int32_t a, int32_t b, int32_t c) {
  int32_t p = a + b - c;
  int32_t pa = p > a ? p - a : a - p;
  int32_t pb = p > b ? p - b : b - p;
  int32_t pc = p > c ? p - c : c - p;

  if (pa <= pb && pa <= pc)
    return a;
  else if (pb <= pc)
    return b;
  else
    return c;
}

static void unfilter_scanline(upng_t* upng, uint8_t *recon, const uint8_t *scanline,
    const uint8_t *precon, uint32_t bytewidth, uint8_t filterType,
    uint32_t length) {
  /*
     For PNG filter method 0
     unfilter a PNG image scanline by scanline.
     When the pixels are smaller than 1 byte, the filter works byte per byte (bytewidth = 1)
     precon is the previous unfiltered scanline, recon the result, scanline the current one
     the incoming scanlines do NOT include the filtertype byte,
     that one is given in the parameter filterType instead
     recon and scanline MAY be the same memory address! precon must be disjoint.
     */

  uint32_t i;
  switch (filterType) {
    case 0:
      for (i = 0; i < length; i++)
        recon[i] = scanline[i];
      break;
    case 1:
      for (i = 0; i < bytewidth; i++)
        recon[i] = scanline[i];
      for (i = bytewidth; i < length; i++)
        recon[i] = scanline[i] + recon[i - bytewidth];
      break;
    case 2:
      if (precon)
        for (i = 0; i < length; i++)
          recon[i] = scanline[i] + precon[i];
      else
        for (i = 0; i < length; i++)
          recon[i] = scanline[i];
      break;
    case 3:
      if (precon) {
        for (i = 0; i < bytewidth; i++)
          recon[i] = scanline[i] + precon[i] / 2;
        for (i = bytewidth; i < length; i++)
          recon[i] = scanline[i] + ((recon[i - bytewidth] + precon[i]) / 2);
      } else {
        for (i = 0; i < bytewidth; i++)
          recon[i] = scanline[i];
        for (i = bytewidth; i < length; i++)
          recon[i] = scanline[i] + recon[i - bytewidth] / 2;
      }
      break;
    case 4:
      if (precon) {
        for (i = 0; i < bytewidth; i++)
          recon[i] = (uint8_t)(scanline[i] + paeth_predictor(0, precon[i], 0));
        for (i = bytewidth; i < length; i++)
          recon[i] = (uint8_t)(scanline[i] + paeth_predictor(recon[i - bytewidth],
                precon[i], precon[i - bytewidth]));
      } else {
        for (i = 0; i < bytewidth; i++)
          recon[i] = scanline[i];
        for (i = bytewidth; i < length; i++)
          recon[i] = (uint8_t)(scanline[i] + paeth_predictor(recon[i - bytewidth], 0, 0));
      }
      break;
    default:
      SET_ERROR(upng, UPNG_EMALFORMED);
      break;
  }
}

static void unfilter(upng_t* upng, uint8_t *out, const uint8_t *in,
    uint32_t w, uint32_t h, uint32_t bpp) {
  /*
     For PNG filter method 0
     this function unfilters a single image
     (e.g. without interlacing this is called once, with Adam7 it's called 7 times)
     out must have enough bytes allocated already,
     in must have the scanlines + 1 filtertype byte per scanline
     w and h are image dimensions or dimensions of reduced image, bpp is bpp per pixel
     in and out are allowed to be the same memory address!
     */

  uint32_t y;
  uint8_t *prevline = 0;

  /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise */
  uint32_t bytewidth = (bpp + 7) / 8;
  uint32_t linebytes = (w * bpp + 7) / 8;

  for (y = 0; y < h; y++) {
    uint32_t outindex = linebytes * y;
    uint32_t inindex = (1 + linebytes) * y; /*the extra filterbyte added to each row */
    uint8_t filterType = in[inindex];

    unfilter_scanline(upng, &out[outindex], &in[inindex + 1], prevline, bytewidth, filterType,
        linebytes);
    if (upng->error != UPNG_EOK) {
      return;
    }

    prevline = &out[outindex];
  }
}

static void remove_padding_bits(uint8_t *out, const uint8_t *in,
    uint32_t olinebits, uint32_t ilinebits, uint32_t h) {
  /*
     After filtering there are still padding bpp if scanlines have non multiple of 8 bit amounts.
     They need to be removed (except at last scanline of (Adam7-reduced) image)
     before working with pure image buffers for the Adam7 code,
     the color convert code and the output to the user.
     in and out are allowed to be the same buffer, in may also be higher but still overlapping;
     in must have >= ilinebits*h bpp, out must have >= olinebits*h bpp,
     olinebits must be <= ilinebits
     also used to move bpp after earlier such operations happened,
     e.g. in a sequence of reduced images from Adam7
     only useful if (ilinebits - olinebits) is a value in the range 1..7
     */
  uint32_t y;
  uint32_t diff = ilinebits - olinebits;
  uint32_t obp = 0, ibp = 0; /*bit pointers */
  for (y = 0; y < h; y++) {
    uint32_t x;
    for (x = 0; x < olinebits; x++) {
      uint8_t bit = (uint8_t)((in[(ibp) >> 3] >> (7 - ((ibp) & 0x7))) & 1);
      ibp++;

      if (bit == 0)
        out[(obp) >> 3] &= (uint8_t)(~(1 << (7 - ((obp) & 0x7))));
      else
        out[(obp) >> 3] |= (1 << (7 - ((obp) & 0x7)));
      ++obp;
    }
    ibp += diff;
  }
}

/*out must be buffer big enough to contain full image,
 * and in must contain the full decompressed data from the IDAT chunks*/
static void post_process_scanlines(upng_t* upng, uint8_t *out, uint8_t *in,
    uint32_t bpp, uint32_t w, uint32_t h) {
  if (bpp == 0) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return;
  }

  if (bpp < 8 && w * bpp != ((w * bpp + 7) / 8) * 8) {
    unfilter(upng, in, in, w, h, bpp);
    if (upng->error != UPNG_EOK) {
      return;
    }
    // remove_padding_bits(out, in, w * bpp, ((w * bpp + 7) / 8) * 8, h);
    // fix for non-byte-aligned images
    uint32_t aligned_width = ((w * bpp + 7) / 8) * 8;
    remove_padding_bits(in, in, aligned_width, aligned_width, h);
  } else {
    /*we can immediatly filter into the out buffer, no other steps needed */
    unfilter(upng, in, in, w, h, bpp);
  }
}

static upng_format determine_format(upng_t* upng) {
  switch (upng->color_type) {
    case UPNG_PLT:
      switch (upng->color_depth) {
        case 1:
          return UPNG_INDEXED1;
        case 2:
          return UPNG_INDEXED2;
        case 4:
          return UPNG_INDEXED4;
        case 8:
          return UPNG_INDEXED8;
        default:
          return UPNG_BADFORMAT;
      }
    case UPNG_LUM:
      switch (upng->color_depth) {
        case 1:
          return UPNG_LUMINANCE1;
        case 2:
          return UPNG_LUMINANCE2;
        case 4:
          return UPNG_LUMINANCE4;
        case 8:
          return UPNG_LUMINANCE8;
        default:
          return UPNG_BADFORMAT;
      }
    case UPNG_RGB:
      switch (upng->color_depth) {
        case 8:
          return UPNG_RGB8;
        case 16:
          return UPNG_RGB16;
        default:
          return UPNG_BADFORMAT;
      }
    case UPNG_LUMA:
      switch (upng->color_depth) {
        case 1:
          return UPNG_LUMINANCE_ALPHA1;
        case 2:
          return UPNG_LUMINANCE_ALPHA2;
        case 4:
          return UPNG_LUMINANCE_ALPHA4;
        case 8:
          return UPNG_LUMINANCE_ALPHA8;
        default:
          return UPNG_BADFORMAT;
      }
    case UPNG_RGBA:
      switch (upng->color_depth) {
        case 8:
          return UPNG_RGBA8;
        case 16:
          return UPNG_RGBA16;
        default:
          return UPNG_BADFORMAT;
      }
    default:
      return UPNG_BADFORMAT;
  }
}

static void upng_free_source(upng_t* upng) {
  if (!upng) {
    return;
  }

  if (upng->source.owning != 0) {
    task_free((void*)upng->source.buffer);
  }

  upng->source.buffer = NULL;
  upng->source.size = 0;
  upng->source.owning = 0;
}

/*read the information from the header and store it in the upng_Info. return value is error*/
upng_error upng_header(upng_t* upng) {
  /* if we have an error state, bail now */
  if (upng->error != UPNG_EOK) {
    return upng->error;
  }

  /* if the state is not NEW (meaning we are ready to parse the header), stop now */
  if (upng->state != UPNG_NEW) {
    return upng->error;
  }

  // verify minimum length for a valid PNG file
  if (upng->source.size < PNG_HEADER_SIZE) {
    SET_ERROR(upng, UPNG_ENOTPNG);
    return upng->error;
  }

  /* check that PNG header matches expected value */
  if (MAKE_WORD_PTR(upng->source.buffer) != PNG_SIGNATURE) {
    SET_ERROR(upng, UPNG_ENOTPNG);
    return upng->error;
  }

  /* check that the first chunk is the IHDR chunk */
  if (MAKE_WORD_PTR(upng->source.buffer + CHUNK_META_SIZE) != CHUNK_IHDR) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /* read the values given in the header */
  upng->width = MAKE_WORD_PTR(upng->source.buffer + 16);
  upng->height = MAKE_WORD_PTR(upng->source.buffer + 20);
  upng->color_depth = upng->source.buffer[24];
  upng->color_type = (upng_color)upng->source.buffer[25];

  /* determine our color format */
  upng->format = determine_format(upng);
  if (upng->format == UPNG_BADFORMAT) {
    SET_ERROR(upng, UPNG_EUNFORMAT);
    return upng->error;
  }

  /* check that the compression method (byte 27) is 0 (only allowed value in spec) */
  if (upng->source.buffer[26] != 0) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /* check that the compression method (byte 27) is 0 (only allowed value in spec) */
  if (upng->source.buffer[27] != 0) {
    SET_ERROR(upng, UPNG_EMALFORMED);
    return upng->error;
  }

  /* check that the compression method (byte 27) is 0
   * (spec allows 1, but uPNG does not support it) */
  if (upng->source.buffer[28] != 0) {
    SET_ERROR(upng, UPNG_EUNINTERLACED);
    return upng->error;
  }

  upng->state = UPNG_HEADER;
  return upng->error;
}


upng_error upng_decode_metadata(upng_t* upng) {
  /* if we have an error state, bail now */
  if (upng->error != UPNG_EOK) {
    return upng->error;
  }

  /* parse the main header, if necessary */
  if (upng->state != UPNG_HEADER) {
    upng_header(upng);
    if ((upng->error != UPNG_EOK) || (upng->state != UPNG_HEADER)) {
      return upng->error;
    }
  }

  /* first byte of the first chunk after the header */
  upng->cursor = upng->source.buffer + PNG_HEADER_SIZE;

  /* scan through the chunks, finding the size of all IDAT chunks, and also
   * verify general well-formed-ness */
  while (upng->cursor < upng->source.buffer + upng->source.size) {
    uint32_t chunk_type = upng_chunk_type(upng->cursor);
    uint32_t data_length = upng_chunk_data_length(upng->cursor);
    const uint8_t *data = upng_chunk_data(upng->cursor);

    /* make sure chunk header+paylaod is not larger than the total compressed */
    if ((uint32_t)(upng->cursor - upng->source.buffer + data_length + CHUNK_META_SIZE) >
        upng->source.size) {
      SET_ERROR(upng, UPNG_EMALFORMED);
      return upng->error;
    }

    /* parse chunks */
    switch (chunk_type) {
      case CHUNK_PLTE:
        task_free(upng->palette);
        upng->palette_entries = data_length / 3; // 3 bytes per color entry
        upng->palette = task_malloc(data_length);
        if (upng->palette == NULL) {
          SET_ERROR(upng, UPNG_ENOMEM);
          return upng->error;
        }
        memcpy(upng->palette, data, data_length);
        break;
      case CHUNK_TRNS:
        task_free(upng->alpha_palette);
        upng->alpha_palette_entries = data_length; // 1 byte per color entry
        // protect against tools that create a tRNS chunk with 0 entries
        if (data_length) {
          upng->alpha_palette = task_malloc(data_length);
          if (upng->alpha_palette == NULL) {
            SET_ERROR(upng, UPNG_ENOMEM);
            return upng->error;
          }
          memcpy(upng->alpha_palette, data, data_length);
        }
        break;
      case CHUNK_FCTL:
        if (upng->apng_frame_control == NULL) {
          upng->apng_frame_control = task_malloc(sizeof(apng_fctl));
          if (upng->apng_frame_control == NULL) {
            SET_ERROR(upng, UPNG_ENOMEM);
            return upng->error;
          }
        }

        /* FCTL - Frame Control CHUNK
         byte
         0    sequence_number (uint32_t) Sequence number of the animation chunk, starting from 0
         4    width           (uint32_t) Width of the following frame
         8    height          (uint32_t) Height of the following frame
         12    x_offset       (uint32_t) X position at which to render the following frame
         16    y_offset       (uint32_t) Y position at which to render the following frame
         20    delay_num      (uint16_t) Frame delay fraction numerator
         22    delay_den      (uint16_t) Frame delay fraction denominator
         24    dispose_op     (uint8_t)  Frame area disposal to be done after rendering this frame
         25    blend_op       (uint8_t)  Type of frame area rendering for this frame
         */

        upng->apng_frame_control->sequence_number = MAKE_WORD_PTR(data);
        upng->apng_frame_control->width = MAKE_WORD_PTR(data + 4);
        upng->apng_frame_control->height = MAKE_WORD_PTR(data + 8);
        upng->apng_frame_control->x_offset = MAKE_WORD_PTR(data + 12);
        upng->apng_frame_control->y_offset = MAKE_WORD_PTR(data + 16);
        upng->apng_frame_control->delay_num = MAKE_SHORT_PTR(data + 20);
        upng->apng_frame_control->delay_den = MAKE_SHORT_PTR(data + 22);
        upng->apng_frame_control->dispose_op = *(data + 24);
        upng->apng_frame_control->blend_op = *(data + 25);
        break;
      case CHUNK_ACTL:
        upng->is_apng = true;
        upng->apng_num_frames = MAKE_WORD_PTR(data);
        upng->apng_num_plays = MAKE_WORD_PTR(data + 4);
        break;
      case CHUNK_IDAT:
        // Stop at these chunks and leave for another stage
        upng->state = UPNG_LOADED;
        return upng->error;
        break;
      case CHUNK_IEND:
        SET_ERROR(upng, UPNG_EMALFORMED);
        upng->state = UPNG_ERROR;
        return upng->error;
        break;
      default:
        if (upng_chunk_type_critical(chunk_type)) {
          SET_ERROR(upng, UPNG_EUNSUPPORTED);
          upng->cursor += data_length + CHUNK_META_SIZE; // forward cursor to next chunk
          return upng->error;
        }
        break;
    }
    upng->cursor += data_length + CHUNK_META_SIZE; // forward cursor to next chunk
  }

  upng->state = UPNG_LOADED;
  return upng->error;
}

/*read a PNG, the result will be in the same color type as the PNG (hence "generic")*/
upng_error upng_decode_image(upng_t* upng) {
  uint8_t* compressed = NULL;
  uint8_t* inflated = NULL;
  uint32_t compressed_size = 0;
  uint32_t inflated_size = 0;
  bool cursor_at_next_frame = false;

  /* if we have an error state, bail now */
  if (upng->error != UPNG_EOK) {
    return upng->error;
  }

  /* parse the main header and additional global data, if necessary */
  if (upng->state != UPNG_LOADED && upng->state != UPNG_DECODED) {
    upng_decode_metadata(upng);
    if (upng->error != UPNG_EOK || upng->state != UPNG_LOADED) {
      return upng->error;
    }
  }


  /* release old result, if any */
  if (upng->buffer) {
    task_free(upng->buffer);
    upng->buffer = NULL;
    upng->size = 0;
  }


  /* scan through the chunks, finding the size of all IDAT chunks, and also
   * verify general well-formed-ness */
  while ((upng->cursor < upng->source.buffer + upng->source.size) && !cursor_at_next_frame) {
    uint32_t chunk_type = upng_chunk_type(upng->cursor);
    uint32_t data_length = upng_chunk_data_length(upng->cursor);
    const uint8_t *data = upng_chunk_data(upng->cursor);

    /* make sure chunk header+payload is not larger than the total compressed */
    if ((uint32_t)(upng->cursor - upng->source.buffer + data_length + CHUNK_META_SIZE) >
        upng->source.size) {
      SET_ERROR(upng, UPNG_EMALFORMED);
      return upng->error;
    }

    /* parse chunks */
    switch (chunk_type) {
      case CHUNK_FCTL:
        if (upng->apng_frame_control == NULL) {
          upng->apng_frame_control = task_malloc(data_length);
          if (upng->apng_frame_control == NULL) {
            SET_ERROR(upng, UPNG_ENOMEM);
            return upng->error;
          }
        }

        /* FCTL - Frame Control CHUNK
         byte
         0    sequence_number (uint32_t) Sequence number of the animation chunk, starting from 0
         4    width           (uint32_t) Width of the following frame
         8    height          (uint32_t) Height of the following frame
         12    x_offset       (uint32_t) X position at which to render the following frame
         16    y_offset       (uint32_t) Y position at which to render the following frame
         20    delay_num      (uint16_t) Frame delay fraction numerator
         22    delay_den      (uint16_t) Frame delay fraction denominator
         24    dispose_op     (uint8_t)  Frame area disposal to be done after rendering this frame
         25    blend_op       (uint8_t)  Type of frame area rendering for this frame
         */

        upng->apng_frame_control->sequence_number = MAKE_WORD_PTR(data);
        upng->apng_frame_control->width = MAKE_WORD_PTR(data + 4);
        upng->apng_frame_control->height = MAKE_WORD_PTR(data + 8);
        upng->apng_frame_control->x_offset = MAKE_WORD_PTR(data + 12);
        upng->apng_frame_control->y_offset = MAKE_WORD_PTR(data + 16);
        upng->apng_frame_control->delay_num = MAKE_SHORT_PTR(data + 20);
        upng->apng_frame_control->delay_den = MAKE_SHORT_PTR(data + 22);
        upng->apng_frame_control->dispose_op = *(data + 24);
        upng->apng_frame_control->blend_op = *(data + 25);
        break;
      case CHUNK_FDAT:
        /* first 4 bytes in fdAT is sequence number, so skip 4 bytes */
        // TODO : fix for multiple consecutive fdAT chunks (PBL-14294)
        compressed = (uint8_t*)(data + 4);
        compressed_size = (data_length - 4);
        cursor_at_next_frame = true; // stop processing chunks at the IDAT/fdAT chunks
        break;
      case CHUNK_IDAT:
        // TODO : fix for multiple consecutive IDAT chunks (PBL-14294)
        compressed = (uint8_t*)(data);
        compressed_size = data_length;
        cursor_at_next_frame = true; // stop processing chunks at the IDAT/fdAT chunks
        break;
      case CHUNK_IEND:
        SET_ERROR(upng, UPNG_EDONE);
        upng->state = UPNG_ERROR; // force future calls to fail
        return upng->error;
        break;
      default:
        if (upng_chunk_type_critical(chunk_type)) {
          SET_ERROR(upng, UPNG_EUNSUPPORTED);
          upng->cursor += data_length + CHUNK_META_SIZE; // forward cursor to next chunk
          return upng->error;
        }
        break;
    }
    upng->cursor += data_length + CHUNK_META_SIZE; // forward cursor to next chunk
  }


  uint32_t width = upng->width;
  uint32_t height = upng->height;
  if (upng->apng_frame_control) {
    width = upng->apng_frame_control->width;
    height = upng->apng_frame_control->height;
  }

  /* allocate space to store inflated (but still filtered) data */
  int32_t width_aligned_bytes = (width * upng_get_bpp(upng) + 7) / 8;
  inflated_size = (width_aligned_bytes * height) + height; // pad byte
  inflated = (uint8_t*)task_malloc(inflated_size);
  if (inflated == NULL) {
    SET_ERROR(upng, UPNG_ENOMEM);
    return upng->error;
  }

  /* decompress image data */
  if (uz_inflate(upng, inflated, inflated_size, compressed, compressed_size) != UPNG_EOK) {
    task_free(inflated);
    return upng->error;
  }

  /* unfilter scanlines */
  post_process_scanlines(upng, inflated, inflated, upng_get_bpp(upng), width, height);
  upng->buffer = inflated;
  upng->size = inflated_size;

  if (upng->error != UPNG_EOK) {
    task_free(upng->buffer);
    upng->buffer = NULL;
    upng->size = 0;
  } else {
    upng->state = UPNG_DECODED;
  }

  return upng->error;
}

upng_t* upng_create(void) {
  upng_t* upng = (upng_t*)task_malloc(sizeof(upng_t));
  if (upng == NULL) {
    return NULL;
  }

  memset(upng, 0, sizeof(upng_t));

  upng->color_type = UPNG_RGBA;
  upng->color_depth = 8;
  upng->format = UPNG_RGBA8;

  upng->state = UPNG_NEW;

  return upng;
}


void upng_load_bytes(upng_t *upng, const uint8_t *buffer, uint32_t size) {
  upng->cursor = buffer;
  upng->source.buffer = buffer;
  upng->source.size = size;
  upng->source.owning = 0;
}


void upng_destroy(upng_t* upng, bool free_image_buffer) {
  if (!upng) {
    return;
  }

  /* deallocate image buffer */
  if (free_image_buffer) {
    task_free(upng->buffer);
  }

  /* deallocate palette buffer */
  task_free(upng->palette);

  /* deallocate alpha_palette buffer */
  task_free(upng->alpha_palette);
  
  /* deallocate apng_frame_control struct */
  task_free(upng->apng_frame_control);

  /* deallocate source buffer */
  upng_free_source(upng);

  /* deallocate struct itself */
  task_free(upng);
}

upng_error upng_get_error(const upng_t* upng) {
  return upng->error;
}

uint32_t upng_get_error_line(const upng_t* upng) {
  return upng->error_line;
}

uint32_t upng_get_width(const upng_t* upng) {
  return upng->width;
}

uint32_t upng_get_height(const upng_t* upng) {
  return upng->height;
}

uint16_t upng_get_palette(const upng_t* upng, rgb **palette) {
  if (palette) {
    *palette = upng->palette;
  }
  return upng->palette_entries;
}

uint16_t upng_get_alpha_palette(const upng_t* upng, uint8_t **alpha_palette) {
  if (alpha_palette) {
    *alpha_palette = upng->alpha_palette;
  }
  return upng->alpha_palette_entries;
}

uint32_t upng_get_bpp(const upng_t* upng) {
  return upng_get_bitdepth(upng) * upng_get_components(upng);
}

uint32_t upng_get_components(const upng_t* upng) {
  switch (upng->color_type) {
    case UPNG_PLT:
      return 1;
    case UPNG_LUM:
      return 1;
    case UPNG_RGB:
      return 3;
    case UPNG_LUMA:
      return 2;
    case UPNG_RGBA:
      return 4;
    default:
      return 0;
  }
}

uint32_t upng_get_bitdepth(const upng_t* upng) {
  return upng->color_depth;
}

uint32_t upng_get_pixelsize(const upng_t* upng) {
  return (upng_get_bitdepth(upng) * upng_get_components(upng));
}

upng_format upng_get_format(const upng_t* upng) {
  return upng->format;
}

const uint8_t* upng_get_buffer(const upng_t* upng) {
  return upng->buffer;
}

uint32_t upng_get_size(const upng_t* upng) {
  return upng->size;
}

// returns if the png is an apng after the upng_load() function
bool upng_is_apng(const upng_t* upng) {
  return upng->is_apng;
}

// retuns the apng num_frames
uint32_t upng_apng_num_frames(const upng_t* upng) {
  uint32_t num_frames = 1;  //default to 1 frame for png images used as apng
  if (upng->is_apng) {
    num_frames = upng->apng_num_frames;
  }
  return num_frames;
}

// retuns the apng num_plays
uint32_t upng_apng_num_plays(const upng_t* upng) {
  uint32_t num_plays = 1;  // default to 1 play for png images used as apng
  if (upng->is_apng) {
    num_plays = upng->apng_num_plays;
  }
  return num_plays;
}

// Pass in a apng_fctl to get the next frames frame control information
bool upng_get_apng_fctl(const upng_t* upng, apng_fctl *apng_frame_control) {
  bool retval = false;
  if (upng->is_apng && apng_frame_control != NULL) {
    *apng_frame_control = *upng->apng_frame_control;
    retval = true;
  }
  return retval;
}
