// SPDX-License-Identifier: LGPL-2.1-or-later
/***************************************************************************************************
 *                                                                                                 *
 *   Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de>                                      *
 *   Copyright (c) 2025 The FreeCAD project association AISBL                                      *
 *                                                                                                 *
 *   This file is part of FreeCAD.                                                                 *
 *                                                                                                 *
 *   FreeCAD is free software: you can redistribute it and/or modify it under the terms of the     *
 *   GNU Lesser General Public License as published by the Free Software Foundation, either        *
 *   version 2.1 of the License, or (at your option) any later version.                            *
 *                                                                                                 *
 *   FreeCAD 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 Lesser General Public License for more details.                                           *
 *                                                                                                 *
 *   You should have received a copy of the GNU Lesser General Public License along with FreeCAD.  *
 *   If not, see <https://www.gnu.org/licenses/>.                                                  *
 *                                                                                                 *
 **************************************************************************************************/

#ifndef SRC_APP_APPLICATIONDIRECTORIES_H_
#define SRC_APP_APPLICATIONDIRECTORIES_H_

#include "FCConfig.h"
#include "FCGlobal.h"

#include <filesystem>
#include <map>
#include <string>
#include <vector>

#ifdef FC_OS_WIN32
#include <QString>
#endif

namespace App {

    /// A helper class to handle application-wide directory management on behalf of the main
    /// App::Application class. Most of this class's methods were originally in Application, and
    /// were extracted here to be more easily testable (and to better follow the single-
    /// responsibility principle, though further work is required in that area).
    class AppExport ApplicationDirectories {

    public:

        /// Constructor
        /// \param config The base configuration dictionary. Used to create the appropriate
        /// directories, and updated to reflect their locations. New code should not directly access
        /// elements of this dictionary to determine directory locations, but should instead use
        /// the public methods of this class to determine the locations needed.
        explicit ApplicationDirectories(std::map<std::string,std::string> &config);

        /// Given argv[0], figure out the home path. Used to initialize the AppHomePath element of
        /// the configuration map prior to instantiating this class.
        static std::filesystem::path findHomePath(const char* sCall);

        /// "Home" here is the parent directory of the actual executable file being run. It is
        /// exposed here primarily for historical compatibility reasons, and new code should almost
        /// certainly NOT use this path for anything. See alternatives in the
        /// `App::ApplicationDirectories` class.
        const std::filesystem::path& getHomePath() const;

        /// Get the user's home directory. This should not be used for many things, use caution when
        /// deciding to use it for anything, there are usually better places.
        const std::filesystem::path& getUserHomePath() const;

        /// Temp path is the location of all temporary files: it is not guaranteed to preserve
        /// information between runs of the program, but *is* guaranteed to exist for the duration
        /// of a single program execution (that is, files are not deleted from it *during* the run).
        const std::filesystem::path& getTempPath() const;

        /// Get a file in the temp directory. WARNING: NOT THREAD-SAFE! Currently just forwards to
        /// the FileInfo class.  TODO: Rewrite to be thread safe
        std::filesystem::path getTempFileName(const std::string & filename = "") const;

        /// The user cache path can be used to store files that the program *prefers* not be deleted
        /// between runs, but that will be recreated or otherwise handled if they do not exist due
        /// to the cache being cleared. There is no guarantee that the files will exist from
        /// run-to-run, but an effort is made to preserve them (unlike the temp directory, which
        /// should never be used to save data between runs).
        const std::filesystem::path& getUserCachePath() const;

        /// The primary directory used to store per-user application data. This is the parent
        /// directory of all installed addons, per-user configuration files, etc. Developers looking
        /// for a place to put per-user data should begin here. Common subdirectories include "Mod",
        /// "Macros", "Materials" and many others that don't begin with the letter "M". This is
        /// typically a versioned directory, though users may choose to use a single path for
        /// multiple versions of the software.
        const std::filesystem::path& getUserAppDataDir() const;

        /// Historically, a single directory was used to store user-created (or user-installed)
        /// macro files. This is the path to that directory. Note that going forward it should *not*
        /// be assumed that all installed macros are located in this directory. This is typically a
        /// versioned directory, though users may choose to use a single path for multiple versions
        /// of the software.
        const std::filesystem::path& getUserMacroDir() const;

        /// The "resource" directory should be used to store non-ephemeral resources such as icons,
        /// templates, hardware setup, etc. -- items that should be preserved from run-to-run of the
        /// program, and between versions. This is *not* a versioned directory, and multiple
        /// versions of the software may access the same data.
        const std::filesystem::path& getResourceDir() const;

        /// Nominally, this is the directory where "help" files are stored, though for historical
        /// reasons several other informational files are stored here as well. It should only be
        /// used for user-facing informational files.
        const std::filesystem::path& getHelpDir() const;

        /// The root path of user config files `user.cfg` and `system.cfg`.
        const std::filesystem::path& getUserConfigPath() const;

        /// The directory of all extension modules. Added to `sys.path` during Python
        /// initialization.
        const std::filesystem::path& getLibraryDir() const;

        /// Get the user's home directory
        static std::filesystem::path getUserHome();

        /// Returns true if the current active directory set is custom, and false if it's the
        /// standard system default.
        bool usingCustomDirectories() const;


        // Versioned-Path Management Methods:

        /// Determine if a given config path is for the current version of the program
        /// \param config The path to check
        bool usingCurrentVersionConfig(std::filesystem::path config) const;

        struct MigrationResult
        {
            std::vector<std::filesystem::path> failedPaths;
            std::vector<std::pair<std::filesystem::path, std::filesystem::path>> migratedPaths;
        };

        /// Migrate a set of versionable configuration directories from the given path to a new
        /// version. The new version's directories cannot exist yet, and the old ones *must* exist.
        /// If the old paths are themselves versioned, then the new paths will be placed at the same
        /// level in the directory structure (e.g., they will be siblings of each entry in paths).
        /// If paths are NOT versioned, the new (versioned) copies will be placed *inside* the
        /// original paths. Returns the paths of entries that could not be copied.
        MigrationResult migrateAllPaths(const std::vector<std::filesystem::path>& paths) const;

        /// A utility method to generate the versioned directory name for a given version. This only
        /// returns the version string, not an entire path. As of FreeCAD 1.1, the string is of the
        /// form "vX-Y" where X is the major version and Y is the minor, but external code should
        /// not assume that is always the form, and should instead use this method, which is
        /// guaranteed stable (that is, given "1" and "1" as the major and minor, it will always
        /// return the string "v1-1", even if in later versions the format changes.
        static std::string versionStringForPath(int major, int minor);

        /// Given an arbitrary path, determine if it has the correct form to be a versioned path
        /// (e.g. is the last component a recognized version of the code). DOES NOT recognize
        /// versions *newer* than the current version, even if the directory name matches the path
        /// naming convention used by the previous version.
        bool isVersionedPath(const std::filesystem::path &startingPath) const;

        /// Given a base path that is expected to contained versioned subdirectories, locate the
        /// directory name (*not* the path, only the final component, the version string itself)
        /// corresponding to the most recent version of the software, up to and including the
        /// current version, but NOT exceeding it.
        std::string mostRecentAvailableConfigVersion(const std::filesystem::path &startingPath) const;

        /// Given a base path that is expected to contained versioned subdirectories, locate the
        /// directory corresponding to the most recent version of the software, up to and including
        /// the current version, but NOT exceeding it.
        std::filesystem::path mostRecentConfigFromBase(const std::filesystem::path &startingPath) const;

        /// A utility method to copy all files and directories from oldPath to newPath, handling the
        /// case where newPath might itself be a subdirectory of oldPath (and *not* attempting that
        /// otherwise-recursive copy). Returns the paths of entries that could not be copied.
        static MigrationResult migrateConfig(const std::filesystem::path& oldPath,
                                              const std::filesystem::path& newPath);

#ifdef FC_OS_WIN32
        /// On Windows, gets the location of the user's "AppData" directory. Invalid on other OSes.
        QString getOldGenericDataLocation();
#endif
        /// Adds subdirectory information to the appData vector for use in constructing full paths to config files, etc.
        static void getSubDirectories(const std::map<std::string,std::string>& mConfig,
                                      std::vector<std::string>& appData);
        /// To a given path it adds the subdirectories where to store application-specific files.
        /// On Linux or BSD a hidden directory (i.e. starting with a dot) is added.
        static void getOldDataLocation(const std::map<std::string,std::string>& mConfig,
                                       std::vector<std::string>& appData);
        /// If the passed path name is not empty, it will be returned, otherwise the user home path of the system will
        /// be returned.
        static std::filesystem::path findUserHomePath(const std::filesystem::path& userHome);

    protected:

        /// Override all application directories with temp directories. Returns true on success and
        /// false if the temp directory creation failed.
        bool startSafeMode(std::map<std::string,std::string>& mConfig);

        enum class MissingDirectoryBehavior : uint8_t {
            create,
            doNotAppend
        };

        /// Take a path and add a version to it, if it's possible to do so. When \a behavior is
        /// `doNotAppend`, no directories are created: the most recent existing versioned
        /// subdirectory is appended if one is found, or the current version is appended if the
        /// base path does not yet exist. When \a behavior is `create`, the current version is
        /// always appended and the directory is created on disk if it does not already exist.
        void appendVersionIfPossible(const std::filesystem::path& basePath,
                                     std::vector<std::string> &subdirs,
                                     MissingDirectoryBehavior behavior) const;

        static std::filesystem::path findPath(
            const std::filesystem::path& stdHome,
            const std::filesystem::path& customHome,
            const std::vector<std::string>& paths,
            bool create);

        void configurePaths(std::map<std::string,std::string> &config);
        void configureResourceDirectory(const std::map<std::string,std::string>& mConfig);
        void configureLibraryDirectory(const std::map<std::string,std::string>& mConfig);
        void configureHelpDirectory(const std::map<std::string,std::string>& mConfig);

        /*!
         * Returns a tuple of path names where to store config, data, and temp. files.
         * The method therefore reads the environment variables:
         * \list
         * \li FREECAD_USER_HOME
         * \li FREECAD_USER_DATA
         * \li FREECAD_USER_TEMP
         * \endlist
         */
        static std::tuple<std::filesystem::path, std::filesystem::path, std::filesystem::path> getCustomPaths();

        /*!
         * Returns a tuple of XDG-compliant standard paths names where to store config, data and cached files.
         * The method therefore reads the environment variables:
         * \list
         * \li XDG_CONFIG_HOME
         * \li XDG_DATA_HOME
         * \li XDG_CACHE_HOME
         * \endlist
         */
        std::tuple<std::filesystem::path, std::filesystem::path, std::filesystem::path, std::filesystem::path> getStandardPaths();

        /// Find the BuildVersionMajor, BuildVersionMinor pair in the config map, convert them to an int tuple, and
        /// return it. If the pair is not found, or cannot be converted to integers, a RuntimeError is raised.
        /// \param config The config map to search.
        /// \return The version tuple.
        static std::tuple<int, int> extractVersionFromConfigMap(const std::map<std::string,std::string> &config);

        /// A utility method to remove any stray null characters from a path (Conda sometimes
        /// injects these for unknown reasons -- see #6892 in the bug tracker).
        /// \param pathAsString The std::string path to sanitize
        /// \returns A path with any stray nulls removed
        static std::filesystem::path sanitizePath(const std::string& pathAsString);

    private:
        std::tuple<int, int> _currentVersion;
        std::filesystem::path _home;
        std::filesystem::path _temp;
        std::filesystem::path _userCache;
        std::filesystem::path _userConfig;
        std::filesystem::path _userAppData;
        std::filesystem::path _userMacro;
        std::filesystem::path _userHome;
        std::filesystem::path _resource;
        std::filesystem::path _library;
        std::filesystem::path _help;

        bool _usingCustomDirectories {false};
    };

} // App

#endif //SRC_APP_APPLICATIONDIRECTORIES_H_
