// Copyright 2013 Google Inc. All Rights Reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the COPYING file in the root of the source
// tree. An additional intellectual property rights grant can be found
// in the file PATENTS. All contributing project authors may
// be found in the AUTHORS file in the root of the source tree.
// -----------------------------------------------------------------------------
//
//  Helper structs and methods for gif2webp tool.
//

#include <assert.h>
#include <stdio.h>

#include "webp/encode.h"
#include "./gif2webp_util.h"

#define DELTA_INFINITY      1ULL << 32
#define KEYFRAME_NONE       -1

//------------------------------------------------------------------------------
// Helper utilities.

static void ClearRectangle(WebPPicture* const picture,
                           int left, int top, int width, int height) {
  int j;
  for (j = top; j < top + height; ++j) {
    uint32_t* const dst = picture->argb + j * picture->argb_stride;
    int i;
    for (i = left; i < left + width; ++i) {
      dst[i] = WEBP_UTIL_TRANSPARENT_COLOR;
    }
  }
}

void WebPUtilClearPic(WebPPicture* const picture,
                      const WebPFrameRect* const rect) {
  if (rect != NULL) {
    ClearRectangle(picture, rect->x_offset, rect->y_offset,
                   rect->width, rect->height);
  } else {
    ClearRectangle(picture, 0, 0, picture->width, picture->height);
  }
}

// TODO: Also used in picture.c. Move to a common location?
// Copy width x height pixels from 'src' to 'dst' honoring the strides.
static void CopyPlane(const uint8_t* src, int src_stride,
                      uint8_t* dst, int dst_stride, int width, int height) {
  while (height-- > 0) {
    memcpy(dst, src, width);
    src += src_stride;
    dst += dst_stride;
  }
}

// Copy pixels from 'src' to 'dst' honoring strides. 'src' and 'dst' are assumed
// to be already allocated.
static void CopyPixels(const WebPPicture* const src, WebPPicture* const dst) {
  assert(src->width == dst->width && src->height == dst->height);
  CopyPlane((uint8_t*)src->argb, 4 * src->argb_stride, (uint8_t*)dst->argb,
            4 * dst->argb_stride, 4 * src->width, src->height);
}

// Given 'src' picture and its frame rectangle 'rect', blend it into 'dst'.
static void BlendPixels(const WebPPicture* const src,
                        const WebPFrameRect* const rect,
                        WebPPicture* const dst) {
  int j;
  assert(src->width == dst->width && src->height == dst->height);
  for (j = rect->y_offset; j < rect->y_offset + rect->height; ++j) {
    int i;
    for (i = rect->x_offset; i < rect->x_offset + rect->width; ++i) {
      const uint32_t src_pixel = src->argb[j * src->argb_stride + i];
      const int src_alpha = src_pixel >> 24;
      if (src_alpha != 0) {
        dst->argb[j * dst->argb_stride + i] = src_pixel;
      }
    }
  }
}

// Replace transparent pixels within 'dst_rect' of 'dst' by those in the 'src'.
static void ReduceTransparency(const WebPPicture* const src,
                               const WebPFrameRect* const rect,
                               WebPPicture* const dst) {
  int i, j;
  assert(src != NULL && dst != NULL && rect != NULL);
  assert(src->width == dst->width && src->height == dst->height);
  for (j = rect->y_offset; j < rect->y_offset + rect->height; ++j) {
    for (i = rect->x_offset; i < rect->x_offset + rect->width; ++i) {
      const uint32_t src_pixel = src->argb[j * src->argb_stride + i];
      const int src_alpha = src_pixel >> 24;
      const uint32_t dst_pixel = dst->argb[j * dst->argb_stride + i];
      const int dst_alpha = dst_pixel >> 24;
      if (dst_alpha == 0 && src_alpha == 0xff) {
        dst->argb[j * dst->argb_stride + i] = src_pixel;
      }
    }
  }
}

// Replace similar blocks of pixels by a 'see-through' transparent block
// with uniform average color.
static void FlattenSimilarBlocks(const WebPPicture* const src,
                                 const WebPFrameRect* const rect,
                                 WebPPicture* const dst) {
  int i, j;
  const int block_size = 8;
  const int y_start = (rect->y_offset + block_size) & ~(block_size - 1);
  const int y_end = (rect->y_offset + rect->height) & ~(block_size - 1);
  const int x_start = (rect->x_offset + block_size) & ~(block_size - 1);
  const int x_end = (rect->x_offset + rect->width) & ~(block_size - 1);
  assert(src != NULL && dst != NULL && rect != NULL);
  assert(src->width == dst->width && src->height == dst->height);
  assert((block_size & (block_size - 1)) == 0);  // must be a power of 2
  // Iterate over each block and count similar pixels.
  for (j = y_start; j < y_end; j += block_size) {
    for (i = x_start; i < x_end; i += block_size) {
      int cnt = 0;
      int avg_r = 0, avg_g = 0, avg_b = 0;
      int x, y;
      const uint32_t* const psrc = src->argb + j * src->argb_stride + i;
      uint32_t* const pdst = dst->argb + j * dst->argb_stride + i;
      for (y = 0; y < block_size; ++y) {
        for (x = 0; x < block_size; ++x) {
          const uint32_t src_pixel = psrc[x + y * src->argb_stride];
          const int alpha = src_pixel >> 24;
          if (alpha == 0xff &&
              src_pixel == pdst[x + y * dst->argb_stride]) {
              ++cnt;
              avg_r += (src_pixel >> 16) & 0xff;
              avg_g += (src_pixel >>  8) & 0xff;
              avg_b += (src_pixel >>  0) & 0xff;
          }
        }
      }
      // If we have a fully similar block, we replace it with an
      // average transparent block. This compresses better in lossy mode.
      if (cnt == block_size * block_size) {
        const uint32_t color = (0x00          << 24) |
                               ((avg_r / cnt) << 16) |
                               ((avg_g / cnt) <<  8) |
                               ((avg_b / cnt) <<  0);
        for (y = 0; y < block_size; ++y) {
          for (x = 0; x < block_size; ++x) {
            pdst[x + y * dst->argb_stride] = color;
          }
        }
      }
    }
  }
}

//------------------------------------------------------------------------------
// Key frame related utilities.

// Returns true if 'curr' frame with frame rectangle 'curr_rect' is a key frame,
// that is, it can be decoded independently of 'prev' canvas.
static int IsKeyFrame(const WebPPicture* const curr,
                      const WebPFrameRect* const curr_rect,
                      const WebPPicture* const prev) {
  int i, j;
  int is_key_frame = 1;

  // If previous canvas (with previous frame disposed) is all transparent,
  // current frame is a key frame.
  for (i = 0; i < prev->width; ++i) {
    for (j = 0; j < prev->height; ++j) {
      const uint32_t prev_alpha = (prev->argb[j * prev->argb_stride + i]) >> 24;
      if (prev_alpha != 0) {
        is_key_frame = 0;
        break;
      }
    }
    if (!is_key_frame) break;
  }
  if (is_key_frame) return 1;

  // If current frame covers the whole canvas and does not contain any
  // transparent pixels that depend on previous canvas, then current frame is
  // a key frame.
  if (curr_rect->width == curr->width && curr_rect->height == curr->height) {
    assert(curr_rect->x_offset == 0 && curr_rect->y_offset == 0);
    is_key_frame = 1;
    for (j = 0; j < prev->height; ++j) {
      for (i = 0; i < prev->width; ++i) {
        const uint32_t prev_alpha =
            (prev->argb[j * prev->argb_stride + i]) >> 24;
        const uint32_t curr_alpha =
            (curr->argb[j * curr->argb_stride + i]) >> 24;
        if (curr_alpha != 0xff && prev_alpha != 0) {
          is_key_frame = 0;
          break;
        }
      }
      if (!is_key_frame) break;
    }
    if (is_key_frame) return 1;
  }

  return 0;
}

// Given 'prev' frame and current frame rectangle 'rect', convert 'curr' frame
// to a key frame.
static void ConvertToKeyFrame(const WebPPicture* const prev,
                              WebPFrameRect* const rect,
                              WebPPicture* const curr) {
  int j;
  assert(curr->width == prev->width && curr->height == prev->height);

  // Replace transparent pixels of current canvas with those from previous
  // canvas (with previous frame disposed).
  for (j = 0; j < curr->height; ++j) {
    int i;
    for (i = 0; i < curr->width; ++i) {
      uint32_t* const curr_pixel = curr->argb + j * curr->argb_stride + i;
      const int curr_alpha = *curr_pixel >> 24;
      if (curr_alpha == 0) {
        *curr_pixel = prev->argb[j * prev->argb_stride + i];
      }
    }
  }

  // Frame rectangle now covers the whole canvas.
  rect->x_offset = 0;
  rect->y_offset = 0;
  rect->width = curr->width;
  rect->height = curr->height;
}

//------------------------------------------------------------------------------
// Encoded frame.

// Used to store two candidates of encoded data for an animation frame. One of
// the two will be chosen later.
typedef struct {
  WebPMuxFrameInfo sub_frame;  // Encoded frame rectangle.
  WebPMuxFrameInfo key_frame;  // Encoded frame if it was converted to keyframe.
} EncodedFrame;

// Release the data contained by 'encoded_frame'.
static void FrameRelease(EncodedFrame* const encoded_frame) {
  if (encoded_frame != NULL) {
    WebPDataClear(&encoded_frame->sub_frame.bitstream);
    WebPDataClear(&encoded_frame->key_frame.bitstream);
    memset(encoded_frame, 0, sizeof(*encoded_frame));
  }
}

//------------------------------------------------------------------------------
// Frame cache.

// Used to store encoded frames that haven't been output yet.
struct WebPFrameCache {
  EncodedFrame* encoded_frames;  // Array of encoded frames.
  size_t size;               // Number of allocated data elements.
  size_t start;              // Start index.
  size_t count;              // Number of valid data elements.
  int flush_count;           // If >0, ‘flush_count’ frames starting from
                             // 'start' are ready to be added to mux.
  int64_t best_delta;        // min(canvas size - frame size) over the frames.
                             // Can be negative in certain cases due to
                             // transparent pixels in a frame.
  int keyframe;              // Index of selected keyframe relative to 'start'.

  size_t kmin;                   // Min distance between key frames.
  size_t kmax;                   // Max distance between key frames.
  size_t count_since_key_frame;  // Frames seen since the last key frame.
  int allow_mixed;           // If true, each frame can be lossy or lossless.
  WebPPicture prev_canvas;   // Previous canvas (properly disposed).
  WebPPicture curr_canvas;   // Current canvas (temporary buffer).
  int is_first_frame;        // True if no frames have been added to the cache
                             // since WebPFrameCacheNew().
};

// Reset the counters in the cache struct. Doesn't touch 'cache->encoded_frames'
// and 'cache->size'.
static void CacheReset(WebPFrameCache* const cache) {
  cache->start = 0;
  cache->count = 0;
  cache->flush_count = 0;
  cache->best_delta = DELTA_INFINITY;
  cache->keyframe = KEYFRAME_NONE;
}

WebPFrameCache* WebPFrameCacheNew(int width, int height,
                                  size_t kmin, size_t kmax, int allow_mixed) {
  WebPFrameCache* cache = (WebPFrameCache*)malloc(sizeof(*cache));
  if (cache == NULL) return NULL;
  CacheReset(cache);
  // sanity init, so we can call WebPFrameCacheDelete():
  cache->encoded_frames = NULL;

  cache->is_first_frame = 1;

  // Picture buffers.
  if (!WebPPictureInit(&cache->prev_canvas) ||
      !WebPPictureInit(&cache->curr_canvas)) {
    return NULL;
  }
  cache->prev_canvas.width = width;
  cache->prev_canvas.height = height;
  cache->prev_canvas.use_argb = 1;
  if (!WebPPictureAlloc(&cache->prev_canvas) ||
      !WebPPictureCopy(&cache->prev_canvas, &cache->curr_canvas)) {
    goto Err;
  }
  WebPUtilClearPic(&cache->prev_canvas, NULL);

  // Cache data.
  cache->allow_mixed = allow_mixed;
  cache->kmin = kmin;
  cache->kmax = kmax;
  cache->count_since_key_frame = 0;
  assert(kmax > kmin);
  cache->size = kmax - kmin;
  cache->encoded_frames =
      (EncodedFrame*)calloc(cache->size, sizeof(*cache->encoded_frames));
  if (cache->encoded_frames == NULL) goto Err;

  return cache;  // All OK.

 Err:
  WebPFrameCacheDelete(cache);
  return NULL;
}

void WebPFrameCacheDelete(WebPFrameCache* const cache) {
  if (cache != NULL) {
    if (cache->encoded_frames != NULL) {
      size_t i;
      for (i = 0; i < cache->size; ++i) {
        FrameRelease(&cache->encoded_frames[i]);
      }
      free(cache->encoded_frames);
    }
    WebPPictureFree(&cache->prev_canvas);
    WebPPictureFree(&cache->curr_canvas);
    free(cache);
  }
}

static int EncodeFrame(const WebPConfig* const config, WebPPicture* const pic,
                       WebPMemoryWriter* const memory) {
  pic->use_argb = 1;
  pic->writer = WebPMemoryWrite;
  pic->custom_ptr = memory;
  if (!WebPEncode(config, pic)) {
    return 0;
  }
  return 1;
}

static void GetEncodedData(const WebPMemoryWriter* const memory,
                           WebPData* const encoded_data) {
  encoded_data->bytes = memory->mem;
  encoded_data->size  = memory->size;
}

#define MIN_COLORS_LOSSY     31  // Don't try lossy below this threshold.
#define MAX_COLORS_LOSSLESS 194  // Don't try lossless above this threshold.
#define MAX_COLOR_COUNT     256  // Power of 2 greater than MAX_COLORS_LOSSLESS.
#define HASH_SIZE (MAX_COLOR_COUNT * 4)
#define HASH_RIGHT_SHIFT     22  // 32 - log2(HASH_SIZE).

// TODO(urvang): Also used in enc/vp8l.c. Move to utils.
// If the number of colors in the 'pic' is at least MAX_COLOR_COUNT, return
// MAX_COLOR_COUNT. Otherwise, return the exact number of colors in the 'pic'.
static int GetColorCount(const WebPPicture* const pic) {
  int x, y;
  int num_colors = 0;
  uint8_t in_use[HASH_SIZE] = { 0 };
  uint32_t colors[HASH_SIZE];
  static const uint32_t kHashMul = 0x1e35a7bd;
  const uint32_t* argb = pic->argb;
  const int width = pic->width;
  const int height = pic->height;
  uint32_t last_pix = ~argb[0];   // so we're sure that last_pix != argb[0]

  for (y = 0; y < height; ++y) {
    for (x = 0; x < width; ++x) {
      int key;
      if (argb[x] == last_pix) {
        continue;
      }
      last_pix = argb[x];
      key = (kHashMul * last_pix) >> HASH_RIGHT_SHIFT;
      while (1) {
        if (!in_use[key]) {
          colors[key] = last_pix;
          in_use[key] = 1;
          ++num_colors;
          if (num_colors >= MAX_COLOR_COUNT) {
            return MAX_COLOR_COUNT;  // Exact count not needed.
          }
          break;
        } else if (colors[key] == last_pix) {
          break;  // The color is already there.
        } else {
          // Some other color sits here, so do linear conflict resolution.
          ++key;
          key &= (HASH_SIZE - 1);  // Key mask.
        }
      }
    }
    argb += pic->argb_stride;
  }
  return num_colors;
}

#undef MAX_COLOR_COUNT
#undef HASH_SIZE
#undef HASH_RIGHT_SHIFT

static int SetFrame(const WebPConfig* const config, int allow_mixed,
                    int is_key_frame, const WebPPicture* const prev_canvas,
                    WebPPicture* const frame, const WebPFrameRect* const rect,
                    const WebPMuxFrameInfo* const info,
                    WebPPicture* const sub_frame, EncodedFrame* encoded_frame) {
  int try_lossless;
  int try_lossy;
  int try_both;
  WebPMemoryWriter mem1, mem2;
  WebPData* encoded_data;
  WebPMuxFrameInfo* const dst =
      is_key_frame ? &encoded_frame->key_frame : &encoded_frame->sub_frame;
  *dst = *info;
  encoded_data = &dst->bitstream;
  WebPMemoryWriterInit(&mem1);
  WebPMemoryWriterInit(&mem2);

  if (!allow_mixed) {
    try_lossless = config->lossless;
    try_lossy = !try_lossless;
  } else {  // Use a heuristic for trying lossless and/or lossy compression.
    const int num_colors = GetColorCount(sub_frame);
    try_lossless = (num_colors < MAX_COLORS_LOSSLESS);
    try_lossy = (num_colors >= MIN_COLORS_LOSSY);
  }
  try_both = try_lossless && try_lossy;

  if (try_lossless) {
    WebPConfig config_ll = *config;
    config_ll.lossless = 1;
    if (!EncodeFrame(&config_ll, sub_frame, &mem1)) {
      goto Err;
    }
  }

  if (try_lossy) {
    WebPConfig config_lossy = *config;
    config_lossy.lossless = 0;
    if (!is_key_frame) {
      // For lossy compression of a frame, it's better to replace transparent
      // pixels of 'curr' with actual RGB values, whenever possible.
      ReduceTransparency(prev_canvas, rect, frame);
      // TODO(later): Investigate if this helps lossless compression as well.
      FlattenSimilarBlocks(prev_canvas, rect, frame);
    }
    if (!EncodeFrame(&config_lossy, sub_frame, &mem2)) {
      goto Err;
    }
  }

  if (try_both) {  // Pick the encoding with smallest size.
    // TODO(later): Perhaps a rough SSIM/PSNR produced by the encoder should
    // also be a criteria, in addition to sizes.
    if (mem1.size <= mem2.size) {
      free(mem2.mem);
      GetEncodedData(&mem1, encoded_data);
    } else {
      free(mem1.mem);
      GetEncodedData(&mem2, encoded_data);
    }
  } else {
    GetEncodedData(try_lossless ? &mem1 : &mem2, encoded_data);
  }
  return 1;

 Err:
  free(mem1.mem);
  free(mem2.mem);
  return 0;
}

#undef MIN_COLORS_LOSSY
#undef MAX_COLORS_LOSSLESS

// Returns cached frame at given 'position' index.
static EncodedFrame* CacheGetFrame(const WebPFrameCache* const cache,
                                   size_t position) {
  assert(cache->start + position < cache->size);
  return &cache->encoded_frames[cache->start + position];
}

// Calculate the penalty incurred if we encode given frame as a key frame
// instead of a sub-frame.
static int64_t KeyFramePenalty(const EncodedFrame* const encoded_frame) {
  return ((int64_t)encoded_frame->key_frame.bitstream.size -
          encoded_frame->sub_frame.bitstream.size);
}

static void DisposeFrame(WebPMuxAnimDispose dispose_method,
                         const WebPFrameRect* const gif_rect,
                         WebPPicture* const frame, WebPPicture* const canvas) {
  if (dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
    WebPUtilClearPic(frame, NULL);
    WebPUtilClearPic(canvas, gif_rect);
  }
}

int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
                           const WebPConfig* const config,
                           const WebPFrameRect* const orig_rect,
                           WebPPicture* const frame,
                           WebPMuxFrameInfo* const info) {
  int ok = 0;
  WebPFrameRect rect = *orig_rect;
  WebPPicture sub_image;  // View extracted from 'frame' with rectangle 'rect'.
  WebPPicture* const prev_canvas = &cache->prev_canvas;
  const size_t position = cache->count;
  const int allow_mixed = cache->allow_mixed;
  EncodedFrame* const encoded_frame = CacheGetFrame(cache, position);
  assert(position < cache->size);

  // Snap to even offsets (and adjust dimensions if needed).
  rect.width += (rect.x_offset & 1);
  rect.height += (rect.y_offset & 1);
  rect.x_offset &= ~1;
  rect.y_offset &= ~1;

  if (!WebPPictureView(frame, rect.x_offset, rect.y_offset,
                       rect.width, rect.height, &sub_image)) {
    return 0;
  }
  info->x_offset = rect.x_offset;
  info->y_offset = rect.y_offset;

  ++cache->count;

  if (cache->is_first_frame || IsKeyFrame(frame, &rect, prev_canvas)) {
    // Add this as a key frame.
    if (!SetFrame(config, allow_mixed, 1, NULL, NULL, NULL, info, &sub_image,
                  encoded_frame)) {
      goto End;
    }
    cache->keyframe = position;
    cache->flush_count = cache->count;
    cache->count_since_key_frame = 0;
    // Update prev_canvas by simply copying from 'curr'.
    CopyPixels(frame, prev_canvas);
  } else {
    ++cache->count_since_key_frame;
    if (cache->count_since_key_frame <= cache->kmin) {
      // Add this as a frame rectangle.
      if (!SetFrame(config, allow_mixed, 0, prev_canvas, frame, &rect, info,
                    &sub_image, encoded_frame)) {
        goto End;
      }
      cache->flush_count = cache->count;
      // Update prev_canvas by blending 'curr' into it.
      BlendPixels(frame, orig_rect, prev_canvas);
    } else {
      WebPPicture full_image;
      WebPMuxFrameInfo full_image_info;
      int frame_added;
      int64_t curr_delta;

      // Add frame rectangle to cache.
      if (!SetFrame(config, allow_mixed, 0, prev_canvas, frame, &rect, info,
                    &sub_image, encoded_frame)) {
        goto End;
      }

      // Convert to a key frame.
      CopyPixels(frame, &cache->curr_canvas);
      ConvertToKeyFrame(prev_canvas, &rect, &cache->curr_canvas);
      if (!WebPPictureView(&cache->curr_canvas, rect.x_offset, rect.y_offset,
                           rect.width, rect.height, &full_image)) {
        goto End;
      }
      full_image_info = *info;
      full_image_info.x_offset = rect.x_offset;
      full_image_info.y_offset = rect.y_offset;

      // Add key frame to cache, too.
      frame_added = SetFrame(config, allow_mixed, 1, NULL, NULL, NULL,
                             &full_image_info, &full_image, encoded_frame);
      WebPPictureFree(&full_image);
      if (!frame_added) goto End;

      // Analyze size difference of the two variants.
      curr_delta = KeyFramePenalty(encoded_frame);
      if (curr_delta <= cache->best_delta) {  // Pick this as keyframe.
        cache->keyframe = position;
        cache->best_delta = curr_delta;
        cache->flush_count = cache->count - 1;  // We can flush previous frames.
      }
      if (cache->count_since_key_frame == cache->kmax) {
        cache->flush_count = cache->count;
        cache->count_since_key_frame = 0;
      }

      // Update prev_canvas by simply copying from 'curr_canvas'.
      CopyPixels(&cache->curr_canvas, prev_canvas);
    }
  }

  DisposeFrame(info->dispose_method, orig_rect, frame, prev_canvas);

  cache->is_first_frame = 0;
  ok = 1;

 End:
  WebPPictureFree(&sub_image);
  if (!ok) {
    FrameRelease(encoded_frame);
    --cache->count;  // We reset the count, as the frame addition failed.
  }
  return ok;
}

WebPMuxError WebPFrameCacheFlush(WebPFrameCache* const cache, int verbose,
                                 WebPMux* const mux) {
  while (cache->flush_count > 0) {
    WebPMuxFrameInfo* info;
    WebPMuxError err;
    EncodedFrame* const curr = CacheGetFrame(cache, 0);
    // Pick frame or full canvas.
    if (cache->keyframe == 0) {
      info = &curr->key_frame;
      info->blend_method = WEBP_MUX_NO_BLEND;
      cache->keyframe = KEYFRAME_NONE;
      cache->best_delta = DELTA_INFINITY;
    } else {
      info = &curr->sub_frame;
      info->blend_method = WEBP_MUX_BLEND;
    }
    // Add to mux.
    err = WebPMuxPushFrame(mux, info, 1);
    if (err != WEBP_MUX_OK) return err;
    if (verbose) {
      printf("Added frame. offset:%d,%d duration:%d dispose:%d blend:%d\n",
             info->x_offset, info->y_offset, info->duration,
             info->dispose_method, info->blend_method);
    }
    FrameRelease(curr);
    ++cache->start;
    --cache->flush_count;
    --cache->count;
    if (cache->keyframe != KEYFRAME_NONE) --cache->keyframe;
  }

  if (cache->count == 0) CacheReset(cache);
  return WEBP_MUX_OK;
}

WebPMuxError WebPFrameCacheFlushAll(WebPFrameCache* const cache, int verbose,
                                    WebPMux* const mux) {
  cache->flush_count = cache->count;  // Force flushing of all frames.
  return WebPFrameCacheFlush(cache, verbose, mux);
}

//------------------------------------------------------------------------------
