// Copyright (C) 2003  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_DIR_NAV_KERNEl_2_
#define DLIB_DIR_NAV_KERNEl_2_

#ifdef DLIB_ISO_CPP_ONLY
#error "DLIB_ISO_CPP_ONLY is defined so you can't use this OS dependent code.  Turn DLIB_ISO_CPP_ONLY off if you want to use it."
#endif


#include "dir_nav_kernel_abstract.h"

#include <string>
#include "../uintn.h"
#include "../algs.h"

#include <sys/types.h>
#include <dirent.h>
#include <libgen.h>
#include <limits.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <chrono>

#if !defined(__USE_LARGEFILE64 ) && !defined(_LARGEFILE64_SOURCE)
#define stat64 stat
#endif

#include <vector>
#include "../stl_checked.h"
#include "../enable_if.h"
#include "../queue.h"

namespace dlib
{

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
    // file object    
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
    
    class file
    {
        /*!
            INITIAL VALUES
                state.name        == name()
                state.full_name   == full_name()
                state.file_size   == size()
                state.last_modified == last_modified()

            CONVENTION
                state.name        == name()
                state.full_name   == full_name()
                state.file_size   == size()
                state.last_modified == last_modified()

        !*/

        friend class directory;

        struct data
        {
            uint64 file_size;
            std::string name;
            std::string full_name;
            std::chrono::time_point<std::chrono::system_clock> last_modified;
        };

        void init(const std::string& name);

    public:

        struct private_constructor{};
        inline file (
            const std::string& name,
            const std::string& full_name,
            const uint64 file_size,
            const std::chrono::time_point<std::chrono::system_clock>& last_modified,
            private_constructor
        )
        {
            state.file_size = file_size;
            state.name = name;
            state.full_name = full_name;
            state.last_modified = last_modified;
        }


        class file_not_found : public error { 
            public: file_not_found(const std::string& s): error(s){}
        };
        
        inline file (
        )
        {
            state.file_size = 0;
        }

        file (
            const std::string& name
        ) { init(name); }

        file (
            const char* name
        ) { init(name); }

        inline const std::string& name (
        ) const { return state.name; }

        inline  const std::string& full_name (
        ) const { return state.full_name; }

        inline uint64 size (
        ) const { return state.file_size; }

        inline std::chrono::time_point<std::chrono::system_clock> last_modified (
        ) const { return state.last_modified; }

        operator std::string (
        ) const { return full_name(); }

        bool operator == (
            const file& rhs
        ) const;

        bool operator != (
            const file& rhs
        ) const { return !(*this == rhs); }

        inline bool operator < (
            const file& item
        ) const { return full_name() < item.full_name(); }

        inline void swap (
            file& item
        ) 
        { 
            exchange(state,item.state); 
        }

    private:

        // member data
        data state;

    };

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
    // directory object    
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
       
    class directory
    {
        /*!
            INITIAL VALUES
                state.name        == name()
                state.full_name   == full_name()

            CONVENTION
                state.name        == name()
                state.full_name   == full_name()
                is_root()          == state.name.size() == 0

        !*/

        void init(const std::string& name);

    public:
        struct private_constructor{};
        inline directory (
            const std::string& name,
            const std::string& full_name,
            private_constructor
        )
        {
            state.name = name;
            state.full_name = full_name;
        }

        struct data
        {
            std::string name;
            std::string full_name;
        };

        class dir_not_found : public error {
            public: dir_not_found(const std::string& s):error(s){}
        };
        class listing_error : public error {
            public: listing_error(const std::string& s):error(s){}
        };
        
        inline directory (
        )
        {
        }

        directory (
            const std::string& name
        ) { init(name); }

        directory (
            const char* name
        ) { init(name); }

        static char get_separator (
        );

        template <
            typename queue_of_files
            >
        void get_files (
            queue_of_files& files
        ) const;

        template <
            typename queue_of_dirs
            >
        void get_dirs (
            queue_of_dirs& dirs
        ) const;

        std::vector<file> get_files (
        ) const
        {
            std::vector<file> temp_vector;
            get_files(temp_vector);
            return temp_vector;
        }

        std::vector<directory> get_dirs (
        ) const
        {
            std::vector<directory> temp_vector;
            get_dirs(temp_vector);
            return temp_vector;
        }

        const directory get_parent (
        ) const;
       
        inline bool is_root (
        ) const { return state.name.size() == 0; }

        inline const std::string& name (
        ) const { return state.name; }

        inline const std::string& full_name (
        ) const { return state.full_name; }

        operator std::string (
        ) const { return full_name(); }

        bool operator == (
            const directory& rhs
        ) const;

        bool operator != (
            const directory& rhs
        ) const { return !(*this == rhs); }

        inline bool operator < (
            const directory& item
        ) const { return full_name() < item.full_name(); }

        inline void swap (
            directory& item
        ) 
        { 
            exchange(state,item.state); 
        }

    private:

        // member data
        data state;

        bool is_root_path (
            const std::string& path
        ) const;
        /*!
            ensures
                - returns true if path is a root path.  
                  Note that this function considers root paths that don't
                  have a trailing separator to also be valid.
        !*/

    };

// ----------------------------------------------------------------------------------------

    inline std::ostream& operator<< (
        std::ostream& out,
        const directory& item
    ) { out << (std::string)item; return out; }

    inline std::ostream& operator<< (
        std::ostream& out,
        const file& item
    ) { out << (std::string)item; return out; }

// ----------------------------------------------------------------------------------------

    inline void swap (
        file& a, 
        file& b 
    ) { a.swap(b); }   

// ----------------------------------------------------------------------------------------

    inline void swap (
        directory& a, 
        directory& b 
    ) { a.swap(b); }  

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
    // templated member function definitions
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    template <
        typename queue_of_files
        >
    typename disable_if<is_std_vector<queue_of_files>,void>::type
    directory_helper_get_files (
        const directory::data& state,
        queue_of_files& files
    ) 
    {
        using namespace std;

        files.clear();
        if (state.full_name.size() == 0)
            throw directory::listing_error("This directory object currently doesn't represent any directory.");

        DIR* ffind = 0;
        struct dirent* data;
        struct stat64 buffer;

        try
        {
            string path = state.full_name;
            // ensure that the path ends with a separator
            if (path[path.size()-1] != directory::get_separator())
                path += directory::get_separator();

            // get a handle to something we can search with
            ffind = opendir(state.full_name.c_str());
            if (ffind == 0)
            {
                throw directory::listing_error("Unable to list the contents of " + state.full_name);
            }

            while(true)
            {
                errno = 0;
                if ( (data = readdir(ffind)) == 0)
                {                    
                    // there was an error or no more files
                    if ( errno == 0)
                    {
                        // there are no more files
                        break;
                    }
                    else
                    {
                        // there was an error
                        throw directory::listing_error("Unable to list the contents of " + state.full_name);
                    }                
                }

                uint64 file_size;
                // get a stat64 structure so we can see if this is a file
                if (::stat64((path+data->d_name).c_str(), &buffer) != 0)
                {
                    // this might be a broken symbolic link.  We can check by calling
                    // readlink and seeing if it finds anything.  
                    char buf[PATH_MAX];
                    ssize_t temp = readlink((path+data->d_name).c_str(),buf,sizeof(buf));
                    if (temp == -1)                    
                        throw directory::listing_error("Unable to list the contents of " + state.full_name);
                    else
                        file_size = static_cast<uint64>(temp);
                }
                else
                {
                    file_size = static_cast<uint64>(buffer.st_size);
                }
                auto last_modified = std::chrono::system_clock::from_time_t(buffer.st_mtime);
#ifdef _BSD_SOURCE 
                last_modified += std::chrono::duration_cast<std::chrono::system_clock::duration>(std::chrono::nanoseconds(buffer.st_atim.tv_nsec));
#endif

                if (S_ISDIR(buffer.st_mode) == 0)
                {
                    // this is actually a file
                    file temp(
                        data->d_name,
                        path+data->d_name,
                        file_size,
                        last_modified,
                        file::private_constructor()
                        );
                    files.enqueue(temp);
                }
            } // while (true)

            if (ffind != 0)
            {
                while (closedir(ffind))
                {
                    if (errno != EINTR)
                        break;
                }
                ffind = 0;
            }

        }
        catch (...)
        {
            if (ffind != 0)
            {
                while (closedir(ffind))
                {
                    if (errno != EINTR)
                        break;
                }
                ffind = 0;
            }
            files.clear();
            throw;
        }
    }

// ----------------------------------------------------------------------------------------

    template <
        typename queue_of_files
        >
    typename enable_if<is_std_vector<queue_of_files>,void>::type 
    directory_helper_get_files (
        const directory::data& state,
        queue_of_files& files
    ) 
    {
        queue<file>::kernel_2a temp_files;
        directory_helper_get_files(state,temp_files);

        files.clear();

        // copy the queue of files into the vector
        temp_files.reset();
        while (temp_files.move_next())
        {
            files.push_back(temp_files.element());
        }
    }

// ----------------------------------------------------------------------------------------

    template <
        typename queue_of_files
        >
    void directory::
    get_files (
        queue_of_files& files
    ) const
    {
        // the reason for this indirection here is because it avoids a bug in
        // the cygwin version of gcc
        directory_helper_get_files(state,files);
    }

// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    template <
        typename queue_of_dirs
        >
    typename disable_if<is_std_vector<queue_of_dirs>,void>::type 
    directory_helper_get_dirs (
        const directory::data& state,
        queue_of_dirs& dirs
    ) 
    {
        using namespace std;

        dirs.clear();
        if (state.full_name.size() == 0)
            throw directory::listing_error("This directory object currently doesn't represent any directory.");

        DIR* ffind = 0;
        struct dirent* data;
        struct stat64 buffer;

        try
        {
            string path = state.full_name;
            // ensure that the path ends with a separator
            if (path[path.size()-1] != directory::get_separator())
                path += directory::get_separator();

            // get a handle to something we can search with
            ffind = opendir(state.full_name.c_str());
            if (ffind == 0)
            {
                throw directory::listing_error("Unable to list the contents of " + state.full_name);
            }

            while(true)
            {
                errno = 0;
                if ( (data = readdir(ffind)) == 0)
                {                    
                    // there was an error or no more files
                    if ( errno == 0)
                    {
                        // there are no more files
                        break;
                    }
                    else
                    {
                        // there was an error
                        throw directory::listing_error("Unable to list the contents of " + state.full_name);
                    }                
                }

                // get a stat64 structure so we can see if this is a file
                if (::stat64((path+data->d_name).c_str(), &buffer) != 0)
                {
                    // just assume this isn't a directory.  It is probably a broken
                    // symbolic link.
                    continue;
                }

                string dtemp(data->d_name);
                if (S_ISDIR(buffer.st_mode) &&
                    dtemp != "." &&
                    dtemp != ".." )
                {
                    // this is a directory so add it to dirs
                    directory temp(dtemp,path+dtemp, directory::private_constructor());
                    dirs.enqueue(temp);
                }
            } // while (true)

            if (ffind != 0)
            {
                while (closedir(ffind))
                {
                    if (errno != EINTR)
                        break;
                }
                ffind = 0;
            }

        }
        catch (...)
        {
            if (ffind != 0)
            {
                while (closedir(ffind))
                {
                    if (errno != EINTR)
                        break;
                }
                ffind = 0;
            }
            dirs.clear();
            throw;
        }
    }

// ----------------------------------------------------------------------------------------

    template <
        typename queue_of_dirs
        >
    typename enable_if<is_std_vector<queue_of_dirs>,void>::type 
    directory_helper_get_dirs (
        const directory::data& state,
        queue_of_dirs& dirs
    ) 
    {
        queue<directory>::kernel_2a temp_dirs;
        directory_helper_get_dirs(state,temp_dirs);

        dirs.clear();

        // copy the queue of dirs into the vector
        temp_dirs.reset();
        while (temp_dirs.move_next())
        {
            dirs.push_back(temp_dirs.element());
        }
    }

// ----------------------------------------------------------------------------------------


    template <
        typename queue_of_dirs
        >
    void directory::
    get_dirs (
        queue_of_dirs& dirs
    ) const
    {
        // the reason for this indirection here is because it avoids a bug in
        // the cygwin version of gcc
        directory_helper_get_dirs(state,dirs);
    }
 
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------

    template <
        typename queue_of_dir
        >
    typename disable_if<is_std_vector<queue_of_dir>,void>::type get_filesystem_roots (
        queue_of_dir& roots
    )
    {
        roots.clear();
        directory dir("/");
        roots.enqueue(dir);
    }

    template <
        typename queue_of_dir
        >
    typename enable_if<is_std_vector<queue_of_dir>,void>::type get_filesystem_roots (
        std::vector<directory>& roots
    )
    {
        roots.clear();
        directory dir("/");
        roots.push_back(dir);
    }

// ----------------------------------------------------------------------------------------

}


#ifdef NO_MAKEFILE
#include "dir_nav_kernel_2.cpp"
#endif

#endif // DLIB_DIR_NAV_KERNEl_2_

