/**
 * @file Sprites.cpp
 * \brief
 * A class for drawing animated sprites from image and mask bitmaps.
 */

#include "Sprites.h"

void Sprites::drawExternalMask(int16_t x, int16_t y, const uint8_t *bitmap,
                               const uint8_t *mask, uint8_t frame, uint8_t mask_frame)
{
  draw(x, y, bitmap, frame, mask, mask_frame, SPRITE_MASKED);
}

void Sprites::drawOverwrite(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame)
{
  draw(x, y, bitmap, frame, NULL, 0, SPRITE_OVERWRITE);
}

void Sprites::drawErase(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame)
{
  draw(x, y, bitmap, frame, NULL, 0, SPRITE_IS_MASK_ERASE);
}

void Sprites::drawSelfMasked(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame)
{
  draw(x, y, bitmap, frame, NULL, 0, SPRITE_IS_MASK);
}

void Sprites::drawPlusMask(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t frame)
{
  draw(x, y, bitmap, frame, NULL, 0, SPRITE_PLUS_MASK);
}


//common functions
void Sprites::draw(int16_t x, int16_t y,
                   const uint8_t *bitmap, uint8_t frame,
                   const uint8_t *mask, uint8_t sprite_frame,
                   uint8_t drawMode)
{
  unsigned int frame_offset;

  if (bitmap == NULL)
    return;

  uint8_t width = pgm_read_byte(bitmap);
  uint8_t height = pgm_read_byte(++bitmap);
  bitmap++;
  if (frame > 0 || sprite_frame > 0) {
    frame_offset = (width * ( height / 8 + ( height % 8 == 0 ? 0 : 1)));
    // sprite plus mask uses twice as much space for each frame
    if (drawMode == SPRITE_PLUS_MASK) {
      frame_offset *= 2;
    } else if (mask != NULL) {
      mask += sprite_frame * frame_offset;
    }
    bitmap += frame * frame_offset;
  }

  // if we're detecting the draw mode then base it on whether a mask
  // was passed as a separate object
  if (drawMode == SPRITE_AUTO_MODE) {
    drawMode = mask == NULL ? SPRITE_UNMASKED : SPRITE_MASKED;
  }
  // assembly optimisation of above code saving 20(+) bytes
//  uint8_t width;
//  uint8_t height;
//  asm volatile(
//    "    lpm   %[width], Z+                 \n\t" // width  = pgm_read_byte(bitmap++);
//    "    lpm   %[height], Z+                \n\t" // height = pgm_read_byte(bitmap++);
//    "    cp    %[frame], __zero_reg__       \n\t" // if (frame > 0 || sprite_frame > 0)
//    "    brne  1f                           \n\t" 
//    "    cp    %[spr_frame], __zero_reg__   \n\t" 
//    "    breq  3f                           \n\t" 
//    "1:                                     \n\t" 
//    "    ldi   r20, 7                       \n\t" //rows = ((height+7)
//    "    add   r20, %[height]               \n\t" 
//    "    ror   r20                          \n\t" //include carry for heights > 248
//    "    lsr   r20                          \n\t" //rows = (height+7) / 8
//    "    lsr   r20                          \n\t" 
//    "    cpi   %[mode], %[sprite_plus_mask] \n\t" //if (drawMode == SPRITE_PLUS_MASK) rows *= 2
//    "    brne  2f                           \n\t"
//    "    lsl   r20                          \n\t" 
//    "2:                                     \n\t" 
//    "    mul   r20, %[width]                \n\t" //frame offset = rows * width
//    "    movw  r20, r0                      \n\t" 
//    "    mul   %[frame] , r20               \n\t" 
//    "    add   %A[bitmap], r0               \n\t" //bitmap += frame * (frame offset & 0xFF)
//    "    adc   %B[bitmap], r1               \n\t" 
//    "    mul   %[frame] , r21               \n\t" //bitmap += frame * (frame_offset >> 8 )) << 8
//    "    add   %B[bitmap], r0               \n\t" 
//    "                                       \n\t" 
//    "    adiw %A[mask], 0                   \n\t" //if (mask != NULL)
//    "    breq  3f                           \n\t" 
//    "                                       \n\t" 
//    "    mul   %[spr_frame] , r20           \n\t"  
//    "    add   %A[mask], r0                 \n\t" //mask += sprite_frame * (frame offset & 0xFF)
//    "    adc   %B[mask], r1                 \n\t" 
//    "    mul   %[spr_frame] , r21           \n\t" //mask += (sprite_frame * (frame_offset >> 8 )) << 8
//    "    add   %B[mask], r0                 \n\t" 
//    "3:                                     \n\t" 
//    "    clr   __zero_reg__                 \n\t" 
//    "4:                                     \n\t" 
//    "    cpi   %[mode], %[sprite_auto_mode] \n\t" //if (drawMode == SPRITE_AUTO_MODE)
//    "    brne  5f                           \n\t" 
//    "    adiw  %A[mask], 0                  \n\t" //if (mask = NULL) drawMode = SPRITE_UNMASKED
//    "    ldi   %[mode], %[sprite_unmasked]  \n\t" 
//    "    breq  5f                           \n\t" 
//    "    ldi   %[mode], %[sprite_masked]    \n\t" //else drawMode = SPRITE_PLUS_MASK
//    "5:                                     \n\t" 
//    : [width]  "=&r" (width),
//      [height] "=&r" (height),
//      [mask]   "+x"  (mask),
//      [bitmap] "+z"  (bitmap),
//      [mode]   "+d"  (drawMode)
//    : [frame]            "r" (frame),
//      [spr_frame]        "r" (sprite_frame),
//      [sprite_plus_mask] "M" (SPRITE_PLUS_MASK),
//      [sprite_auto_mode] "M" (SPRITE_AUTO_MODE),
//      [sprite_unmasked]  "M" (SPRITE_UNMASKED),
//      [sprite_masked]    "M" (SPRITE_MASKED)
//    : "r20", "r21"
//  );
  drawBitmap(x, y, bitmap, mask, width, height, drawMode);
}

void Sprites::drawBitmap(int16_t x, int16_t y,
                         const uint8_t *bitmap, const uint8_t *mask,
                         uint8_t w, uint8_t h, uint8_t draw_mode)
{
  // no need to draw at all if we're offscreen
  if (x + w <= 0 || x > WIDTH - 1 || y + h <= 0 || y > HEIGHT - 1)
    return;

  if (bitmap == NULL)
    return;

  // xOffset technically doesn't need to be 16 bit but the math operations
  // are measurably faster if it is
  uint16_t xOffset, ofs;
  int8_t yOffset = y & 7;
  int8_t sRow = y / 8;
  uint8_t loop_h, start_h, rendered_width;

  if (y < 0 && yOffset > 0) {
    sRow--;
  }

  // if the left side of the render is offscreen skip those loops
  if (x < 0) {
    xOffset = abs(x);
  } else {
    xOffset = 0;
  }

  // if the right side of the render is offscreen skip those loops
  if (x + w > WIDTH - 1) {
    rendered_width = ((WIDTH - x) - xOffset);
  } else {
    rendered_width = (w - xOffset);
  }

  // if the top side of the render is offscreen skip those loops
  if (sRow < -1) {
    start_h = abs(sRow) - 1;
  } else {
    start_h = 0;
  }

  loop_h = h / 8 + (h % 8 > 0 ? 1 : 0); // divide, then round up

  // if (sRow + loop_h - 1 > (HEIGHT/8)-1)
  if (sRow + loop_h > (HEIGHT / 8)) {
    loop_h = (HEIGHT / 8) - sRow;
  }

  // prepare variables for loops later so we can compare with 0
  // instead of comparing two variables
  loop_h -= start_h;

  sRow += start_h;
  ofs = (sRow * WIDTH) + x + xOffset;
  uint8_t *bofs = (uint8_t *)bitmap + (start_h * w) + xOffset;
  uint8_t data;

  uint8_t mul_amt = 1 << yOffset;
  uint16_t mask_data;
  uint16_t bitmap_data;

  switch (draw_mode) {
    case SPRITE_UNMASKED:
      // we only want to mask the 8 bits of our own sprite, so we can
      // calculate the mask before the start of the loop
      mask_data = ~(0xFF * mul_amt);
      // really if yOffset = 0 you have a faster case here that could be
      // optimized
      for (uint8_t a = 0; a < loop_h; a++) {
        for (uint8_t iCol = 0; iCol < rendered_width; iCol++) {
          bitmap_data = pgm_read_byte(bofs) * mul_amt;

          if (sRow >= 0) {
            data = Arduboy2Base::sBuffer[ofs];
            data &= (uint8_t)(mask_data);
            data |= (uint8_t)(bitmap_data);
            Arduboy2Base::sBuffer[ofs] = data;
          }
          if (yOffset != 0 && sRow < 7) {
            const size_t index = static_cast<uint16_t>(ofs + WIDTH);
            data = Arduboy2Base::sBuffer[index];
            data &= (uint8_t)(mask_data >> 8);
            data |= (uint8_t)(bitmap_data >> 8);
            Arduboy2Base::sBuffer[index] = data;
          }
          ofs++;
          bofs++;
        }
        sRow++;
        bofs += w - rendered_width;
        ofs += WIDTH - rendered_width;
      }
      break;

    case SPRITE_IS_MASK:
      for (uint8_t a = 0; a < loop_h; a++) {
        for (uint8_t iCol = 0; iCol < rendered_width; iCol++) {
          bitmap_data = pgm_read_byte(bofs) * mul_amt;
          if (sRow >= 0) {
            Arduboy2Base::sBuffer[ofs] |= (uint8_t)(bitmap_data);
          }
          if (yOffset != 0 && sRow < 7) {
            const size_t index = static_cast<uint16_t>(ofs + WIDTH);
            Arduboy2Base::sBuffer[index] |= (uint8_t)(bitmap_data >> 8);
          }
          ofs++;
          bofs++;
        }
        sRow++;
        bofs += w - rendered_width;
        ofs += WIDTH - rendered_width;
      }
      break;

    case SPRITE_IS_MASK_ERASE:
      for (uint8_t a = 0; a < loop_h; a++) {
        for (uint8_t iCol = 0; iCol < rendered_width; iCol++) {
          bitmap_data = pgm_read_byte(bofs) * mul_amt;
          if (sRow >= 0) {
            Arduboy2Base::sBuffer[ofs]  &= ~(uint8_t)(bitmap_data);
          }
          if (yOffset != 0 && sRow < 7) {
            const size_t index = static_cast<uint16_t>(ofs + WIDTH);
            Arduboy2Base::sBuffer[index] &= ~(uint8_t)(bitmap_data >> 8);
          }
          ofs++;
          bofs++;
        }
        sRow++;
        bofs += w - rendered_width;
        ofs += WIDTH - rendered_width;
      }
      break;

    case SPRITE_MASKED:
      uint8_t *mask_ofs;
      mask_ofs = (uint8_t *)mask + (start_h * w) + xOffset;
      for (uint8_t a = 0; a < loop_h; a++) {
        for (uint8_t iCol = 0; iCol < rendered_width; iCol++) {
          // NOTE: you might think in the yOffset==0 case that this results
          // in more effort, but in all my testing the compiler was forcing
          // 16-bit math to happen here anyways, so this isn't actually
          // compiling to more code than it otherwise would. If the offset
          // is 0 the high part of the word will just never be used.

          // load data and bit shift
          // mask needs to be bit flipped
          mask_data = ~(pgm_read_byte(mask_ofs) * mul_amt);
          bitmap_data = pgm_read_byte(bofs) * mul_amt;

          if (sRow >= 0) {
            data = Arduboy2Base::sBuffer[ofs];
            data &= (uint8_t)(mask_data);
            data |= (uint8_t)(bitmap_data);
            Arduboy2Base::sBuffer[ofs] = data;
          }
          if (yOffset != 0 && sRow < 7) {
            const size_t index = static_cast<uint16_t>(ofs + WIDTH);
            data = Arduboy2Base::sBuffer[index];
            data &= (uint8_t)(mask_data >> 8);
            data |= (uint8_t)(bitmap_data >> 8);
            Arduboy2Base::sBuffer[index] = data;
          }
          ofs++;
          mask_ofs++;
          bofs++;
        }
        sRow++;
        bofs += w - rendered_width;
        mask_ofs += w - rendered_width;
        ofs += WIDTH - rendered_width;
      }
      break;


    case SPRITE_PLUS_MASK:
      // *2 because we use double the bits (mask + bitmap)
      bofs = (uint8_t *)(bitmap + ((start_h * w) + xOffset) * 2);

      uint8_t xi = rendered_width; // counter for x loop below

      asm volatile(
        "push r28\n" // save Y
        "push r29\n"
        "movw r28, %[buffer_ofs]\n" // Y = buffer_ofs_2
        "adiw r28, 63\n" // buffer_ofs_2 = buffer_ofs + 128
        "adiw r28, 63\n"
        "adiw r28, 2\n"
        "loop_y:\n"
        "loop_x:\n"
        // load bitmap and mask data
        "lpm %A[bitmap_data], Z+\n"
        "lpm %A[mask_data], Z+\n"

        // shift mask and buffer data
        "tst %[yOffset]\n"
        "breq skip_shifting\n"
        "mul %A[bitmap_data], %[mul_amt]\n"
        "movw %[bitmap_data], r0\n"
        "mul %A[mask_data], %[mul_amt]\n"
        "movw %[mask_data], r0\n"

        // SECOND PAGE
        // if yOffset != 0 && sRow < 7
        "cpi %[sRow], 7\n"
        "brge end_second_page\n"
        // then
        "ld %[data], Y\n"
        "com %B[mask_data]\n" // invert high byte of mask
        "and %[data], %B[mask_data]\n"
        "or %[data], %B[bitmap_data]\n"
        // update buffer, increment
        "st Y+, %[data]\n"

        "end_second_page:\n"
        "skip_shifting:\n"

        // FIRST PAGE
        // if sRow >= 0
        "tst %[sRow]\n"
        "brmi skip_first_page\n"
        "ld %[data], %a[buffer_ofs]\n"
        // then
        "com %A[mask_data]\n"
        "and %[data], %A[mask_data]\n"
        "or %[data], %A[bitmap_data]\n"
        // update buffer, increment
        "st %a[buffer_ofs]+, %[data]\n"
        "jmp end_first_page\n"

        "skip_first_page:\n"
        // since no ST Z+ when skipped we need to do this manually
        "adiw %[buffer_ofs], 1\n"

        "end_first_page:\n"

        // "x_loop_next:\n"
        "dec %[xi]\n"
        "brne loop_x\n"

        // increment y
        "next_loop_y:\n"
        "dec %[yi]\n"
        "breq finished\n"
        "mov %[xi], %[x_count]\n" // reset x counter
        // sRow++;
        "inc %[sRow]\n"
        "clr __zero_reg__\n"
        // sprite_ofs += (w - rendered_width) * 2;
        "add %A[sprite_ofs], %A[sprite_ofs_jump]\n"
        "adc %B[sprite_ofs], __zero_reg__\n"
        // buffer_ofs += WIDTH - rendered_width;
        "add %A[buffer_ofs], %A[buffer_ofs_jump]\n"
        "adc %B[buffer_ofs], __zero_reg__\n"
        // buffer_ofs_page_2 += WIDTH - rendered_width;
        "add r28, %A[buffer_ofs_jump]\n"
        "adc r29, __zero_reg__\n"

        "rjmp loop_y\n"
        "finished:\n"
        // put the Y register back in place
        "pop r29\n"
        "pop r28\n"
        "clr __zero_reg__\n" // just in case
        : [xi] "+&a" (xi),
        [yi] "+&a" (loop_h),
        [sRow] "+&a" (sRow), // CPI requires an upper register (r16-r23)
        [data] "=&l" (data),
        [mask_data] "=&l" (mask_data),
        [bitmap_data] "=&l" (bitmap_data)
        :
        [screen_width] "M" (WIDTH),
        [x_count] "l" (rendered_width), // lower register
        [sprite_ofs] "z" (bofs),
        [buffer_ofs] "x" (Arduboy2Base::sBuffer+ofs),
        [buffer_ofs_jump] "a" (WIDTH-rendered_width), // upper reg (r16-r23)
        [sprite_ofs_jump] "a" ((w-rendered_width)*2), // upper reg (r16-r23)

        // [sprite_ofs_jump] "r" (0),
        [yOffset] "l" (yOffset), // lower register
        [mul_amt] "l" (mul_amt) // lower register
        // NOTE: We also clobber r28 and r29 (y) but sometimes the compiler
        // won't allow us, so in order to make this work we don't tell it
        // that we clobber them. Instead, we push/pop to preserve them.
        // Then we need to guarantee that the the compiler doesn't put one of
        // our own variables into r28/r29.
        // We do that by specifying all the inputs and outputs use either
        // lower registers (l) or simple (r16-r23) upper registers (a).
        : // pushes/clobbers/pops r28 and r29 (y)
      );
      break;
  }
}
