//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#ifndef MATERIALX_FILE_H
#define MATERIALX_FILE_H

/// @file
/// Cross-platform support for file and search paths

#include <MaterialXFormat/Export.h>

#include <MaterialXCore/Util.h>

MATERIALX_NAMESPACE_BEGIN

class FilePath;
using FilePathVec = vector<FilePath>;

extern MX_FORMAT_API const string PATH_LIST_SEPARATOR;
extern MX_FORMAT_API const string MATERIALX_SEARCH_PATH_ENV_VAR;

/// @class FilePath
/// A generic file path, supporting both syntactic and file system operations.
class MX_FORMAT_API FilePath
{
  public:
    enum Type
    {
        TypeRelative = 0,
        TypeAbsolute = 1,
        TypeNetwork = 2
    };

    enum Format
    {
        FormatWindows = 0,
        FormatPosix = 1,
#if defined(_WIN32)
        FormatNative = FormatWindows
#else
        FormatNative = FormatPosix
#endif
    };

  public:
    FilePath() :
        _type(TypeRelative)
    {
    }
    ~FilePath() = default;

    bool operator==(const FilePath& rhs) const
    {
        return _vec == rhs._vec &&
               _type == rhs._type;
    }
    bool operator!=(const FilePath& rhs) const
    {
        return !(*this == rhs);
    }

    /// @name Syntactic Operations
    /// @{

    /// Construct a path from a standard string.
    FilePath(const string& str)
    {
        assign(str);
    }

    /// Construct a path from a C-style string.
    FilePath(const char* str)
    {
        assign(str ? string(str) : EMPTY_STRING);
    }

    /// Convert a path to a standard string.
    operator string() const
    {
        return asString();
    }

    /// Assign a path from a standard string.
    void assign(const string& str);

    /// Return this path as a standard string with the given format.
    string asString(Format format = FormatNative) const;

    /// Return true if the given path is empty.
    bool isEmpty() const
    {
        return _vec.empty();
    }

    /// Return true if the given path is absolute.
    bool isAbsolute() const
    {
        return _type != TypeRelative;
    }

    /// Return the base name of the given path, with leading directory
    /// information removed.
    const string& getBaseName() const
    {
        if (isEmpty())
        {
            return EMPTY_STRING;
        }
        return _vec[_vec.size() - 1];
    }

    /// Return the parent directory of the given path, if any.  If no
    /// parent directory is present, then the empty path is returned.
    FilePath getParentPath() const
    {
        FilePath parent(*this);
        if (!parent.isEmpty())
        {
            parent._vec.pop_back();
        }
        return parent;
    }

    /// Return the file extension of the given path.
    string getExtension() const
    {
        const string& baseName = getBaseName();
        size_t i = baseName.rfind('.');
        return i != string::npos ? baseName.substr(i + 1) : EMPTY_STRING;
    }

    /// Add a file extension to the given path.
    void addExtension(const string& ext)
    {
        assign(asString() + "." + ext);
    }

    /// Remove the file extension, if any, from the given path.
    void removeExtension()
    {
        if (!isEmpty())
        {
            string& baseName = _vec[_vec.size() - 1];
            size_t i = baseName.rfind('.');
            if (i != string::npos)
            {
                baseName.resize(i);
            }
        }
    }

    /// Concatenate two paths with a directory separator, returning the
    /// combined path.
    FilePath operator/(const FilePath& rhs) const;

    /// Return the number of strings in the path.
    size_t size() const
    {
        return _vec.size();
    }

    /// Return the string at the given index.
    string operator[](size_t index)
    {
        return _vec[index];
    }

    /// Return the const string at the given index.
    const string& operator[](size_t index) const
    {
        return _vec[index];
    }

    /// Return a normalized version of the given path, collapsing current path and
    /// parent path references so that 'a/./b' and 'c/../d/../a/b' become 'a/b'.
    FilePath getNormalized() const;

    /// @}
    /// @name File System Operations
    /// @{

    /// Return true if the given path exists on the file system.
    bool exists() const;

    /// Return true if the given path is a directory on the file system.
    bool isDirectory() const;

    /// Return a vector of all files in the given directory with the given extension.
    /// If extension is empty all files are returned.
    FilePathVec getFilesInDirectory(const string& extension = EMPTY_STRING) const;

    /// Return a vector of all directories at or beneath the given path.
    FilePathVec getSubDirectories() const;

    /// Create a directory on the file system at the given path.
    /// If recursive is true, any missing parent directories will be created as well.
    void createDirectory(bool recursive = false) const;

    /// Set the current working directory of the file system.
    bool setCurrentPath();

    /// @}

    /// Return the current working directory of the file system.
    static FilePath getCurrentPath();

    /// Return the directory containing the executable module.
    static FilePath getModulePath();

  private:
    StringVec _vec;
    Type _type;
};

/// @class FileSearchPath
/// A sequence of file paths, which may be queried to find the first instance
/// of a given filename on the file system.
class MX_FORMAT_API FileSearchPath
{
  public:
    using Iterator = FilePathVec::iterator;
    using ConstIterator = FilePathVec::const_iterator;

  public:
    FileSearchPath() = default;

    /// Construct a search path from a string.
    /// @param searchPath A string containing a sequence of file paths joined
    ///    by separator characters.
    /// @param sep The set of separator characters used in the search path.
    ///    Defaults to the PATH_LIST_SEPARATOR character.
    FileSearchPath(const string& searchPath, const string& sep = PATH_LIST_SEPARATOR)
    {
        for (const string& path : splitString(searchPath, sep))
        {
            if (!path.empty())
            {
                append(FilePath(path));
            }
        }
    }

    /// Convert this sequence to a string using the given separator.
    string asString(const string& sep = PATH_LIST_SEPARATOR) const
    {
        string str;
        for (size_t i = 0; i < _paths.size(); i++)
        {
            str += _paths[i];
            if (i + 1 < _paths.size())
            {
                str += sep;
            }
        }
        return str;
    }

    /// Append the given path to the sequence.
    void append(const FilePath& path)
    {
        _paths.push_back(path);
    }

    /// Append the given search path to the sequence.
    void append(const FileSearchPath& searchPath)
    {
        for (const FilePath& path : searchPath)
        {
            _paths.push_back(path);
        }
    }

    /// Prepend the given path to the sequence.
    void prepend(const FilePath& path)
    {
        _paths.insert(_paths.begin(), path);
    }

    /// Clear all paths from the sequence.
    void clear()
    {
        _paths.clear();
    }

    /// Return the number of paths in the sequence.
    size_t size() const
    {
        return _paths.size();
    }

    /// Return true if the search path is empty.
    bool isEmpty() const
    {
        return _paths.empty();
    }

    /// Return the path at the given index.
    FilePath& operator[](size_t index)
    {
        return _paths[index];
    }

    /// Return the const path at the given index.
    const FilePath& operator[](size_t index) const
    {
        return _paths[index];
    }

    /// Given an input filename, iterate through each path in this sequence,
    /// returning the first combined path found on the file system.
    /// On success, the combined path is returned; otherwise the original
    /// filename is returned unmodified.
    FilePath find(const FilePath& filename) const
    {
        if (_paths.empty() || filename.isEmpty())
        {
            return filename;
        }
        if (!filename.isAbsolute())
        {
            for (const FilePath& path : _paths)
            {
                FilePath combined = path / filename;
                if (combined.exists())
                {
                    return combined;
                }
            }
        }
        return filename;
    }

    /// @name Iterators
    /// @{

    Iterator begin() { return _paths.begin(); }
    ConstIterator begin() const { return _paths.begin(); }

    Iterator end() { return _paths.end(); }
    ConstIterator end() const { return _paths.end(); }

    /// @}

  private:
    FilePathVec _paths;
};

/// Return a FileSearchPath object from search path environment variable.
MX_FORMAT_API FileSearchPath getEnvironmentPath(const string& sep = PATH_LIST_SEPARATOR);

MATERIALX_NAMESPACE_END

#endif
