#pragma once

#include "indexer/feature_decl.hpp"
#include "indexer/feature_meta.hpp"

#include "coding/reader.hpp"
#include "coding/string_utf8_multilang.hpp"
#include "coding/value_opt_string.hpp"

#include <algorithm>
#include <array>
#include <optional>
#include <string>
#include <vector>

struct FeatureParamsBase;
class FeatureType;

namespace feature
{
enum HeaderMask
{
  HEADER_MASK_TYPE = 7U,
  HEADER_MASK_HAS_NAME = 1U << 3,
  HEADER_MASK_HAS_LAYER = 1U << 4,
  HEADER_MASK_GEOMTYPE = 3U << 5,
  HEADER_MASK_HAS_ADDINFO = 1U << 7
};

enum class HeaderGeomType : uint8_t
{
  /// Coding geometry feature type in 2 bits.
  Point = 0,         /// point feature (addinfo = rank)
  Line = 1U << 5,    /// linear feature (addinfo = ref)
  Area = 1U << 6,    /// area feature (addinfo = house)
  PointEx = 3U << 5  /// point feature (addinfo = house)
};

static constexpr int kMaxTypesCount = HEADER_MASK_TYPE + 1;  // 8, because there should be no features with 0 types

enum Layer : int8_t
{
  LAYER_LOW = -10,
  LAYER_EMPTY = 0,
  LAYER_HIGH = 10
};

class TypesHolder
{
public:
  using Types = std::array<uint32_t, kMaxTypesCount>;

  TypesHolder() = default;
  explicit TypesHolder(GeomType geomType) : m_geomType(geomType) {}
  explicit TypesHolder(FeatureType & f);

  void Assign(uint32_t type)
  {
    m_types[0] = type;
    m_size = 1;
  }

  template <class IterT>
  void Assign(IterT beg, IterT end)
  {
    m_size = std::distance(beg, end);
    CHECK_LESS_OR_EQUAL(m_size, kMaxTypesCount, ());
    std::copy(beg, end, m_types.begin());
  }

  void Add(uint32_t type)
  {
    CHECK_LESS(m_size, kMaxTypesCount, ());
    m_types[m_size++] = type;
  }

  void SafeAdd(uint32_t type)
  {
    if (!Has(type))
    {
      if (m_size < kMaxTypesCount)
        Add(type);
      else
        LOG(LWARNING, ("Type could not be added, MaxTypesCount exceeded"));
    }
  }

  GeomType GetGeomType() const { return m_geomType; }

  size_t Size() const { return m_size; }
  bool Empty() const { return (m_size == 0); }

  uint32_t front() const
  {
    ASSERT(m_size > 0, ());
    return m_types[0];
  }
  auto begin() const { return m_types.cbegin(); }
  auto end() const { return m_types.cbegin() + m_size; }
  auto begin() { return m_types.begin(); }
  auto end() { return m_types.begin() + m_size; }

  /// Assume that m_types is already sorted by SortBySpec function.
  uint32_t GetBestType() const
  {
    // 0 - is an empty type.
    return (m_size > 0 ? m_types[0] : 0);
  }

  bool Has(uint32_t type) const { return base::IsExist(*this, type); }

  // More _natural_ way of checking than Has, including subclass types hierarchy.
  // "railway-station-subway" holder returns true for "railway-station" input.
  bool HasWithSubclass(uint32_t type) const;

  template <typename Fn>
  bool RemoveIf(Fn && fn)
  {
    size_t const oldSize = m_size;

    auto const e = std::remove_if(begin(), end(), std::forward<Fn>(fn));
    m_size = std::distance(begin(), e);

    return (m_size != oldSize);
  }

  void Remove(uint32_t type);

  /// Used in tests only to check UselessTypesChecker
  /// (which is used in the generator to discard least important types if max types count is exceeded).
  void SortByUseless();

  /// Sort types by it's specification (more detailed type goes first). Should be used in client app.
  void SortBySpec();

  bool Equals(TypesHolder const & other) const;

  std::vector<std::string> ToObjectNames() const;

private:
  Types m_types = {};
  size_t m_size = 0;

  GeomType m_geomType = GeomType::Undefined;
};

std::string DebugPrint(TypesHolder const & holder);

uint8_t CalculateHeader(size_t const typesCount, HeaderGeomType const headerGeomType, FeatureParamsBase const & params);
}  // namespace feature

struct FeatureParamsBase
{
  StringUtf8Multilang name;
  StringNumericOptimal house;
  std::string ref;
  int8_t layer;
  uint8_t rank;

  FeatureParamsBase() : layer(feature::LAYER_EMPTY), rank(0) {}

  void MakeZero();
  bool SetDefaultNameIfEmpty(std::string const & s);

  bool operator==(FeatureParamsBase const & rhs) const;

  bool IsValid() const;
  std::string DebugString() const;

  /// @return true if feature doesn't have any drawable strings (names, houses, etc).
  bool IsEmptyNames() const;

  template <class Sink>
  void Write(Sink & sink, uint8_t header) const
  {
    using namespace feature;

    if (header & HEADER_MASK_HAS_NAME)
      name.WriteNonEmpty(sink);

    if (header & HEADER_MASK_HAS_LAYER)
      WriteToSink(sink, layer);

    if (header & HEADER_MASK_HAS_ADDINFO)
    {
      auto const headerGeomType = static_cast<HeaderGeomType>(header & HEADER_MASK_GEOMTYPE);
      switch (headerGeomType)
      {
      case HeaderGeomType::Point: WriteToSink(sink, rank); break;
      case HeaderGeomType::Line: rw::WriteNonEmpty(sink, ref); break;
      case HeaderGeomType::Area:
      case HeaderGeomType::PointEx: house.Write(sink); break;
      }
    }
  }

  template <class TSrc>
  void Read(TSrc & src, uint8_t header)
  {
    using namespace feature;

    if (header & HEADER_MASK_HAS_NAME)
      name.ReadNonEmpty(src);

    if (header & HEADER_MASK_HAS_LAYER)
      layer = ReadPrimitiveFromSource<int8_t>(src);

    if (header & HEADER_MASK_HAS_ADDINFO)
    {
      auto const headerGeomType = static_cast<HeaderGeomType>(header & HEADER_MASK_GEOMTYPE);
      switch (headerGeomType)
      {
      case HeaderGeomType::Point: rank = ReadPrimitiveFromSource<uint8_t>(src); break;
      case HeaderGeomType::Line: rw::ReadNonEmpty(src, ref); break;
      case HeaderGeomType::Area:
      case HeaderGeomType::PointEx: house.Read(src); break;
      }
    }
  }
};

class FeatureParams : public FeatureParamsBase
{
  static char const * kHNLogTag;

public:
  using Types = std::vector<uint32_t>;

  void ClearName();

  bool AddName(std::string_view lang, std::string_view s);

  static bool LooksLikeHouseNumber(std::string const & hn);
  void SetHouseNumberAndHouseName(std::string houseNumber, std::string houseName);
  bool AddHouseNumber(std::string houseNumber);

  void SetGeomType(feature::GeomType t);
  void SetGeomTypePointEx();
  feature::GeomType GetGeomType() const;

  void AddType(uint32_t t) { m_types.push_back(t); }

  /// Special function to replace a regular railway station type with
  /// the special subway type for the correspondent city.
  void SetRwSubwayType(char const * cityName);

  enum TypesResult
  {
    TYPES_GOOD,
    TYPES_EMPTY,
    TYPES_EXCEED_MAX
  };
  TypesResult FinishAddingTypesEx();
  bool FinishAddingTypes() { return FinishAddingTypesEx() != TYPES_EMPTY; }

  // For logging purpose.
  std::string PrintTypes();

  void SetType(uint32_t t);
  bool PopAnyType(uint32_t & t);
  bool PopExactType(uint32_t t);
  bool IsTypeExist(uint32_t t) const;
  bool IsTypeExist(uint32_t comp, uint8_t level) const;

  /// @return Type that matches |comp| with |level| in classificator hierarchy.
  ///   ftype::GetEmptyValue() if type not found.
  uint32_t FindType(uint32_t comp, uint8_t level) const;

  bool IsValid() const;

  uint8_t GetHeader() const;

  template <class Sink>
  void Write(Sink & sink) const
  {
    uint8_t const header = GetHeader();
    WriteToSink(sink, header);

    for (size_t i = 0; i < m_types.size(); ++i)
      WriteVarUint(sink, GetIndexForType(m_types[i]));

    Base::Write(sink, header);
  }

  template <class Source>
  void Read(Source & src)
  {
    using namespace feature;

    uint8_t const header = ReadPrimitiveFromSource<uint8_t>(src);
    m_geomType = static_cast<HeaderGeomType>(header & HEADER_MASK_GEOMTYPE);

    size_t const count = (header & HEADER_MASK_TYPE) + 1;
    for (size_t i = 0; i < count; ++i)
      m_types.push_back(GetTypeForIndex(ReadVarUint<uint32_t>(src)));

    Base::Read(src, header);
  }

  /// @todo Make protected and update EditableMapObject code.
  Types m_types;

  friend std::string DebugPrint(FeatureParams const & p);

private:
  using Base = FeatureParamsBase;

  feature::HeaderGeomType GetHeaderGeomType() const;
  static uint32_t GetIndexForType(uint32_t t);
  static uint32_t GetTypeForIndex(uint32_t i);

  std::optional<feature::HeaderGeomType> m_geomType;
};

/// @todo Take out into generator library.
class FeatureBuilderParams : public FeatureParams
{
public:
  /// Assign parameters except geometry type.
  void SetParams(FeatureBuilderParams const & rhs)
  {
    FeatureParamsBase::operator=(rhs);

    m_types = rhs.m_types;
    m_addrTags = rhs.m_addrTags;
    m_metadata = rhs.m_metadata;
    m_reversedGeometry = rhs.m_reversedGeometry;
  }

  void MakeZero()
  {
    m_metadata.Clear();
    m_addrTags.Clear();
    FeatureParams::MakeZero();
  }

  /// @name Used to store address to temporary TEMP_ADDR_EXTENSION file.
  /// @{
  void SetAddress(feature::AddressData && addr) { m_addrTags = std::move(addr); }

  void SetStreet(std::string s);
  std::string_view GetStreet() const;

  template <class TSink>
  void SerializeAddress(TSink & sink) const
  {
    m_addrTags.SerializeForMwmTmp(sink);
  }
  /// @}

  void SetPostcode(std::string s);
  std::string_view GetPostcode() const;

  feature::Metadata const & GetMetadata() const { return m_metadata; }
  feature::Metadata & GetMetadata() { return m_metadata; }

  template <class Sink>
  void Write(Sink & sink) const
  {
    FeatureParams::Write(sink);
    m_metadata.SerializeForMwmTmp(sink);
    m_addrTags.SerializeForMwmTmp(sink);
  }

  template <class Source>
  void Read(Source & src)
  {
    FeatureParams::Read(src);
    m_metadata.DeserializeFromMwmTmp(src);
    m_addrTags.DeserializeFromMwmTmp(src);
  }

  bool GetReversedGeometry() const { return m_reversedGeometry; }
  void SetReversedGeometry(bool reversedGeometry) { m_reversedGeometry = reversedGeometry; }

  /// @return true If any inconsistency was found here.
  bool RemoveInconsistentTypes();

  void ClearPOIAttribs();

  friend std::string DebugPrint(FeatureBuilderParams const & p);

private:
  bool m_reversedGeometry = false;
  feature::Metadata m_metadata;
  feature::AddressData m_addrTags;
};
