/**
 *  \file SC_Filesystem.hpp
 *
 *  \brief Filesystem utilities for SuperCollider.
 *
 *  This header provides a singleton class, SC_Filesystem, which handles many
 *  important filesystem functions:
 *  - Getting and setting application directories (such as config, resource, and extension)
 *  - Expanding ~ to the home directory
 *  - Alias resolution
 *  - Globbing
 *  - Conversion between Path and UTF-8 encoded string
 *
 *  \note This header replaced an older implementation by Tim Walters, SC_DirUtils.hpp. Most of the globbing and
 *        directory calculation code is his.
 *
 *  $Authors: Brian Heim, Tim Walters $
 *
 *  \version Latest revision by Brian Heim, 2017
 *
 *  $Date: 2017-04-30 $
 *
 *  $Contact: brianlheim@gmail.com $
 *
 *  Created on: 2017-04-03
 *
 *  Original revision by Tim Walters, 2005-10-19.
 */
/*
 *  Copyright (C) Tim Walters, 2005. All rights reserved.
 *  Copyright (C) Brian Heim, 2017. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License as
 *  published by the Free Software Foundation; either version 2 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 *  02110-1301 USA
 */

#pragma once

#ifndef PATH_MAX
#    define PATH_MAX MAX_PATH
#endif

#ifndef MAXPATHLEN
#    define MAXPATHLEN PATH_MAX
#endif

/// Name of the folder used for system and user extensions.
#define SC_FOLDERNAME_EXTENSIONS "Extensions"

/// Name of "SuperCollider" folder. Could probably pop this up another level of importance.
#define SC_FOLDERNAME_APPLICATION_NAME "SuperCollider"

/// Default IDE name.
#define SC_DEFAULT_IDE_NAME "none"

/// Name of the plugins subdirectory of the Resources dir
#define SC_PLUGIN_DIR_NAME "plugins"

/// Extension for server plugins.
#ifndef SC_PLUGIN_EXT
#    define SC_PLUGIN_EXT ".scx"
#endif

#include <map> // map
#include <algorithm> // std::transform
#include <string> // std::string
#include <filesystem> // std::filesystem

#include "SC_Codecvt.hpp" // path_to_utf8_str

/** \class SC_Filesystem
 *
 *  \brief A singleton class providing SuperCollider filesystem utilities.
 *
 *  - Getting and setting application directories (such as config, resource, and extension)
 *  - Expanding ~ to the home directory
 *  - Alias resolution
 *  - Globbing
 *  - Conversion between Path and UTF-8 encoded string
 */
class SC_Filesystem {
public:
    //--------------------------------------------------------------------------------------//
    //------------------------------ PUBLIC CLASS MEMBERS ----------------------------------//
    //--------------------------------------------------------------------------------------//

    //---------------------------------- PUBLIC TYPES --------------------------------------//

    enum class DirName;
    struct Glob;

    typedef std::filesystem::path Path; ///< Path type.
    typedef std::map<DirName, Path> DirMap; ///< Type of directory name-to-path map.

    /// SuperCollider common directory names.
    enum class DirName {
        SystemAppSupport,
        SystemExtension,
        UserHome,
        UserAppSupport,
        UserExtension,
        UserConfig,
        Resource,
        MyDocuments // only on Windows, used for Platform.myDocumentsDir in sclang
    };

    //------------------------------- SINGLETON INSTANCE -----------------------------------//

    /// Singleton instance.
    static SC_Filesystem& instance() {
        static SC_Filesystem instance;
        return instance;
    }

    //--------------------------- COMMON DIRECTORY MANAGEMENT ------------------------------//

    /// Get path associated with a common directory; the path is initialized if necessary.
    Path getDirectory(const DirName& dn) {
        const DirMap::const_iterator& it = mDirectoryMap.find(dn);
        if (it != mDirectoryMap.end()) {
            return it->second;
        }
        mDirectoryMap[dn] = defaultDirectory(dn);
        return mDirectoryMap[dn];
    }

    /// Set path associated with a common directory.
    inline void setDirectory(const DirName& dn, const Path& p) { mDirectoryMap[dn] = p; }

    //-------------------------------- GENERAL UTILITIES -----------------------------------//

    /** \brief Checks whether a directory should be compiled.
     * \param p a directory path
     * \return True if the directory should not be traversed during compilation.
     *
     * Specifically, returns true if the directory name, ignoreing case, is equal to:
     * - one of ".svn", ".git", or "_darcs"
     * - one of "help" or "ignore"
     * - a string starting with "scide_" but not ending with the current IDE name
     * - one of "windows", "osx", "iphone", or "linux", but that is not the name of the current platform.
     */
    bool shouldNotCompileDirectory(const Path& p) const {
        std::string dirname = SC_Codecvt::path_to_utf8_str(p.filename());
        std::transform(dirname.begin(), dirname.end(), dirname.begin(), ::tolower);
        return dirname == "help" || dirname == "ignore" || dirname == ".svn" || dirname == ".git" || dirname == "_darcs"
            || isUnusedIdeDirectoryName(dirname) || isNonHostPlatformDirectoryName(dirname);
    }

    /// Expands a path starting with `~` to use the user's home directory.
    Path expandTilde(const Path& p) {
        static const Path tilde("~");
        if (!p.empty() && *p.begin() == tilde) {
            // If the first element in the path is tilde, just concatenate the rest of the path onto it
            const Path homeDir = getDirectory(DirName::UserHome);
            const std::string homeDir_utf8 = SC_Codecvt::path_to_utf8_str(homeDir);
            const std::string inPath_utf8 = SC_Codecvt::path_to_utf8_str(p);
            const std::string result = homeDir_utf8 + inPath_utf8.substr(1);
            return SC_Codecvt::utf8_str_to_path(result);
        } else {
            return p;
        }
    }

    //---------------------------------- IDE UTILITIES -------------------------------------//

    /// Get the IDE name. "none" is the default.
    const std::string& getIdeName() const { return mIdeName; }

    /// Set the IDE name.
    const void setIdeName(const std::string& s) { mIdeName = s; }

    /// Returns true if the IDE name is "none" (the default)
    const bool usingIde() const { return mIdeName != SC_DEFAULT_IDE_NAME; }

    //--------------------------------------------------------------------------------------//
    //------------------------------ PUBLIC STATIC MEMBERS ---------------------------------//
    //--------------------------------------------------------------------------------------//

    //-------------------------------- GENERAL UTILITIES -----------------------------------//

    /** \brief Resolves an alias on macOS.
     * \param p a path to resolve
     * \param isAlias set to true if p is an alias
     * \return An empty path if resolution failed; otherwise, the resolved path.
     *
     * If the path was not an alias, a copy is returned. */
    // Could possibly be split into `isAlias` and `resolveAlias` to avoid
    // unnecessary copying - bh
    static Path resolveIfAlias(const Path& p, bool& isAlias);

    //--------------------------------- GLOB UTILITIES -------------------------------------//

    /** \brief Makes a glob.
     * \param pattern a UTF-8 encoded string
     *
     * Constructs a Glob from a given pattern. Call globNext to get the next
     * Path in the glob. Client is responsible for calling freeGlob. */
    static Glob* makeGlob(const char* pattern);

    /// Next available Path in the glob. Returns empty path to indicate end.
    static Path globNext(Glob* g);

    /// Frees a Glob.
    static void freeGlob(Glob* g);

private:
    //--------------------------------------------------------------------------------------//
    //--------------------------------- PRIVATE MEMBERS ------------------------------------//
    //--------------------------------------------------------------------------------------//

    //---------------------------- CONSTRUCTORS & ASSIGNMENT -------------------------------//

    SC_Filesystem(SC_Filesystem const&) = delete;
    void operator=(SC_Filesystem const&) = delete;

    SC_Filesystem(): mDirectoryMap(), mIdeName(SC_DEFAULT_IDE_NAME) {}

    //----------------------------------- IDE UTILITIES ------------------------------------//

    bool isUnusedIdeDirectoryName(const std::string& s) const {
        return s.size() > 6 && s.substr(0, 6) == "scide_" && s.substr(6) != getIdeName();
    }

    //------------------------------ PRIVATE STATIC MEMBERS --------------------------------//

    /** Of the four strings "windows", "osx", "linux", "iphone", returns true if `p` is one of the
     * three that doesn't correspond to this platform. */
    static bool isNonHostPlatformDirectoryName(const std::string& p);

    static Path defaultDirectory(const DirName& dn) {
        switch (dn) {
        case DirName::SystemAppSupport:
            return defaultSystemAppSupportDirectory();
        case DirName::SystemExtension:
            return defaultSystemExtensionDirectory();
        case DirName::UserHome:
            return defaultUserHomeDirectory();
        case DirName::UserAppSupport:
            return defaultUserAppSupportDirectory();
        case DirName::UserExtension:
            return defaultUserExtensionDirectory();
        case DirName::UserConfig:
            return defaultUserConfigDirectory();
        case DirName::Resource:
            return defaultResourceDirectory();
#ifdef _WIN32
        case DirName::MyDocuments:
            return defaultMyDocumentsDirectory();
#endif
        default:
            return Path();
        }
    }

    static Path defaultSystemAppSupportDirectory();
    static Path defaultUserHomeDirectory();
    static Path defaultUserAppSupportDirectory();
    static Path defaultUserConfigDirectory();
    static Path defaultResourceDirectory();

#ifdef _WIN32
    // this function has no analogue on other OSs, yet.
    static Path defaultMyDocumentsDirectory();
#endif

    static Path defaultSystemExtensionDirectory() {
        const Path& p = defaultSystemAppSupportDirectory();
        return p.empty() ? p : p / SC_FOLDERNAME_EXTENSIONS;
    }

    static Path defaultUserExtensionDirectory() {
        const Path& p = defaultUserAppSupportDirectory();
        return p.empty() ? p : p / SC_FOLDERNAME_EXTENSIONS;
    }

    //------------------------------- PRIVATE DATA MEMBERS ---------------------------------//

    DirMap mDirectoryMap;
    std::string mIdeName;
};
