/* SPDX-License-Identifier: Apache 2.0 */
/* Copyright 2023 - Present, Light Transport Entertainment Inc. */

/* TODO:
 *
 * - [ ] Stream decoding API
 * - [ ] Stream encoding API
 *
 */

#ifndef NANOZLIB_H_
#define NANOZLIB_H_

#include <stdint.h>
#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef enum nanoz_status {
  NANOZ_SUCCESS = 0,
  NANOZ_ERROR = -1,  // general error code.
  NANOZ_ERROR_INVALID_ARGUMENT = -2,
  NANOZ_ERROR_CORRUPTED = -3,
  NANOZ_ERROR_INTERNAL = -4,
} nanoz_status_t;

#if 0  // TODO
/* Up to 2GB chunk. */
typedef (*nanoz_stream_read)(const uint8_t *addr, uint8_t *dst_addr, const uint32_t read_bytes, const void *user_ptr);
typedef (*nanoz_stream_write)(const uint8_t *addr, const uint32_t write_bytes, const void *user_ptr);
#endif

/* TODO: Get uncompressed size function */

/*
 * zlib decompression. Up to 2GB compressed data.
 *
 * @param[in] src_addr Source buffer address containing compressed data.
 * @param[in] src_size Source buffer bytes.
 * @param[in] dst_size Destination buffer size. Must be larger than or equal to uncompressed size.
 * @param[out] dst_addr Destination buffer address.
 * @param[out] uncompressed_size Uncompressed bytes.
 * contain `uncompressed_size` bytes.
 * @return NANOZ_SUCCESS upon success.
 *
 * TODO: return error message string.
 */
nanoz_status_t nanoz_uncompress(const unsigned char *src_addr,
                                int32_t src_size,
                                const uint64_t dst_size,
                                unsigned char *dst_addr,
                                uint64_t *uncompressed_size);

/*
 * Compute compress bound.
 */
uint64_t nanoz_compressBound(uint64_t sourceLen);

/*
 * zlib compression. Currently we use stb's zlib_compress
 *
 * @param[in] data Input data
 * @param[in] data_len Input data bytes(up to 2GB)
 * @param[out] out_len Input data
 * @param[in] quality Compression quality(5 or more. Usually 8)
 *
 * @return Compressed bytes upon success. NULL when failed to compress or any input parameter is wrong.
 */
unsigned char *nanoz_compress(unsigned char *data, int data_len, int *out_len,
                              int quality);

#if 0  // TODO
nanoz_status_t nanoz_stream_uncompress(nanoz_stream_read *reader, nanoz_stream_writer *writer);
#endif

#ifdef __cplusplus
}
#endif

#if defined(NANOZLIB_IMPLEMENTATION)

#define WUFFS_IMPLEMENTATION

#define WUFFS_CONFIG__STATIC_FUNCTIONS

#define WUFFS_CONFIG__MODULES
#define WUFFS_CONFIG__MODULE__BASE
#define WUFFS_CONFIG__MODULE__CRC32
#define WUFFS_CONFIG__MODULE__ADLER32
#define WUFFS_CONFIG__MODULE__DEFLATE
#define WUFFS_CONFIG__MODULE__ZLIB

#include "wuffs-v0.3.c"

#define WORK_BUFFER_ARRAY_SIZE \
  WUFFS_ZLIB__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE

nanoz_status_t nanoz_uncompress(const unsigned char *src_addr,
                                const int32_t src_size,
                                const uint64_t dst_size,
                                unsigned char *dst_addr,
                                uint64_t *uncompressed_size_out) {
// WUFFS_ZLIB__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE = 1, its tiny bytes and
// safe to alloc worbuf at heap location.
#if WORK_BUFFER_ARRAY_SIZE > 0
  uint8_t work_buffer_array[WORK_BUFFER_ARRAY_SIZE];
#else
  // Not all C/C++ compilers support 0-length arrays.
  uint8_t work_buffer_array[1];
#endif

  if (!src_addr) {
    return NANOZ_ERROR_INVALID_ARGUMENT;
  }

  if (src_size < 4) {
    return NANOZ_ERROR_INVALID_ARGUMENT;
  }

  if (!dst_addr) {
    return NANOZ_ERROR_INVALID_ARGUMENT;
  }

  if (dst_size < 1) {
    return NANOZ_ERROR_INVALID_ARGUMENT;
  }

  if (!uncompressed_size_out) {
    return NANOZ_ERROR_INVALID_ARGUMENT;
  }

  wuffs_zlib__decoder dec;
  wuffs_base__status status =
      wuffs_zlib__decoder__initialize(&dec, sizeof dec, WUFFS_VERSION, 0);
  if (!wuffs_base__status__is_ok(&status)) {
    // wuffs_base__status__message(&status);
    return NANOZ_ERROR_INTERNAL;
  }

  // TODO: Streamed decoding?

  wuffs_base__io_buffer dst;
  dst.data.ptr = dst_addr;
  dst.data.len = dst_size;
  dst.meta.wi = 0;
  dst.meta.ri = 0;
  dst.meta.pos = 0;
  dst.meta.closed = false;

  wuffs_base__io_buffer src;
  src.data.ptr = const_cast<uint8_t *>(src_addr);  // remove const
  src.data.len = src_size;
  src.meta.wi = src_size;
  src.meta.ri = 0;
  src.meta.pos = 0;
  src.meta.closed = false;

  status = wuffs_zlib__decoder__transform_io(
      &dec, &dst, &src,
      wuffs_base__make_slice_u8(work_buffer_array, WORK_BUFFER_ARRAY_SIZE));

  uint64_t uncompressed_size{0};

  if (dst.meta.wi) {
    dst.meta.ri = dst.meta.wi;
    uncompressed_size = dst.meta.wi;
    wuffs_base__io_buffer__compact(&dst);
  }

  if (status.repr == wuffs_base__suspension__short_read) {
    // ok
  } else if (status.repr == wuffs_base__suspension__short_write) {
    // read&write should succeed at once.
    return NANOZ_ERROR_CORRUPTED;
  }

  const char *stat_msg = wuffs_base__status__message(&status);
  if (stat_msg) {
    return NANOZ_ERROR_INTERNAL;
  }

  (*uncompressed_size_out) = uncompressed_size;

  return NANOZ_SUCCESS;
}

#ifndef NANOZ_MALLOC
#define NANOZ_MALLOC(sz) malloc(sz)
#define NANOZ_REALLOC(p, newsz) realloc(p, newsz)
#define NANOZ_FREE(p) free(p)
#endif

#ifndef NANOZ_REALLOC_SIZED
#define NANOZ_REALLOC_SIZED(p, oldsz, newsz) NANOZ_REALLOC(p, newsz)
#endif

#ifndef NANOZ_MEMMOVE
#define NANOZ_MEMMOVE(a, b, sz) memmove(a, b, sz)
#endif

#define NANOZ_UCHAR(x) (unsigned char)((x)&0xff)

// #ifndef NANOZ_ZLIB_COMPRESS
//  stretchy buffer; nanoz__sbpush() == vector<>::push_back() --
//  nanoz__sbcount() == vector<>::size()
#define nanoz__sbraw(a) ((int *)(void *)(a)-2)
#define nanoz__sbm(a) nanoz__sbraw(a)[0]
#define nanoz__sbn(a) nanoz__sbraw(a)[1]

#define nanoz__sbneedgrow(a, n) ((a) == 0 || nanoz__sbn(a) + n >= nanoz__sbm(a))
#define nanoz__sbmaybegrow(a, n) \
  (nanoz__sbneedgrow(a, (n)) ? nanoz__sbgrow(a, n) : 0)
#define nanoz__sbgrow(a, n) nanoz__sbgrowf((void **)&(a), (n), sizeof(*(a)))

#define nanoz__sbpush(a, v) \
  (nanoz__sbmaybegrow(a, 1), (a)[nanoz__sbn(a)++] = (v))
#define nanoz__sbcount(a) ((a) ? nanoz__sbn(a) : 0)
#define nanoz__sbfree(a) ((a) ? NANOZ_FREE(nanoz__sbraw(a)), 0 : 0)

static void *nanoz__sbgrowf(void **arr, int increment, int itemsize) {
  int m = *arr ? 2 * nanoz__sbm(*arr) + increment : increment + 1;
  void *p = NANOZ_REALLOC_SIZED(
      *arr ? nanoz__sbraw(*arr) : 0,
      *arr ? (nanoz__sbm(*arr) * itemsize + sizeof(int) * 2) : 0,
      itemsize * m + sizeof(int) * 2);
  if (!p) {
    return nullptr;
  }

  if (p) {
    if (!*arr) ((int *)p)[1] = 0;
    *arr = (void *)((int *)p + 2);
    nanoz__sbm(*arr) = m;
  }
  return *arr;
}

static unsigned char *nanoz__zlib_flushf(unsigned char *data,
                                         unsigned int *bitbuffer,
                                         int *bitcount) {
  while (*bitcount >= 8) {
    nanoz__sbpush(data, NANOZ_UCHAR(*bitbuffer));
    *bitbuffer >>= 8;
    *bitcount -= 8;
  }
  return data;
}

static int nanoz__zlib_bitrev(int code, int codebits) {
  int res = 0;
  while (codebits--) {
    res = (res << 1) | (code & 1);
    code >>= 1;
  }
  return res;
}

static unsigned int nanoz__zlib_countm(unsigned char *a, unsigned char *b,
                                       int limit) {
  int i;
  for (i = 0; i < limit && i < 258; ++i)
    if (a[i] != b[i]) break;
  return i;
}

static unsigned int nanoz__zhash(unsigned char *data) {
  uint32_t hash = data[0] + (data[1] << 8) + (data[2] << 16);
  hash ^= hash << 3;
  hash += hash >> 5;
  hash ^= hash << 4;
  hash += hash >> 17;
  hash ^= hash << 25;
  hash += hash >> 6;
  return hash;
}

#define nanoz__zlib_flush() (out = nanoz__zlib_flushf(out, &bitbuf, &bitcount))
#define nanoz__zlib_add(code, codebits) \
  (bitbuf |= (code) << bitcount, bitcount += (codebits), nanoz__zlib_flush())
#define nanoz__zlib_huffa(b, c) nanoz__zlib_add(nanoz__zlib_bitrev(b, c), c)
// default huffman tables
#define nanoz__zlib_huff1(n) nanoz__zlib_huffa(0x30 + (n), 8)
#define nanoz__zlib_huff2(n) nanoz__zlib_huffa(0x190 + (n)-144, 9)
#define nanoz__zlib_huff3(n) nanoz__zlib_huffa(0 + (n)-256, 7)
#define nanoz__zlib_huff4(n) nanoz__zlib_huffa(0xc0 + (n)-280, 8)
#define nanoz__zlib_huff(n)            \
  ((n) <= 143   ? nanoz__zlib_huff1(n) \
   : (n) <= 255 ? nanoz__zlib_huff2(n) \
   : (n) <= 279 ? nanoz__zlib_huff3(n) \
                : nanoz__zlib_huff4(n))
#define nanoz__zlib_huffb(n) \
  ((n) <= 143 ? nanoz__zlib_huff1(n) : nanoz__zlib_huff2(n))

#define nanoz__ZHASH 16384

// #endif // NANOZ_ZLIB_COMPRESS

unsigned char *nanoz_compress(unsigned char *data, int data_len, int *out_len,
                              int quality) {
  static unsigned short lengthc[] = {
      3,  4,  5,  6,  7,  8,  9,  10, 11,  13,  15,  17,  19,  23,  27,
      31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 259};
  static unsigned char lengtheb[] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
                                     1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
                                     4, 4, 4, 4, 5, 5, 5, 5, 0};
  static unsigned short distc[] = {
      1,    2,    3,    4,    5,    7,     9,     13,    17,   25,   33,
      49,   65,   97,   129,  193,  257,   385,   513,   769,  1025, 1537,
      2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 32768};
  static unsigned char disteb[] = {0, 0, 0,  0,  1,  1,  2,  2,  3,  3,
                                   4, 4, 5,  5,  6,  6,  7,  7,  8,  8,
                                   9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
  unsigned int bitbuf = 0;
  int i, j, bitcount = 0;
  unsigned char *out = NULL;

  if (!data) {
    return NULL;
  }

  if (data_len < 1) {
    return NULL;
  }

  if (!out_len) {
    return NULL;
  }

  unsigned char ***hash_table =
      (unsigned char ***)NANOZ_MALLOC(nanoz__ZHASH * sizeof(unsigned char **));
  if (hash_table == NULL) return NULL;
  if (quality < 5) quality = 5;

  nanoz__sbpush(out, 0x78);  // DEFLATE 32K window
  nanoz__sbpush(out, 0x5e);  // FLEVEL = 1
  nanoz__zlib_add(1, 1);     // BFINAL = 1
  nanoz__zlib_add(1, 2);     // BTYPE = 1 -- fixed huffman

  for (i = 0; i < nanoz__ZHASH; ++i) hash_table[i] = NULL;

  i = 0;
  while (i < data_len - 3) {
    // hash next 3 bytes of data to be compressed
    int h = nanoz__zhash(data + i) & (nanoz__ZHASH - 1), best = 3;
    unsigned char *bestloc = 0;
    unsigned char **hlist = hash_table[h];
    int n = nanoz__sbcount(hlist);
    for (j = 0; j < n; ++j) {
      if (hlist[j] - data > i - 32768) {  // if entry lies within window
        int d = nanoz__zlib_countm(hlist[j], data + i, data_len - i);
        if (d >= best) {
          best = d;
          bestloc = hlist[j];
        }
      }
    }
    // when hash table entry is too long, delete half the entries
    if (hash_table[h] && nanoz__sbn(hash_table[h]) == 2 * quality) {
      NANOZ_MEMMOVE(hash_table[h], hash_table[h] + quality,
                    sizeof(hash_table[h][0]) * quality);
      nanoz__sbn(hash_table[h]) = quality;
    }
    nanoz__sbpush(hash_table[h], data + i);

    if (bestloc) {
      // "lazy matching" - check match at *next* byte, and if it's better, do
      // cur byte as literal
      h = nanoz__zhash(data + i + 1) & (nanoz__ZHASH - 1);
      hlist = hash_table[h];
      n = nanoz__sbcount(hlist);
      for (j = 0; j < n; ++j) {
        if (hlist[j] - data > i - 32767) {
          int e = nanoz__zlib_countm(hlist[j], data + i + 1, data_len - i - 1);
          if (e > best) {  // if next match is better, bail on current match
            bestloc = NULL;
            break;
          }
        }
      }
    }

    if (bestloc) {
      int d = (int)(data + i - bestloc);  // distance back
      // NANOZ_ASSERT(d <= 32767 && best <= 258);
      if (d <= 32767 && best <= 258) {
        // OK
      } else {
        return NULL;  // FIXME: may leak
      }
      for (j = 0; best > lengthc[j + 1] - 1; ++j)
        ;
      nanoz__zlib_huff(j + 257);
      if (lengtheb[j]) nanoz__zlib_add(best - lengthc[j], lengtheb[j]);
      for (j = 0; d > distc[j + 1] - 1; ++j)
        ;
      nanoz__zlib_add(nanoz__zlib_bitrev(j, 5), 5);
      if (disteb[j]) nanoz__zlib_add(d - distc[j], disteb[j]);
      i += best;
    } else {
      nanoz__zlib_huffb(data[i]);
      ++i;
    }
  }
  // write out final bytes
  for (; i < data_len; ++i) nanoz__zlib_huffb(data[i]);
  nanoz__zlib_huff(256);  // end of block
  // pad with 0 bits to byte boundary
  while (bitcount) nanoz__zlib_add(0, 1);

  for (i = 0; i < nanoz__ZHASH; ++i) (void)nanoz__sbfree(hash_table[i]);
  NANOZ_FREE(hash_table);

  // store uncompressed instead if compression was worse
  if (nanoz__sbn(out) > data_len + 2 + ((data_len + 32766) / 32767) * 5) {
    nanoz__sbn(out) = 2;  // truncate to DEFLATE 32K window and FLEVEL = 1
    for (j = 0; j < data_len;) {
      int blocklen = data_len - j;
      if (blocklen > 32767) blocklen = 32767;
      nanoz__sbpush(
          out,
          data_len - j == blocklen);  // BFINAL = ?, BTYPE = 0 -- no compression
      nanoz__sbpush(out, NANOZ_UCHAR(blocklen));  // LEN
      nanoz__sbpush(out, NANOZ_UCHAR(blocklen >> 8));
      nanoz__sbpush(out, NANOZ_UCHAR(~blocklen));  // NLEN
      nanoz__sbpush(out, NANOZ_UCHAR(~blocklen >> 8));
      memcpy(out + nanoz__sbn(out), data + j, blocklen);
      nanoz__sbn(out) += blocklen;
      j += blocklen;
    }
  }

  {
    // compute adler32 on input
    unsigned int s1 = 1, s2 = 0;
    int blocklen = (int)(data_len % 5552);
    j = 0;
    while (j < data_len) {
      for (i = 0; i < blocklen; ++i) {
        s1 += data[j + i];
        s2 += s1;
      }
      s1 %= 65521;
      s2 %= 65521;
      j += blocklen;
      blocklen = 5552;
    }
    nanoz__sbpush(out, NANOZ_UCHAR(s2 >> 8));
    nanoz__sbpush(out, NANOZ_UCHAR(s2));
    nanoz__sbpush(out, NANOZ_UCHAR(s1 >> 8));
    nanoz__sbpush(out, NANOZ_UCHAR(s1));
  }
  *out_len = nanoz__sbn(out);
  // make returned pointer freeable
  NANOZ_MEMMOVE(nanoz__sbraw(out), out, *out_len);
  return (unsigned char *)nanoz__sbraw(out);
}

// from zlib
uint64_t nanoz_compressBound(uint64_t sourceLen)
{
  // TODO: Overflow check?
  return sourceLen + (sourceLen >> 12ull) + (sourceLen >> 14ull) +
         (sourceLen >> 25ull) + 13ull;
}


#endif  // NANOZDEC_IMPLEMENTATION

#endif /* NANOZDEC_H_ */
