#include "storage/country_tree.hpp"

#include "platform/platform.hpp"

#include "coding/reader.hpp"

#include "base/assert.hpp"
#include "base/logging.hpp"
#include "base/stl_helpers.hpp"

#include "cppjansson/cppjansson.hpp"

namespace storage
{
using namespace std;

// Mwm subtree attributes. They can be calculated based on information contained in countries.txt.
// The first in the pair is number of mwms in a subtree. The second is sum of sizes of
// all mwms in a subtree.
using MwmSubtreeAttrs = pair<MwmCounter, MwmSize>;

namespace
{
class StoreCountries
{
  CountryTree & m_countries;
  CountriesInfo & m_info;
  OldMwmMapping m_idsMapping;

public:
  StoreCountries(CountryTree & countries, CountriesInfo & countriesInfo) : m_countries(countries), m_info(countriesInfo)
  {}
  ~StoreCountries()
  {
    for (auto & entry : m_info.m_affiliations)
      base::SortUnique(entry.second);
  }

  Country * InsertToCountryTree(CountryId const & id, MwmSize mapSize, string const & mapSha1, size_t depth,
                                CountryId const & parent)
  {
    Country country(id, parent);
    if (mapSize)
      country.SetFile(platform::CountryFile{id, mapSize, mapSha1});
    return &m_countries.AddAtDepth(depth, std::move(country));
  }

  void InsertOldMwmMapping(CountryId const & newId, CountryId const & oldId) { m_idsMapping[oldId].insert(newId); }

  void InsertAffiliation(CountryId const & countryId, string affiliation)
  {
    ASSERT(!affiliation.empty(), ());
    ASSERT(!countryId.empty(), ());
    m_info.m_affiliations.emplace(std::move(affiliation), CountriesVec()).first->second.push_back(countryId);
  }

  void InsertCountryNameSynonym(CountryId const & countryId, string synonym)
  {
    ASSERT(!synonym.empty(), ());
    ASSERT(!countryId.empty(), ());
    VERIFY(m_info.m_countryNameSynonyms.emplace(std::move(synonym), countryId).second, (countryId));
  }

  void InsertMwmTopCityGeoId(CountryId const & countryId, uint64_t const & geoObjectId)
  {
    ASSERT(!countryId.empty(), ());
    ASSERT_NOT_EQUAL(geoObjectId, 0, ());
    VERIFY(m_info.m_mwmTopCityGeoIds.emplace(countryId, base::GeoObjectId(geoObjectId)).second, (countryId));
  }

  void InsertTopCountryGeoIds(CountryId const & countryId, vector<uint64_t> const & geoObjectIds)
  {
    ASSERT(!countryId.empty(), ());
    ASSERT(!geoObjectIds.empty(), ());
    vector<base::GeoObjectId> ids(geoObjectIds.cbegin(), geoObjectIds.cend());
    VERIFY(m_info.m_mwmTopCountryGeoIds.emplace(countryId, std::move(ids)).second, (countryId));
  }

  void InsertOldCountry(CountryId const & countryId, string oldId)
  {
    ASSERT(!oldId.empty(), ());
    ASSERT(!countryId.empty(), ());

    /// @todo Possible 1 -> many entries in case if we unite regions.
    /// Current countries.txt example is "Caribisch Nederland".
    m_info.m_mwmToOld.emplace(countryId, std::move(oldId));
  }

  OldMwmMapping GetMapping() const { return m_idsMapping; }
};

}  // namespace

// CountryTree::Node -------------------------------------------------------------------------------

CountryTree::Node * CountryTree::Node::AddAtDepth(size_t level, Country && value)
{
  Node * node = this;
  while (--level > 0 && !node->m_children.empty())
    node = node->m_children.back().get();
  ASSERT_EQUAL(level, 0, ());
  return node->Add(std::move(value));
}

CountryTree::Node const & CountryTree::Node::Parent() const
{
  CHECK(HasParent(), ());
  return *m_parent;
}

CountryTree::Node const & CountryTree::Node::Child(size_t index) const
{
  ASSERT_LESS(index, m_children.size(), ());
  return *m_children[index];
}

void CountryTree::Node::ForEachChild(CountryTree::Node::NodeCallback const & f)
{
  for (auto & child : m_children)
    f(*child);
}

void CountryTree::Node::ForEachChild(CountryTree::Node::NodeCallback const & f) const
{
  for (auto const & child : m_children)
    f(*child);
}

void CountryTree::Node::ForEachDescendant(CountryTree::Node::NodeCallback const & f)
{
  for (auto & child : m_children)
  {
    f(*child);
    child->ForEachDescendant(f);
  }
}

void CountryTree::Node::ForEachDescendant(CountryTree::Node::NodeCallback const & f) const
{
  for (auto const & child : m_children)
  {
    f(*child);
    child->ForEachDescendant(f);
  }
}

void CountryTree::Node::ForEachInSubtree(CountryTree::Node::NodeCallback const & f)
{
  f(*this);
  for (auto & child : m_children)
    child->ForEachInSubtree(f);
}

void CountryTree::Node::ForEachInSubtree(CountryTree::Node::NodeCallback const & f) const
{
  f(*this);
  for (auto const & child : m_children)
    child->ForEachInSubtree(f);
}

void CountryTree::Node::ForEachAncestorExceptForTheRoot(CountryTree::Node::NodeCallback const & f)
{
  if (m_parent == nullptr || m_parent->m_parent == nullptr)
    return;
  f(*m_parent);
  m_parent->ForEachAncestorExceptForTheRoot(f);
}

void CountryTree::Node::ForEachAncestorExceptForTheRoot(CountryTree::Node::NodeCallback const & f) const
{
  if (m_parent == nullptr || m_parent->m_parent == nullptr)
    return;
  f(*m_parent);
  m_parent->ForEachAncestorExceptForTheRoot(f);
}

CountryTree::Node * CountryTree::Node::Add(Country && value)
{
  m_children.emplace_back(std::make_unique<Node>(std::move(value), this));
  return m_children.back().get();
}

// CountryTree -------------------------------------------------------------------------------------

CountryTree::Node const & CountryTree::GetRoot() const
{
  CHECK(m_countryTree, ());
  return *m_countryTree;
}

CountryTree::Node & CountryTree::GetRoot()
{
  CHECK(m_countryTree, ());
  return *m_countryTree;
}

Country & CountryTree::AddAtDepth(size_t level, Country && value)
{
  Node * added = nullptr;
  if (level == 0)
  {
    ASSERT(IsEmpty(), ());
    m_countryTree = std::make_unique<Node>(std::move(value), nullptr);  // Creating the root node.
    added = m_countryTree.get();
  }
  else
  {
    added = m_countryTree->AddAtDepth(level, std::move(value));
  }

  ASSERT(added, ());
  m_countryTreeMap.insert(make_pair(added->Value().Name(), added));
  return added->Value();
}

void CountryTree::Clear()
{
  m_countryTree.reset();
  m_countryTreeMap.clear();
}

void CountryTree::Find(CountryId const & key, NodesBufferT & found) const
{
  found.clear();
  if (IsEmpty())
    return;

  // Root is already in m_countryTreeMap (inserted by AddAtDepth), no special-case needed.
  auto const range = m_countryTreeMap.equal_range(key);
  for (auto it = range.first; it != range.second; ++it)
    found.push_back(it->second);
}

CountryTree::Node const * CountryTree::FindFirst(CountryId const & key) const
{
  if (IsEmpty())
    return nullptr;

  auto const it = m_countryTreeMap.find(key);
  return it != m_countryTreeMap.end() ? it->second : nullptr;
}

CountryTree::Node const * CountryTree::FindFirstLeaf(CountryId const & key) const
{
  if (IsEmpty())
    return nullptr;

  NodesBufferT found;
  Find(key, found);

  for (auto node : found)
    if (node->ChildrenCount() == 0)
      return node;
  return nullptr;
}

MwmSubtreeAttrs LoadGroupImpl(size_t depth, json_t * node, CountryId const & parent, StoreCountries & store)
{
  CountryId id;
  FromJSONObject(node, "id", id);

  {
    vector<string> strings;
    FromJSONObjectOptionalField(node, "old", strings);
    for (auto & v : strings)
      store.InsertOldCountry(id, std::move(v));

    strings.clear();
    FromJSONObjectOptionalField(node, "country_name_synonyms", strings);
    for (auto & v : strings)
      store.InsertCountryNameSynonym(id, std::move(v));

    strings.clear();
    FromJSONObjectOptionalField(node, "affiliations", strings);
    for (auto & v : strings)
      store.InsertAffiliation(id, std::move(v));

    uint64_t geoObjectId = 0;
    FromJSONObjectOptionalField(node, "top_city_geo_id", geoObjectId);
    if (geoObjectId != 0)
      store.InsertMwmTopCityGeoId(id, geoObjectId);

    vector<uint64_t> topCountryIds;
    FromJSONObjectOptionalField(node, "top_countries_geo_ids", topCountryIds);
    if (!topCountryIds.empty())
      store.InsertTopCountryGeoIds(id, topCountryIds);
  }

  int nodeSize;
  FromJSONObjectOptionalField(node, "s", nodeSize);
  ASSERT_LESS_OR_EQUAL(0, nodeSize, ());

  string nodeHash;
  FromJSONObjectOptionalField(node, "sha1_base64", nodeHash);

  // We expect that mwm and routing files should be less than 2GB.
  Country * addedNode = store.InsertToCountryTree(id, nodeSize, nodeHash, depth, parent);

  MwmCounter mwmCounter = 0;
  MwmSize mwmSize = 0;
  vector<json_t *> children;
  FromJSONObjectOptionalField(node, "g", children);
  if (children.empty())
  {
    mwmCounter = 1;  // It's a leaf. Any leaf contains one mwm.
    mwmSize = nodeSize;
  }
  else
  {
    for (json_t * child : children)
    {
      MwmSubtreeAttrs const childAttr = LoadGroupImpl(depth + 1, child, id, store);
      mwmCounter += childAttr.first;
      mwmSize += childAttr.second;
    }
  }

  if (addedNode != nullptr)
    addedNode->SetSubtreeAttrs(mwmCounter, mwmSize);

  return make_pair(mwmCounter, mwmSize);
}

bool LoadCountriesImpl(json_t * root, StoreCountries & store)
{
  try
  {
    LoadGroupImpl(0 /* depth */, root, kInvalidCountryId, store);
    return true;
  }
  catch (base::Json::Exception const & e)
  {
    LOG(LERROR, (e.Msg()));
    return false;
  }
}

int64_t LoadCountriesFromBuffer(string const & jsonBuffer, CountryTree & countries, CountriesInfo & countriesInfo)
{
  countries.Clear();
  countriesInfo.Clear();

  int64_t version = -1;
  try
  {
    base::Json root(jsonBuffer.c_str());
    FromJSONObject(root.get(), "v", version);

    StoreCountries store(countries, countriesInfo);
    if (!LoadCountriesImpl(root.get(), store))
      return -1;
  }
  catch (base::Json::Exception const & e)
  {
    LOG(LWARNING, (e.Msg()));
  }
  return version;
}

namespace
{
unique_ptr<Reader> GetReaderImpl(Platform & pl, string const & file, string const & scope)
{
  try
  {
    return pl.GetReader(file, scope);
  }
  catch (RootException const &)
  {}
  return nullptr;
}
}  // namespace

int64_t LoadCountriesFromFile(string const & path, CountryTree & countries, CountriesInfo & countriesInfo)
{
  string json;
  int64_t version = -1;

  // Choose the latest version from "resource" or "writable":
  // w > r in case of autoupdates
  // r > w in case of a new countries file with an updated app

  auto & pl = GetPlatform();
  auto reader = GetReaderImpl(pl, path, "fr");
  if (reader)
  {
    reader->ReadAsString(json);
    version = LoadCountriesFromBuffer(json, countries, countriesInfo);
  }

  reader = GetReaderImpl(pl, path, "w");
  if (reader)
  {
    CountryTree newCountries;
    CountriesInfo newCountriesInfo;

    reader->ReadAsString(json);
    int64_t const newVersion = LoadCountriesFromBuffer(json, newCountries, newCountriesInfo);

    if (newVersion > version)
    {
      version = newVersion;

      countries = std::move(newCountries);
      countriesInfo = std::move(newCountriesInfo);
    }
  }

  return version;
}

}  // namespace storage
