/*
 * Copyright (C) 2014 Open Source Robotics Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

#include <algorithm>
#include <regex>
#include <string>
#include <vector>

#include "gz/transport/TopicUtils.hh"

namespace gz::transport
{

/// \brief The separator used within the liveliness token.
const char TopicUtils::kTokenSeparator[] = "/";

/// \brief The separator used to concatenate type names.
const char TopicUtils::kTypeSeparator[] = "&";

/// \brief A common prefix for all liveliness tokens.
const char TopicUtils::kTokenPrefix[] = "@gz";

/// \brief A replacement for the slash when mangling names.
const char TopicUtils::kSlashReplacement = '%';

//////////////////////////////////////////////////
bool TopicUtils::IsValidNamespace(const std::string &_ns)
{
  // An empty namespace is valid, so take a shortcut here.
  if (_ns.empty())
    return true;

  // Too long string is not valid.
  if (_ns.size() > kMaxNameLength)
    return false;

  // "/" is not valid.
  if (_ns == "/")
    return false;

  // If the topic name has a '~' is not valid.
  if (_ns.find("~") != std::string::npos)
    return false;

  // If the topic name has a white space is not valid.
  if (_ns.find(" ") != std::string::npos)
    return false;

  // It is not allowed to have two consecutive slashes.
  if (_ns.find("//") != std::string::npos)
    return false;

  // It the topic name has a '@' is not valid.
  if (_ns.find("@") != std::string::npos)
    return false;

  // If the topic name has a ':=' is not valid.
  if (_ns.find(":=") != std::string::npos)
    return false;

  return true;
}

//////////////////////////////////////////////////
bool TopicUtils::IsValidPartition(const std::string &_partition)
{
  // A valid namespace is also a valid partition.
  return IsValidNamespace(_partition);
}

//////////////////////////////////////////////////
bool TopicUtils::IsValidTopic(const std::string &_topic)
{
  return IsValidNamespace(_topic) && !_topic.empty();
}

//////////////////////////////////////////////////
bool TopicUtils::FullyQualifiedName(const std::string &_partition,
  const std::string &_ns, const std::string &_topic, std::string &_name)
{
  // Sanity check, first things first.
  if (!IsValidPartition(_partition) || !IsValidNamespace(_ns) ||
      !IsValidTopic(_topic))
  {
    return false;
  }

  std::string partition = _partition;
  std::string ns = _ns;
  std::string topic = _topic;

  // If partition is not empty and does not start with slash, add it.
  if (!partition.empty() && partition.front() != '/')
    partition.insert(0, 1, '/');

  // If the partition contains a trailing slash, remove it.
  if (!partition.empty() && partition.back() == '/')
    partition.pop_back();

  // If the namespace does not contain a trailing slash, append it.
  if (ns.empty() || ns.back() != '/')
    ns.push_back('/');

  // If the namespace does not start with slash, add it.
  if (ns.empty() || ns.front() != '/')
    ns.insert(0, 1, '/');

  // If the topic ends in "/", remove it.
  if (!topic.empty() && topic.back() == '/')
    topic.pop_back();

  // If the topic does starts with '/' is considered an absolute topic and the
  // namespace will not be prefixed.
  if (!topic.empty() && topic.front() == '/')
    _name = topic;
  else
    _name = ns + topic;

  // Add the partition prefix.
  _name.insert(0, "@" + partition + "@");

  // Too long string is not valid.
  if (_name.size() > kMaxNameLength)
    return false;

  return true;
}

//////////////////////////////////////////////////
bool TopicUtils::DecomposeFullyQualifiedTopic(
  const std::string &_fullyQualifiedName,
  std::string &_partition,
  std::string &_namespaceAndTopic)
{
  const std::size_t firstAt = _fullyQualifiedName.find_first_of("@");
  const std::size_t lastAt = _fullyQualifiedName.find_last_of("@");

  if ( firstAt != 0
    || firstAt == lastAt
    || lastAt == _fullyQualifiedName.size() - 1)
  {
    return false;
  }

  std::string possiblePartition = _fullyQualifiedName.substr(
    firstAt + 1, lastAt - firstAt - 1);
  std::string possibleTopic = _fullyQualifiedName.substr(lastAt + 1);

  if (!IsValidPartition(possiblePartition) || !IsValidTopic(possibleTopic))
  {
    return false;
  }

  _partition = possiblePartition;
  _namespaceAndTopic = possibleTopic;
  return true;
}

//////////////////////////////////////////////////
bool TopicUtils::DecomposeLivelinessTokenHelper(
  const std::string &_token,
  std::string &_prefix,
  std::string &_partition,
  std::string &_namespaceAndTopic,
  std::string &_pUUID,
  std::string &_nUUID,
  std::string &_entityType,
  std::string &_remainingToken)
{
  auto nDelims = static_cast<int>(
    std::count(_token.begin(), _token.end(), '/'));
  if (nDelims < 10)
    return false;

  std::string token = _token;

  std::size_t firstAt = token.find_first_of("@");
  if ( firstAt != 0)
    return false;

  firstAt = token.find_first_of("/");

  // Prefix
  std::string possiblePrefix = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Partition
  std::string possiblePartition = token.substr(0, firstAt);
  possiblePartition = DemangleName(possiblePartition);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Process UUID
  std::string possibleProcUUID = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Node UUID
  std::string possibleNodeUUID = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Entity UUID
  std::string unused = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Entity type
  std::string possibleEntityType = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Mangled enclave
  unused = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Mangled namespace
  unused = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Node name
  unused = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Topic
  std::string possibleTopic = token.substr(0, firstAt);
  possibleTopic = DemangleName(possibleTopic);
  token.erase(0, firstAt + 1);

  if (!IsValidPartition(possiblePartition) || !IsValidTopic(possibleTopic))
    return false;

  _prefix = possiblePrefix;
  _partition = possiblePartition;
  _namespaceAndTopic = possibleTopic;
  _pUUID = possibleProcUUID;
  _nUUID = possibleNodeUUID;
  _entityType = possibleEntityType;
  _remainingToken = token;
  return true;
}

//////////////////////////////////////////////////
bool TopicUtils::DecomposeLivelinessToken(
  const std::string &_token,
  std::string &_prefix,
  std::string &_partition,
  std::string &_namespaceAndTopic,
  std::string &_pUUID,
  std::string &_nUUID,
  std::string &_entityType,
  std::string &_msgType)
{
  std::string token;
  if (!DecomposeLivelinessTokenHelper(_token, _prefix, _partition,
        _namespaceAndTopic, _pUUID, _nUUID, _entityType, token))
  {
    return false;
  }

  auto nDelims = static_cast<int>(
    std::count(token.begin(), token.end(), '/'));
  if (nDelims != 2)
    return false;

  std::size_t firstAt = token.find_first_of("/");

  // TypeName
  std::string possibleMsgType = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Type hash
  std::string unused = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  // QoS
  unused = token;

  _msgType = possibleMsgType;
  return true;
}

//////////////////////////////////////////////////
bool TopicUtils::DecomposeLivelinessToken(
    const std::string &_token,
    std::string &_prefix,
    std::string &_partition,
    std::string &_namespaceAndTopic,
    std::string &_pUUID,
    std::string &_nUUID,
    std::string &_entityType,
    std::string &_reqType,
    std::string &_repType)
{
  std::string token;
  if (!DecomposeLivelinessTokenHelper(_token, _prefix, _partition,
        _namespaceAndTopic, _pUUID, _nUUID, _entityType, token))
  {
    return false;
  }

  auto nDelims = static_cast<int>(
    std::count(token.begin(), token.end(), '/'));
  if (nDelims != 2)
    return false;

  std::size_t firstAt = token.find_first_of("/");

  // TypeName
  std::string mangledTypes = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  std::vector<std::string> types;
  if (!DemangleType(mangledTypes, types))
    return false;

  if (types.size() != 2u)
    return false;

  firstAt = token.find_first_of("/");
  if ( firstAt == 0)
    return false;

  // Type hash
  std::string unused = token.substr(0, firstAt);
  token.erase(0, firstAt + 1);

  // QoS
  unused = token;

  _reqType = types.at(0);
  _repType = types.at(1);
  return true;
}

//////////////////////////////////////////////////
std::string TopicUtils::AsValidTopic(const std::string &_topic)
{
  std::string validTopic{_topic};

  // Substitute spaces with _
  validTopic = std::regex_replace(validTopic, std::regex(" "), "_");

  // Remove special characters and combinations
  validTopic = std::regex_replace(validTopic, std::regex("@|~|//|:="), "");

  if (!IsValidTopic(validTopic))
  {
    return std::string();
  }

  return validTopic;
}

//////////////////////////////////////////////////
std::string TopicUtils::CreateLivelinessTokenHelper(
  const std::string &_fullyQualifiedTopic,
  const std::string &_pUuid,
  const std::string &_nUuid,
  const std::string &_entityType)
{
  std::string partition;
  std::string topic;
  if (!DecomposeFullyQualifiedTopic(_fullyQualifiedTopic, partition, topic))
    return "";

  return
    std::string(kTokenPrefix) + kTokenSeparator +
    MangleName(partition) + kTokenSeparator +
    _pUuid + kTokenSeparator +
    _nUuid + kTokenSeparator +
    _nUuid + kTokenSeparator +
    _entityType + kTokenSeparator +
    "%" + kTokenSeparator +
    "%" + kTokenSeparator +
    "%" + kTokenSeparator +
    MangleName(topic) + kTokenSeparator;
}

//////////////////////////////////////////////////
std::string TopicUtils::CreateLivelinessToken(
  const std::string &_fullyQualifiedTopic,
  const std::string &_pUuid,
  const std::string &_nUuid,
  const std::string &_entityType,
  const std::string &_typeName)
{
  return
    CreateLivelinessTokenHelper(
      _fullyQualifiedTopic, _pUuid, _nUuid, _entityType) +
    _typeName + kTokenSeparator +
    "%" + kTokenSeparator +
    "%";
}

//////////////////////////////////////////////////
std::string TopicUtils::CreateLivelinessToken(
  const std::string &_fullyQualifiedTopic,
  const std::string &_pUuid,
  const std::string &_nUuid,
  const std::string &_entityType,
  const std::string &_reqTypeName,
  const std::string &_repTypeName)
{
  std::string mangledTypes;
  if (!MangleType({_reqTypeName, _repTypeName}, mangledTypes))
    return "";

  return
    CreateLivelinessTokenHelper(
      _fullyQualifiedTopic, _pUuid, _nUuid, _entityType) +
    mangledTypes + kTokenSeparator +
    "%" + kTokenSeparator +
    "%";
}

//////////////////////////////////////////////////
std::string TopicUtils::MangleName(const std::string &_input)
{
  std::string output = "";
  for (std::size_t i = 0; i < _input.length(); ++i)
  {
    if (_input[i] == '/')
      output += kSlashReplacement;
    else
      output += _input[i];
  }
  return output;
}

//////////////////////////////////////////////////
std::string TopicUtils::DemangleName(const std::string &_input)
{
  std::string output = "";
  for (std::size_t i = 0; i < _input.length(); ++i)
  {
    if (_input[i] == kSlashReplacement)
      output += kTokenSeparator;
    else
      output += _input[i];
  }
  return output;
}

//////////////////////////////////////////////////
bool TopicUtils::MangleType(const std::vector<std::string> &_input,
  std::string &_output)
{
  _output.clear();

  if (_input.empty())
    return false;

  for (auto type : _input)
  {
    if (type.empty())
      return false;

    if (type.find_first_of(kTypeSeparator) != std::string::npos)
      return false;

    _output += type + kTypeSeparator;
  }

  _output.pop_back();
  return true;
}

//////////////////////////////////////////////////
bool TopicUtils::DemangleType(const std::string &_input,
  std::vector<std::string> &_output)
{
  _output.clear();

  if (_input.empty())
    return false;

  std::string token = _input;

  std::size_t firstAt = token.find_first_of(kTypeSeparator);
  while (firstAt != std::string::npos)
  {
    std::string type = token.substr(0, firstAt);

    if (type.empty())
      return false;

    _output.push_back(type);
    token.erase(0, firstAt + 1);
    firstAt = token.find_first_of(kTypeSeparator);
  }

  if (token.empty())
    return false;

  _output.push_back(token);

  return true;
}
}  // namespace gz::transport
