// Copyright (c) 2023, ETH Zurich and UNC Chapel Hill.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
//     * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
//       its contributors may be used to endorse or promote products derived
//       from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

#ifndef COLMAP_SRC_RETRIEVAL_INVERTED_FILE_H_
#define COLMAP_SRC_RETRIEVAL_INVERTED_FILE_H_

#include <algorithm>
#include <bitset>
#include <cstdint>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include <Eigen/Core>

#include "retrieval/geometry.h"
#include "retrieval/inverted_file_entry.h"
#include "retrieval/utils.h"
#include "util/alignment.h"
#include "util/logging.h"
#include "util/math.h"

namespace colmap {
namespace retrieval {

// Implements an inverted file, including the ability to compute image scores
// and matches. The template parameter is the length of the binary vectors
// in the Hamming Embedding.
// This class is based on an original implementation by Torsten Sattler.
template <int kEmbeddingDim>
class InvertedFile {
 public:
  typedef Eigen::VectorXf DescType;
  typedef FeatureGeometry GeomType;
  typedef InvertedFileEntry<kEmbeddingDim> EntryType;

  enum Status {
    UNUSABLE = 0x00,
    HAS_EMBEDDING = 0x01,
    ENTRIES_SORTED = 0x02,
    USABLE = 0x03,
  };

  InvertedFile();

  // The number of added entries.
  size_t NumEntries() const;

  // Return all entries in the file.
  const std::vector<EntryType>& GetEntries() const;

  // Whether the Hamming embedding was computed for this file.
  bool HasHammingEmbedding() const;

  // Whether the entries in this file are sorted.
  bool EntriesSorted() const;

  // Whether this file is usable for scoring, i.e. the entries are sorted and
  // the Hamming embedding has been computed.
  bool IsUsable() const;

  // Adds an inverted file entry given a projected descriptor and its image
  // information stored in an inverted file entry. In particular, this function
  // generates the binary descriptor for the inverted file entry and then stores
  // the entry in the inverted file.
  void AddEntry(const int image_id, typename DescType::Index feature_idx,
                const DescType& descriptor, const GeomType& geometry);

  // Sorts the inverted file entries in ascending order of image ids. This is
  // required for efficient scoring and must be called before ScoreFeature.
  void SortEntries();

  // Clear all entries in this file.
  void ClearEntries();

  // Reset all computed weights/thresholds and clear all entries.
  void Reset();

  // Given a projected descriptor, returns the corresponding binary string.
  void ConvertToBinaryDescriptor(
      const DescType& descriptor,
      std::bitset<kEmbeddingDim>* binary_descriptor) const;

  // Compute the idf-weight for this inverted file.
  void ComputeIDFWeight(const int num_total_images);

  // Return the idf-weight of this inverted file.
  float IDFWeight() const;

  // Given a set of descriptors, learns the thresholds required for the Hamming
  // embedding. Each row in descriptors represents a single descriptor projected
  // into the kEmbeddingDim dimensional space used for Hamming embedding.
  void ComputeHammingEmbedding(
      const Eigen::Matrix<float, Eigen::Dynamic, kEmbeddingDim>& descriptors);

  // Given a query feature, performs inverted file scoring.
  void ScoreFeature(const DescType& descriptor,
                    std::vector<ImageScore>* image_scores) const;

  // Get the identifiers of all indexed images in this file.
  void GetImageIds(std::unordered_set<int>* ids) const;

  // For each image in the inverted file, computes the self-similarity of each
  // image in the file (the part caused by this word) and adds the weight to the
  // entry corresponding to that image. This function is useful to determine the
  // normalization factor for each image that is used during retrieval.
  void ComputeImageSelfSimilarities(
      std::unordered_map<int, double>* self_similarities) const;

  // Read/write the inverted file from/to a binary file.
  void Read(std::ifstream* ifs);
  void Write(std::ofstream* ofs) const;

 private:
  // Whether the inverted file is initialized.
  uint8_t status_;

  // The inverse document frequency weight of this inverted file.
  float idf_weight_;

  // The entries of the inverted file system.
  std::vector<EntryType> entries_;

  // The thresholds used for Hamming embedding.
  DescType thresholds_;

  // The functor to derive a voting weight from a Hamming distance.
  static const HammingDistWeightFunctor<kEmbeddingDim>
      hamming_dist_weight_functor_;
};

////////////////////////////////////////////////////////////////////////////////
// Implementation
////////////////////////////////////////////////////////////////////////////////

template <int kEmbeddingDim>
const HammingDistWeightFunctor<kEmbeddingDim>
    InvertedFile<kEmbeddingDim>::hamming_dist_weight_functor_;

template <int kEmbeddingDim>
InvertedFile<kEmbeddingDim>::InvertedFile()
    : status_(UNUSABLE), idf_weight_(0.0f) {
  static_assert(kEmbeddingDim % 8 == 0,
                "Dimensionality of projected space needs to"
                " be a multiple of 8.");
  static_assert(kEmbeddingDim > 0,
                "Dimensionality of projected space needs to be > 0.");

  thresholds_.resize(kEmbeddingDim);
  thresholds_.setZero();
}

template <int kEmbeddingDim>
size_t InvertedFile<kEmbeddingDim>::NumEntries() const {
  return entries_.size();
}

template <int kEmbeddingDim>
const std::vector<typename InvertedFile<kEmbeddingDim>::EntryType>&
InvertedFile<kEmbeddingDim>::GetEntries() const {
  return entries_;
}

template <int kEmbeddingDim>
bool InvertedFile<kEmbeddingDim>::HasHammingEmbedding() const {
  return status_ & HAS_EMBEDDING;
}

template <int kEmbeddingDim>
bool InvertedFile<kEmbeddingDim>::EntriesSorted() const {
  return status_ & ENTRIES_SORTED;
}

template <int kEmbeddingDim>
bool InvertedFile<kEmbeddingDim>::IsUsable() const {
  return status_ & USABLE;
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::AddEntry(const int image_id,
                                           typename DescType::Index feature_idx,
                                           const DescType& descriptor,
                                           const GeomType& geometry) {
  CHECK_GE(image_id, 0);
  CHECK_EQ(descriptor.size(), kEmbeddingDim);
  EntryType entry;
  entry.image_id = image_id;
  entry.feature_idx = feature_idx;
  entry.geometry = geometry;
  ConvertToBinaryDescriptor(descriptor, &entry.descriptor);
  entries_.push_back(entry);
  status_ &= ~ENTRIES_SORTED;
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::SortEntries() {
  std::sort(entries_.begin(), entries_.end(),
            [](const EntryType& entry1, const EntryType& entry2) {
              return entry1.image_id < entry2.image_id;
            });
  status_ |= ENTRIES_SORTED;
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::ClearEntries() {
  entries_.clear();
  status_ &= ~ENTRIES_SORTED;
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::Reset() {
  status_ = UNUSABLE;
  idf_weight_ = 0.0f;
  entries_.clear();
  thresholds_.setZero();
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::ConvertToBinaryDescriptor(
    const DescType& descriptor,
    std::bitset<kEmbeddingDim>* binary_descriptor) const {
  CHECK_EQ(descriptor.size(), kEmbeddingDim);
  for (int i = 0; i < kEmbeddingDim; ++i) {
    (*binary_descriptor)[i] = descriptor[i] > thresholds_[i];
  }
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::ComputeIDFWeight(const int num_total_images) {
  if (entries_.empty()) {
    return;
  }

  std::unordered_set<int> image_ids;
  GetImageIds(&image_ids);

  idf_weight_ = std::log(static_cast<double>(num_total_images) /
                         static_cast<double>(image_ids.size()));
}

template <int kEmbeddingDim>
float InvertedFile<kEmbeddingDim>::IDFWeight() const {
  return idf_weight_;
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::ComputeHammingEmbedding(
    const Eigen::Matrix<float, Eigen::Dynamic, kEmbeddingDim>& descriptors) {
  const int num_descriptors = static_cast<int>(descriptors.rows());
  if (num_descriptors < 2) {
    return;
  }

  std::vector<float> elements(num_descriptors);
  for (int n = 0; n < kEmbeddingDim; ++n) {
    for (int i = 0; i < num_descriptors; ++i) {
      elements[i] = descriptors(i, n);
    }
    thresholds_[n] = Median(elements);
  }

  status_ |= HAS_EMBEDDING;
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::ScoreFeature(
    const DescType& descriptor, std::vector<ImageScore>* image_scores) const {
  CHECK_EQ(descriptor.size(), kEmbeddingDim);

  image_scores->clear();

  if (!IsUsable()) {
    return;
  }

  if (entries_.size() == 0) {
    return;
  }

  const float squared_idf_weight = idf_weight_ * idf_weight_;

  std::bitset<kEmbeddingDim> bin_descriptor;
  ConvertToBinaryDescriptor(descriptor, &bin_descriptor);

  ImageScore image_score;
  image_score.image_id = entries_.front().image_id;
  image_score.score = 0.0f;
  int num_image_votes = 0;

  // Note that this assumes that the entries are sorted using SortEntries
  // according to their image identifiers.
  for (const auto& entry : entries_) {
    if (image_score.image_id < entry.image_id) {
      if (num_image_votes > 0) {
        // Finalizes the voting since we now know how many features from
        // the database image match the current image feature. This is
        // required to perform burstiness normalization (cf. Eqn. 2 in
        // Arandjelovic, Zisserman: Scalable descriptor
        // distinctiveness for location recognition. ACCV 2014).
        // Notice that the weight from the descriptor matching is already
        // accumulated in image_score.score, i.e., we only need
        // to apply the burstiness weighting.
        image_score.score /= std::sqrt(static_cast<float>(num_image_votes));
        image_score.score *= squared_idf_weight;
        image_scores->push_back(image_score);
      }

      image_score.image_id = entry.image_id;
      image_score.score = 0.0f;
      num_image_votes = 0;
    }

    const size_t hamming_dist = (bin_descriptor ^ entry.descriptor).count();

    if (hamming_dist <= hamming_dist_weight_functor_.kMaxHammingDistance) {
      image_score.score += hamming_dist_weight_functor_(hamming_dist);
      num_image_votes += 1;
    }
  }

  // Add the voting for the largest image_id in the entries.
  if (num_image_votes > 0) {
    image_score.score /= std::sqrt(static_cast<float>(num_image_votes));
    image_score.score *= squared_idf_weight;
    image_scores->push_back(image_score);
  }
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::GetImageIds(
    std::unordered_set<int>* ids) const {
  for (const EntryType& entry : entries_) {
    ids->insert(entry.image_id);
  }
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::ComputeImageSelfSimilarities(
    std::unordered_map<int, double>* self_similarities) const {
  const double squared_idf_weight = idf_weight_ * idf_weight_;
  for (const auto& entry : entries_) {
    (*self_similarities)[entry.image_id] += squared_idf_weight;
  }
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::Read(std::ifstream* ifs) {
  CHECK(ifs->is_open());

  ifs->read(reinterpret_cast<char*>(&status_), sizeof(uint8_t));
  ifs->read(reinterpret_cast<char*>(&idf_weight_), sizeof(float));

  for (int i = 0; i < kEmbeddingDim; ++i) {
    ifs->read(reinterpret_cast<char*>(&thresholds_[i]), sizeof(float));
  }

  uint32_t num_entries = 0;
  ifs->read(reinterpret_cast<char*>(&num_entries), sizeof(uint32_t));
  entries_.resize(num_entries);

  for (uint32_t i = 0; i < num_entries; ++i) {
    entries_[i].Read(ifs);
  }
}

template <int kEmbeddingDim>
void InvertedFile<kEmbeddingDim>::Write(std::ofstream* ofs) const {
  CHECK(ofs->is_open());

  ofs->write(reinterpret_cast<const char*>(&status_), sizeof(uint8_t));
  ofs->write(reinterpret_cast<const char*>(&idf_weight_), sizeof(float));

  for (int i = 0; i < kEmbeddingDim; ++i) {
    ofs->write(reinterpret_cast<const char*>(&thresholds_[i]), sizeof(float));
  }

  const uint32_t num_entries = static_cast<uint32_t>(entries_.size());
  ofs->write(reinterpret_cast<const char*>(&num_entries), sizeof(uint32_t));

  for (uint32_t i = 0; i < num_entries; ++i) {
    entries_[i].Write(ofs);
  }
}

}  // namespace retrieval
}  // namespace colmap

#endif  // COLMAP_SRC_RETRIEVAL_INVERTED_FILE_H_
