/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmFileMonitor.h"

#include "cmAlgorithms.h"
#include "cmsys/SystemTools.hxx"

#include <cassert>
#include <cstddef>
#include <unordered_map>
#include <utility>

namespace {
void on_directory_change(uv_fs_event_t* handle, const char* filename,
                         int events, int status);
void on_fs_close(uv_handle_t* handle);
} // namespace

class cmIBaseWatcher
{
public:
  virtual ~cmIBaseWatcher() = default;

  virtual void Trigger(const std::string& pathSegment, int events,
                       int status) const = 0;
  virtual std::string Path() const = 0;
  virtual uv_loop_t* Loop() const = 0;

  virtual void StartWatching() = 0;
  virtual void StopWatching() = 0;

  virtual std::vector<std::string> WatchedFiles() const = 0;
  virtual std::vector<std::string> WatchedDirectories() const = 0;
};

class cmVirtualDirectoryWatcher : public cmIBaseWatcher
{
public:
  ~cmVirtualDirectoryWatcher() override { cmDeleteAll(this->Children); }

  cmIBaseWatcher* Find(const std::string& ps)
  {
    const auto i = this->Children.find(ps);
    return (i == this->Children.end()) ? nullptr : i->second;
  }

  void Trigger(const std::string& pathSegment, int events,
               int status) const final
  {
    if (pathSegment.empty()) {
      for (auto const& child : this->Children) {
        child.second->Trigger(std::string(), events, status);
      }
    } else {
      const auto i = this->Children.find(pathSegment);
      if (i != this->Children.end()) {
        i->second->Trigger(std::string(), events, status);
      }
    }
  }

  void StartWatching() override
  {
    for (auto const& child : this->Children) {
      child.second->StartWatching();
    }
  }

  void StopWatching() override
  {
    for (auto const& child : this->Children) {
      child.second->StopWatching();
    }
  }

  std::vector<std::string> WatchedFiles() const final
  {
    std::vector<std::string> result;
    for (auto const& child : this->Children) {
      for (std::string const& f : child.second->WatchedFiles()) {
        result.push_back(f);
      }
    }
    return result;
  }

  std::vector<std::string> WatchedDirectories() const override
  {
    std::vector<std::string> result;
    for (auto const& child : this->Children) {
      for (std::string const& dir : child.second->WatchedDirectories()) {
        result.push_back(dir);
      }
    }
    return result;
  }

  void Reset()
  {
    cmDeleteAll(this->Children);
    this->Children.clear();
  }

  void AddChildWatcher(const std::string& ps, cmIBaseWatcher* watcher)
  {
    assert(!ps.empty());
    assert(this->Children.find(ps) == this->Children.end());
    assert(watcher);

    this->Children.emplace(std::make_pair(ps, watcher));
  }

private:
  std::unordered_map<std::string, cmIBaseWatcher*> Children; // owned!
};

// Root of all the different (on windows!) root directories:
class cmRootWatcher : public cmVirtualDirectoryWatcher
{
public:
  cmRootWatcher(uv_loop_t* loop)
    : mLoop(loop)
  {
    assert(loop);
  }

  std::string Path() const final
  {
    assert(false);
    return std::string();
  }
  uv_loop_t* Loop() const final { return this->mLoop; }

private:
  uv_loop_t* const mLoop; // no ownership!
};

// Real directories:
class cmRealDirectoryWatcher : public cmVirtualDirectoryWatcher
{
public:
  cmRealDirectoryWatcher(cmVirtualDirectoryWatcher* p, const std::string& ps)
    : Parent(p)
    , PathSegment(ps)
  {
    assert(p);
    assert(!ps.empty());

    p->AddChildWatcher(ps, this);
  }

  void StartWatching() final
  {
    if (!this->Handle) {
      this->Handle = new uv_fs_event_t;

      uv_fs_event_init(this->Loop(), this->Handle);
      this->Handle->data = this;
      uv_fs_event_start(this->Handle, &on_directory_change, Path().c_str(), 0);
    }
    cmVirtualDirectoryWatcher::StartWatching();
  }

  void StopWatching() final
  {
    if (this->Handle) {
      uv_fs_event_stop(this->Handle);
      if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(this->Handle))) {
        uv_close(reinterpret_cast<uv_handle_t*>(this->Handle), &on_fs_close);
      }
      this->Handle = nullptr;
    }
    cmVirtualDirectoryWatcher::StopWatching();
  }

  uv_loop_t* Loop() const final { return this->Parent->Loop(); }

  std::vector<std::string> WatchedDirectories() const override
  {
    std::vector<std::string> result = { Path() };
    for (std::string const& dir :
         cmVirtualDirectoryWatcher::WatchedDirectories()) {
      result.push_back(dir);
    }
    return result;
  }

protected:
  cmVirtualDirectoryWatcher* const Parent;
  const std::string PathSegment;

private:
  uv_fs_event_t* Handle = nullptr; // owner!
};

// Root directories:
class cmRootDirectoryWatcher : public cmRealDirectoryWatcher
{
public:
  cmRootDirectoryWatcher(cmRootWatcher* p, const std::string& ps)
    : cmRealDirectoryWatcher(p, ps)
  {
  }

  std::string Path() const final { return this->PathSegment; }
};

// Normal directories below root:
class cmDirectoryWatcher : public cmRealDirectoryWatcher
{
public:
  cmDirectoryWatcher(cmRealDirectoryWatcher* p, const std::string& ps)
    : cmRealDirectoryWatcher(p, ps)
  {
  }

  std::string Path() const final
  {
    return this->Parent->Path() + this->PathSegment + "/";
  }
};

class cmFileWatcher : public cmIBaseWatcher
{
public:
  cmFileWatcher(cmRealDirectoryWatcher* p, const std::string& ps,
                cmFileMonitor::Callback cb)
    : Parent(p)
    , PathSegment(ps)
    , CbList({ std::move(cb) })
  {
    assert(p);
    assert(!ps.empty());
    p->AddChildWatcher(ps, this);
  }

  void StartWatching() final {}

  void StopWatching() final {}

  void AppendCallback(cmFileMonitor::Callback const& cb)
  {
    this->CbList.push_back(cb);
  }

  std::string Path() const final
  {
    return this->Parent->Path() + this->PathSegment;
  }

  std::vector<std::string> WatchedDirectories() const final { return {}; }

  std::vector<std::string> WatchedFiles() const final
  {
    return { this->Path() };
  }

  void Trigger(const std::string& ps, int events, int status) const final
  {
    assert(ps.empty());
    assert(status == 0);
    static_cast<void>(ps);

    const std::string path = this->Path();
    for (cmFileMonitor::Callback const& cb : this->CbList) {
      cb(path, events, status);
    }
  }

  uv_loop_t* Loop() const final { return this->Parent->Loop(); }

private:
  cmRealDirectoryWatcher* Parent;
  const std::string PathSegment;
  std::vector<cmFileMonitor::Callback> CbList;
};

namespace {

void on_directory_change(uv_fs_event_t* handle, const char* filename,
                         int events, int status)
{
  const cmIBaseWatcher* const watcher =
    static_cast<const cmIBaseWatcher*>(handle->data);
  const std::string pathSegment(filename ? filename : "");
  watcher->Trigger(pathSegment, events, status);
}

void on_fs_close(uv_handle_t* handle)
{
  delete reinterpret_cast<uv_fs_event_t*>(handle);
}

} // namespace

cmFileMonitor::cmFileMonitor(uv_loop_t* l)
  : Root(new cmRootWatcher(l))
{
}

cmFileMonitor::~cmFileMonitor()
{
  delete this->Root;
}

void cmFileMonitor::MonitorPaths(const std::vector<std::string>& paths,
                                 Callback const& cb)
{
  for (std::string const& p : paths) {
    std::vector<std::string> pathSegments;
    cmsys::SystemTools::SplitPath(p, pathSegments, true);
    const bool pathIsFile = !cmsys::SystemTools::FileIsDirectory(p);

    const size_t segmentCount = pathSegments.size();
    if (segmentCount < 2) { // Expect at least rootdir and filename
      continue;
    }
    cmVirtualDirectoryWatcher* currentWatcher = this->Root;
    for (size_t i = 0; i < segmentCount; ++i) {
      assert(currentWatcher);

      const bool fileSegment = (i == segmentCount - 1 && pathIsFile);
      const bool rootSegment = (i == 0);
      assert(
        !(fileSegment &&
          rootSegment)); // Can not be both filename and root part of the path!

      const std::string& currentSegment = pathSegments[i];
      if (currentSegment.empty()) {
        continue;
      }

      cmIBaseWatcher* nextWatcher = currentWatcher->Find(currentSegment);
      if (!nextWatcher) {
        if (rootSegment) { // Root part
          assert(currentWatcher == this->Root);
          nextWatcher = new cmRootDirectoryWatcher(this->Root, currentSegment);
          assert(currentWatcher->Find(currentSegment) == nextWatcher);
        } else if (fileSegment) { // File part
          assert(currentWatcher != this->Root);
          nextWatcher = new cmFileWatcher(
            dynamic_cast<cmRealDirectoryWatcher*>(currentWatcher),
            currentSegment, cb);
          assert(currentWatcher->Find(currentSegment) == nextWatcher);
        } else { // Any normal directory in between
          nextWatcher = new cmDirectoryWatcher(
            dynamic_cast<cmRealDirectoryWatcher*>(currentWatcher),
            currentSegment);
          assert(currentWatcher->Find(currentSegment) == nextWatcher);
        }
      } else {
        if (fileSegment) {
          auto filePtr = dynamic_cast<cmFileWatcher*>(nextWatcher);
          assert(filePtr);
          filePtr->AppendCallback(cb);
          continue;
        }
      }
      currentWatcher = dynamic_cast<cmVirtualDirectoryWatcher*>(nextWatcher);
    }
  }
  this->Root->StartWatching();
}

void cmFileMonitor::StopMonitoring()
{
  this->Root->StopWatching();
  this->Root->Reset();
}

std::vector<std::string> cmFileMonitor::WatchedFiles() const
{
  std::vector<std::string> result;
  if (this->Root) {
    result = this->Root->WatchedFiles();
  }
  return result;
}

std::vector<std::string> cmFileMonitor::WatchedDirectories() const
{
  std::vector<std::string> result;
  if (this->Root) {
    result = this->Root->WatchedDirectories();
  }
  return result;
}
