// 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_1_
#define DLIB_DIR_NAV_KERNEl_1_

#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 "../platform.h"


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

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

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; }

        operator std::string (
        ) const { return 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; }

        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:

        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 data
        {
            std::string name;
            std::string full_name;
        };


        /*
            The reason we don't just make this constructor actually
            private is because doing it this way avoids a bug that 
            sometimes occurs in visual studio 7.1.  The bug has 
            something to do with templated friend functions 
            such as the get_filesystem_roots() function below if 
            it was declared as a friend template of this class. 
        */
        struct private_constructor{};
        inline directory (
            const std::string& name,
            const std::string& full_name,
            private_constructor 
        )
        {
            state.name = name;
            state.full_name = 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; }

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

    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();
        const DWORD mask = GetLogicalDrives();
        DWORD bit = 1;
        char buf[] = "A:\\";

        do
        {
            if (mask & bit)
            {
                directory dir("",buf,directory::private_constructor());
                roots.enqueue(dir);
            }
            bit <<= 1;
            ++buf[0];
        } while (buf[0] != 'Z');
    }

    template <
        typename queue_of_dir
        >
    typename enable_if<is_std_vector<queue_of_dir>,void>::type get_filesystem_roots (
        queue_of_dir& roots
    )
    {
        roots.clear();
        const DWORD mask = GetLogicalDrives();
        DWORD bit = 1;
        char buf[] = "A:\\";

        do
        {
            if (mask & bit)
            {
                directory dir("",buf,directory::private_constructor());
                roots.push_back(dir);
            }
            bit <<= 1;
            ++buf[0];
        } while (buf[0] != 'Z');
    }

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

    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;
        typedef directory::listing_error listing_error;
        typedef file::private_constructor private_constructor;

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

        HANDLE ffind = INVALID_HANDLE_VALUE;
        try
        {
            WIN32_FIND_DATAA data;
            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();
            
            ffind = FindFirstFileA((path+"*").c_str(), &data);
            if (ffind == INVALID_HANDLE_VALUE)
            {
                throw listing_error("Unable to list the contents of " + state.full_name);
            }


            bool no_more_files = false;
            do
            {
                if ((data.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) == 0)
                {
                    uint64 file_size = data.nFileSizeHigh;                                   
                    file_size <<= 32;
                    file_size |= data.nFileSizeLow;

                    ULARGE_INTEGER ull;
                    ull.LowPart = data.ftLastWriteTime.dwLowDateTime;
                    ull.HighPart = data.ftLastWriteTime.dwHighDateTime;
                    std::chrono::nanoseconds epoch(100 * (ull.QuadPart - 116444736000000000));
                    auto last_modified = std::chrono::time_point<std::chrono::system_clock>(std::chrono::duration_cast<std::chrono::system_clock::duration>(epoch));
                    
                    // this is a file so add it to the queue
                    file temp(data.cFileName,path+data.cFileName,file_size, last_modified, private_constructor());
                    files.enqueue(temp);
                }

                if (FindNextFileA(ffind,&data) == 0)
                {
                    // an error occurred
                    if ( GetLastError() == ERROR_NO_MORE_FILES)
                    {
                        // there are no more files
                        no_more_files = true;
                    }
                    else
                    {
                        // there was an error
                        throw listing_error("Unable to list the contents of " + state.full_name);
                    }  
                }
            } while (no_more_files == false);

            FindClose(ffind); 
            ffind = INVALID_HANDLE_VALUE;
        }
        catch (...)
        {
            if (ffind != INVALID_HANDLE_VALUE)
                FindClose(ffind);    
            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 mingw 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;
        typedef directory::listing_error listing_error;
        typedef directory::private_constructor private_constructor;

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

        HANDLE dfind = INVALID_HANDLE_VALUE;
        try
        {
            WIN32_FIND_DATAA data;
            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();
            
            dfind = FindFirstFileA((path+"*").c_str(), &data);
            if (dfind == INVALID_HANDLE_VALUE)
            {
                throw listing_error("Unable to list the contents of " + state.full_name);
            }


            bool no_more_files = false;
            do
            {
                string tname(data.cFileName);
                if ((data.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) != 0 &&
                    tname != "." &&
                    tname != "..")
                {
                    // this is a directory so add it to the queue
                    directory temp(tname,path+tname,private_constructor());
                    dirs.enqueue(temp);
                }

                if (FindNextFileA(dfind,&data) == 0)
                {
                    // an error occurred
                    if ( GetLastError() == ERROR_NO_MORE_FILES)
                    {
                        // there are no more files
                        no_more_files = true;
                    }
                    else
                    {
                        // there was an error
                        throw listing_error("Unable to list the contents of " + state.full_name);
                    }  
                }
            } while (no_more_files == false);

            FindClose(dfind);  
            dfind = INVALID_HANDLE_VALUE;
        }
        catch (...)
        {         
            if (dfind != INVALID_HANDLE_VALUE)
                FindClose(dfind);
            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 mingw version of gcc
        directory_helper_get_dirs(state,dirs);
    }

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

}


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

#endif // DLIB_DIR_NAV_KERNEl_1_

