/**
 * @file   tile.cc
 *
 * @section LICENSE
 *
 * The MIT License
 *
 * @copyright Copyright (c) 2017-2020 TileDB, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @section DESCRIPTION
 *
 * This file implements class Tile.
 */

#include "tiledb/sm/tile/tile.h"
#include "tiledb/common/logger.h"
#include "tiledb/sm/enums/datatype.h"

#include <iostream>

using namespace tiledb::common;

namespace tiledb {
namespace sm {

/* ****************************** */
/*           STATIC API           */
/* ****************************** */

Status Tile::compute_chunk_size(
    const uint64_t tile_size,
    const uint32_t tile_dim_num,
    const uint64_t tile_cell_size,
    uint32_t* const chunk_size) {
  const uint32_t dim_num = tile_dim_num > 0 ? tile_dim_num : 1;
  const uint64_t dim_tile_size = tile_size / dim_num;
  const uint64_t dim_cell_size = tile_cell_size / dim_num;

  uint64_t chunk_size64 =
      std::min(constants::max_tile_chunk_size, dim_tile_size);
  chunk_size64 = chunk_size64 / dim_cell_size * dim_cell_size;
  chunk_size64 = std::max(chunk_size64, dim_cell_size);
  if (chunk_size64 > std::numeric_limits<uint32_t>::max()) {
    return LOG_STATUS(Status::TileError("Chunk size exceeds uint32_t"));
  }

  *chunk_size = chunk_size64;
  return Status::Ok();
}

Status Tile::buffer_to_contiguous_fixed_chunks(
    const Buffer& buffer,
    const uint32_t tile_dim_num,
    const uint64_t tile_cell_size,
    ChunkedBuffer* chunked_buffer) {
  return buffer_to_contiguous_fixed_chunks(
      buffer.data(),
      buffer.size(),
      tile_dim_num,
      tile_cell_size,
      chunked_buffer);
}

Status Tile::buffer_to_contiguous_fixed_chunks(
    void* buffer,
    const uint64_t buffer_size,
    const uint32_t tile_dim_num,
    const uint64_t tile_cell_size,
    ChunkedBuffer* chunked_buffer) {
  // Calculate the chunk size for 'buff'.
  uint32_t chunk_size;
  RETURN_NOT_OK(compute_chunk_size(
      buffer_size, tile_dim_num, tile_cell_size, &chunk_size));

  // Initialize contiguous, fixed size 'chunked_buffer_'.
  RETURN_NOT_OK(chunked_buffer->init_fixed_size(
      ChunkedBuffer::BufferAddressing::CONTIGUOUS, buffer_size, chunk_size));

  RETURN_NOT_OK(chunked_buffer->set_contiguous(buffer));
  RETURN_NOT_OK(chunked_buffer->set_size(buffer_size));

  return Status::Ok();
}

/* ****************************** */
/*   CONSTRUCTORS & DESTRUCTORS   */
/* ****************************** */

Tile::Tile() {
  chunked_buffer_ = nullptr;
  offset_ = 0;
  cell_size_ = 0;
  dim_num_ = 0;
  owns_chunked_buffer_ = true;
  pre_filtered_size_ = 0;
  format_version_ = 0;
  type_ = Datatype::INT32;
}

Tile::Tile(
    const Datatype type,
    const uint64_t cell_size,
    const unsigned int dim_num,
    ChunkedBuffer* const chunked_buffer,
    const bool owns_buff)
    : chunked_buffer_(chunked_buffer)
    , offset_(0)
    , cell_size_(cell_size)
    , dim_num_(dim_num)
    , format_version_(0)
    , owns_chunked_buffer_(owns_buff)
    , pre_filtered_size_(0)
    , type_(type) {
}

Tile::Tile(
    const uint32_t format_version,
    const Datatype type,
    const uint64_t cell_size,
    const unsigned int dim_num,
    ChunkedBuffer* const chunked_buffer,
    const bool owns_buff)
    : chunked_buffer_(chunked_buffer)
    , offset_(0)
    , cell_size_(cell_size)
    , dim_num_(dim_num)
    , format_version_(format_version)
    , owns_chunked_buffer_(owns_buff)
    , pre_filtered_size_(0)
    , type_(type) {
}

Tile::Tile(const Tile& tile)
    : Tile() {
  // Make a deep-copy clone
  auto clone = tile.clone(true);
  // Swap with the clone
  swap(clone);
}

Tile::Tile(Tile&& tile)
    : Tile() {
  // Swap with the argument
  swap(tile);
}

Tile::~Tile() {
  if (owns_chunked_buffer_ && chunked_buffer_ != nullptr) {
    chunked_buffer_->free();
    delete chunked_buffer_;
  }
}

Tile& Tile::operator=(const Tile& tile) {
  // Free existing buffer if owned.
  if (owns_chunked_buffer_) {
    if (chunked_buffer_) {
      chunked_buffer_->free();
      delete chunked_buffer_;
      chunked_buffer_ = nullptr;
    }
    owns_chunked_buffer_ = false;
  }

  // Make a deep-copy clone
  auto clone = tile.clone(true);
  // Swap with the clone
  swap(clone);

  return *this;
}

Tile& Tile::operator=(Tile&& tile) {
  // Swap with the argument
  swap(tile);

  return *this;
}

/* ****************************** */
/*               API              */
/* ****************************** */

uint64_t Tile::cell_num() const {
  return size() / cell_size_;
}

Status Tile::init_unfiltered(
    uint32_t format_version,
    Datatype type,
    uint64_t tile_size,
    uint64_t cell_size,
    unsigned int dim_num) {
  cell_size_ = cell_size;
  dim_num_ = dim_num;
  type_ = type;
  format_version_ = format_version;

  chunked_buffer_ = new ChunkedBuffer();
  if (chunked_buffer_ == nullptr)
    return LOG_STATUS(Status::TileError(
        "Cannot initialize tile; ChunkedBuffer allocation failed"));

  uint32_t chunk_size;
  RETURN_NOT_OK(
      compute_chunk_size(tile_size, dim_num, cell_size_, &chunk_size));

  RETURN_NOT_OK(chunked_buffer_->init_fixed_size(
      ChunkedBuffer::BufferAddressing::CONTIGUOUS, tile_size, chunk_size));

  return Status::Ok();
}

Status Tile::init_filtered(
    uint32_t format_version,
    Datatype type,
    uint64_t cell_size,
    unsigned int dim_num) {
  cell_size_ = cell_size;
  dim_num_ = dim_num;
  type_ = type;
  format_version_ = format_version;

  chunked_buffer_ = new ChunkedBuffer();
  if (chunked_buffer_ == nullptr)
    return LOG_STATUS(Status::TileError(
        "Cannot initialize tile; ChunkedBuffer allocation failed"));

  return Status::Ok();
}

void Tile::advance_offset(uint64_t nbytes) {
  offset_ += nbytes;
}

ChunkedBuffer* Tile::chunked_buffer() const {
  return chunked_buffer_;
}

Tile Tile::clone(bool deep_copy) const {
  Tile clone;
  clone.cell_size_ = cell_size_;
  clone.dim_num_ = dim_num_;
  clone.format_version_ = format_version_;
  clone.pre_filtered_size_ = pre_filtered_size_;
  clone.type_ = type_;
  clone.offset_ = offset_;
  clone.filtered_buffer_ = filtered_buffer_;

  if (deep_copy) {
    clone.owns_chunked_buffer_ = owns_chunked_buffer_;
    if (owns_chunked_buffer_ && chunked_buffer_ != nullptr) {
      clone.chunked_buffer_ = new ChunkedBuffer();
      // Calls ChunkedBuffer copy-assign, which performs a deep copy.
      *clone.chunked_buffer_ = *chunked_buffer_;
    } else {
      clone.chunked_buffer_ = chunked_buffer_;
    }
  } else {
    clone.owns_chunked_buffer_ = false;
    clone.chunked_buffer_ = chunked_buffer_;
  }

  return clone;
}

uint64_t Tile::cell_size() const {
  return cell_size_;
}

unsigned int Tile::dim_num() const {
  return dim_num_;
}

void Tile::disown_buff() {
  owns_chunked_buffer_ = false;
}

bool Tile::owns_buff() const {
  return owns_chunked_buffer_;
}

bool Tile::empty() const {
  assert(!filtered());
  return (chunked_buffer_ == nullptr) || (chunked_buffer_->size() == 0);
}

bool Tile::filtered() const {
  assert(!(filtered_buffer_.alloced_size() > 0 && chunked_buffer_->size() > 0));
  return filtered_buffer_.alloced_size() > 0;
}

Buffer* Tile::filtered_buffer() {
  return &filtered_buffer_;
}

uint32_t Tile::format_version() const {
  return format_version_;
}

bool Tile::full() const {
  assert(!filtered());
  return !empty() && offset_ >= chunked_buffer_->capacity();
}

uint64_t Tile::offset() const {
  return offset_;
}

uint64_t Tile::pre_filtered_size() const {
  return pre_filtered_size_;
}

Status Tile::read(void* buffer, uint64_t nbytes) {
  assert(!filtered());
  RETURN_NOT_OK(chunked_buffer_->read(buffer, nbytes, offset_));
  offset_ += nbytes;

  return Status::Ok();
}

Status Tile::read(
    void* const buffer, const uint64_t nbytes, const uint64_t offset) const {
  assert(!filtered());
  return chunked_buffer_->read(buffer, nbytes, offset);
}

void Tile::reset() {
  reset_offset();
  reset_size();
}

void Tile::reset_offset() {
  offset_ = 0;
}

void Tile::reset_size() {
  assert(!filtered());
  chunked_buffer_->set_size(0);
}

void Tile::set_offset(uint64_t offset) {
  offset_ = offset;
}

void Tile::set_pre_filtered_size(uint64_t pre_filtered_size) {
  pre_filtered_size_ = pre_filtered_size;
}

uint64_t Tile::size() const {
  assert(!filtered());
  return (chunked_buffer_ == nullptr) ? 0 : chunked_buffer_->size();
}

bool Tile::stores_coords() const {
  return dim_num_ > 0;
}

Datatype Tile::type() const {
  return type_;
}

Status Tile::write(ConstBuffer* buf) {
  assert(!filtered());
  RETURN_NOT_OK(chunked_buffer_->write(buf->cur_data(), buf->size(), offset_));
  offset_ += buf->size();

  return Status::Ok();
}

Status Tile::write(ConstBuffer* buf, uint64_t nbytes) {
  assert(!filtered());
  RETURN_NOT_OK(chunked_buffer_->write(buf->cur_data(), nbytes, offset_));
  offset_ += nbytes;

  return Status::Ok();
}

Status Tile::write(const void* data, uint64_t nbytes) {
  assert(!filtered());
  RETURN_NOT_OK(chunked_buffer_->write(data, nbytes, offset_));
  offset_ += nbytes;

  return Status::Ok();
}

Status Tile::zip_coordinates() {
  assert(dim_num_ > 0);

  // For easy reference
  const uint64_t tile_size = chunked_buffer_->size();
  const uint64_t coord_size = cell_size_ / dim_num_;
  const uint64_t cell_num = tile_size / cell_size_;

  // Coordinate tiles are always contiguously allocated.
  assert(
      chunked_buffer_->buffer_addressing() ==
      ChunkedBuffer::BufferAddressing::CONTIGUOUS);

  // Fetch the internal, contiguous buffer.
  void* buffer;
  RETURN_NOT_OK(chunked_buffer_->get_contiguous(&buffer));
  char* const tile_c = static_cast<char*>(buffer);

  // Create a tile clone
  char* const tile_tmp = static_cast<char*>(std::malloc(tile_size));
  assert(tile_tmp);
  std::memcpy(tile_tmp, tile_c, tile_size);

  // Zip coordinates
  uint64_t ptr_tmp = 0;
  for (unsigned int j = 0; j < dim_num_; ++j) {
    uint64_t ptr = j * coord_size;
    for (uint64_t i = 0; i < cell_num; ++i) {
      std::memcpy(tile_c + ptr, tile_tmp + ptr_tmp, coord_size);
      ptr += cell_size_;
      ptr_tmp += coord_size;
    }
  }

  // Clean up
  std::free((void*)tile_tmp);

  return Status::Ok();
}

/* ****************************** */
/*          PRIVATE METHODS       */
/* ****************************** */

void Tile::swap(Tile& tile) {
  // Note swapping buffer pointers here.
  std::swap(filtered_buffer_, tile.filtered_buffer_);
  std::swap(chunked_buffer_, tile.chunked_buffer_);
  std::swap(offset_, tile.offset_);
  std::swap(cell_size_, tile.cell_size_);
  std::swap(dim_num_, tile.dim_num_);
  std::swap(format_version_, tile.format_version_);
  std::swap(owns_chunked_buffer_, tile.owns_chunked_buffer_);
  std::swap(pre_filtered_size_, tile.pre_filtered_size_);
  std::swap(type_, tile.type_);
}

}  // namespace sm
}  // namespace tiledb