//===-- PathMappingList.cpp -----------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include <climits>
#include <cstring>
#include <optional>

#include "lldb/Host/FileSystem.h"
#include "lldb/Host/PosixApi.h"
#include "lldb/Target/PathMappingList.h"
#include "lldb/Utility/FileSpec.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/Stream.h"
#include "lldb/lldb-private-enumerations.h"

using namespace lldb;
using namespace lldb_private;

namespace {
  // We must normalize our path pairs that we store because if we don't then
  // things won't always work. We found a case where if we did:
  // (lldb) settings set target.source-map . /tmp
  // We would store a path pairs of "." and "/tmp" as raw strings. If the debug
  // info contains "./foo/bar.c", the path will get normalized to "foo/bar.c".
  // When PathMappingList::RemapPath() is called, it expects the path to start
  // with the raw path pair, which doesn't work anymore because the paths have
  // been normalized when the debug info was loaded. So we need to store
  // nomalized path pairs to ensure things match up.
std::string NormalizePath(llvm::StringRef path) {
  // If we use "path" to construct a FileSpec, it will normalize the path for
  // us. We then grab the string.
  return FileSpec(path).GetPath();
}
}
// PathMappingList constructor
PathMappingList::PathMappingList() : m_pairs() {}

PathMappingList::PathMappingList(ChangedCallback callback, void *callback_baton)
    : m_pairs(), m_callback(callback), m_callback_baton(callback_baton) {}

PathMappingList::PathMappingList(const PathMappingList &rhs)
    : m_pairs(rhs.m_pairs) {}

const PathMappingList &PathMappingList::operator=(const PathMappingList &rhs) {
  if (this != &rhs) {
    std::scoped_lock<std::mutex, std::mutex, std::mutex> locks(
        m_callback_mutex, m_pairs_mutex, rhs.m_pairs_mutex);
    m_pairs = rhs.m_pairs;
    m_callback = nullptr;
    m_callback_baton = nullptr;
    m_mod_id = rhs.m_mod_id;
  }
  return *this;
}

PathMappingList::~PathMappingList() = default;

void PathMappingList::AppendNoLock(llvm::StringRef path,
                                   llvm::StringRef replacement) {
  ++m_mod_id;
  m_pairs.emplace_back(pair(NormalizePath(path), NormalizePath(replacement)));
}

void PathMappingList::Notify(bool notify) const {
  ChangedCallback callback = nullptr;
  void *baton = nullptr;
  {
    std::lock_guard<std::mutex> lock(m_callback_mutex);
    callback = m_callback;
    baton = m_callback_baton;
  }
  if (notify && callback)
    callback(*this, baton);
}

void PathMappingList::Append(llvm::StringRef path, llvm::StringRef replacement,
                             bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    AppendNoLock(path, replacement);
  }
  Notify(notify);
}

void PathMappingList::Append(const PathMappingList &rhs, bool notify) {
  {
    std::scoped_lock<std::mutex, std::mutex> locks(m_pairs_mutex,
                                                   rhs.m_pairs_mutex);
    ++m_mod_id;
    if (rhs.m_pairs.empty())
      return;
    const_iterator pos, end = rhs.m_pairs.end();
    for (pos = rhs.m_pairs.begin(); pos != end; ++pos)
      m_pairs.push_back(*pos);
  }
  Notify(notify);
}

bool PathMappingList::AppendUnique(llvm::StringRef path,
                                   llvm::StringRef replacement, bool notify) {
  auto normalized_path = NormalizePath(path);
  auto normalized_replacement = NormalizePath(replacement);
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    for (const auto &pair : m_pairs) {
      if (pair.first.GetStringRef() == normalized_path &&
          pair.second.GetStringRef() == normalized_replacement)
        return false;
    }
    AppendNoLock(path, replacement);
  }
  Notify(notify);
  return true;
}

void PathMappingList::Insert(llvm::StringRef path, llvm::StringRef replacement,
                             uint32_t index, bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    ++m_mod_id;
    iterator insert_iter;
    if (index >= m_pairs.size())
      insert_iter = m_pairs.end();
    else
      insert_iter = m_pairs.begin() + index;
    m_pairs.emplace(insert_iter,
                    pair(NormalizePath(path), NormalizePath(replacement)));
  }
  Notify(notify);
}

bool PathMappingList::Replace(llvm::StringRef path, llvm::StringRef replacement,
                              uint32_t index, bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    if (index >= m_pairs.size())
      return false;
    ++m_mod_id;
    m_pairs[index] = pair(NormalizePath(path), NormalizePath(replacement));
  }
  Notify(notify);
  return true;
}

bool PathMappingList::Remove(size_t index, bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    if (index >= m_pairs.size())
      return false;

    ++m_mod_id;
    iterator iter = m_pairs.begin() + index;
    m_pairs.erase(iter);
  }
  Notify(notify);
  return true;
}

// For clients which do not need the pair index dumped, pass a pair_index >= 0
// to only dump the indicated pair.
void PathMappingList::Dump(Stream *s, int pair_index) {
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  unsigned int numPairs = m_pairs.size();

  if (pair_index < 0) {
    unsigned int index;
    for (index = 0; index < numPairs; ++index)
      s->Printf("[%d] \"%s\" -> \"%s\"\n", index,
                m_pairs[index].first.GetCString(),
                m_pairs[index].second.GetCString());
  } else {
    if (static_cast<unsigned int>(pair_index) < numPairs)
      s->Printf("%s -> %s", m_pairs[pair_index].first.GetCString(),
                m_pairs[pair_index].second.GetCString());
  }
}

llvm::json::Value PathMappingList::ToJSON() const {
  llvm::json::Array entries;
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  for (const auto &pair : m_pairs) {
    llvm::json::Array entry{pair.first.GetStringRef().str(),
                            pair.second.GetStringRef().str()};
    entries.emplace_back(std::move(entry));
  }
  return entries;
}

void PathMappingList::Clear(bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    if (!m_pairs.empty())
      ++m_mod_id;
    m_pairs.clear();
  }
  Notify(notify);
}

bool PathMappingList::RemapPath(ConstString path,
                                ConstString &new_path) const {
  if (std::optional<FileSpec> remapped = RemapPath(path.GetStringRef())) {
    new_path.SetString(remapped->GetPath());
    return true;
  }
  return false;
}

/// Append components to path, applying style.
static void AppendPathComponents(FileSpec &path, llvm::StringRef components,
                                 llvm::sys::path::Style style) {
  auto component = llvm::sys::path::begin(components, style);
  auto e = llvm::sys::path::end(components);
  while (component != e &&
         llvm::sys::path::is_separator(*component->data(), style))
    ++component;
  for (; component != e; ++component)
    path.AppendPathComponent(*component);
}

std::optional<FileSpec> PathMappingList::RemapPath(llvm::StringRef mapping_path,
                                                   bool only_if_exists) const {
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  if (m_pairs.empty() || mapping_path.empty())
    return {};
  LazyBool path_is_relative = eLazyBoolCalculate;

  for (const auto &it : m_pairs) {
    llvm::StringRef prefix = it.first.GetStringRef();
    // We create a copy of mapping_path because StringRef::consume_from
    // effectively modifies the instance itself.
    llvm::StringRef path = mapping_path;
    if (!path.consume_front(prefix)) {
      // Relative paths won't have a leading "./" in them unless "." is the
      // only thing in the relative path so we need to work around "."
      // carefully.
      if (prefix != ".")
        continue;
      // We need to figure out if the "path" argument is relative. If it is,
      // then we should remap, else skip this entry.
      if (path_is_relative == eLazyBoolCalculate) {
        path_is_relative =
            FileSpec(path).IsRelative() ? eLazyBoolYes : eLazyBoolNo;
      }
      if (!path_is_relative)
        continue;
    }
    FileSpec remapped(it.second.GetStringRef());
    auto orig_style = FileSpec::GuessPathStyle(prefix).value_or(
        llvm::sys::path::Style::native);
    AppendPathComponents(remapped, path, orig_style);
    if (!only_if_exists || FileSystem::Instance().Exists(remapped))
      return remapped;
  }
  return {};
}

std::optional<llvm::StringRef>
PathMappingList::ReverseRemapPath(const FileSpec &file, FileSpec &fixed) const {
  std::string path = file.GetPath();
  llvm::StringRef path_ref(path);
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  for (const auto &it : m_pairs) {
    llvm::StringRef removed_prefix = it.second.GetStringRef();
    if (!path_ref.consume_front(it.second.GetStringRef()))
      continue;
    auto orig_file = it.first.GetStringRef();
    auto orig_style = FileSpec::GuessPathStyle(orig_file).value_or(
        llvm::sys::path::Style::native);
    fixed.SetFile(orig_file, orig_style);
    AppendPathComponents(fixed, path_ref, orig_style);
    return removed_prefix;
  }
  return std::nullopt;
}

std::optional<FileSpec>
PathMappingList::FindFile(const FileSpec &orig_spec) const {
  // We must normalize the orig_spec again using the host's path style,
  // otherwise there will be mismatch between the host and remote platform
  // if they use different path styles.
  if (auto remapped = RemapPath(NormalizePath(orig_spec.GetPath()),
                                /*only_if_exists=*/true))
    return remapped;

  return {};
}

bool PathMappingList::Replace(llvm::StringRef path, llvm::StringRef new_path,
                              bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    uint32_t idx = FindIndexForPathNoLock(path);
    if (idx >= m_pairs.size())
      return false;
    ++m_mod_id;
    m_pairs[idx].second = ConstString(new_path);
  }
  Notify(notify);
  return true;
}

bool PathMappingList::Remove(ConstString path, bool notify) {
  {
    std::lock_guard<std::mutex> lock(m_pairs_mutex);
    iterator pos = FindIteratorForPath(path);
    if (pos == m_pairs.end())
      return false;

    ++m_mod_id;
    m_pairs.erase(pos);
  }
  Notify(notify);
  return true;
}

PathMappingList::const_iterator
PathMappingList::FindIteratorForPath(ConstString path) const {
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  const_iterator pos;
  const_iterator begin = m_pairs.begin();
  const_iterator end = m_pairs.end();

  for (pos = begin; pos != end; ++pos) {
    if (pos->first == path)
      break;
  }
  return pos;
}

PathMappingList::iterator
PathMappingList::FindIteratorForPath(ConstString path) {
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  iterator pos;
  iterator begin = m_pairs.begin();
  iterator end = m_pairs.end();

  for (pos = begin; pos != end; ++pos) {
    if (pos->first == path)
      break;
  }
  return pos;
}

bool PathMappingList::GetPathsAtIndex(uint32_t idx, ConstString &path,
                                      ConstString &new_path) const {
  std::lock_guard<std::mutex> lock(m_pairs_mutex);
  if (idx < m_pairs.size()) {
    path = m_pairs[idx].first;
    new_path = m_pairs[idx].second;
    return true;
  }
  return false;
}

uint32_t
PathMappingList::FindIndexForPathNoLock(llvm::StringRef orig_path) const {
  const ConstString path = ConstString(NormalizePath(orig_path));
  const_iterator pos;
  const_iterator begin = m_pairs.begin();
  const_iterator end = m_pairs.end();

  for (pos = begin; pos != end; ++pos) {
    if (pos->first == path)
      return std::distance(begin, pos);
  }
  return UINT32_MAX;
}
