/*
    This file is part of TON Blockchain Library.
    TON Blockchain 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.
    TON Blockchain 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 TON Blockchain Library.  If not, see .
*/
#include "MicrochunkTree.h"
#include "Torrent.h"
#include "vm/cells/CellSlice.h"
#include "vm/cells/MerkleProof.h"
namespace ton {
static td::Ref prun(const td::Ref &node) {
  vm::CellBuilder cb;
  cb.store_long(static_cast(vm::Cell::SpecialType::PrunnedBranch), 8);
  cb.store_long(1, 8);
  cb.store_bytes(node->get_hash(0).as_slice());
  cb.store_long(node->get_depth(0), 16);
  return cb.finalize(true);
}
MicrochunkTree::Builder::Builder(td::uint64 file_size, td::uint64 prun_size)
    : file_size_(file_size), prun_size_(prun_size) {
  total_size_ = MICROCHUNK_SIZE;
  while (total_size_ < file_size) {
    total_size_ *= 2;
  }
}
void MicrochunkTree::Builder::add_data(td::Slice s) {
  CHECK(cur_size_ + s.size() <= file_size_);
  while (s.size() > 0) {
    size_t buf_ptr = cur_size_ % MICROCHUNK_SIZE;
    size_t buf_remaining = MICROCHUNK_SIZE - buf_ptr;
    if (buf_remaining > s.size()) {
      memcpy(cur_microchunk_ + buf_ptr, s.data(), s.size());
      cur_size_ += s.size();
      return;
    }
    memcpy(cur_microchunk_ + buf_ptr, s.data(), buf_remaining);
    cur_size_ += buf_remaining;
    s.remove_prefix(buf_remaining);
    add_microchunk(td::Slice(cur_microchunk_, MICROCHUNK_SIZE));
  }
}
MicrochunkTree MicrochunkTree::Builder::finalize() {
  CHECK(cur_size_ == file_size_);
  if (cur_size_ % MICROCHUNK_SIZE != 0) {
    size_t buf_ptr = cur_size_ % MICROCHUNK_SIZE;
    size_t buf_remaining = MICROCHUNK_SIZE - buf_ptr;
    memset(cur_microchunk_ + buf_ptr, 0, buf_remaining);
    cur_size_ += buf_remaining;
    add_microchunk(td::Slice(cur_microchunk_, MICROCHUNK_SIZE));
  }
  memset(cur_microchunk_, 0, MICROCHUNK_SIZE);
  while (cur_size_ < total_size_) {
    add_microchunk(td::Slice(cur_microchunk_, MICROCHUNK_SIZE));
    cur_size_ += MICROCHUNK_SIZE;
  }
  CHECK(proof_.size() == 1);
  MicrochunkTree tree(vm::CellBuilder::create_merkle_proof(std::move(proof_[0])));
  CHECK(tree.total_size_ == total_size_);
  return tree;
}
void MicrochunkTree::Builder::add_microchunk(td::Slice s) {
  CHECK(s.size() == MICROCHUNK_SIZE);
  td::Ref node = vm::CellBuilder().store_zeroes(2).store_bytes(s).finalize_novm();
  while (!proof_.empty() && proof_.back()->get_depth(0) == node->get_depth(0)) {
    td::Ref left = std::move(proof_.back());
    proof_.pop_back();
    node = vm::CellBuilder().store_zeroes(2).store_ref(std::move(left)).store_ref(std::move(node)).finalize_novm();
    if ((MICROCHUNK_SIZE << node->get_depth(0)) <= prun_size_) {
      node = prun(node);
    }
  }
  proof_.push_back(std::move(node));
}
MicrochunkTree::MicrochunkTree(td::Ref root_proof) : root_proof_(root_proof) {
  td::Ref virt_root = vm::MerkleProof::virtualize(root_proof_, 1);
  CHECK(!virt_root.is_null());
  CHECK(virt_root->get_depth() <= 50);
  total_size_ = MICROCHUNK_SIZE << virt_root->get_depth();
  root_hash_ = virt_root->get_hash().bits();
}
class GetMicrochunkProof {
 public:
  GetMicrochunkProof(td::uint64 l, td::uint64 r, Torrent &torrent) : l(l), r(r), torrent(torrent) {
  }
  td::Result> unprun(td::uint64 il, td::uint64 ir) {
    if (ir - il == MicrochunkTree::MICROCHUNK_SIZE) {
      TRY_RESULT(data, get_microchunk(il));
      return vm::CellBuilder().store_zeroes(2).store_bytes(data).finalize_novm();
    }
    td::uint64 imid = (il + ir) / 2;
    TRY_RESULT(node_l, unprun(il, imid));
    TRY_RESULT(node_r, unprun(imid, ir));
    td::Ref node =
        vm::CellBuilder().store_zeroes(2).store_ref(std::move(node_l)).store_ref(std::move(node_r)).finalize_novm();
    if (l >= ir || il >= r) {
      node = prun(node);
    }
    return node;
  }
  td::Result> unprun(const td::Ref &node, td::uint64 il, td::uint64 ir) {
    vm::CellSlice cs(vm::NoVm(), node);
    if (!cs.is_special()) {
      return node;
    }
    TRY_RESULT(result, unprun(il, ir));
    if (result->get_hash(0) != node->get_hash(0)) {
      return td::Status::Error("Hash mismatch");
    }
    return result;
  }
  td::Result> get_proof(td::Ref node, td::uint64 il, td::uint64 ir) {
    if (l >= ir || il >= r) {
      return prun(node);
    }
    if (ir - il == MicrochunkTree::MICROCHUNK_SIZE) {
      return unprun(node, il, ir);
    }
    if (l <= il && ir <= r) {
      return prun(node);
    }
    td::uint64 imid = (il + ir) / 2;
    TRY_RESULT_ASSIGN(node, unprun(node, il, ir));
    vm::CellSlice cs(vm::NoVm(), node);
    if (cs.size_ext() != 2 + (2 << 16)) {
      return td::Status::Error("Invalid node in microchunk tree");
    }
    TRY_RESULT(node_l, get_proof(cs.prefetch_ref(0), il, imid));
    TRY_RESULT(node_r, get_proof(cs.prefetch_ref(1), imid, ir));
    return vm::CellBuilder().store_zeroes(2).store_ref(std::move(node_l)).store_ref(std::move(node_r)).finalize_novm();
  }
 private:
  td::uint64 l, r;
  Torrent &torrent;
  td::uint64 cache_offset = 0;
  std::string cache;
  td::Result get_microchunk(td::uint64 l) {
    DCHECK(l % MicrochunkTree::MICROCHUNK_SIZE == 0);
    td::uint64 r = l + MicrochunkTree::MICROCHUNK_SIZE;
    if (!(cache_offset <= l && r <= cache_offset + cache.size())) {
      td::uint64 piece_size = torrent.get_info().piece_size;
      td::uint64 piece_i = l / piece_size;
      if (piece_i < torrent.get_info().pieces_count()) {
        TRY_RESULT(piece, torrent.get_piece_data(piece_i));
        piece.resize(piece_size, '\0');
        cache = std::move(piece);
      } else {
        cache = std::string(piece_size, '\0');
      }
      cache_offset = piece_i * piece_size;
    }
    return td::Slice{cache.data() + (l - cache_offset), MicrochunkTree::MICROCHUNK_SIZE};
  }
};
td::Result> MicrochunkTree::get_proof(td::uint64 l, td::uint64 r, Torrent &torrent) const {
  if (root_proof_.is_null()) {
    return td::Status::Error("Empty microchunk tree");
  }
  if (l % MICROCHUNK_SIZE != 0 || r % MICROCHUNK_SIZE != 0 || l >= r || r > total_size_) {
    return td::Status::Error("Invalid range");
  }
  if (!torrent.inited_info()) {
    return td::Status::Error("Torrent info is not ready");
  }
  if (!torrent.get_info().piece_size % MICROCHUNK_SIZE != 0) {
    return td::Status::Error("Invalid piece size in torrent");
  }
  td::Ref root_raw = vm::CellSlice(vm::NoVm(), root_proof_).prefetch_ref();
  TRY_RESULT(result, GetMicrochunkProof(l, r, torrent).get_proof(std::move(root_raw), 0, total_size_));
  return vm::CellBuilder::create_merkle_proof(std::move(result));
}
td::Result MicrochunkTree::Builder::build_for_torrent(Torrent &torrent, td::uint64 prun_size) {
  if (!torrent.inited_info()) {
    return td::Status::Error("Torrent info is not available");
  }
  const TorrentInfo &info = torrent.get_info();
  Builder builder(info.file_size, prun_size);
  td::uint64 pieces_count = info.pieces_count();
  for (td::uint64 i = 0; i < pieces_count; ++i) {
    TRY_RESULT(piece, torrent.get_piece_data(i));
    builder.add_data(piece);
  }
  MicrochunkTree tree = builder.finalize();
  return tree;
}
}  // namespace ton