// Aseprite
// Copyright (C) 2019-2022  Igara Studio S.A.
// Copyright (C) 2001-2018  David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

/* Some of the original code to handle PIDLs come from the
   MiniExplorer example of the Vaca library:
   https://github.com/dacap/vaca
   Copyright (C) by David Capello (MIT License)
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "app/file_system.h"

#include "base/fs.h"
#include "base/string.h"
#include "os/surface.h"
#include "os/system.h"
#include "os/window.h"

#include <algorithm>
#include <cstdio>
#include <map>
#include <utility>
#include <vector>

#ifdef _WIN32
  #include "base/win/comptr.h"

  #include <windows.h>

  #include <shlobj.h>
  #include <shlwapi.h>

  #define MYPC_CSLID  "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}"
#else
  #include <dirent.h>
  #include <sys/stat.h>
#endif

//////////////////////////////////////////////////////////////////////

#ifndef MAX_PATH
  #define MAX_PATH 4096
#endif

#define NOTINITIALIZED  "{*empty*}"

#define FS_TRACE(...)           // TRACE

namespace app {

namespace {

class FileItem;
using FileItemMap = std::map<std::string, FileItem*>;

// the root of the file-system
FileItem* rootitem = nullptr;
FileItemMap* fileitems_map = nullptr;
unsigned int current_file_system_version = 0;

#ifdef _WIN32
  base::ComPtr<IMalloc> shl_imalloc;
  base::ComPtr<IShellFolder> shl_idesktop;
#endif

// a position in the file-system
class FileItem final : public IFileItem {
public:
  // TODO make all these fields private
  std::string m_keyname;
  std::string m_filename;
  std::string m_displayname;
  FileItem* m_parent;
  FileItemList m_children;
  unsigned int m_version;
  bool m_removed;
  mutable bool m_is_folder;
  std::atomic<double> m_thumbnailProgress;
  std::atomic<os::Surface*> m_thumbnail;
#ifdef _WIN32
  LPITEMIDLIST m_pidl;            // relative to parent
  LPITEMIDLIST m_fullpidl;        // relative to the Desktop folder
                                  // (like a full path-name, because the
                                  // desktop is the root on Windows)
#endif

  FileItem(FileItem* parent);
  ~FileItem();

  void insertChildSorted(FileItem* child);
  int compare(const FileItem& that) const;

  bool operator<(const FileItem& that) const { return compare(that) < 0; }
  bool operator>(const FileItem& that) const { return compare(that) > 0; }
  bool operator==(const FileItem& that) const { return compare(that) == 0; }
  bool operator!=(const FileItem& that) const { return compare(that) != 0; }

  // IFileItem impl
  bool isFolder() const override;
  bool isBrowsable() const override;
  bool isHidden() const override;
  bool isExistent() const override;

  const std::string& keyName() const override;
  const std::string& fileName() const override;
  const std::string& displayName() const override;

  IFileItem* parent() const override;
  const FileItemList& children() override;
  void createDirectory(const std::string& dirname) override;

  bool hasExtension(const base::paths& extensions) override;

  double getThumbnailProgress() override { return m_thumbnailProgress; }
  void setThumbnailProgress(double progress) override {
    m_thumbnailProgress = progress;
  }

  bool needThumbnail() const override {
    return
      !isBrowsable() &&
      m_thumbnail == nullptr &&
      m_thumbnailProgress < 1.0;
  }

  os::SurfaceRef getThumbnail() override;
  void setThumbnail(const os::SurfaceRef& thumbnail) override;

  // Calls "delete this"
  void deleteItem() {
    FileSystemModule::instance()->ItemRemoved(this);

    if (m_parent) {
      auto& container = m_parent->m_children;
      auto it = std::find(container.begin(), container.end(), this);
      if (it != container.end())
        container.erase(it);
    }

    auto it = fileitems_map->find(m_keyname);
    if (it != fileitems_map->end())
      fileitems_map->erase(it);

    // Delete all children recursively
    for (auto ichild : m_children) {
      FileItem* child = static_cast<FileItem*>(ichild);
      child->m_parent = nullptr;
      child->deleteItem();
    }

    delete this;
  }
};

} // anonymous namespace

// A more easy PIDLs interface (without using the SH* & IL* routines of W2K)
#ifdef _WIN32
  static SFGAOF get_pidl_attrib(FileItem* fileitem, SFGAOF attrib);
  static void update_by_pidl(FileItem* fileitem, SFGAOF attrib);
  static LPITEMIDLIST concat_pidl(LPITEMIDLIST pidlHead, LPITEMIDLIST pidlTail);
  static UINT get_pidl_size(LPITEMIDLIST pidl);
  static LPITEMIDLIST get_next_pidl(LPITEMIDLIST pidl);
  static LPITEMIDLIST get_last_pidl(LPITEMIDLIST pidl);
  static LPITEMIDLIST clone_pidl(LPITEMIDLIST pidl);
  static LPITEMIDLIST remove_last_pidl(LPITEMIDLIST pidl);
  static void free_pidl(LPITEMIDLIST pidl);
  static std::string get_key_for_pidl(LPITEMIDLIST pidl);

  static FileItem* get_fileitem_by_fullpidl(LPITEMIDLIST pidl, bool create_if_not);
  static void put_fileitem(FileItem* fileitem);
#else
  static FileItem* get_fileitem_by_path(const std::string& path, bool create_if_not);
  static std::string remove_backslash_if_needed(const std::string& filename);
  static std::string get_key_for_filename(const std::string& filename);
  static void put_fileitem(FileItem* fileitem);
#endif

FileSystemModule* FileSystemModule::m_instance = nullptr;

FileSystemModule::FileSystemModule()
{
  ASSERT(m_instance == NULL);
  m_instance = this;

  fileitems_map = new FileItemMap;

#ifdef _WIN32
  /* get the IMalloc interface */
  HRESULT hr = SHGetMalloc(&shl_imalloc);
  if (hr != S_OK)
    throw std::runtime_error("Error initializing file system. Report this problem. (SHGetMalloc failed.)");

  /* get desktop IShellFolder interface */
  hr = SHGetDesktopFolder(&shl_idesktop);
  if (hr != S_OK)
    throw std::runtime_error("Error initializing file system. Report this problem. (SHGetDesktopFolder failed.)");
#endif

  // first version of the file system
  ++current_file_system_version;

  // get the root element of the file system (this will create
  // the 'rootitem' FileItem)
  getRootFileItem();
}

FileSystemModule::~FileSystemModule()
{
  ASSERT(m_instance == this);

  for (auto it=fileitems_map->begin(); it!=fileitems_map->end(); ++it) {
    delete it->second;
  }
  fileitems_map->clear();

#ifdef _WIN32
  // Release interfaces
  shl_idesktop.reset();
  shl_imalloc.reset();
#endif

  delete fileitems_map;

  m_instance = nullptr;
}

FileSystemModule* FileSystemModule::instance()
{
  return m_instance;
}

void FileSystemModule::refresh()
{
  ++current_file_system_version;
}

IFileItem* FileSystemModule::getRootFileItem()
{
  FileItem* fileitem;

  if (rootitem)
    return rootitem;

  fileitem = new FileItem(NULL);
  rootitem = fileitem;

  //LOG("FS: Creating root fileitem %p\n", rootitem);

#ifdef _WIN32
  {
    // get the desktop PIDL
    LPITEMIDLIST pidl = NULL;
    HRESULT hr = SHGetSpecialFolderLocation(NULL, CSIDL_DESKTOP, &pidl);
    if (hr != S_OK) {
      // TODO do something better
      ASSERT(false);
      exit(1);
    }
    fileitem->m_pidl = pidl;
    fileitem->m_fullpidl = pidl;

    SFGAOF attrib = SFGAO_FOLDER;
    shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST *)&pidl, &attrib);

    update_by_pidl(fileitem, attrib);
  }
#else
  {
    const char* root = "/";

    fileitem->m_filename = root;
    fileitem->m_displayname = root;
    fileitem->m_is_folder = true;
  }
#endif

  // insert the file-item in the hash-table
  put_fileitem(fileitem);
  return fileitem;
}

IFileItem* FileSystemModule::getFileItemFromPath(const std::string& path)
{
  IFileItem* fileitem = NULL;

  //LOG("FS: get_fileitem_from_path(%s)\n", path.c_str());

#ifdef _WIN32
  {
    ULONG cbEaten = 0UL;
    LPITEMIDLIST fullpidl = NULL;
    SFGAOF attrib = SFGAO_FOLDER;

    // Default folder is desktop folder (the root item in the hierarchy)
    if (path.empty()) {
      fileitem = getRootFileItem();
      //LOG("FS: > %p (root)\n", fileitem);
      return fileitem;
    }

    if (shl_idesktop->ParseDisplayName
          (NULL, NULL,
           const_cast<LPWSTR>(base::from_utf8(path).c_str()),
           &cbEaten, &fullpidl, &attrib) != S_OK) {
      //LOG("FS: > (null)\n");
      return NULL;
    }

    fileitem = get_fileitem_by_fullpidl(fullpidl, true);
    free_pidl(fullpidl);
  }
#else
  {
    // The default folder is the user home folder
    if (path.empty()) {
      fileitem = get_fileitem_by_path(base::get_user_docs_folder(), true);
    }
    else {
      std::string buf = remove_backslash_if_needed(path);
      fileitem = get_fileitem_by_path(buf, true);
    }
  }
#endif

  //LOG("FS: get_fileitem_from_path(%s) -> %p\n", path.c_str(), fileitem);

  return fileitem;
}

// ======================================================================
// FileItem class (IFileItem implementation)
// ======================================================================

bool FileItem::isFolder() const
{
  return m_is_folder;
}

bool FileItem::isBrowsable() const
{
  ASSERT(m_filename != NOTINITIALIZED);

  return m_is_folder;
}

bool FileItem::isHidden() const
{
  ASSERT(m_displayname != NOTINITIALIZED);

#ifdef _WIN32
  return false;
#else
  return m_displayname[0] == '.';
#endif
}

bool FileItem::isExistent() const
{
  const std::string& fn = fileName();

#ifdef _WIN32
  if (!fn.empty() && fn.front() == ':') { // It's a PIDL of a special location
    FS_TRACE("FS: isExistent() %s -> PIDL exists\n", fn.c_str());
    return true;
  }
#endif

  bool result = false;

  if (base::is_directory(fn)) {
    result = true;
    if (!m_is_folder)
      m_is_folder = true;       // Update the "is folder" flag
  }
  else if (base::is_file(fn)) {
    result = true;
    if (m_is_folder)
      m_is_folder = false;
  }

  FS_TRACE("FS: isExistent() %s -> %s\n", fn.c_str(), (result ? "exists": "DOESN'T EXIST"));
  return result;
}

const std::string& FileItem::keyName() const
{
  ASSERT(m_keyname != NOTINITIALIZED);

  return m_keyname;
}

const std::string& FileItem::fileName() const
{
  ASSERT(m_filename != NOTINITIALIZED);

  return m_filename;
}

const std::string& FileItem::displayName() const
{
  ASSERT(m_displayname != NOTINITIALIZED);

  return m_displayname;
}

IFileItem* FileItem::parent() const
{
  if (this == rootitem)
    return NULL;
  else {
    ASSERT(m_parent);
    return m_parent;
  }
}

const FileItemList& FileItem::children()
{
  // Is the file-item a folder?
  if (isFolder() &&
      // if the children list is empty, or the file-system version
      // change (it's like to say: the current m_children list
      // is outdated)...
      (m_children.empty() ||
       current_file_system_version > m_version)) {
    FileItemList::iterator it;
    FileItem* child;

    // we have to mark current items as deprecated
    for (it=m_children.begin();
         it!=m_children.end(); ++it) {
      child = static_cast<FileItem*>(*it);
      child->m_removed = true;
    }

    //LOG("FS: Loading files for %p (%s)\n", fileitem, fileitem->displayname);
#ifdef _WIN32
    {
      base::ComPtr<IShellFolder> pFolder;
      HRESULT hr;

      if (this == rootitem) {
        pFolder = shl_idesktop;
      }
      else {
        hr = shl_idesktop->BindToObject(
          m_fullpidl, nullptr,
          IID_IShellFolder, (LPVOID *)&pFolder);

        if (hr != S_OK)
          pFolder = nullptr;
      }

      if (pFolder) {
        base::ComPtr<IEnumIDList> pEnum;
        ULONG c, fetched;

        // Get the interface to enumerate subitems
        hr = pFolder->EnumObjects(
          reinterpret_cast<HWND>(os::instance()->defaultWindow()->nativeHandle()),
          SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnum);

        if (hr == S_OK && pEnum) {
          LPITEMIDLIST itempidl[256];
          SFGAOF attribs[256];

          // Enumerate the items in the folder
          while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
            // Request the SFGAO_FOLDER attribute to know what of the
            // item is file or a folder
            for (c=0; c<fetched; ++c) {
              attribs[c] = SFGAO_FOLDER;
              pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs+c);
            }

            // Generate the FileItems
            for (c=0; c<fetched; ++c) {
              LPITEMIDLIST fullpidl = concat_pidl(m_fullpidl,
                                                  itempidl[c]);

              child = get_fileitem_by_fullpidl(fullpidl, false);
              if (!child) {
                child = new FileItem(this);

                child->m_pidl = itempidl[c];
                child->m_fullpidl = fullpidl;

                update_by_pidl(child, attribs[c]);
                put_fileitem(child);
              }
              else {
                ASSERT(child->m_parent == this);
                free_pidl(fullpidl);
                free_pidl(itempidl[c]);
              }

              insertChildSorted(child);
            }
          }
        }
      }
    }
#else
    {
      DIR* dir = opendir(m_filename.c_str());
      if (dir) {
        dirent* entry;
        while ((entry = readdir(dir)) != NULL) {
          FileItem* child;
          std::string fn = entry->d_name;
          std::string fullfn = base::join_path(m_filename, fn);

          if (fn == "." || fn == "..")
            continue;

          child = get_fileitem_by_path(fullfn, false);
          if (!child) {
            child = new FileItem(this);

            bool is_folder;
            struct stat fileStat;

            stat(fullfn.c_str(), &fileStat);

            if ((fileStat.st_mode & S_IFMT) == S_IFLNK) {
              is_folder = base::is_directory(fullfn);
            }
            else {
              is_folder = ((fileStat.st_mode & S_IFMT) == S_IFDIR);
            }

            child->m_filename = fullfn;
            child->m_displayname = fn;
            child->m_is_folder = is_folder;

            put_fileitem(child);
          }
          else {
            ASSERT(child->m_parent == this);
          }

          insertChildSorted(child);
        }
        closedir(dir);
      }
    }
#endif

    // check old file-items (maybe removed directories or file-items)
    for (it=m_children.begin();
         it!=m_children.end(); ) {
      child = static_cast<FileItem*>(*it);
      ASSERT(child);

      if (child && child->m_removed) {
        it = m_children.erase(it);
        child->m_parent = nullptr;
        child->deleteItem();
      }
      else
        ++it;
    }

    // now this file-item is updated
    m_version = current_file_system_version;
  }

  return m_children;
}

void FileItem::createDirectory(const std::string& dirname)
{
  base::make_directory(base::join_path(m_filename, dirname));

  // Invalidate the children list.
  m_version = 0;
}

bool FileItem::hasExtension(const base::paths& extensions)
{
  ASSERT(m_filename != NOTINITIALIZED);

  return base::has_file_extension(m_filename, extensions);
}

os::SurfaceRef FileItem::getThumbnail()
{
  os::SurfaceRef ref(m_thumbnail.load());
  if (ref)
    ref->ref();        // base::Ref(T*) doesn't add an extra reference
  return ref;
}

void FileItem::setThumbnail(const os::SurfaceRef& newThumbnail)
{
  m_thumbnailProgress = 1.0;

  if (newThumbnail)
    newThumbnail->ref();
  auto old = m_thumbnail.exchange(newThumbnail.get());
  if (old)
    old->unref();
}

FileItem::FileItem(FileItem* parent)
{
  FS_TRACE("FS: Creating %p fileitem with parent %p\n", this, parent);

  m_keyname = NOTINITIALIZED;
  m_filename = NOTINITIALIZED;
  m_displayname = NOTINITIALIZED;
  m_parent = parent;
  m_version = current_file_system_version;
  m_removed = false;
  m_is_folder = false;
  m_thumbnailProgress = 0.0;
  m_thumbnail = nullptr;
#ifdef _WIN32
  m_pidl = NULL;
  m_fullpidl = NULL;
#endif
}

FileItem::~FileItem()
{
  FS_TRACE("FS: Destroying FileItem() with parent %p\n", m_parent);

  m_thumbnail.exchange(nullptr);

#ifdef _WIN32
  if (m_fullpidl && m_fullpidl != m_pidl) {
    free_pidl(m_fullpidl);
    m_fullpidl = NULL;
  }

  if (m_pidl) {
    free_pidl(m_pidl);
    m_pidl = NULL;
  }
#endif
}

void FileItem::insertChildSorted(FileItem* child)
{
  // this file-item wasn't removed from the last lookup
  child->m_removed = false;

  // if the fileitem is already in the list we can go back
  if (std::find(m_children.begin(), m_children.end(), child) != m_children.end())
    return;

  for (auto it=m_children.begin(), end=m_children.end(); it!=end; ++it) {
    if (*child < *static_cast<FileItem*>(*it)) {
      m_children.insert(it, child);
      return;
    }
  }

  m_children.push_back(child);
}

int FileItem::compare(const FileItem& that) const
{
  if (isFolder()) {
    if (!that.isFolder())
      return -1;
  }
  else if (that.isFolder())
    return 1;

  return base::compare_filenames(m_displayname, that.m_displayname);
}

//////////////////////////////////////////////////////////////////////
// PIDLS: Only for Win32
//////////////////////////////////////////////////////////////////////

#ifdef _WIN32

static bool calc_is_folder(std::string filename, SFGAOF attrib)
{
  return ((attrib & SFGAO_FOLDER) == SFGAO_FOLDER)
    && (base::get_file_extension(filename) != "zip")
    && ((!filename.empty() && (*filename.begin()) != ':') || (filename == MYPC_CSLID));
}

static SFGAOF get_pidl_attrib(FileItem* fileitem, SFGAOF attrib)
{
  ASSERT(fileitem->m_pidl);
  ASSERT(fileitem->m_parent);

  HRESULT hr;

  base::ComPtr<IShellFolder> pFolder;
  if (fileitem->m_parent == rootitem)
    pFolder = shl_idesktop;
  else {
    hr = shl_idesktop->BindToObject(fileitem->m_parent->m_fullpidl,
                                    nullptr, IID_IShellFolder, (LPVOID*)&pFolder);
    if (hr != S_OK)
      pFolder = nullptr;
  }

  if (pFolder) {
    SFGAOF attrib2 = SFGAO_FOLDER;
    hr = pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)&fileitem->m_pidl, &attrib2);
    if (hr == S_OK)
      attrib = attrib2;
  }
  return attrib;
}

// Updates the names of the file-item through its PIDL
static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
{
  STRRET strret;
  WCHAR pszName[MAX_PATH];
  base::ComPtr<IShellFolder> pFolder;
  HRESULT hr;

  if (fileitem == rootitem)
    pFolder = shl_idesktop;
  else {
    ASSERT(fileitem->m_parent);
    hr = shl_idesktop->BindToObject(fileitem->m_parent->m_fullpidl,
                                    nullptr, IID_IShellFolder, (LPVOID*)&pFolder);
    if (hr != S_OK)
      pFolder = nullptr;
  }

  // Get the file name

  if (pFolder &&
      pFolder->GetDisplayNameOf(fileitem->m_pidl,
                                SHGDN_NORMAL | SHGDN_FORPARSING,
                                &strret) == S_OK) {
    StrRetToBuf(&strret, fileitem->m_pidl, pszName, MAX_PATH);
    fileitem->m_filename = base::to_utf8(pszName);
  }
  else if (shl_idesktop->GetDisplayNameOf(fileitem->m_fullpidl,
                                          SHGDN_NORMAL | SHGDN_FORPARSING,
                                          &strret) == S_OK) {
    StrRetToBuf(&strret, fileitem->m_fullpidl, pszName, MAX_PATH);
    fileitem->m_filename = base::to_utf8(pszName);
  }
  else
    fileitem->m_filename = "ERR";

  // Is it a folder?

  fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);

  // Get the name to display

  if (fileitem->isFolder() &&
      pFolder &&
      pFolder->GetDisplayNameOf(fileitem->m_pidl,
                                SHGDN_INFOLDER,
                                &strret) == S_OK) {
    StrRetToBuf(&strret, fileitem->m_pidl, pszName, MAX_PATH);
    fileitem->m_displayname = base::to_utf8(pszName);
  }
  else if (fileitem->isFolder() &&
           shl_idesktop->GetDisplayNameOf(fileitem->m_fullpidl,
                                          SHGDN_INFOLDER,
                                          &strret) == S_OK) {
    StrRetToBuf(&strret, fileitem->m_fullpidl, pszName, MAX_PATH);
    fileitem->m_displayname = base::to_utf8(pszName);
  }
  else {
    fileitem->m_displayname = base::get_file_name(fileitem->m_filename);
  }
}

static LPITEMIDLIST concat_pidl(LPITEMIDLIST pidlHead, LPITEMIDLIST pidlTail)
{
  LPITEMIDLIST pidlNew;
  UINT cb1, cb2;

  ASSERT(pidlHead);
  ASSERT(pidlTail);

  cb1 = get_pidl_size(pidlHead) - sizeof(pidlHead->mkid.cb);
  cb2 = get_pidl_size(pidlTail);

  pidlNew = (LPITEMIDLIST)shl_imalloc->Alloc(cb1 + cb2);
  if (pidlNew) {
    CopyMemory(pidlNew, pidlHead, cb1);
    CopyMemory(((LPSTR)pidlNew) + cb1, pidlTail, cb2);
  }

  return pidlNew;
}

static UINT get_pidl_size(LPITEMIDLIST pidl)
{
  UINT cbTotal = 0;

  if (pidl) {
    cbTotal += sizeof(pidl->mkid.cb); /* null terminator */

    while (pidl) {
      cbTotal += pidl->mkid.cb;
      pidl = get_next_pidl(pidl);
    }
  }

  return cbTotal;
}

static LPITEMIDLIST get_next_pidl(LPITEMIDLIST pidl)
{
  if (pidl != NULL && pidl->mkid.cb > 0) {
    pidl = (LPITEMIDLIST)(((LPBYTE)(pidl)) + pidl->mkid.cb);
    if (pidl->mkid.cb > 0)
      return pidl;
  }

  return NULL;
}

static LPITEMIDLIST get_last_pidl(LPITEMIDLIST pidl)
{
  LPITEMIDLIST pidlLast = pidl;
  LPITEMIDLIST pidlNew = NULL;

  while (pidl) {
    pidlLast = pidl;
    pidl = get_next_pidl(pidl);
  }

  if (pidlLast) {
    ULONG sz = get_pidl_size(pidlLast);
    pidlNew = (LPITEMIDLIST)shl_imalloc->Alloc(sz);
    CopyMemory(pidlNew, pidlLast, sz);
  }

  return pidlNew;
}

static LPITEMIDLIST clone_pidl(LPITEMIDLIST pidl)
{
  ULONG sz = get_pidl_size(pidl);
  LPITEMIDLIST pidlNew = (LPITEMIDLIST)shl_imalloc->Alloc(sz);

  CopyMemory(pidlNew, pidl, sz);

  return pidlNew;
}

static LPITEMIDLIST remove_last_pidl(LPITEMIDLIST pidl)
{
  LPITEMIDLIST pidlFirst = pidl;
  LPITEMIDLIST pidlLast = pidl;

  while (pidl) {
    pidlLast = pidl;
    pidl = get_next_pidl(pidl);
  }

  if (pidlLast)
    pidlLast->mkid.cb = 0;

  return pidlFirst;
}

static void free_pidl(LPITEMIDLIST pidl)
{
  shl_imalloc->Free(pidl);
}

static std::string get_key_for_pidl(LPITEMIDLIST pidl)
{
#if 0
  char *key = base_malloc(get_pidl_size(pidl)+1);
  UINT c, i = 0;

  while (pidl) {
    for (c=0; c<pidl->mkid.cb; ++c) {
      if (pidl->mkid.abID[c])
        key[i++] = pidl->mkid.abID[c];
      else
        key[i++] = 1;
    }
    pidl = get_next_pidl(pidl);
  }
  key[i] = 0;

  return key;
#else
  STRRET strret;
  WCHAR pszName[MAX_PATH];
  WCHAR key[4096] = { 0 };
  int len;

  // Go pidl by pidl from the fullpidl to the root (desktop)
  //LOG("FS: ***\n");
  pidl = clone_pidl(pidl);
  while (pidl->mkid.cb > 0) {
    if (shl_idesktop->GetDisplayNameOf(pidl,
                                       SHGDN_INFOLDER | SHGDN_FORPARSING,
                                       &strret) == S_OK) {
      if (StrRetToBuf(&strret, pidl, pszName, MAX_PATH) != S_OK)
        pszName[0] = 0;

      //LOG("FS: + %s\n", pszName);

      len = wcslen(pszName);
      if (len > 0) {
        if (*key) {
          if (pszName[len-1] != L'\\') {
            memmove(key+len+1, key, sizeof(WCHAR)*(wcslen(key)+1));
            key[len] = L'\\';
          }
          else
            memmove(key+len, key, sizeof(WCHAR)*(wcslen(key)+1));
        }
        else
          key[len] = 0;

        memcpy(key, pszName, sizeof(WCHAR)*len);
      }
    }
    remove_last_pidl(pidl);
  }
  free_pidl(pidl);

  //LOG("FS: =%s\n***\n", key);
  return base::to_utf8(key);
#endif
}

static FileItem* get_fileitem_by_fullpidl(LPITEMIDLIST fullpidl, bool create_if_not)
{
  auto key = get_key_for_pidl(fullpidl);
  auto it = fileitems_map->find(key);
  if (it != fileitems_map->end()) {
    FileItem* item = it->second;
    if (item->isExistent())
      return item;
    else {
      item->deleteItem();
      return nullptr;
    }
  }

  if (!create_if_not)
    return nullptr;

  // Validate if the fullpidl exists.
  SFGAOF attrib = SFGAO_FOLDER | SFGAO_VALIDATE;
  HRESULT hr = shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&fullpidl, &attrib);
  if (hr != S_OK)
    return nullptr;

  // new file-item
  auto fileitem = std::make_unique<FileItem>(nullptr);
  fileitem->m_fullpidl = clone_pidl(fullpidl);

  {
    LPITEMIDLIST parent_fullpidl = clone_pidl(fileitem->m_fullpidl);
    remove_last_pidl(parent_fullpidl);

    fileitem->m_pidl = get_last_pidl(fileitem->m_fullpidl);
    fileitem->m_parent = get_fileitem_by_fullpidl(parent_fullpidl, true);

    free_pidl(parent_fullpidl);

    // The parent folder is sometimes deleted for some reasons. In
    // that case, m_parent becomes nullptr, so we cannot use it
    // anymore. Here we just return nullptr to indicate that the item
    // doesn't exist anymore.
    if (fileitem->m_parent == nullptr)
      return nullptr;

    // Get specific pidl attributes
    if (fileitem->m_pidl &&
        fileitem->m_parent) {
      attrib = get_pidl_attrib(fileitem.get(), attrib);
    }
  }

  update_by_pidl(fileitem.get(), attrib);
  put_fileitem(fileitem.get());

  //LOG("FS: fileitem %p created %s with parent %p\n", fileitem, fileitem->keyname.c_str(), fileitem->parent);

  return fileitem.release();
}

// Inserts the fileitem in the hash map of items.
static void put_fileitem(FileItem* fileitem)
{
  ASSERT(fileitem->m_filename != NOTINITIALIZED);
  ASSERT(fileitem->m_keyname == NOTINITIALIZED);

  fileitem->m_keyname = get_key_for_pidl(fileitem->m_fullpidl);

  ASSERT(fileitem->m_keyname != NOTINITIALIZED);

#ifdef _DEBUG
  auto it = fileitems_map->find(get_key_for_pidl(fileitem->m_fullpidl));
  ASSERT(it == fileitems_map->end());
#endif

  // insert this file-item in the hash-table
  fileitems_map->insert(std::make_pair(fileitem->m_keyname, fileitem));
}

#else

//////////////////////////////////////////////////////////////////////
// POSIX functions
//////////////////////////////////////////////////////////////////////

static FileItem* get_fileitem_by_path(const std::string& path, bool create_if_not)
{
  if (path.empty())
    return rootitem;

  auto key = get_key_for_filename(path);
  auto it = fileitems_map->find(key);
  if (it != fileitems_map->end()) {
    FileItem* item = it->second;
    if (item->isExistent())
      return item;
    else {
      item->deleteItem();
      return nullptr;
    }
  }

  if (!create_if_not)
    return NULL;

  // get the attributes of the file
  bool is_folder = false;
  if (!base::is_file(path)) {
    if (!base::is_directory(path))
      return NULL;

    is_folder = true;
  }

  // new file-item
  FileItem* fileitem = new FileItem(NULL);

  fileitem->m_filename = path;
  fileitem->m_displayname = base::get_file_name(path);
  fileitem->m_is_folder = is_folder;

  // get the parent
  {
    std::string parent_path = remove_backslash_if_needed(base::join_path(base::get_file_path(path), ""));
    fileitem->m_parent = get_fileitem_by_path(parent_path, true);
  }

  put_fileitem(fileitem);

  return fileitem;
}

static std::string remove_backslash_if_needed(const std::string& filename)
{
  if (!filename.empty() && base::is_path_separator(*(filename.end()-1))) {
    int len = filename.size();

    // This is just the root '/' slash
    if (len == 1)
      return filename;
    else
      return base::remove_path_separator(filename);
  }
  return filename;
}

static std::string get_key_for_filename(const std::string& filename)
{
  std::string buf(filename);
  buf = base::fix_path_separators(buf);
  return buf;
}

static void put_fileitem(FileItem* fileitem)
{
  ASSERT(fileitem->m_filename != NOTINITIALIZED);
  ASSERT(fileitem->m_keyname == NOTINITIALIZED);

  fileitem->m_keyname = get_key_for_filename(fileitem->m_filename);

  ASSERT(fileitem->m_keyname != NOTINITIALIZED);

  // insert this file-item in the hash-table
  fileitems_map->insert(std::make_pair(fileitem->m_keyname, fileitem));
}

#endif

} // namespace app
