#include "search/house_numbers_matcher.hpp"

#include "indexer/string_set.hpp"

#include <algorithm>
#include <iterator>
#include <limits>
#include <sstream>

#include <boost/iterator/transform_iterator.hpp>

using boost::make_transform_iterator;

namespace search
{
namespace house_numbers
{
using namespace std;
using namespace strings;

namespace
{
// Common strings in house numbers.
// To get this list, just run:
//
// ./clusterize-tag-values.lisp house-number-strings path-to-taginfo-db.db > strings.txt
// cat strings.txt |
// awk '{ if ($1 >= 100 && length($3) != 0) { printf("\"%s\",\n", $3) } }' |
// sort | uniq
//
// *NOTE* there is a list of exceptions at the end.

/// @todo By VNG: This list looks hillarious :) Definitely should set some lower bound number
/// to filter very exotic entries in addr:housenumber.

// Removed street keywords for now and ALL one-letter strings. It is sensitive for search speed, because:
// LooksLikeHouseNumber -> MatchBuildingsWithStreets -> *heavy* StreetVicinityLoader::GetStreet
// "av", "avenida",
// "ca", "cal",  "calle",  "carrera", "court",
// "da", "de", "di".
// "ga",
// "ł", "la",
// "ne",
// "pa", "par", "park", "plaza",
// "rd", "ro", "road",
// "so", "south", "st", "street",
// "vi",
// "way", "we", "west",

char const * g_strings[] = {
    "aa", "ab", "abc", "ac", "ad", "ae", "af", "ag", "ah", "ai", "aj", "ak", "al", "am", "an", "ao", "ap", "aq", "ar",
    "are", "as", "at", "au", "aw", "ax", "ay", "az", "azm", "ba", "bab", "bah", "bak", "bb", "bc", "bd", "be", "bedr",
    "ben", "bf", "bg", "bh", "bij", "bis", "bk", "bl", "bldg", "blk", "bloc", "block", "bloco", "blok", "bm", "bmn",
    "bn", "bo", "boe", "bol", "bor", "bov", "box", "bp", "br", "bra", "brc", "bs", "bsa", "bu", "building", "bv", "bwn",
    "bx", "by", "cab", "cat", "cbi", "cbu", "cc", "ccz", "cd", "ce", "centre", "cfn", "cgc", "cjg", "cl", "club",
    "cottage", "cottages", "cso", "cum", "db", "dd", "df", "dia", "dvu", "ec", "ee", "eh", "em", "en", "esm", "ev",
    "fdo", "fer", "ff", "flat", "flats", "floor", "gar", "gara", "gas", "gb", "gg", "gr", "grg", "ha", "haus", "hh",
    "hl", "ho", "house", "hr", "hs", "hv", "ii", "iii", "int", "iv", "ix", "jab", "jf", "jj", "jms", "jtg", "ka", "kab",
    "kk", "kmb", "kmk", "knn", "koy", "kp", "kra", "ksn", "kud", "ldo", "ll", "local", "loja", "lot", "lote", "lsb",
    "lt", "mac", "mad", "mah", "mak", "mat", "mb", "mbb", "mbn", "mch", "mei", "mks", "mm", "mny", "mo", "mok", "mor",
    "msb", "mtj", "mtk", "mvd", "na", "ncc", "nij", "nn", "no", "nr", "nst", "nu", "nut", "of", "ofof", "old", "one",
    "oo", "opl", "pa", "pap", "pav", "pb", "pch", "pg", "ph", "phd", "pkf", "plot", "po", "pos", "pp", "pr", "pra",
    "pya", "qq", "quater", "ra", "rbo", "rear", "reisach", "rk", "rm", "rosso", "rs", "rw", "sab", "sal", "sav", "sb",
    "sba", "sbb", "sbl", "sbn", "sbx", "sc", "sch", "sco", "seb", "sep", "sf", "sgr", "sir", "sj", "sl", "sm", "sn",
    "snc", "som", "sp", "spi", "spn", "ss", "sta", "stc", "std", "stiege", "suite", "sur", "tam", "ter", "terrace",
    "tf", "th", "the", "tl", "to", "torre", "tr", "traf", "trd", "ts", "tt", "tu", "uhm", "unit", "utc", "vii", "wa",
    "wf", "wink", "wrh", "ws", "wsb", "xx", "za", "zh", "zona", "zu", "zw", "א", "ב", "ג", "α", "бб", "бл", "вл", "вх",
    "лит", "разр", "стр", "тп", "уч", "участок", "ა", "丁目", "之", "号", "號",

    // List of exceptions
    "владение"};

// Common strings in house numbers.
// To get this list, just run:
//
// ./clusterize-tag-values.lisp house-number path-to-taginfo-db.db > numbers.txt
// tail -n +2 numbers.txt  | head -78 | sed 's/^.*) \(.*\) \[.*$/"\1"/g;s/[ -/]//g;s/$/,/' |
// sort | uniq
vector<string> const g_patterns = {"BL", "BLN", "BLNSL", "BN", "BNL", "BNSL", "L", "LL", "LN", "LNL", "LNLN", "LNN",
                                   "N", "NBL", "NBLN", "NBN", "NBNBN", "NBNL", "NL", "NLBN", "NLL", "NLLN", "NLN",
                                   "NLNL", "NLS", "NLSN", "NN", "NNBN", "NNL", "NNLN", "NNN", "NNS", "NS", "NSN", "NSS",
                                   "S", "SL", "SLL", "SLN", "SN", "SNBNSS", "SNL", "SNN", "SS", "SSN", "SSS", "SSSS",

                                   // List of exceptions
                                   "NNBNL"};

// List of patterns which look like house numbers more than other patterns. Constructed by hand.
vector<string> const g_patternsStrict = {"N", "NBN", "NBL", "NL"};

// List of common synonyms for building parts. Constructed by hand.
char const * g_buildingPartSynonyms[] = {"building", "bldg", "bld",   "bl",  "unit",     "block", "blk",  "корпус",
                                         "корп",     "кор",  "литер", "лит", "строение", "стр",   "блок", "бл"};

// List of common stop words for buildings. Constructed by hand.
UniString const g_stopWords[] = {MakeUniString("дом"), MakeUniString("house"), MakeUniString("д")};

bool IsStopWord(UniString const & s, bool isPrefix)
{
  for (auto const & p : g_stopWords)
    if ((isPrefix && StartsWith(p, s)) || (!isPrefix && p == s))
      return true;
  return false;
}

class BuildingPartSynonymsMatcher
{
public:
  using Synonyms = StringSet<UniChar, 4>;

  BuildingPartSynonymsMatcher()
  {
    for (auto const & s : g_buildingPartSynonyms)
    {
      UniString const us = MakeUniString(s);
      m_synonyms.Add(us.begin(), us.end());
    }
  }

  // Returns true if |s| looks like a building synonym.
  inline bool Has(UniString const & s) const { return m_synonyms.Has(s.begin(), s.end()) == Synonyms::Status::Full; }

private:
  Synonyms m_synonyms;
};

class StringsMatcher
{
public:
  using Strings = StringSet<UniChar, 8>;

  StringsMatcher()
  {
    for (auto const & s : g_strings)
    {
      UniString const us = MakeUniString(s);
      m_strings.Add(us.begin(), us.end());
    }

    for (auto const & s : g_buildingPartSynonyms)
    {
      UniString const us = MakeUniString(s);
      m_strings.Add(us.begin(), us.end());
    }
  }

  // Returns true when |s| may be a full substring of a house number,
  // or a prefix of some valid substring of a house number, when
  // |isPrefix| is true.
  bool Has(UniString const & s, bool isPrefix) const
  {
    auto const status = m_strings.Has(s.begin(), s.end());
    switch (status)
    {
    case Strings::Status::Absent: return false;
    case Strings::Status::Prefix: return isPrefix;
    case Strings::Status::Full: return true;
    }
    UNREACHABLE();
  }

private:
  Strings m_strings;
};

class HouseNumberClassifier
{
public:
  using Patterns = StringSet<Token::Type, 4>;

  HouseNumberClassifier(vector<string> const & patterns = g_patterns)
  {
    for (auto const & p : patterns)
      m_patterns.Add(make_transform_iterator(p.begin(), &CharToType), make_transform_iterator(p.end(), &CharToType));
  }

  // Returns true when the string |s| looks like a valid house number,
  // (or a prefix of some valid house number, when |isPrefix| is
  // true).
  bool LooksGood(UniString const & s, bool isPrefix) const
  {
    TokensT parse;
    Tokenize(s, isPrefix, parse);

    size_t i = 0;
    for (size_t j = 0; j != parse.size(); ++j)
    {
      auto const & token = parse[j];
      auto const type = token.m_type;
      switch (type)
      {
      case Token::TYPE_SEPARATOR: break;
      case Token::TYPE_GROUP_SEPARATOR: break;
      case Token::TYPE_HYPHEN: break;
      case Token::TYPE_SLASH: break;
      case Token::TYPE_STRING:
      {
        if (IsStopWord(token.m_value, token.m_prefix))
          break;
        if (!m_matcher.Has(token.m_value, token.m_prefix))
          return false;
        [[fallthrough]];
      }
      case Token::TYPE_LETTER:
      {
        if (j == 0 && IsStopWord(token.m_value, token.m_prefix))
          break;
        [[fallthrough]];
      }
      case Token::TYPE_NUMBER:
      case Token::TYPE_BUILDING_PART:
      case Token::TYPE_BUILDING_PART_OR_LETTER:
        parse[i] = std::move(parse[j]);
        ASSERT(!parse[i].m_value.empty(), ());
        ++i;
      }
    }
    parse.resize(i);

    auto const status = m_patterns.Has(make_transform_iterator(parse.begin(), &TokenToType),
                                       make_transform_iterator(parse.end(), &TokenToType));
    switch (status)
    {
    case Patterns::Status::Absent: return false;
    case Patterns::Status::Prefix: return true;
    case Patterns::Status::Full: return true;
    }
    UNREACHABLE();
  }

private:
  static Token::Type CharToType(char c)
  {
    switch (c)
    {
    case 'N': return Token::TYPE_NUMBER;
    case 'S': return Token::TYPE_STRING;
    case 'B': return Token::TYPE_BUILDING_PART;
    case 'L': return Token::TYPE_LETTER;
    case 'U': return Token::TYPE_BUILDING_PART_OR_LETTER;
    default: CHECK(false, ("Unexpected character:", c)); return Token::TYPE_SEPARATOR;
    }
    UNREACHABLE();
  }

  static Token::Type TokenToType(Token const & token) { return token.m_type; }

  StringsMatcher m_matcher;
  Patterns m_patterns;
};

Token::Type GetCharType(UniChar c)
{
  static UniString const kSeps = MakeUniString(" \t\"\\().#~");
  static UniString const kGroupSeps = MakeUniString(",|;+");

  if (IsASCIIDigit(c))
    return Token::TYPE_NUMBER;
  if (find(kSeps.begin(), kSeps.end(), c) != kSeps.end())
    return Token::TYPE_SEPARATOR;
  if (find(kGroupSeps.begin(), kGroupSeps.end(), c) != kGroupSeps.end())
    return Token::TYPE_GROUP_SEPARATOR;
  if (c == '-')
    return Token::TYPE_HYPHEN;
  if (c == '/')
    return Token::TYPE_SLASH;
  return Token::TYPE_STRING;
}

bool IsLiteralType(Token::Type type)
{
  return type == Token::TYPE_STRING || type == Token::TYPE_LETTER || type == Token::TYPE_BUILDING_PART_OR_LETTER;
}

// Leaves only numbers and letters, removes all trailing prefix
// tokens. Then, does following:
//
// * when there is at least one number, drops all tokens until the
//   number and sorts the rest
// * when there are no numbers at all, sorts tokens
void SimplifyParse(TokensT & tokens)
{
  if (!tokens.empty() && tokens.back().m_prefix)
    tokens.pop_back();

  size_t i = 0;
  size_t j = 0;
  while (j != tokens.size() && tokens[j].m_type != Token::TYPE_NUMBER)
    ++j;
  for (; j != tokens.size(); ++j)
  {
    auto const type = tokens[j].m_type;
    if (type == Token::TYPE_NUMBER || type == Token::TYPE_LETTER)
      tokens[i++] = tokens[j];
  }

  if (i != 0)
  {
    tokens.resize(i);
    sort(tokens.begin() + 1, tokens.end());
  }
  else
  {
    sort(tokens.begin(), tokens.end());
  }
}

// Returns true when a sequence denoted by [b2, e2) is a subsequence
// of [b1, e1).
template <typename T1, typename T2>
bool IsSubsequence(T1 b1, T1 e1, T2 b2, T2 e2)
{
  for (; b2 != e2; ++b1, ++b2)
  {
    while (b1 != e1 && *b1 < *b2)
      ++b1;
    if (b1 == e1 || *b1 != *b2)
      return false;
  }
  return true;
}

bool IsBuildingPartSynonym(UniString const & s)
{
  static BuildingPartSynonymsMatcher const kMatcher;
  return kMatcher.Has(s);
}

bool IsShortBuildingSynonym(UniString const & t)
{
  static UniString const kSynonyms[] = {MakeUniString("к"), MakeUniString("с")};
  for (auto const & s : kSynonyms)
    if (t == s)
      return true;
  return false;
}

template <typename Fn>
void ForEachGroup(TokensT const & ts, Fn && fn)
{
  size_t i = 0;
  while (i < ts.size())
  {
    while (i < ts.size() && ts[i].m_type == Token::TYPE_GROUP_SEPARATOR)
      ++i;

    size_t j = i;
    while (j < ts.size() && ts[j].m_type != Token::TYPE_GROUP_SEPARATOR)
      ++j;

    if (i != j)
      fn(i, j);

    i = j;
  }
}

template <typename Fn>
void TransformString(UniString && token, Fn && fn)
{
  static UniString const kLiter = MakeUniString("лит");

  size_t const size = token.size();

  if (IsBuildingPartSynonym(token))
  {
    fn(std::move(token), Token::TYPE_BUILDING_PART);
  }
  else if (size == 4 && StartsWith(token, kLiter))
  {
    fn(UniString(token.begin(), token.begin() + 3), Token::TYPE_BUILDING_PART);
    fn(UniString(token.begin() + 3, token.end()), Token::TYPE_LETTER);
  }
  else if (size == 2)
  {
    UniString firstLetter(token.begin(), token.begin() + 1);
    if (IsShortBuildingSynonym(firstLetter))
    {
      fn(std::move(firstLetter), Token::TYPE_BUILDING_PART);
      fn(UniString(token.begin() + 1, token.end()), Token::TYPE_LETTER);
    }
    else
    {
      fn(std::move(token), Token::TYPE_STRING);
    }
  }
  else if (size == 1)
  {
    if (IsShortBuildingSynonym(token))
      fn(std::move(token), Token::TYPE_BUILDING_PART_OR_LETTER);
    else
      fn(std::move(token), Token::TYPE_LETTER);
  }
  else
  {
    fn(std::move(token), Token::TYPE_STRING);
  }
}
}  // namespace

uint64_t ToUInt(UniString const & s)
{
  uint64_t res = 0;
  uint64_t pow = 1;

  int i = int(s.size()) - 1;
  ASSERT(i >= 0 && i < std::numeric_limits<uint64_t>::digits10, (i));
  for (; i >= 0; --i)
  {
    ASSERT(IsASCIIDigit(s[i]), (s[i]));

    res += (s[i] - '0') * pow;
    pow *= 10;
  }
  return res;
}

void Tokenize(UniString s, bool isPrefix, TokensT & ts)
{
  MakeLowerCaseInplace(s);
  auto addToken = [&ts](UniString && value, Token::Type type) { ts.emplace_back(std::move(value), type); };

  size_t i = 0;
  while (i < s.size())
  {
    Token::Type const type = GetCharType(s[i]);

    size_t j = i + 1;
    while (j < s.size() && GetCharType(s[j]) == type)
      ++j;

    if (type != Token::TYPE_SEPARATOR)
    {
      UniString token(s.begin() + i, s.begin() + j);
      if (type == Token::TYPE_STRING)
      {
        if (j != s.size() || !isPrefix)
        {
          TransformString(std::move(token), addToken);
        }
        else if (i + 1 == j)
        {
          ts.emplace_back(std::move(token), Token::TYPE_LETTER);
        }
        else
        {
          ts.emplace_back(std::move(token), Token::TYPE_STRING);
          ts.back().m_prefix = true;
        }
      }
      else
      {
        addToken(std::move(token), type);
      }
    }

    i = j;
  }

  // Quite hacky loop from ts.size() - 1 towards 0.
  for (size_t i = ts.size() - 1; i < ts.size(); --i)
  {
    if (ts[i].m_type != Token::TYPE_BUILDING_PART_OR_LETTER)
      continue;
    if (i + 1 == ts.size() || ts[i + 1].m_type == Token::TYPE_BUILDING_PART)
      ts[i].m_type = Token::TYPE_LETTER;
    else if (ts[i + 1].m_type == Token::TYPE_NUMBER)
      ts[i].m_type = Token::TYPE_BUILDING_PART;
  }
}

void ParseHouseNumber(UniString const & s, vector<TokensT> & parses)
{
  TokensT tokens;
  Tokenize(s, false /* isPrefix */, tokens);

  bool numbersSequence = true;
  ForEachGroup(tokens, [&tokens, &numbersSequence](size_t i, size_t j)
  {
    switch (j - i)
    {
    case 0: break;
    case 1: numbersSequence = numbersSequence && tokens[i].m_type == Token::TYPE_NUMBER; break;
    case 2:
      numbersSequence =
          numbersSequence && tokens[i].m_type == Token::TYPE_NUMBER && IsLiteralType(tokens[i + 1].m_type);
      break;
    default: numbersSequence = false; break;
    }
  });

  size_t const oldSize = parses.size();
  if (numbersSequence)
  {
    ForEachGroup(tokens, [&tokens, &parses](size_t i, size_t j)
    {
      parses.emplace_back();
      auto & parse = parses.back();
      for (size_t k = i; k < j; ++k)
        parse.emplace_back(std::move(tokens[k]));
    });
  }
  else
  {
    parses.emplace_back(std::move(tokens));
  }

  for (size_t i = oldSize; i < parses.size(); ++i)
    SimplifyParse(parses[i]);
}

void ParseQuery(UniString const & query, bool queryIsPrefix, TokensT & parse)
{
  Tokenize(query, queryIsPrefix, parse);
  SimplifyParse(parse);
}

bool HouseNumbersMatch(UniString const & houseNumber, TokensT const & queryParse)
{
  ASSERT(!houseNumber.empty() && !queryParse.empty(), ());

  // Fast pre-check, helps to early exit without complex house number parsing.
  if (IsASCIIDigit(houseNumber[0]) && IsASCIIDigit(queryParse[0].m_value[0]) &&
      houseNumber[0] != queryParse[0].m_value[0])
  {
    return false;
  }

  vector<TokensT> houseNumberParses;
  ParseHouseNumber(houseNumber, houseNumberParses);

  for (auto & parse : houseNumberParses)
  {
    if (parse.empty())
      continue;
    if (parse[0] == queryParse[0] &&
        (IsSubsequence(parse.begin() + 1, parse.end(), queryParse.begin() + 1, queryParse.end()) ||
         IsSubsequence(queryParse.begin() + 1, queryParse.end(), parse.begin() + 1, parse.end())))
    {
      return true;
    }
  }
  return false;
}

bool HouseNumbersMatchConscription(UniString const & houseNumber, TokensT const & queryParse)
{
  auto const beg = houseNumber.begin();
  auto const end = houseNumber.end();
  auto i = std::find(beg, end, '/');
  if (i != end)
  {
    // Conscription number / street number.
    return HouseNumbersMatch(UniString(beg, i), queryParse) || HouseNumbersMatch(UniString(i + 1, end), queryParse);
  }
  return HouseNumbersMatch(houseNumber, queryParse);
}

bool HouseNumbersMatchRange(std::string_view const hnRange, TokensT const & queryParse, feature::InterpolType interpol)
{
  ASSERT(!queryParse.empty() && interpol != feature::InterpolType::None, ());

  if (queryParse[0].m_type != Token::TYPE_NUMBER)
    return false;

  uint64_t const val = ToUInt(queryParse[0].m_value);
  bool const isEven = (val % 2 == 0);
  if (interpol == feature::InterpolType::Odd && isEven)
    return false;
  if (interpol == feature::InterpolType::Even && !isEven)
    return false;

  // Generator makes valid normalized values.
  size_t const i = hnRange.find(':');
  if (i == std::string_view::npos)
  {
    ASSERT(false, (hnRange));
    return false;
  }

  uint64_t left, right;
  if (!strings::to_uint(hnRange.substr(0, i), left) || !strings::to_uint(hnRange.substr(i + 1), right))
  {
    ASSERT(false, (hnRange));
    return false;
  }

  return left < val && val < right;
}

bool LooksLikeHouseNumber(UniString const & s, bool isPrefix)
{
  static HouseNumberClassifier const classifier;
  return classifier.LooksGood(s, isPrefix);
}

bool LooksLikeHouseNumber(string const & s, bool isPrefix)
{
  return LooksLikeHouseNumber(MakeUniString(s), isPrefix);
}

bool LooksLikeHouseNumberStrict(UniString const & s)
{
  static HouseNumberClassifier const classifier(g_patternsStrict);
  return classifier.LooksGood(s, false /* isPrefix */);
}

bool LooksLikeHouseNumberStrict(string const & s)
{
  return LooksLikeHouseNumberStrict(MakeUniString(s));
}

string DebugPrint(Token::Type type)
{
  switch (type)
  {
  case Token::TYPE_NUMBER: return "Number";
  case Token::TYPE_SEPARATOR: return "Separator";
  case Token::TYPE_GROUP_SEPARATOR: return "GroupSeparator";
  case Token::TYPE_HYPHEN: return "Hyphen";
  case Token::TYPE_SLASH: return "Slash";
  case Token::TYPE_STRING: return "String";
  case Token::TYPE_BUILDING_PART: return "BuildingPart";
  case Token::TYPE_LETTER: return "Letter";
  case Token::TYPE_BUILDING_PART_OR_LETTER: return "BuildingPartOrLetter";
  }
  return "Unknown";
}

string DebugPrint(Token const & token)
{
  ostringstream os;
  os << "Token [" << DebugPrint(token.m_value) << ", " << DebugPrint(token.m_type) << "]";
  return os.str();
}
}  // namespace house_numbers
}  // namespace search
