//===-- ModuleSpec.h --------------------------------------------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//

#ifndef LLDB_CORE_MODULESPEC_H
#define LLDB_CORE_MODULESPEC_H

#include "lldb/Host/FileSystem.h"
#include "lldb/Target/PathMappingList.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/DataExtractor.h"
#include "lldb/Utility/FileSpec.h"
#include "lldb/Utility/Iterable.h"
#include "lldb/Utility/Stream.h"
#include "lldb/Utility/UUID.h"
#include "lldb/lldb-forward.h"

#include "llvm/Support/Chrono.h"

#include <memory>
#include <mutex>
#include <vector>

namespace lldb_private {

class ModuleSpec {
public:
  ModuleSpec() = default;

  /// If the \c extractor_sp argument is passed, its contents will be used
  /// as the module contents instead of trying to read them from
  /// \c file_spec .
  ModuleSpec(const FileSpec &file_spec, const UUID &uuid = UUID(),
             lldb::DataExtractorSP extractor_sp = lldb::DataExtractorSP())
      : m_file(file_spec), m_uuid(uuid), m_object_offset(0),
        m_extractor_sp(extractor_sp) {
    if (extractor_sp)
      m_object_size = extractor_sp->GetByteSize();
    else if (m_file)
      m_object_size = FileSystem::Instance().GetByteSize(file_spec);
  }

  ModuleSpec(const FileSpec &file_spec, const ArchSpec &arch)
      : m_file(file_spec), m_arch(arch), m_object_offset(0),
        m_object_size(FileSystem::Instance().GetByteSize(file_spec)) {}

  FileSpec *GetFileSpecPtr() { return (m_file ? &m_file : nullptr); }

  const FileSpec *GetFileSpecPtr() const {
    return (m_file ? &m_file : nullptr);
  }

  FileSpec &GetFileSpec() { return m_file; }

  const FileSpec &GetFileSpec() const { return m_file; }

  FileSpec *GetPlatformFileSpecPtr() {
    return (m_platform_file ? &m_platform_file : nullptr);
  }

  const FileSpec *GetPlatformFileSpecPtr() const {
    return (m_platform_file ? &m_platform_file : nullptr);
  }

  FileSpec &GetPlatformFileSpec() { return m_platform_file; }

  const FileSpec &GetPlatformFileSpec() const { return m_platform_file; }

  FileSpec *GetSymbolFileSpecPtr() {
    return (m_symbol_file ? &m_symbol_file : nullptr);
  }

  const FileSpec *GetSymbolFileSpecPtr() const {
    return (m_symbol_file ? &m_symbol_file : nullptr);
  }

  FileSpec &GetSymbolFileSpec() { return m_symbol_file; }

  const FileSpec &GetSymbolFileSpec() const { return m_symbol_file; }

  ArchSpec *GetArchitecturePtr() {
    return (m_arch.IsValid() ? &m_arch : nullptr);
  }

  const ArchSpec *GetArchitecturePtr() const {
    return (m_arch.IsValid() ? &m_arch : nullptr);
  }

  ArchSpec &GetArchitecture() { return m_arch; }

  const ArchSpec &GetArchitecture() const { return m_arch; }

  UUID *GetUUIDPtr() { return (m_uuid.IsValid() ? &m_uuid : nullptr); }

  const UUID *GetUUIDPtr() const {
    return (m_uuid.IsValid() ? &m_uuid : nullptr);
  }

  UUID &GetUUID() { return m_uuid; }

  const UUID &GetUUID() const { return m_uuid; }

  ConstString &GetObjectName() { return m_object_name; }

  ConstString GetObjectName() const { return m_object_name; }

  uint64_t GetObjectOffset() const { return m_object_offset; }

  void SetObjectOffset(uint64_t object_offset) {
    m_object_offset = object_offset;
  }

  uint64_t GetObjectSize() const { return m_object_size; }

  void SetObjectSize(uint64_t object_size) { m_object_size = object_size; }

  /// Get the load address of a module in process memory. If the optional
  /// has no value, there is no load address for this module spec.
  std::optional<lldb::addr_t> GetLoadAddress() const { return m_load_addr; }

  /// Set the load address of a module in process memory.
  void SetLoadAddress(lldb::addr_t addr) { m_load_addr = addr; }

  void ClearLoadAddress() { m_load_addr.reset(); }

  llvm::sys::TimePoint<> &GetObjectModificationTime() {
    return m_object_mod_time;
  }

  const llvm::sys::TimePoint<> &GetObjectModificationTime() const {
    return m_object_mod_time;
  }

  PathMappingList &GetSourceMappingList() const { return m_source_mappings; }

  lldb::DataExtractorSP GetExtractor() const { return m_extractor_sp; }

  lldb::TargetSP GetTargetSP() const { return m_target_wp.lock(); }

  /// Set the target to be used when resolving a module.
  ///
  /// A target can help locate a module specified by a ModuleSpec. The target
  /// settings, like the executable and debug info search paths, can be
  /// essential. The target's platform can also be used to locate or download
  /// the specified module.
  void SetTarget(lldb::TargetSP target) { m_target_wp = target; }

  lldb::PlatformSP GetPlatformSP() const { return m_platform_wp.lock(); }

  /// Set the platform to be used when resolving a module.
  ///
  /// This is useful when a Target is not yet available (e.g., during target
  /// creation) but a Platform is. The platform can be used to invoke locate
  /// module callbacks and other platform-specific module resolution logic.
  void SetPlatform(lldb::PlatformSP platform) { m_platform_wp = platform; }

  void Clear() {
    m_file.Clear();
    m_platform_file.Clear();
    m_symbol_file.Clear();
    m_arch.Clear();
    m_uuid.Clear();
    m_object_name.Clear();
    m_object_offset = 0;
    m_object_size = 0;
    m_source_mappings.Clear(false);
    m_extractor_sp.reset();
    m_object_mod_time = llvm::sys::TimePoint<>();
    m_target_wp.reset();
    m_platform_wp.reset();
    m_load_addr.reset();
  }

  explicit operator bool() const {
    if (m_file)
      return true;
    if (m_platform_file)
      return true;
    if (m_symbol_file)
      return true;
    if (m_arch.IsValid())
      return true;
    if (m_uuid.IsValid())
      return true;
    if (m_object_name)
      return true;
    if (m_object_size)
      return true;
    if (m_object_mod_time != llvm::sys::TimePoint<>())
      return true;
    if (m_load_addr.has_value())
      return true;
    return false;
  }

  void Dump(Stream &strm) const {
    bool dumped_something = false;
    if (m_file) {
      strm.PutCString("file = '");
      strm << m_file;
      strm.PutCString("'");
      dumped_something = true;
    }
    if (m_platform_file) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.PutCString("platform_file = '");
      strm << m_platform_file;
      strm.PutCString("'");
      dumped_something = true;
    }
    if (m_symbol_file) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.PutCString("symbol_file = '");
      strm << m_symbol_file;
      strm.PutCString("'");
      dumped_something = true;
    }
    if (m_arch.IsValid()) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.Printf("arch = ");
      m_arch.DumpTriple(strm.AsRawOstream());
      dumped_something = true;
    }
    if (m_uuid.IsValid()) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.PutCString("uuid = ");
      m_uuid.Dump(strm);
      dumped_something = true;
    }
    if (m_object_name) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.Printf("object_name = %s", m_object_name.GetCString());
      dumped_something = true;
    }
    if (m_object_offset > 0) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.Printf("object_offset = %" PRIu64, m_object_offset);
      dumped_something = true;
    }
    if (m_object_size > 0) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.Printf("object size = %" PRIu64, m_object_size);
      dumped_something = true;
    }
    if (m_object_mod_time != llvm::sys::TimePoint<>()) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.Format("object_mod_time = {0:x+}",
                  uint64_t(llvm::sys::toTimeT(m_object_mod_time)));
      dumped_something = true;
    }
    if (m_load_addr.has_value()) {
      if (dumped_something)
        strm.PutCString(", ");
      strm.Printf("load_addr = 0x%" PRIx64, m_load_addr.value());
      dumped_something = true;
    }
  }

  bool Matches(const ModuleSpec &match_module_spec,
               bool exact_arch_match) const {
    if (match_module_spec.GetUUIDPtr() &&
        match_module_spec.GetUUID() != GetUUID())
      return false;
    if (match_module_spec.GetObjectName() &&
        match_module_spec.GetObjectName() != GetObjectName())
      return false;
    if (!FileSpec::Match(match_module_spec.GetFileSpec(), GetFileSpec()))
      return false;
    if (GetPlatformFileSpec() &&
        !FileSpec::Match(match_module_spec.GetPlatformFileSpec(),
                         GetPlatformFileSpec())) {
      return false;
    }
    // Only match the symbol file spec if there is one in this ModuleSpec
    if (GetSymbolFileSpec() &&
        !FileSpec::Match(match_module_spec.GetSymbolFileSpec(),
                         GetSymbolFileSpec())) {
      return false;
    }
    if (match_module_spec.GetArchitecturePtr()) {
      if (exact_arch_match) {
        if (!GetArchitecture().IsExactMatch(
                match_module_spec.GetArchitecture()))
          return false;
      } else {
        if (!GetArchitecture().IsCompatibleMatch(
                match_module_spec.GetArchitecture()))
          return false;
      }
    }
    // Only match on load address if they both have a valid value.
    if (m_load_addr.has_value() && match_module_spec.m_load_addr.has_value() &&
        match_module_spec.GetLoadAddress() != GetLoadAddress())
      return false;
    return true;
  }

protected:
  FileSpec m_file;
  FileSpec m_platform_file;
  FileSpec m_symbol_file;
  ArchSpec m_arch;
  UUID m_uuid;
  ConstString m_object_name;
  /// The target used when resolving a module. A target can help locate a module
  /// specified by a ModuleSpec. The target settings, like the executable and
  /// debug info search paths, can be essential. The target's platform can also
  /// be used to locate or download the specified module.
  std::weak_ptr<Target> m_target_wp;
  /// The platform used when resolving a module. This is useful when a Target
  /// is not yet available (e.g., during target creation) but a Platform is.
  std::weak_ptr<Platform> m_platform_wp;
  uint64_t m_object_offset = 0;
  uint64_t m_object_size = 0;
  llvm::sys::TimePoint<> m_object_mod_time;
  mutable PathMappingList m_source_mappings;
  lldb::DataExtractorSP m_extractor_sp = {};
  /// The load address of the module in a process. This allows for modules
  /// to be uniquely identified and created by reading an object file from
  /// memory when we can't locate the correct file on disk. Useful for post
  /// mortem debugging when we might not be able to locate symbols for the
  /// core file, but we can read the object file from memory.
  std::optional<lldb::addr_t> m_load_addr = std::nullopt;
};

class ModuleSpecList {
public:
  ModuleSpecList() = default;

  ModuleSpecList(const ModuleSpecList &rhs) {
    std::lock_guard<std::recursive_mutex> lhs_guard(m_mutex);
    std::lock_guard<std::recursive_mutex> rhs_guard(rhs.m_mutex);
    m_specs = rhs.m_specs;
  }

  ~ModuleSpecList() = default;

  ModuleSpecList &operator=(const ModuleSpecList &rhs) {
    if (this != &rhs) {
      std::lock(m_mutex, rhs.m_mutex);
      std::lock_guard<std::recursive_mutex> lhs_guard(m_mutex, std::adopt_lock);
      std::lock_guard<std::recursive_mutex> rhs_guard(rhs.m_mutex,
                                                      std::adopt_lock);
      m_specs = rhs.m_specs;
    }
    return *this;
  }

  size_t GetSize() const {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    return m_specs.size();
  }

  void Clear() {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    m_specs.clear();
  }

  void Append(const ModuleSpec &spec) {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    m_specs.push_back(spec);
  }

  void Append(const ModuleSpecList &rhs) {
    std::lock_guard<std::recursive_mutex> lhs_guard(m_mutex);
    std::lock_guard<std::recursive_mutex> rhs_guard(rhs.m_mutex);
    m_specs.insert(m_specs.end(), rhs.m_specs.begin(), rhs.m_specs.end());
  }

  // The index "i" must be valid and this can't be used in multi-threaded code
  // as no mutex lock is taken.
  ModuleSpec &GetModuleSpecRefAtIndex(size_t i) { return m_specs[i]; }

  bool GetModuleSpecAtIndex(size_t i, ModuleSpec &module_spec) const {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    if (i < m_specs.size()) {
      module_spec = m_specs[i];
      return true;
    }
    module_spec.Clear();
    return false;
  }

  bool FindMatchingModuleSpec(const ModuleSpec &module_spec,
                              ModuleSpec &match_module_spec) const {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    bool exact_arch_match = true;
    for (auto spec : m_specs) {
      if (spec.Matches(module_spec, exact_arch_match)) {
        match_module_spec = spec;
        return true;
      }
    }

    // If there was an architecture, retry with a compatible arch
    if (module_spec.GetArchitecturePtr()) {
      exact_arch_match = false;
      for (auto spec : m_specs) {
        if (spec.Matches(module_spec, exact_arch_match)) {
          match_module_spec = spec;
          return true;
        }
      }
    }
    match_module_spec.Clear();
    return false;
  }

  void FindMatchingModuleSpecs(const ModuleSpec &module_spec,
                               ModuleSpecList &matching_list) const {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    bool exact_arch_match = true;
    const size_t initial_match_count = matching_list.GetSize();
    for (auto spec : m_specs) {
      if (spec.Matches(module_spec, exact_arch_match))
        matching_list.Append(spec);
    }

    // If there was an architecture, retry with a compatible arch if no matches
    // were found
    if (module_spec.GetArchitecturePtr() &&
        (initial_match_count == matching_list.GetSize())) {
      exact_arch_match = false;
      for (auto spec : m_specs) {
        if (spec.Matches(module_spec, exact_arch_match))
          matching_list.Append(spec);
      }
    }
  }

  void Dump(Stream &strm) {
    std::lock_guard<std::recursive_mutex> guard(m_mutex);
    uint32_t idx = 0;
    for (auto spec : m_specs) {
      strm.Printf("[%u] ", idx);
      spec.Dump(strm);
      strm.EOL();
      ++idx;
    }
  }

  typedef std::vector<ModuleSpec> collection;
  typedef LockingAdaptedIterable<std::recursive_mutex, collection>
      ModuleSpecIterable;

  ModuleSpecIterable ModuleSpecs() {
    return ModuleSpecIterable(m_specs, m_mutex);
  }

protected:
  collection m_specs;                         ///< The collection of modules.
  mutable std::recursive_mutex m_mutex;
};

} // namespace lldb_private

#endif // LLDB_CORE_MODULESPEC_H
