/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsICOEncoder.h"

#include "BMPHeaders.h"
#include "mozilla/EndianUtils.h"
#include "nsBMPEncoder.h"
#include "nsCRT.h"
#include "nsPNGEncoder.h"
#include "nsStreamUtils.h"
#include "nsString.h"
#include "nsTArray.h"

using namespace mozilla;
using namespace mozilla::image;

NS_IMPL_ISUPPORTS(nsICOEncoder, imgIEncoder, nsIInputStream,
                  nsIAsyncInputStream)

nsICOEncoder::nsICOEncoder()
    : mICOFileHeader{},
      mICODirEntry{},
      mImageBufferStart(nullptr),
      mImageBufferCurr(nullptr),
      mImageBufferSize(0),
      mImageBufferReadPoint(0),
      mFinished(false),
      mUsePNG(true),
      mNotifyThreshold(0) {}

nsICOEncoder::~nsICOEncoder() {
  if (mImageBufferStart) {
    free(mImageBufferStart);
    mImageBufferStart = nullptr;
    mImageBufferCurr = nullptr;
  }
}

NS_IMETHODIMP
nsICOEncoder::SetColorSpaceInfo(imgIEncoder::CICPColourPrimaries,
                                imgIEncoder::CICPTransferCharacteristics,
                                imgIEncoder::CICPMatrixCoefficients, bool) {
  return NS_OK;
}

// nsICOEncoder::InitFromData
// Two output options are supported: format=<png|bmp>;bpp=<bpp_value>
// format specifies whether to use png or bitmap format
// bpp specifies the bits per pixel to use where bpp_value can be 24 or 32
NS_IMETHODIMP
nsICOEncoder::InitFromData(const uint8_t* aData, uint32_t aLength,
                           uint32_t aWidth, uint32_t aHeight, uint32_t aStride,
                           uint32_t aInputFormat,
                           const nsAString& aOutputOptions,
                           const nsACString& aRandomizationKey) {
  // validate input format
  if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA &&
      aInputFormat != INPUT_FORMAT_HOSTARGB) {
    return NS_ERROR_INVALID_ARG;
  }

  // Stride is the padded width of each row, so it better be longer
  if ((aInputFormat == INPUT_FORMAT_RGB && aStride < aWidth * 3) ||
      ((aInputFormat == INPUT_FORMAT_RGBA ||
        aInputFormat == INPUT_FORMAT_HOSTARGB) &&
       aStride < aWidth * 4)) {
    NS_WARNING("Invalid stride for InitFromData");
    return NS_ERROR_INVALID_ARG;
  }

  nsresult rv;
  rv = StartImageEncode(aWidth, aHeight, aInputFormat, aOutputOptions);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = AddImageFrame(aData, aLength, aWidth, aHeight, aStride, aInputFormat,
                     aOutputOptions);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = EndImageEncode();
  return rv;
}

// Returns the number of bytes in the image buffer used
// For an ICO file, this is all bytes in the buffer.
NS_IMETHODIMP
nsICOEncoder::GetImageBufferUsed(uint32_t* aOutputSize) {
  NS_ENSURE_ARG_POINTER(aOutputSize);
  *aOutputSize = mImageBufferSize;
  return NS_OK;
}

// Returns a pointer to the start of the image buffer
NS_IMETHODIMP
nsICOEncoder::GetImageBuffer(char** aOutputBuffer) {
  NS_ENSURE_ARG_POINTER(aOutputBuffer);
  *aOutputBuffer = reinterpret_cast<char*>(mImageBufferStart);
  return NS_OK;
}

NS_IMETHODIMP
nsICOEncoder::AddImageFrame(const uint8_t* aData, uint32_t aLength,
                            uint32_t aWidth, uint32_t aHeight, uint32_t aStride,
                            uint32_t aInputFormat,
                            const nsAString& aFrameOptions) {
  if (mUsePNG) {
    mContainedEncoder = new nsPNGEncoder();
    nsresult rv;
    nsAutoString noParams;
    rv = mContainedEncoder->InitFromData(aData, aLength, aWidth, aHeight,
                                         aStride, aInputFormat, noParams,
                                         VoidCString());
    NS_ENSURE_SUCCESS(rv, rv);

    uint32_t PNGImageBufferSize;
    mContainedEncoder->GetImageBufferUsed(&PNGImageBufferSize);
    mImageBufferSize =
        ICONFILEHEADERSIZE + ICODIRENTRYSIZE + PNGImageBufferSize;
    mImageBufferStart = static_cast<uint8_t*>(malloc(mImageBufferSize));
    if (!mImageBufferStart) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
    mImageBufferCurr = mImageBufferStart;
    mICODirEntry.mBytesInRes = PNGImageBufferSize;

    EncodeFileHeader();
    EncodeInfoHeader();

    char* imageBuffer;
    rv = mContainedEncoder->GetImageBuffer(&imageBuffer);
    NS_ENSURE_SUCCESS(rv, rv);
    memcpy(mImageBufferCurr, imageBuffer, PNGImageBufferSize);
    mImageBufferCurr += PNGImageBufferSize;
  } else {
    mContainedEncoder = new nsBMPEncoder();
    nsresult rv;

    nsAutoString params;
    params.AppendLiteral("bpp=");
    params.AppendInt(mICODirEntry.mBitCount);

    rv = mContainedEncoder->InitFromData(aData, aLength, aWidth, aHeight,
                                         aStride, aInputFormat, params,
                                         VoidCString());
    NS_ENSURE_SUCCESS(rv, rv);

    uint32_t andMaskSize = ((GetRealWidth() + 31) / 32) * 4 *  // row AND mask
                           GetRealHeight();                    // num rows

    uint32_t BMPImageBufferSize;
    mContainedEncoder->GetImageBufferUsed(&BMPImageBufferSize);
    mImageBufferSize =
        ICONFILEHEADERSIZE + ICODIRENTRYSIZE + BMPImageBufferSize + andMaskSize;
    mImageBufferStart = static_cast<uint8_t*>(malloc(mImageBufferSize));
    if (!mImageBufferStart) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
    mImageBufferCurr = mImageBufferStart;

    // Icon files that wrap a BMP file must not include the BITMAPFILEHEADER
    // section at the beginning of the encoded BMP data, so we must skip over
    // bmp::FILE_HEADER_LENGTH bytes when adding the BMP content to the icon
    // file.
    mICODirEntry.mBytesInRes =
        BMPImageBufferSize - bmp::FILE_HEADER_LENGTH + andMaskSize;

    // Encode the icon headers
    EncodeFileHeader();
    EncodeInfoHeader();

    char* imageBuffer;
    rv = mContainedEncoder->GetImageBuffer(&imageBuffer);
    NS_ENSURE_SUCCESS(rv, rv);
    memcpy(mImageBufferCurr, imageBuffer + bmp::FILE_HEADER_LENGTH,
           BMPImageBufferSize - bmp::FILE_HEADER_LENGTH);
    // We need to fix the BMP height to be *2 for the AND mask
    uint32_t fixedHeight = GetRealHeight() * 2;
    NativeEndian::swapToLittleEndianInPlace(&fixedHeight, 1);
    // The height is stored at an offset of 8 from the DIB header
    memcpy(mImageBufferCurr + 8, &fixedHeight, sizeof(fixedHeight));
    mImageBufferCurr += BMPImageBufferSize - bmp::FILE_HEADER_LENGTH;

    // Calculate rowsize in DWORD's
    uint32_t rowSize = ((GetRealWidth() + 31) / 32) * 4;  // + 31 to round up
    int32_t currentLine = GetRealHeight();

    // Write out the AND mask
    while (currentLine > 0) {
      currentLine--;
      uint8_t* encoded = mImageBufferCurr + currentLine * rowSize;
      uint8_t* encodedEnd = encoded + rowSize;
      while (encoded != encodedEnd) {
        *encoded = 0;  // make everything visible
        encoded++;
      }
    }

    mImageBufferCurr += andMaskSize;
  }

  return NS_OK;
}

// See ::InitFromData for other info.
NS_IMETHODIMP
nsICOEncoder::StartImageEncode(uint32_t aWidth, uint32_t aHeight,
                               uint32_t aInputFormat,
                               const nsAString& aOutputOptions) {
  // can't initialize more than once
  if (mImageBufferStart || mImageBufferCurr) {
    return NS_ERROR_ALREADY_INITIALIZED;
  }

  // validate input format
  if (aInputFormat != INPUT_FORMAT_RGB && aInputFormat != INPUT_FORMAT_RGBA &&
      aInputFormat != INPUT_FORMAT_HOSTARGB) {
    return NS_ERROR_INVALID_ARG;
  }

  // Icons are only 1 byte, so make sure our bitmap is in range
  if (aWidth > 256 || aHeight > 256) {
    return NS_ERROR_INVALID_ARG;
  }

  // parse and check any provided output options
  uint16_t bpp = 24;
  bool usePNG = true;
  nsresult rv = ParseOptions(aOutputOptions, bpp, usePNG);
  NS_ENSURE_SUCCESS(rv, rv);
  MOZ_ASSERT(bpp <= 32);

  mUsePNG = usePNG;

  InitFileHeader();
  // The width and height are stored as 0 when we have a value of 256
  InitInfoHeader(bpp, aWidth == 256 ? 0 : (uint8_t)aWidth,
                 aHeight == 256 ? 0 : (uint8_t)aHeight);

  return NS_OK;
}

NS_IMETHODIMP
nsICOEncoder::EndImageEncode() {
  // must be initialized
  if (!mImageBufferStart || !mImageBufferCurr) {
    return NS_ERROR_NOT_INITIALIZED;
  }

  mFinished = true;
  NotifyListener();

  // if output callback can't get enough memory, it will free our buffer
  if (!mImageBufferStart || !mImageBufferCurr) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  return NS_OK;
}

// Parses the encoder options and sets the bits per pixel to use and PNG or BMP
// See InitFromData for a description of the parse options
nsresult nsICOEncoder::ParseOptions(const nsAString& aOptions,
                                    uint16_t& aBppOut, bool& aUsePNGOut) {
  // If no parsing options just use the default of 24BPP and PNG yes
  if (aOptions.Length() == 0) {
    aUsePNGOut = true;
    aBppOut = 24;
  }

  // Parse the input string into a set of name/value pairs.
  // From format: format=<png|bmp>;bpp=<bpp_value>
  // to format: [0] = format=<png|bmp>, [1] = bpp=<bpp_value>
  nsTArray<nsCString> nameValuePairs;
  ParseString(NS_ConvertUTF16toUTF8(aOptions), ';', nameValuePairs);

  // For each name/value pair in the set
  for (unsigned i = 0; i < nameValuePairs.Length(); ++i) {
    // Split the name value pair [0] = name, [1] = value
    nsTArray<nsCString> nameValuePair;
    ParseString(nameValuePairs[i], '=', nameValuePair);
    if (nameValuePair.Length() != 2) {
      return NS_ERROR_INVALID_ARG;
    }

    // Parse the format portion of the string format=<png|bmp>;bpp=<bpp_value>
    if (nameValuePair[0].Equals("format", nsCaseInsensitiveCStringComparator)) {
      if (nameValuePair[1].Equals("png", nsCaseInsensitiveCStringComparator)) {
        aUsePNGOut = true;
      } else if (nameValuePair[1].Equals("bmp",
                                         nsCaseInsensitiveCStringComparator)) {
        aUsePNGOut = false;
      } else {
        return NS_ERROR_INVALID_ARG;
      }
    }

    // Parse the bpp portion of the string format=<png|bmp>;bpp=<bpp_value>
    if (nameValuePair[0].Equals("bpp", nsCaseInsensitiveCStringComparator)) {
      if (nameValuePair[1].EqualsLiteral("24")) {
        aBppOut = 24;
      } else if (nameValuePair[1].EqualsLiteral("32")) {
        aBppOut = 32;
      } else {
        return NS_ERROR_INVALID_ARG;
      }
    }
  }

  return NS_OK;
}

NS_IMETHODIMP
nsICOEncoder::Close() {
  if (mImageBufferStart) {
    free(mImageBufferStart);
    mImageBufferStart = nullptr;
    mImageBufferSize = 0;
    mImageBufferReadPoint = 0;
    mImageBufferCurr = nullptr;
  }

  return NS_OK;
}

// Obtains the available bytes to read
NS_IMETHODIMP
nsICOEncoder::Available(uint64_t* _retval) {
  if (!mImageBufferStart || !mImageBufferCurr) {
    return NS_BASE_STREAM_CLOSED;
  }

  *_retval = GetCurrentImageBufferOffset() - mImageBufferReadPoint;
  return NS_OK;
}

// Obtains the stream's status
NS_IMETHODIMP
nsICOEncoder::StreamStatus() {
  return mImageBufferStart && mImageBufferCurr ? NS_OK : NS_BASE_STREAM_CLOSED;
}

// [noscript] Reads bytes which are available
NS_IMETHODIMP
nsICOEncoder::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) {
  return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval);
}

// [noscript] Reads segments
NS_IMETHODIMP
nsICOEncoder::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
                           uint32_t aCount, uint32_t* _retval) {
  uint32_t maxCount = GetCurrentImageBufferOffset() - mImageBufferReadPoint;
  if (maxCount == 0) {
    *_retval = 0;
    return mFinished ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK;
  }

  if (aCount > maxCount) {
    aCount = maxCount;
  }

  nsresult rv = aWriter(
      this, aClosure,
      reinterpret_cast<const char*>(mImageBufferStart + mImageBufferReadPoint),
      0, aCount, _retval);
  if (NS_SUCCEEDED(rv)) {
    NS_ASSERTION(*_retval <= aCount, "bad write count");
    mImageBufferReadPoint += *_retval;
  }
  // errors returned from the writer end here!
  return NS_OK;
}

NS_IMETHODIMP
nsICOEncoder::IsNonBlocking(bool* _retval) {
  *_retval = true;
  return NS_OK;
}

NS_IMETHODIMP
nsICOEncoder::AsyncWait(nsIInputStreamCallback* aCallback, uint32_t aFlags,
                        uint32_t aRequestedCount, nsIEventTarget* aTarget) {
  if (aFlags != 0) {
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  if (mCallback || mCallbackTarget) {
    return NS_ERROR_UNEXPECTED;
  }

  mCallbackTarget = aTarget;
  // 0 means "any number of bytes except 0"
  mNotifyThreshold = aRequestedCount;
  if (!aRequestedCount) {
    mNotifyThreshold = 1024;  // We don't want to notify incessantly
  }

  // We set the callback absolutely last, because NotifyListener uses it to
  // determine if someone needs to be notified.  If we don't set it last,
  // NotifyListener might try to fire off a notification to a null target
  // which will generally cause non-threadsafe objects to be used off the
  // main thread
  mCallback = aCallback;

  // What we are being asked for may be present already
  NotifyListener();
  return NS_OK;
}

NS_IMETHODIMP
nsICOEncoder::CloseWithStatus(nsresult aStatus) { return Close(); }

void nsICOEncoder::NotifyListener() {
  if (mCallback && (GetCurrentImageBufferOffset() - mImageBufferReadPoint >=
                        mNotifyThreshold ||
                    mFinished)) {
    nsCOMPtr<nsIInputStreamCallback> callback;
    if (mCallbackTarget) {
      callback = NS_NewInputStreamReadyEvent("nsICOEncoder::NotifyListener",
                                             mCallback, mCallbackTarget);
    } else {
      callback = mCallback;
    }

    NS_ASSERTION(callback, "Shouldn't fail to make the callback");
    // Null the callback first because OnInputStreamReady could reenter
    // AsyncWait
    mCallback = nullptr;
    mCallbackTarget = nullptr;
    mNotifyThreshold = 0;

    callback->OnInputStreamReady(this);
  }
}

// Initializes the icon file header mICOFileHeader
void nsICOEncoder::InitFileHeader() {
  memset(&mICOFileHeader, 0, sizeof(mICOFileHeader));
  mICOFileHeader.mReserved = 0;
  mICOFileHeader.mType = 1;
  mICOFileHeader.mCount = 1;
}

// Initializes the icon directory info header mICODirEntry
void nsICOEncoder::InitInfoHeader(uint16_t aBPP, uint8_t aWidth,
                                  uint8_t aHeight) {
  memset(&mICODirEntry, 0, sizeof(mICODirEntry));
  mICODirEntry.mBitCount = aBPP;
  mICODirEntry.mBytesInRes = 0;
  mICODirEntry.mColorCount = 0;
  mICODirEntry.mWidth = aWidth;
  mICODirEntry.mHeight = aHeight;
  mICODirEntry.mImageOffset = ICONFILEHEADERSIZE + ICODIRENTRYSIZE;
  mICODirEntry.mPlanes = 1;
  mICODirEntry.mReserved = 0;
}

// Encodes the icon file header mICOFileHeader
void nsICOEncoder::EncodeFileHeader() {
  IconFileHeader littleEndianIFH = mICOFileHeader;
  NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mReserved, 1);
  NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mType, 1);
  NativeEndian::swapToLittleEndianInPlace(&littleEndianIFH.mCount, 1);

  memcpy(mImageBufferCurr, &littleEndianIFH.mReserved,
         sizeof(littleEndianIFH.mReserved));
  mImageBufferCurr += sizeof(littleEndianIFH.mReserved);
  memcpy(mImageBufferCurr, &littleEndianIFH.mType,
         sizeof(littleEndianIFH.mType));
  mImageBufferCurr += sizeof(littleEndianIFH.mType);
  memcpy(mImageBufferCurr, &littleEndianIFH.mCount,
         sizeof(littleEndianIFH.mCount));
  mImageBufferCurr += sizeof(littleEndianIFH.mCount);
}

// Encodes the icon directory info header mICODirEntry
void nsICOEncoder::EncodeInfoHeader() {
  IconDirEntry littleEndianmIDE = mICODirEntry;

  NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mPlanes, 1);
  NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mBitCount, 1);
  NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mBytesInRes, 1);
  NativeEndian::swapToLittleEndianInPlace(&littleEndianmIDE.mImageOffset, 1);

  memcpy(mImageBufferCurr, &littleEndianmIDE.mWidth,
         sizeof(littleEndianmIDE.mWidth));
  mImageBufferCurr += sizeof(littleEndianmIDE.mWidth);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mHeight,
         sizeof(littleEndianmIDE.mHeight));
  mImageBufferCurr += sizeof(littleEndianmIDE.mHeight);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mColorCount,
         sizeof(littleEndianmIDE.mColorCount));
  mImageBufferCurr += sizeof(littleEndianmIDE.mColorCount);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mReserved,
         sizeof(littleEndianmIDE.mReserved));
  mImageBufferCurr += sizeof(littleEndianmIDE.mReserved);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mPlanes,
         sizeof(littleEndianmIDE.mPlanes));
  mImageBufferCurr += sizeof(littleEndianmIDE.mPlanes);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mBitCount,
         sizeof(littleEndianmIDE.mBitCount));
  mImageBufferCurr += sizeof(littleEndianmIDE.mBitCount);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mBytesInRes,
         sizeof(littleEndianmIDE.mBytesInRes));
  mImageBufferCurr += sizeof(littleEndianmIDE.mBytesInRes);
  memcpy(mImageBufferCurr, &littleEndianmIDE.mImageOffset,
         sizeof(littleEndianmIDE.mImageOffset));
  mImageBufferCurr += sizeof(littleEndianmIDE.mImageOffset);
}
