// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "components/image_fetcher/ios/webp_decoder.h"

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#include <stdint.h>

#include "base/logging.h"
#include "base/metrics/histogram_macros.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

namespace {

class WebpDecoderDelegate : public webp_transcode::WebpDecoder::Delegate {
 public:
  WebpDecoderDelegate() = default;

  NSData* data() const { return decoded_image_; }

  // WebpDecoder::Delegate methods
  void OnFinishedDecoding(bool success) override {
    if (!success)
      decoded_image_ = nil;
  }

  void SetImageFeatures(
      size_t total_size,
      webp_transcode::WebpDecoder::DecodedImageFormat format) override {
    decoded_image_ = [[NSMutableData alloc] initWithCapacity:total_size];
  }

  void OnDataDecoded(NSData* data) override {
    DCHECK(decoded_image_);
    [decoded_image_ appendData:data];
  }

 private:
  ~WebpDecoderDelegate() override {}
  NSMutableData* decoded_image_;

  DISALLOW_COPY_AND_ASSIGN(WebpDecoderDelegate);
};

// Content-type header for WebP images.
const char kWEBPFirstMagicPattern[] = "RIFF";
const char kWEBPSecondMagicPattern[] = "WEBP";

const uint8_t kNumIfdEntries = 15;
const unsigned int kExtraDataSize = 16;
// 10b for signature/header + n * 12b entries + 4b for IFD terminator:
const unsigned int kExtraDataOffset = 10 + 12 * kNumIfdEntries + 4;
const unsigned int kHeaderSize = kExtraDataOffset + kExtraDataSize;
const int kRecompressionThreshold = 64 * 64;  // Threshold in pixels.
const CGFloat kJpegQuality = 0.85;

// Adapted from libwebp example dwebp.c.
void PutLE16(uint8_t* const dst, uint32_t value) {
  dst[0] = (value >> 0) & 0xff;
  dst[1] = (value >> 8) & 0xff;
}

void PutLE32(uint8_t* const dst, uint32_t value) {
  PutLE16(dst + 0, (value >> 0) & 0xffff);
  PutLE16(dst + 2, (value >> 16) & 0xffff);
}

void WriteTiffHeader(uint8_t* dst,
                     int width,
                     int height,
                     int bytes_per_px,
                     bool has_alpha) {
  // For non-alpha case, we omit tag 0x152 (ExtraSamples).
  const uint8_t num_ifd_entries =
      has_alpha ? kNumIfdEntries : kNumIfdEntries - 1;
  uint8_t tiff_header[kHeaderSize] = {
      0x49, 0x49, 0x2a, 0x00,  // little endian signature
      8, 0, 0, 0,              // offset to the unique IFD that follows
      // IFD (offset = 8). Entries must be written in increasing tag order.
      num_ifd_entries, 0,  // Number of entries in the IFD (12 bytes each).
      0x00, 0x01, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0,  //  10: Width  (TBD)
      0x01, 0x01, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0,  //  22: Height (TBD)
      0x02, 0x01, 3, 0, bytes_per_px, 0, 0, 0,   //  34: BitsPerSample: 8888
      kExtraDataOffset + 0, 0, 0, 0, 0x03, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0,
      0,                                         //  46: Compression: none
      0x06, 0x01, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0,  //  58: Photometric: RGB
      0x11, 0x01, 4, 0, 1, 0, 0, 0,              //  70: Strips offset:
      kHeaderSize, 0, 0, 0,                      //      data follows header
      0x12, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0,  //  82: Orientation: topleft
      0x15, 0x01, 3, 0, 1, 0, 0, 0,              //  94: SamplesPerPixels
      bytes_per_px, 0, 0, 0, 0x16, 0x01, 3, 0, 1, 0, 0, 0, 0, 0, 0,
      0,                                         // 106: Rows per strip (TBD)
      0x17, 0x01, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0,  // 118: StripByteCount (TBD)
      0x1a, 0x01, 5, 0, 1, 0, 0, 0,              // 130: X-resolution
      kExtraDataOffset + 8, 0, 0, 0, 0x1b, 0x01, 5, 0, 1, 0, 0,
      0,  // 142: Y-resolution
      kExtraDataOffset + 8, 0, 0, 0, 0x1c, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0,
      0,                                         // 154: PlanarConfiguration
      0x28, 0x01, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0,  // 166: ResolutionUnit (inch)
      0x52, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0,  // 178: ExtraSamples: rgbA
      0, 0, 0, 0,                                // 190: IFD terminator
      // kExtraDataOffset:
      8, 0, 8, 0, 8, 0, 8, 0,  // BitsPerSample
      72, 0, 0, 0, 1, 0, 0, 0  // 72 pixels/inch, for X/Y-resolution
  };

  // Fill placeholders in IFD:
  PutLE32(tiff_header + 10 + 8, width);
  PutLE32(tiff_header + 22 + 8, height);
  PutLE32(tiff_header + 106 + 8, height);
  PutLE32(tiff_header + 118 + 8, width * bytes_per_px * height);
  if (!has_alpha)
    PutLE32(tiff_header + 178, 0);

  memcpy(dst, tiff_header, kHeaderSize);
}

}  // namespace

namespace webp_transcode {

// static
NSData* WebpDecoder::DecodeWebpImage(NSData* webp_image) {
  scoped_refptr<WebpDecoderDelegate> delegate(new WebpDecoderDelegate);

  scoped_refptr<webp_transcode::WebpDecoder> decoder(
      new webp_transcode::WebpDecoder(delegate.get()));

  decoder->OnDataReceived(webp_image);
  DLOG_IF(ERROR, !delegate->data()) << "WebP image decoding failed.";
  return delegate->data();
}

// static
bool WebpDecoder::IsWebpImage(const std::string& image_data) {
  if (image_data.length() < 12)
    return false;
  return image_data.compare(0, 4, kWEBPFirstMagicPattern) == 0 &&
         image_data.compare(8, 4, kWEBPSecondMagicPattern) == 0;
}

// static
size_t WebpDecoder::GetHeaderSize() {
  return kHeaderSize;
}

WebpDecoder::WebpDecoder(WebpDecoder::Delegate* delegate)
    : delegate_(delegate), state_(READING_FEATURES), has_alpha_(0) {
  DCHECK(delegate_.get());
  const bool rv = WebPInitDecoderConfig(&config_);
  DCHECK(rv);
}

WebpDecoder::~WebpDecoder() {
  WebPFreeDecBuffer(&config_.output);
}

void WebpDecoder::OnDataReceived(NSData* data) {
  DCHECK(data);
  switch (state_) {
    case READING_FEATURES:
      DoReadFeatures(data);
      break;
    case READING_DATA:
      DoReadData(data);
      break;
    case DONE:
      DLOG(WARNING) << "Received WebP data but decoding is finished. Ignoring.";
      break;
  }
}

void WebpDecoder::Stop() {
  if (state_ != DONE) {
    state_ = DONE;
    DLOG(WARNING) << "Unexpected end of WebP data.";
    delegate_->OnFinishedDecoding(false);
  }
}

void WebpDecoder::DoReadFeatures(NSData* data) {
  DCHECK_EQ(READING_FEATURES, state_);
  DCHECK(data);
  if (features_)
    [features_ appendData:data];
  else
    features_.reset([[NSMutableData alloc] initWithData:data]);
  VP8StatusCode status =
      WebPGetFeatures(static_cast<const uint8_t*>([features_ bytes]),
                      [features_ length], &config_.input);
  switch (status) {
    case VP8_STATUS_OK: {
      has_alpha_ = config_.input.has_alpha;
      const uint32_t width = config_.input.width;
      const uint32_t height = config_.input.height;
      const size_t bytes_per_px = has_alpha_ ? 4 : 3;
      const int stride = bytes_per_px * width;
      const size_t image_data_size = stride * height;
      const size_t total_size = image_data_size + kHeaderSize;
      // Force pre-multiplied alpha.
      config_.output.colorspace = has_alpha_ ? MODE_rgbA : MODE_RGB;
      config_.output.u.RGBA.stride = stride;
      // Create the output buffer.
      config_.output.u.RGBA.size = image_data_size;
      uint8_t* dst = static_cast<uint8_t*>(malloc(total_size));
      if (!dst) {
        DLOG(ERROR) << "Could not allocate WebP decoding buffer (size = "
                    << total_size << ").";
        delegate_->OnFinishedDecoding(false);
        state_ = DONE;
        break;
      }
      WriteTiffHeader(dst, width, height, bytes_per_px, has_alpha_);
      output_buffer_.reset([[NSData alloc] initWithBytesNoCopy:dst
                                                        length:total_size
                                                  freeWhenDone:YES]);
      config_.output.is_external_memory = 1;
      config_.output.u.RGBA.rgba = dst + kHeaderSize;
      // Start decoding.
      state_ = READING_DATA;
      incremental_decoder_.reset(WebPINewDecoder(&config_.output));
      DoReadData(features_);
      features_.reset();
      break;
    }
    case VP8_STATUS_NOT_ENOUGH_DATA:
      // Do nothing.
      break;
    default:
      DLOG(ERROR) << "Error in WebP image features.";
      delegate_->OnFinishedDecoding(false);
      state_ = DONE;
      break;
  }
}

void WebpDecoder::DoReadData(NSData* data) {
  DCHECK_EQ(READING_DATA, state_);
  DCHECK(incremental_decoder_);
  DCHECK(data);
  VP8StatusCode status =
      WebPIAppend(incremental_decoder_.get(),
                  static_cast<const uint8_t*>([data bytes]), [data length]);
  switch (status) {
    case VP8_STATUS_SUSPENDED:
      // Do nothing: re-compression to JPEG or PNG cannot be done incrementally.
      // Wait for the whole image to be decoded.
      break;
    case VP8_STATUS_OK: {
      bool rv = DoSendData();
      DLOG_IF(ERROR, !rv) << "Error in WebP image conversion.";
      state_ = DONE;
      delegate_->OnFinishedDecoding(rv);
      break;
    }
    default:
      DLOG(ERROR) << "Error in WebP image decoding.";
      delegate_->OnFinishedDecoding(false);
      state_ = DONE;
      break;
  }
}

bool WebpDecoder::DoSendData() {
  DCHECK_EQ(READING_DATA, state_);
  int width, height;
  uint8_t* data_ptr = WebPIDecGetRGB(incremental_decoder_.get(), nullptr,
                                     &width, &height, nullptr);
  if (!data_ptr)
    return false;
  DCHECK_EQ(static_cast<const uint8_t*>([output_buffer_ bytes]) + kHeaderSize,
            data_ptr);
  base::scoped_nsobject<NSData> result_data;
  // When the WebP image is larger than |kRecompressionThreshold| it is
  // compressed to JPEG or PNG. Otherwise, the uncompressed TIFF is used.
  DecodedImageFormat format = TIFF;
  if (width * height > kRecompressionThreshold) {
    base::scoped_nsobject<UIImage> tiff_image(
        [[UIImage alloc] initWithData:output_buffer_]);
    if (!tiff_image)
      return false;
    // Compress to PNG if the image is transparent, JPEG otherwise.
    // TODO(droger): Use PNG instead of JPEG if the WebP image is lossless.
    if (has_alpha_) {
      result_data.reset(UIImagePNGRepresentation(tiff_image));
      format = PNG;
    } else {
      result_data.reset(UIImageJPEGRepresentation(tiff_image, kJpegQuality));
      format = JPEG;
    }
    if (!result_data)
      return false;
  } else {
    result_data.reset(output_buffer_);
  }
  UMA_HISTOGRAM_ENUMERATION("WebP.DecodedImageFormat", format,
                            DECODED_FORMAT_COUNT);
  delegate_->SetImageFeatures([result_data length], format);
  delegate_->OnDataDecoded(result_data);
  output_buffer_.reset();
  return true;
}

}  // namespace webp_transcode
