/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "system-precomp.h"  // Precompiled headers
//
#ifdef _WIN32
#ifdef _MSC_VER
#include <sys/utime.h>
#endif
#include <direct.h>
#include <io.h>
#include <windows.h>
#else
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
#include <utime.h>

#include <cerrno>
#include <cstring>
#include <ctime>
#endif

#include <mrpt/core/exceptions.h>
#include <mrpt/system/CDirectoryExplorer.h>
#include <mrpt/system/filesystem.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <algorithm>
#include <cstdio>
#include <iostream>
#include <queue>

#if STD_FS_IS_EXPERIMENTAL
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#else
#include <filesystem>
namespace fs = std::filesystem;
#endif

using namespace mrpt::system;
using namespace std;

/*---------------------------------------------------------------
    explore
 ---------------------------------------------------------------*/
CDirectoryExplorer::TFileInfoList CDirectoryExplorer::explore(
    const string& path, const unsigned long in_mask)
{
  MRPT_START

  unsigned long mask = in_mask;

  CDirectoryExplorer::TFileInfoList outList;

  // The path terminated in "/" or "\\"
  string searchPath(path);
  if (searchPath.size())
    if (searchPath[searchPath.size() - 1] != '\\' && searchPath[searchPath.size() - 1] != '/')
    {
#ifdef _WIN32
      searchPath += '\\';
#else
      searchPath.push_back('/');
#endif
    }

    // cout << "searchPath:"<<searchPath<<endl;

#ifdef _WIN32
  // ====================
  // WINDOWS VERSION
  // ====================
  WIN32_FIND_DATAA f;
  TFileInfo newEntry;

  string searchPath_mask = searchPath + string("*.*");

  HANDLE h = FindFirstFileA(searchPath_mask.c_str(), &f);
  if (h == INVALID_HANDLE_VALUE)
    THROW_EXCEPTION_FMT(
        "Error starting exploration of '%s' (does path exist?)", searchPath.c_str());

  // Include the FILE_ATTRIB_ARCHIVE flag for files:
  if (mask & FILE_ATTRIB_ARCHIVE) mask |= FILE_ATTRIBUTE_NORMAL;
  do
  {
    if ((mask & f.dwFileAttributes) != 0)  // Passes the user masks:
    {
      // File name:
      newEntry.name = string(f.cFileName);

      // Complete file path:
      newEntry.wholePath = searchPath;
      newEntry.wholePath += newEntry.name;

      // File size:
      newEntry.fileSize = ((uint64_t)f.nFileSizeLow) + (((uint64_t)f.nFileSizeHigh) << 32);

      // File times:
      struct stat statDat;
      if (stat(newEntry.wholePath.c_str(), &statDat))
      {
        FindClose(h);
        THROW_EXCEPTION_FMT("Cannot get stat for file: '%s'", newEntry.wholePath.c_str());
      }

      newEntry.modTime = mrpt::Clock::fromDouble(static_cast<double>(statDat.st_mtime));
      newEntry.accessTime = mrpt::Clock::fromDouble(static_cast<double>(statDat.st_atime));

      // Flags:
      newEntry.isDir = 0 != (statDat.st_mode & _S_IFDIR);
      newEntry.isSymLink = false;  // (We donnot look for this in Windows, by now...)

      // Save:
      outList.push_back(newEntry);
    }
  } while (FindNextFileA(h, &f));

  FindClose(h);  // Ignore possible errors..

// Done
#else
  // ====================
  // LINUX VERSION
  // ====================
  TFileInfo newEntry;
  struct dirent* ent;

  DIR* dir = opendir(searchPath.c_str());
  if (!dir)
    THROW_EXCEPTION_FMT(
        "Error starting exploration of '%s' (does path exist?)", searchPath.c_str());

  while ((ent = readdir(dir)) != nullptr)
  {
    if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue;

    // File name:
    newEntry.name = string(ent->d_name);

    // Complete absolute file path:
    newEntry.wholePath = fs::absolute(fs::path(searchPath + newEntry.name));

    // File times:
    struct stat statDat
    {
    }, lstatDat{};

    if (stat(newEntry.wholePath.c_str(), &statDat))
      continue;  // Ignore it: permissions problem or broken symlink?

    newEntry.modTime = mrpt::Clock::fromDouble(static_cast<double>(statDat.st_mtime));
    newEntry.accessTime = mrpt::Clock::fromDouble(static_cast<double>(statDat.st_atime));

    // Flags:
    newEntry.isDir = S_ISDIR(statDat.st_mode);

    if (((mask & FILE_ATTRIB_ARCHIVE) != 0 && !newEntry.isDir) ||
        ((mask & FILE_ATTRIB_DIRECTORY) != 0 && newEntry.isDir))
    {
      // File size:
      newEntry.fileSize = (intmax_t)statDat.st_size;

      // Is it a symbolic link?? Need to call "lstat":
      if (!lstat(newEntry.wholePath.c_str(), &lstatDat))
      {
        newEntry.isSymLink = S_ISLNK(lstatDat.st_mode);
      }
      else
        newEntry.isSymLink = false;

      // Save:
      outList.push_back(newEntry);
    }
  }

  closedir(dir);

// Done
#endif

  return outList;

  MRPT_END
}

// Auxiliary function to order by name, ascending
bool cmpFileEntriesName_Asc(
    const CDirectoryExplorer::TFileInfo& a, const CDirectoryExplorer::TFileInfo& b)
{
  return a.wholePath < b.wholePath;
}
bool cmpFileEntriesName_Desc(
    const CDirectoryExplorer::TFileInfo& a, const CDirectoryExplorer::TFileInfo& b)
{
  return a.wholePath > b.wholePath;
}

/*---------------------------------------------------------------
    sortByName
 ---------------------------------------------------------------*/
void CDirectoryExplorer::sortByName(TFileInfoList& lstFiles, bool ascendingOrder)
{
  std::sort(
      lstFiles.begin(), lstFiles.end(),
      ascendingOrder ? cmpFileEntriesName_Asc : cmpFileEntriesName_Desc);
}

/*---------------------------------------------------------------
    filterByExtension
 ---------------------------------------------------------------*/
void CDirectoryExplorer::filterByExtension(TFileInfoList& lstFiles, const std::string& extension)
{
  int i, n = (int)lstFiles.size();
  for (i = n - 1; i >= 0; i--)
  {
    if (0 != os::_strcmpi(
                 mrpt::system::extractFileExtension(lstFiles[i].name).c_str(), extension.c_str()))
    {
      // Does not match:
      lstFiles.erase(lstFiles.begin() + i);
    }
  }
}
