/*
    RawSpeed - RAW file decoder.

    Copyright (C) 2009-2014 Klaus Post
    Copyright (C) 2014 Pedro Côrte-Real
    Copyright (C) 2017-2018 Roman Lebedev

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "rawspeedconfig.h"
#include "decompressors/PanasonicV4Decompressor.h"
#include "adt/Array1DRef.h"
#include "adt/Array2DRef.h"
#include "adt/Bit.h"
#include "adt/Casts.h"
#include "adt/Invariant.h"
#include "adt/Mutex.h"
#include "adt/Point.h"
#include "common/Common.h"
#include "common/RawImage.h"
#include "decoders/RawDecoderException.h"
#include "io/Buffer.h"
#include "io/ByteStream.h"
#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <limits>
#include <utility>
#include <vector>

namespace rawspeed {

PanasonicV4Decompressor::PanasonicV4Decompressor(RawImage img,
                                                 ByteStream input_,
                                                 bool zero_is_not_bad,
                                                 uint32_t section_split_offset_)
    : mRaw(std::move(img)), zero_is_bad(!zero_is_not_bad),
      section_split_offset(section_split_offset_) {
  if (mRaw->getCpp() != 1 || mRaw->getDataType() != RawImageType::UINT16 ||
      mRaw->getBpp() != sizeof(uint16_t))
    ThrowRDE("Unexpected component count / data type");

  if (!mRaw->dim.hasPositiveArea() || mRaw->dim.x % PixelsPerPacket != 0) {
    ThrowRDE("Unexpected image dimensions found: (%i; %i)", mRaw->dim.x,
             mRaw->dim.y);
  }

  if (BlockSize < section_split_offset)
    ThrowRDE("Bad section_split_offset: %u, less than BlockSize (%u)",
             section_split_offset, BlockSize);

  // Naive count of bytes that given pixel count requires.
  invariant(mRaw->dim.area() % PixelsPerPacket == 0);
  const auto bytesTotal = (mRaw->dim.area() / PixelsPerPacket) * BytesPerPacket;
  invariant(bytesTotal > 0);

  // If section_split_offset is zero, then that we need to read the normal
  // amount of bytes. But if it is not, then we need to round up to multiple of
  // BlockSize, because of splitting&rotation of each BlockSize's slice in half
  // at section_split_offset bytes.
  const auto bufSize =
      section_split_offset == 0 ? bytesTotal : roundUp(bytesTotal, BlockSize);

  if (bufSize > std::numeric_limits<ByteStream::size_type>::max())
    ThrowRDE("Raw dimensions require input buffer larger than supported");

  input = input_.peekStream(implicit_cast<Buffer::size_type>(bufSize));

  chopInputIntoBlocks();
}

void PanasonicV4Decompressor::chopInputIntoBlocks() {
  auto pixelToCoordinate = [width = mRaw->dim.x](unsigned pixel) {
    return iPoint2D(pixel % width, pixel / width);
  };

  // If section_split_offset == 0, last block may not be full.
  const auto blocksTotal =
      roundUpDivisionSafe(input.getRemainSize(), BlockSize);
  invariant(blocksTotal > 0);
  invariant(blocksTotal * PixelsPerBlock >= mRaw->dim.area());
  assert(blocksTotal <= std::numeric_limits<uint32_t>::max());
  assert(blocksTotal <= std::numeric_limits<size_t>::max());
  blocks.reserve(implicit_cast<size_t>(blocksTotal));

  unsigned currPixel = 0;
  std::generate_n(
      std::back_inserter(blocks), blocksTotal, [&, pixelToCoordinate]() {
        invariant(input.getRemainSize() != 0);
        const auto blockSize = std::min(input.getRemainSize(), BlockSize);
        invariant(blockSize > 0);
        invariant(blockSize % BytesPerPacket == 0);
        const auto packets = blockSize / BytesPerPacket;
        invariant(packets > 0);
        const auto pixels = packets * PixelsPerPacket;
        invariant(pixels > 0);

        ByteStream bs = input.getStream(blockSize);
        iPoint2D beginCoord = pixelToCoordinate(currPixel);
        currPixel += pixels;
        iPoint2D endCoord = pixelToCoordinate(currPixel);
        return Block(bs, beginCoord, endCoord);
      });
  assert(blocks.size() == blocksTotal);
  assert(currPixel >= mRaw->dim.area());
  assert(input.getRemainSize() == 0);

  // Clamp the end coordinate for the last block.
  blocks.back().endCoord = mRaw->dim;
  blocks.back().endCoord.y -= 1;
}

class PanasonicV4Decompressor::ProxyStream {
  ByteStream block;
  const uint32_t section_split_offset;
  std::vector<uint8_t> buf;

  int vbits = 0;

  void parseBlock() {
    assert(buf.empty());
    invariant(block.getRemainSize() <= BlockSize);
    invariant(section_split_offset <= BlockSize);

    Buffer FirstSection = block.getBuffer(section_split_offset);
    Buffer SecondSection = block.getBuffer(block.getRemainSize());

    // get one more byte, so the return statement of getBits does not have
    // to special case for accessing the last byte
    buf.reserve(BlockSize + 1UL);

    // First copy the second section. This makes it the first section.
    buf.insert(buf.end(), SecondSection.begin(), SecondSection.end());
    // Now append the original 1'st section right after the new 1'st section.
    buf.insert(buf.end(), FirstSection.begin(), FirstSection.end());

    invariant(block.getRemainSize() == 0);

    // get one more byte, so the return statement of getBits does not have
    // to special case for accessing the last byte
    buf.emplace_back(0);
  }

public:
  ProxyStream(ByteStream block_, int section_split_offset_)
      : block(block_), section_split_offset(section_split_offset_) {
    parseBlock();
  }

  uint32_t getBits(int nbits) noexcept {
    vbits = (vbits - nbits) & 0x1ffff;
    int byte = vbits >> 3 ^ 0x3ff0;
    return (buf[byte] | buf[byte + 1UL] << 8) >> (vbits & 7) & ~(-(1 << nbits));
  }
};

inline void PanasonicV4Decompressor::processPixelPacket(
    ProxyStream& bits, int row, int col,
    std::vector<uint32_t>* zero_pos) const noexcept {
  const Array2DRef<uint16_t> out(mRaw->getU16DataAsUncroppedArray2DRef());

  int sh = 0;

  std::array<int, 2> pred;
  pred.fill(0);

  std::array<int, 2> nonz;
  nonz.fill(0);

  int u = 0;

  for (int p = 0; p < PixelsPerPacket; ++p, ++col) {
    const int c = p & 1;

    // FIXME: this is actually just `p % 3 == 2`, cleanup after perf is good.
    if (u == 2) {
      sh = extractHighBits(4U, bits.getBits(2), /*effectiveBitwidth=*/3);
      u = -1;
    }

    if (nonz[c]) {
      int j = bits.getBits(8);
      if (j) {
        pred[c] -= 0x80 << sh;
        if (pred[c] < 0 || sh == 4)
          pred[c] &= ~(-(1 << sh));
        pred[c] += j << sh;
      }
    } else {
      nonz[c] = bits.getBits(8);
      if (nonz[c] || p > 11)
        pred[c] = nonz[c] << 4 | bits.getBits(4);
    }

    out(row, col) = implicit_cast<uint16_t>(pred[c]);

    if (zero_is_bad && 0 == pred[c])
      zero_pos->push_back((row << 16) | col);

    u++;
  }
}

void PanasonicV4Decompressor::processBlock(
    const Block& block, std::vector<uint32_t>* zero_pos) const noexcept {
  ProxyStream bits(block.bs, section_split_offset);

  for (int row = block.beginCoord.y; row <= block.endCoord.y; row++) {
    int col = 0;
    // First row may not begin at the first column.
    if (block.beginCoord.y == row)
      col = block.beginCoord.x;

    int endCol = mRaw->dim.x;
    // Last row may end before the last column.
    if (block.endCoord.y == row)
      endCol = block.endCoord.x;

    invariant(col % PixelsPerPacket == 0);
    invariant(endCol % PixelsPerPacket == 0);

    for (; col < endCol; col += PixelsPerPacket)
      processPixelPacket(bits, row, col, zero_pos);
  }
}

void PanasonicV4Decompressor::decompressThread() const noexcept {
  std::vector<uint32_t> zero_pos;

  assert(!blocks.empty());

#ifdef HAVE_OPENMP
#pragma omp for schedule(static)
#endif
  for (const auto& block :
       Array1DRef(blocks.data(), implicit_cast<int>(blocks.size()))) {
    try {
      processBlock(block, &zero_pos);
    } catch (...) {
      // We should not get any exceptions here.
      __builtin_unreachable();
    }
  }

  if (zero_is_bad && !zero_pos.empty()) {
    MutexLocker guard(&mRaw->mBadPixelMutex);
    mRaw->mBadPixelPositions.insert(mRaw->mBadPixelPositions.end(),
                                    zero_pos.begin(), zero_pos.end());
  }
}

void PanasonicV4Decompressor::decompress() const noexcept {
  assert(!blocks.empty());
#ifdef HAVE_OPENMP
#pragma omp parallel default(none)                                             \
    num_threads(rawspeed_get_number_of_processor_cores())
#endif
  decompressThread();
}

} // namespace rawspeed
