//
// G5 Encoder
// A 1-bpp image encoding library
//
// Written by Larry Bank
// Copyright (c) 2024 BitBank Software, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//    http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===========================================================================
//

#include "Group5.h"

/* Number of consecutive 1 bits in a byte from MSB to LSB */
static uint8_t bitcount[256] =
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 0-15 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 16-31 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 32-47 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 48-63 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 64-79 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 80-95 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 96-111 */
         0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  /* 112-127 */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  /* 128-143 */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  /* 144-159 */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  /* 160-175 */
         1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  /* 176-191 */
         2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,  /* 192-207 */
         2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,  /* 208-223 */
         3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,  /* 224-239 */
         4,4,4,4,4,4,4,4,5,5,5,5,6,6,7,8}; /* 240-255 */

/* Table of vertical codes for G5 encoding */
/* code followed by length, starting with v(-3) */
static const uint8_t vtable[14] =
        {3,7,     /* V(-3) = 0000011 */
         3,6,     /* V(-2) = 000011  */
         3,3,     /* V(-1) = 011     */
         1,1,     /* V(0)  = 1       */
         2,3,     /* V(1)  = 010     */
         2,6,     /* V(2)  = 000010  */
         2,7};    /* V(3)  = 0000010 */


static void G5ENCInsertCode(G5_BUFFERED_BITS *bb, BIGUINT ulCode, int iLen)
{
    if ((bb->ulBitOff + iLen) > REGISTER_WIDTH) { // need to write data
        bb->ulBits |= (ulCode >> (bb->ulBitOff + iLen - REGISTER_WIDTH)); // partial bits on first word
        *(BIGUINT *)bb->pBuf = __builtin_bswap32(bb->ulBits);
        bb->pBuf += sizeof(BIGUINT);
        bb->ulBits = ulCode << ((REGISTER_WIDTH*2) - (bb->ulBitOff + iLen));
        bb->ulBitOff += iLen - REGISTER_WIDTH;
    } else {
        bb->ulBits |= (ulCode << (REGISTER_WIDTH - bb->ulBitOff - iLen));
        bb->ulBitOff += iLen;
    }
} /* G5ENCInsertCode() */
//
// Flush any buffered bits to the output
//
static void G5ENCFlushBits(G5_BUFFERED_BITS *bb)
{
    while (bb->ulBitOff >= 8)
    {
        *bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
        bb->ulBits <<= 8;
        bb->ulBitOff -= 8;
    }
    if (bb->ulBitOff) { // partial byte?
        *bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
    }
    bb->ulBitOff = 0;
    bb->ulBits = 0;
} /* G5ENCFlushBits() */
//
// Initialize the compressor
// This must be called before adding data to the output
//
static int g5_encode_init(G5ENCIMAGE *pImage, int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
{
    int iError = G5_SUCCESS;
    
    if (pImage == NULL || iHeight <= 0)
        return G5_INVALID_PARAMETER;
    pImage->iWidth = iWidth; // image size
    pImage->iHeight = iHeight;
    pImage->pCur = pImage->CurFlips;
    pImage->pRef = pImage->RefFlips;
    pImage->pOutBuf = pOut; // optional output buffer
    pImage->iOutSize = iOutSize; // output buffer pre-allocated size
    pImage->iDataSize = 0; // no data yet
    pImage->y = 0;
    for (int i=0; i<MAX_IMAGE_FLIPS; i++) {
        pImage->RefFlips[i] = iWidth;
        pImage->CurFlips[i] = iWidth;
    }
    pImage->bb.pBuf = pImage->pOutBuf;
    pImage->bb.ulBits = 0;
    pImage->bb.ulBitOff = 0;
    pImage->iError = iError;
    return iError;
} /* g5_encode_init() */
//
// Internal function to convert uncompressed 1-bit per pixel data
// into the run-end data needed to feed the G5 encoder
//
static int G5ENCEncodeLine(unsigned char *buf, int xsize, int16_t *pDest)
{
int iCount, xborder;
uint8_t i, c;
int8_t cBits;
int iLen;
int16_t x;
int16_t *pLimit = pDest + (MAX_IMAGE_FLIPS-4);

   xborder = xsize;
   iCount = (xsize + 7) >> 3; /* Number of bytes per line */
   cBits = 8;
   iLen = 0; /* Current run length */
   x = 0;

   c = *buf++;  /* Get the first byte to start */
   iCount--;
   while (iCount >=0) {
      if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
      i = bitcount[c]; /* Get the number of consecutive bits */
      iLen += i; /* Add this length to total run length */
      c <<= i;
      cBits -= i; /* Minus the number in a byte */
      if (cBits <= 0)
         {
         iLen += cBits; /* Adjust length */
         cBits = 8;
         c = *buf++;  /* Get another data byte */
         iCount--;
         continue; /* Keep doing white until color change */
         }
      c = ~c; /* flip color to count black pixels */
   /* Store the white run length */
      xborder -= iLen;
      if (xborder < 0)
         {
         iLen += xborder; /* Make sure run length is not past end */
         break;
         }
      x += iLen;
      *pDest++ = x;
      iLen = 0;
doblack:
      i = bitcount[c]; /* Get consecutive bits */
      iLen += i; /* Add to total run length */
      c <<= i;
      cBits -= i;
      if (cBits <= 0)
         {
         iLen += cBits; /* Adjust length */
         cBits = 8;
         c = *buf++;  /* Get another data byte */
         c = ~c;   /* Flip color to find black */
         iCount--;
         if (iCount < 0)
            break;
         goto doblack;
         }
   /* Store the black run length */
      c = ~c;       /* Flip color again to find white pixels */
      xborder -= iLen;
      if (xborder < 0)
         {
         iLen += xborder; /* Make sure run length is not past end */
         break;
         }
      x += iLen;
      *pDest++ = x;
      iLen = 0;
      } /* while */

   if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
   *pDest++ = xsize;
   *pDest++ = xsize; // Store a few more XSIZE to end the line
   *pDest++ = xsize; // so that the compressor doesn't go past
   *pDest++ = xsize; // the end of the line
   return G5_SUCCESS;
} /* G5ENCEncodeLine() */
//
// Compress a line of pixels and add it to the output
// the input format is expected to be MSB (most significant bit) first
// for example, pixel 0 is in byte 0 at bit 7 (0x80)
// Returns G5ENC_SUCCESS for each line if all is well and G5ENC_IMAGE_COMPLETE
// for the last line
//
static int g5_encode_encodeLine(G5ENCIMAGE *pImage, uint8_t *pPixels)
{
int16_t a0, a0_c, b2, a1;
int dx, run1, run2;
int xsize, iErr, iHighWater;
int iCur, iRef, iLen;
int iHLen; // number of bits for long horizontal codes
int16_t *CurFlips, *RefFlips;
G5_BUFFERED_BITS bb;

    if (pImage == NULL || pPixels == NULL)
        return G5_INVALID_PARAMETER;
    iHighWater = pImage->iOutSize - 32;
    iHLen = 32 - __builtin_clz(pImage->iWidth);
    memcpy(&bb, &pImage->bb, sizeof(G5_BUFFERED_BITS)); // keep local copy
    CurFlips = pImage->pCur;
    RefFlips = pImage->pRef;
    xsize = pImage->iWidth; /* For performance reasons */

    // Convert the incoming line of pixels into run-end data
    iErr = G5ENCEncodeLine(pPixels, pImage->iWidth, CurFlips);
    if (iErr != G5_SUCCESS) return iErr; // exceeded the maximum number of color changes
    /* Encode this line as G5 */
    a0 = a0_c = 0;
    iCur = iRef = 0;
    while (a0 < xsize) {
        b2 = RefFlips[iRef+1];
        a1 = CurFlips[iCur];
        if (b2 < a1) { /* Is b2 to the left of a1? */
            /* yes, do pass mode */
            a0 = b2;
            iRef += 2;
            G5ENCInsertCode(&bb, 1, 4); /* Pass code = 0001 */
        } else { /* Try vertical and horizontal mode */
            dx = RefFlips[iRef] - a1;  /* b1 - a1 */
            if (dx > 3 || dx < -3) { /* Horizontal mode */
                G5ENCInsertCode(&bb, 1, 3); /* Horizontal code = 001 */
                run1 = CurFlips[iCur] - a0;
                run2 = CurFlips[iCur+1] - CurFlips[iCur];
                if (run1 < 8) {
                    if (run2 < 8) { // short, short
                        G5ENCInsertCode(&bb, HORIZ_SHORT_SHORT, 2); /* short, short = 00 */
                        G5ENCInsertCode(&bb, run1, 3);
                        G5ENCInsertCode(&bb, run2, 3);
                    } else { // short, long
                        G5ENCInsertCode(&bb, HORIZ_SHORT_LONG, 2); /* short, long = 01 */
                        G5ENCInsertCode(&bb, run1, 3);
                        G5ENCInsertCode(&bb, run2, iHLen);
                    }
                } else { // first run is long
                    if (run2 < 8) { // long, short
                        G5ENCInsertCode(&bb, HORIZ_LONG_SHORT, 2); /* long, short = 10 */
                        G5ENCInsertCode(&bb, run1, iHLen);
                        G5ENCInsertCode(&bb, run2, 3);
                    } else { // long, long
                        G5ENCInsertCode(&bb, HORIZ_LONG_LONG, 2); /* long, long = 11 */
                        G5ENCInsertCode(&bb, run1, iHLen);
                        G5ENCInsertCode(&bb, run2, iHLen);
                    }
                }
               a0 = CurFlips[iCur+1]; /* a0 = a2 */
               if (a0 != xsize) {
                  iCur += 2; /* Skip two color flips */
                   while (RefFlips[iRef] != xsize && RefFlips[iRef] <= a0) {
                       iRef += 2;
                   }
                }
            } else { /* Vertical mode */
               dx = (dx + 3) * 2; /* Convert to index table */
                G5ENCInsertCode(&bb, vtable[dx], vtable[dx+1]);
                a0 = a1;
                a0_c = 1-a0_c;
                if (a0 != xsize) {
                    if (iRef != 0) {
                        iRef -= 2;
                    }
                    iRef++; /* Skip a color change in cur and ref */
                    iCur++;
                    while (RefFlips[iRef] <= a0 && RefFlips[iRef] != xsize) {
                        iRef += 2;
                    }
                }
            } /* vertical mode */
        } /* horiz/vert mode */
    } /* while x < xsize */
    iLen = (int)(bb.pBuf-pImage->pOutBuf);
    if (iLen >= iHighWater) { // not enough space
           pImage->iError = iErr = G5_DATA_OVERFLOW; // we don't have a better error
           return iErr;
    }
    if (pImage->y == pImage->iHeight-1) { // last line of image
        G5ENCFlushBits(&bb); // output the final buffered bits
        // wrap up final output
        pImage->iDataSize = 1 + (int)(bb.pBuf-pImage->pOutBuf);
        iErr = G5_ENCODE_COMPLETE;
    }
    pImage->pCur = RefFlips; // swap current and reference lines
    pImage->pRef = CurFlips;
    pImage->y++;
    memcpy(&pImage->bb, &bb, sizeof(bb));
    return iErr;
} /* g5_encode_encodeLine() */
//
// Returns the number of bytes of G5 created by the encoder
//
static int g5_encode_getOutSize(G5ENCIMAGE *pImage)
{
    int iSize = 0;
    if (pImage != NULL)
        iSize = pImage->iDataSize;
    return iSize;
} /* g5_encode_getOutSize() */
