/**
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

/**
 * @file GzipInflateTransformation.cc
 */

#include <cstring>
#include <vector>
#include <zlib.h>
#include <cinttypes>
#include <string_view>
#include "tscpp/api/TransformationPlugin.h"
#include "tscpp/api/GzipInflateTransformation.h"
#include "logging_internal.h"

using namespace atscppapi::transformations;
using std::vector;

namespace
{
const int    WINDOW_BITS          = 31; // Always use 31 for gzip.
unsigned int INFLATE_SCALE_FACTOR = 6;
} // namespace

/**
 * @private
 */
struct atscppapi::transformations::GzipInflateTransformationState : noncopyable {
  z_stream                   z_stream_;
  bool                       z_stream_initialized_;
  int64_t                    bytes_produced_;
  TransformationPlugin::Type transformation_type_;

  GzipInflateTransformationState(TransformationPlugin::Type type)
    : z_stream_initialized_(false), bytes_produced_(0), transformation_type_(type)
  {
    memset(&z_stream_, 0, sizeof(z_stream_));

    int err = inflateInit2(&z_stream_, WINDOW_BITS);
    if (Z_OK != err) {
      LOG_ERROR("inflateInit2 failed with error code '%d'.", err);
    } else {
      z_stream_initialized_ = true;
    }
  };

  ~GzipInflateTransformationState()
  {
    if (z_stream_initialized_) {
      int err = inflateEnd(&z_stream_);
      if (Z_OK != err && Z_STREAM_END != err) {
        LOG_ERROR("Unable to inflateEnd(), returned error code '%d'", err);
      }
    }
  };
};

GzipInflateTransformation::GzipInflateTransformation(Transaction &transaction, TransformationPlugin::Type type)
  : TransformationPlugin(transaction, type)
{
  state_ = std::make_unique<GzipInflateTransformationState>(type);
}

GzipInflateTransformation::~GzipInflateTransformation() {}

void
GzipInflateTransformation::consume(std::string_view data)
{
  if (data.size() == 0) {
    return;
  }

  if (!state_->z_stream_initialized_) {
    LOG_ERROR("Unable to inflate output because the z_stream was not initialized.");
    return;
  }

  int          err                = Z_OK;
  int          iteration          = 0;
  int          inflate_block_size = INFLATE_SCALE_FACTOR * data.size();
  vector<char> buffer(inflate_block_size);

  // Setup the compressed input
  state_->z_stream_.next_in  = reinterpret_cast<unsigned char *>(const_cast<char *>(data.data()));
  state_->z_stream_.avail_in = data.length();

  // Loop while we have more data to inflate
  while (state_->z_stream_.avail_in > 0 && err != Z_STREAM_END) {
    LOG_DEBUG("Iteration %d: Gzip has %d bytes to inflate", ++iteration, state_->z_stream_.avail_in);

    // Setup where the decompressed output will go
    // next_out needs to be set to nullptr before we return since it points to a local buffer
    // coverity[WRAPPER_ESCAPE: FALSE]
    state_->z_stream_.next_out  = reinterpret_cast<unsigned char *>(&buffer[0]);
    state_->z_stream_.avail_out = inflate_block_size;

    // Uncompress the data
    err = inflate(&state_->z_stream_, Z_SYNC_FLUSH);

    if (err != Z_OK && err != Z_STREAM_END) {
      LOG_ERROR("Iteration %d: Inflate failed with error '%d'", iteration, err);
      state_->z_stream_.next_out = nullptr;
      return;
    }

    LOG_DEBUG("Iteration %d: Gzip inflated a total of %d bytes, producingOutput...", iteration,
              (inflate_block_size - state_->z_stream_.avail_out));
    produce(std::string_view(&buffer[0], (inflate_block_size - state_->z_stream_.avail_out)));
    state_->bytes_produced_ += (inflate_block_size - state_->z_stream_.avail_out);
  }
  state_->z_stream_.next_out = nullptr;
}

void
GzipInflateTransformation::handleInputComplete()
{
  int64_t bytes_written = setOutputComplete();
  if (state_->bytes_produced_ != bytes_written) {
    LOG_ERROR("Gzip bytes produced sanity check failed, inflated bytes = %" PRId64 " != written bytes = %" PRId64,
              state_->bytes_produced_, bytes_written);
  }
}
