/**
 @file fileutils.cpp
 
 @author Morgan McGuire, graphics3d.com
 
 @author  2002-06-06
 @edited  2010-02-05
 */

#include <cstring>
#include <cstdio>
#include "G3D/platform.h"
#include "G3D/fileutils.h"
#include "G3D/BinaryInput.h"
#include "G3D/BinaryOutput.h"
#include "G3D/g3dmath.h"
#include "G3D/stringutils.h"
#include "G3D/Set.h"
#include "G3D/g3dfnmatch.h"
#include "G3D/FileSystem.h"

#include <sys/stat.h>
#include <sys/types.h>
#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */
    #include "zip.h"
#endif

#ifdef G3D_WINDOWS
   // Needed for _getcwd
   #include <direct.h>
   #include <io.h>
#else
    #include <dirent.h>
    #include <fnmatch.h>
    #include <unistd.h>
    #define _getcwd getcwd
    #define _stat stat
#endif


namespace G3D {
    
namespace _internal {
    Set<std::string> currentFilesUsed;
}

std::string pathConcat(const std::string& dirname, const std::string& file) {
    // Ensure that the directory ends in a slash
    if ((dirname.size() != 0) && 
        (dirname[dirname.size() - 1] != '/') &&
        (dirname[dirname.size() - 1] != '\\') &&
        (dirname[dirname.size() - 1] != ':')) {
        return dirname + '/' + file;
    } else {
        return dirname + file;
    }
}

std::string resolveFilename(const std::string& filename) {
    if (filename.size() >= 1) {
        if ((filename[0] == '/') || (filename[0] == '\\')) {
            // Already resolved
            return filename;
        } else {

            #ifdef G3D_WINDOWS
                if ((filename.size() >= 2) && (filename[1] == ':')) {
                    // There is a drive spec on the front.
                    if ((filename.size() >= 3) && ((filename[2] == '\\') || 
                                                   (filename[2] == '/'))) {
                        // Already fully qualified
                        return filename;
                    } else {
                        // The drive spec is relative to the
                        // working directory on that drive.
                        debugAssertM(false, "Files of the form d:path are"
                                     " not supported (use a fully qualified"
                                     " name).");
                        return filename;
                    }
                }
            #endif
        }
    }

    char buffer[1024];

    // Prepend the working directory.
    (void)_getcwd(buffer, 1024);

    return format("%s/%s", buffer, filename.c_str());
}

bool zipfileExists(const std::string& filename) {
    std::string    outZipfile;
    std::string    outInternalFile;
    return zipfileExists(filename, outZipfile, outInternalFile);
}


std::string readWholeFile(const std::string& filename) {

    std::string s;

    debugAssert(filename != "");
    if (!  FileSystem::exists(filename)) {
        throw (filename + " not found");
    }
    FileSystem::markFileUsed(filename);
    std::string zipfile;

    if (! FileSystem::inZipfile(filename, zipfile)) {
        // Not in zipfile
        if (! FileSystem::exists(filename)) {
            throw FileNotFound(filename, std::string("File not found in readWholeFile: ") + filename);
        }

        int64 length = FileSystem::size(filename);

        char* buffer = (char*)System::alignedMalloc(length + 1, 16);
        debugAssert(buffer);
        FILE* f = FileSystem::fopen(filename.c_str(), "rb");
        debugAssert(f);
        int64 ret = fread(buffer, 1, length, f);
        debugAssert(ret == length);(void)ret;
        FileSystem::fclose(f);

        buffer[length] = '\0';    
        s = std::string(buffer);

        System::alignedFree(buffer);

    } else {

        // In zipfile
        FileSystem::markFileUsed(zipfile);

#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */    
        // Zipfiles require Unix-style slashes
        std::string internalFile = FilePath::canonicalize(filename.substr(zipfile.length() + 1));
        struct zip* z = zip_open(zipfile.c_str(), ZIP_CHECKCONS, NULL);
        {
            struct zip_stat info;
            zip_stat_init( &info );    // TODO: Docs unclear if zip_stat_init is required.
            zip_stat(z, internalFile.c_str(), ZIP_FL_NOCASE, &info);

            // Add NULL termination
            char* buffer = reinterpret_cast<char*>(System::alignedMalloc(info.size + 1, 16));
            buffer[info.size] = '\0';

            struct zip_file* zf = zip_fopen( z, internalFile.c_str(), ZIP_FL_NOCASE );
            if (zf == NULL) {
                throw std::string("\"") + internalFile + "\" inside \"" + zipfile + "\" could not be opened.";
            } else {
                const int64 bytesRead = zip_fread( zf, buffer, (info.size));
                debugAssertM((int64)bytesRead == (int64)info.size,
                             internalFile + " was corrupt because it unzipped to the wrong size.");
                (void)bytesRead;
                zip_fclose( zf );
            }
            // Copy the string
            s = buffer;
            System::alignedFree(buffer);
        }
        zip_close( z );
#endif
    }

    return s;
}


int64 fileLength(const std::string& filename) {
    struct _stat st;
    int result = _stat(filename.c_str(), &st);
    
    if (result == -1) {
#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */
        std::string zip, contents;
        if(zipfileExists(filename, zip, contents)){
            int64 requiredMem;

                        struct zip *z = zip_open( zip.c_str(), ZIP_CHECKCONS, NULL );
                        debugAssertM(z != NULL, zip + ": zip open failed.");
            {
                                struct zip_stat info;
                                zip_stat_init( &info );    // TODO: Docs unclear if zip_stat_init is required.
                                int success = zip_stat( z, contents.c_str(), ZIP_FL_NOCASE, &info );
                (void)success;
                                debugAssertM(success == 0, zip + ": " + contents + ": zip stat failed.");
                                requiredMem = info.size;
            }
                        zip_close( z );
            return requiredMem;
        } else {
        return -1;
        }
#else
        return -1;
#endif
    }

    return st.st_size;
}

///////////////////////////////////////////////////////////////////////////////
void writeWholeFile(
    const std::string&          filename,
    const std::string&          str,
    bool                        flush) {

    // Make sure the directory exists.
    std::string root, base, ext, path;
    Array<std::string> pathArray;
    parseFilename(filename, root, pathArray, base, ext); 

    path = root + stringJoin(pathArray, '/');
    if (! FileSystem::exists(path, false)) {
        FileSystem::createDirectory(path);
    }

    FILE* file = FileSystem::fopen(filename.c_str(), "wb");

    debugAssert(file);

    fwrite(str.c_str(), str.size(), 1, file);

    if (flush) {
        fflush(file);
    }

    FileSystem::fclose(file);
}

///////////////////////////////////////////////////////////////////////////////

/**
 Creates the directory (which may optionally end in a /)
 and any parents needed to reach it.
 */
void createDirectory(
    const std::string&  dir) {
    
    if (dir == "") {
        return;
    }

    std::string d;

    // Add a trailing / if there isn't one.
    switch (dir[dir.size() - 1]) {
    case '/':
    case '\\':
        d = dir;
        break;

    default:
        d = dir + "/";
    }

    // If it already exists, do nothing
    if (FileSystem::exists(d.substr(0, d.size() - 1))) {
        return;
    }

    // Parse the name apart
    std::string root, base, ext;
    Array<std::string> path;

    std::string lead;
    parseFilename(d, root, path, base, ext);
    debugAssert(base == "");
    debugAssert(ext == "");

    // Begin with an extra period so "c:\" becomes "c:\.\" after
    // appending a path and "c:" becomes "c:.\", not root: "c:\"
    std::string p = root + ".";

    // Create any intermediate that doesn't exist
    for (int i = 0; i < path.size(); ++i) {
        p += "/" + path[i];
        if (! FileSystem::exists(p)) {
            // Windows only requires one argument to mkdir,
            // where as unix also requires the permissions.
#           ifndef G3D_WINDOWS
                mkdir(p.c_str(), 0777);
#            else
                _mkdir(p.c_str());
#            endif
        }
    }
}


///////////////////////////////////////////////////////////////////////////////

#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */
/* Helper methods for zipfileExists()*/
// Given a string (the drive) and an array (the path), computes the directory
static void _zip_resolveDirectory(std::string& completeDir, const std::string& drive, const Array<std::string>& path, const int length){
    completeDir = drive;
    int tempLength;
    // if the given length is longer than the array, we correct it
    if(length > path.length()){
        tempLength = path.length();
    } else{
        tempLength = length;
    }

    for(int t = 0; t < tempLength; ++t){
        if(t > 0){
            completeDir += "/";
        }
        completeDir += path[t];
    }
}


/** assumes that zipDir references a .zip file */
static bool _zip_zipContains(const std::string& zipDir, const std::string& desiredFile){
        struct zip *z = zip_open( zipDir.c_str(), ZIP_CHECKCONS, NULL );
    //the last parameter, an int, determines case sensitivity:
    //1 is sensitive, 2 is not, 0 is default
        int test = zip_name_locate( z, desiredFile.c_str(), ZIP_FL_NOCASE );
        zip_close( z );
    if(test == -1){
        return false;
    }
    return true;
}
#endif


/** If no zipfile exists, outZipfile and outInternalFile are unchanged */
bool zipfileExists(const std::string& filename, std::string& outZipfile,
                   std::string& outInternalFile){
#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */
    Array<std::string> path;
    std::string drive, base, ext, zipfile, infile;
    parseFilename(filename, drive, path, base, ext);
    
    // Put the filename back together
    if ((base != "") && (ext != "")) {
        infile = base + "." + ext;
    } else {
        infile = base + ext;
    }
    
    // Remove all standalone single dots (".") from path
    for (int i = 0; i < path.length(); ++i) {
        if (path[i] == ".") {
            path.remove(i);
            --i;
        }
    }
    
    // Remove non-leading ".." from path
    for (int i = 1; i < path.length(); ++i) {
        if ((path[i] == "..") && (i > 0) && (path[i - 1] != "..")) {
            // Remove both i and i - 1
            path.remove(i - 1, 2);
            i -= 2;
        }
    }
    
    // Walk the path backwards, accumulating pieces onto the infile until
    // we find a zipfile that contains it
    for (int t = 0; t < path.length(); ++t){
        _zip_resolveDirectory(zipfile, drive, path,  path.length() - t);
        if (t > 0) {
            infile = path[path.length() - t] + "/" + infile;
        }

        if (endsWith(zipfile, "..")) {
            return false;
        }
        
        if (FileSystem::exists(zipfile)) {
            // test if it actually is a zipfile
            // if not, return false, a bad
            // directory structure has been given,
            // not a .zip
            if (FileSystem::isZipfile(zipfile)){
                
                if (_zip_zipContains(zipfile, infile)){
                    outZipfile = zipfile;
                    outInternalFile = infile;
                    return true;
                } else {
                    return false;
                }
            } else {
                // the directory structure was valid but did not point to a .zip
                return false;
            }
        }
        
    }
#else
    (void)filename;
    (void)outZipfile;
    (void)outInternalFile;
#endif
    // not a valid directory structure ever, 
    // obviously no .zip was found within the path 
    return false;
}    

///////////////////////////////////////////////////////////////////////////////

std::string generateFilenameBase(const std::string& prefix, const std::string& suffix) {
    Array<std::string> exist;

    // Note "template" is a reserved word in C++
    std::string templat = prefix + System::currentDateString() + "_";
    FileSystem::getFiles(templat + "*", exist, true);
    
    // Remove extensions
    for (int i = 0; i < exist.size(); ++i) {
        const std::string ext = FilePath::ext(exist[i]);
        if (ext.length() > 0) {
            exist[i] = exist[i].substr(0, exist[i].length() - ext.length() - 1);
        }
    }

    int num = 0;
    std::string result;
    templat += "%03d" + suffix;
    do {
        result = format(templat.c_str(), num);
        ++num;
    } while (exist.contains(result));

    return result;
}

///////////////////////////////////////////////////////////////////////////////

void copyFile(
    const std::string&          source,
    const std::string&          dest) {

    #ifdef G3D_WINDOWS
        CopyFileA(source.c_str(), dest.c_str(), FALSE);
    #else
        // TODO: don't use BinaryInput and BinaryOutput
        // Read it all in, then dump it out
        BinaryInput  in(source, G3D_LITTLE_ENDIAN);
        BinaryOutput out(dest, G3D_LITTLE_ENDIAN);
        out.writeBytes(in.getCArray(), in.size());
        out.commit(false);
    #endif
}

//////////////////////////////////////////////////////////////////////////////

void parseFilename(
    const std::string&  filename,
    std::string&        root,
    Array<std::string>& path,
    std::string&        base,
    std::string&        ext) {

    std::string f = filename;

    root = "";
    path.clear();
    base = "";
    ext = "";

    if (f == "") {
        // Empty filename
        return;
    }

    // See if there is a root/drive spec.
    if ((f.size() >= 2) && (f[1] == ':')) {
        
        if ((f.size() > 2) && isSlash(f[2])) {
        
            // e.g.  c:\foo
            root = f.substr(0, 3);
            f = f.substr(3, f.size() - 3);
        
        } else {
        
            // e.g.  c:foo
            root = f.substr(2);
            f = f.substr(2, f.size() - 2);

        }

    } else if ((f.size() >= 2) && isSlash(f[0]) && isSlash(f[1])) {
        
        // e.g. //foo
        root = f.substr(0, 2);
        f = f.substr(2, f.size() - 2);

    } else if (isSlash(f[0])) {
        
        root = f.substr(0, 1);
        f = f.substr(1, f.size() - 1);

    }

    // Pull the extension off
    {
        // Find the period
        size_t i = f.rfind('.');

        if (i != std::string::npos) {
            // Make sure it is after the last slash!
            size_t j = maxNotNPOS(f.rfind('/'), f.rfind('\\'));
            if ((j == std::string::npos) || (i > j)) {
                ext = f.substr(i + 1, f.size() - i - 1);
                f = f.substr(0, i);
            }
        }
    }

    // Pull the basename off
    {
        // Find the last slash
        size_t i = maxNotNPOS(f.rfind('/'), f.rfind('\\'));
        
        if (i == std::string::npos) {
            
            // There is no slash; the basename is the whole thing
            base = f;
            f    = "";

        } else if ((i != std::string::npos) && (i < f.size() - 1)) {
            
            base = f.substr(i + 1, f.size() - i - 1);
            f    = f.substr(0, i);

        }
    }

    // Parse what remains into path.
    size_t prev, cur = 0;

    while (cur < f.size()) {
        prev = cur;
        
        // Allow either slash
        size_t i = f.find('/', prev + 1);
        size_t j = f.find('\\', prev + 1);
        if (i == std::string::npos) {
            i = f.size();
        }

        if (j == std::string::npos) {
            j = f.size();
        }

        cur = min(i, j);

        if (cur == std::string::npos) {
            cur = f.size();
        }

        path.append(f.substr(prev, cur - prev));
        ++cur;
    }
}


/**
 Helper for getFileList and getDirectoryList.

 @param wantFiles       If false, returns the directories, otherwise
                        returns the files.
 @param includePath     If true, the names include paths
 */
static void getFileOrDirListNormal
(const std::string&    filespec,
 Array<std::string>&    files,
 bool            wantFiles,
 bool                   includePath) {
    
    bool test = wantFiles ? true : false;
    
    std::string path = "";

    // Find the place where the path ends and the file-spec begins
    size_t i = filespec.rfind('/');
    size_t j = filespec.rfind('\\');
    
    // Drive letters on Windows can separate a path
    size_t k = filespec.rfind(':');
    
    if (((j != std::string::npos) && (j > i)) ||
        (i == std::string::npos)) {
        i = j;
    }
    
    if (((k != std::string::npos) && (k > i)) ||
        (i == std::string::npos)) {
        i = k;
    }
    
    // If there is a path, pull it off
    if (i != std::string::npos) {
        path = filespec.substr(0, i + 1);
    }
   
    std::string prefix = path;

    if (path.size() > 0) {
        // Strip the trailing character
        path = path.substr(0, path.size() - 1);
    }

#   ifdef G3D_WINDOWS
    {
       struct _finddata_t fileinfo;

        long handle = (long)_findfirst(filespec.c_str(), &fileinfo);
        int result = handle;

        while (result != -1) {
            if ((((fileinfo.attrib & _A_SUBDIR) == 0) == test) && 
                strcmp(fileinfo.name, ".") &&
                strcmp(fileinfo.name, "..")) {
                
                if (includePath) {
                    files.append(prefix + fileinfo.name);
                } else {
                    files.append(fileinfo.name);
                }
            }
            
            result = _findnext(handle, &fileinfo);
        }
    }
#   else
    {
        if (path == "") {
            // Empty paths don't work on Unix
            path = ".";
        }

        // Unix implementation
        DIR* dir = opendir(path.c_str());

        if (dir != NULL) {
            struct dirent* entry = readdir(dir);

            while (entry != NULL) {

                // Exclude '.' and '..'
                if ((strcmp(entry->d_name, ".") != 0) &&
                    (strcmp(entry->d_name, "..") != 0)) {
                    
                    // Form a name with a path
                    std::string filename = prefix + entry->d_name;
                    // See if this is a file or a directory
                    struct _stat st;
                    bool exists = _stat(filename.c_str(), &st) != -1;

                    if (exists &&

                        // Make sure it has the correct type
                        (((st.st_mode & S_IFDIR) == 0) == test) &&

                        // Make sure it matches the wildcard
                        (fnmatch(filespec.c_str(),
                                 filename.c_str(),
                                 FNM_PATHNAME) == 0)) {
                        
                        if (includePath) {
                            files.append(filename);
                        } else {
                            files.append(entry->d_name);
                        }
                    }
                }

                entry = readdir(dir);
            }
            closedir(dir);
        }
    }
#   endif
}

#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */
/**
 @param path   The zipfile name (no trailing slash)
 @param prefix Directory inside the zipfile. No leading slash, must have trailing slash if non-empty.
 @param file   Name inside the zipfile that we are testing to see if it matches prefix + "*"
 */
static void _zip_addEntry(const std::string& path,
                          const std::string& prefix,
                          const std::string& file,
                          Set<std::string>& files,
                          bool wantFiles,
                          bool includePath) {

    // Make certain we are within the desired parent folder (prefix)
    if (beginsWith(file, prefix)) {
        // validityTest was prefix/file
        
        // Extract everything to the right of the prefix
        std::string s = file.substr(prefix.length());
        
        if (s == "") {
            // This was the name of the prefix
            return;
        }
        
        // See if there are any slashes
        size_t slashPos = s.find('/');
        
        bool add = false;
        
        if (slashPos == std::string::npos) {
            // No slashes, so s must be a file
            add = wantFiles;
        } else if (! wantFiles) {
            // Not all zipfiles list directories as explicit entries.
            // Because of this, if we're looking for directories and see
            // any path longer than prefix, we must add the subdirectory. 
            // The Set will fix duplicates for us.
            s = s.substr(0, slashPos);
            add = true;
        }
        
        if (add) {
            if (includePath) {
                files.insert(path + "/" + prefix + s);
            } else {
                files.insert(s);
            }
        }
    }
}
#endif


static void getFileOrDirListZip(const std::string& path,
                                const std::string& prefix,
                                Array<std::string>& files,
                                bool wantFiles,
                                bool includePath){
#if _HAVE_ZIP /* G3DFIX: Use ZIP-library only if defined */
    struct zip *z = zip_open( path.c_str(), ZIP_CHECKCONS, NULL );

    Set<std::string> fileSet;

    int count = zip_get_num_files( z );
    for( int i = 0; i < count; ++i ) {
        struct zip_stat info;
        zip_stat_init( &info );    // TODO: Docs unclear if zip_stat_init is required.
        zip_stat_index( z, i, ZIP_FL_NOCASE, &info );
        _zip_addEntry(path, prefix, info.name, fileSet, wantFiles, includePath);
    }
    
    zip_close( z );
    
    fileSet.getMembers(files);
#else
    (void)path;
    (void)prefix;
    (void)files;
    (void)wantFiles;
    (void)includePath;
#endif
}


static void determineFileOrDirList(
    const std::string&            filespec,
    Array<std::string>&            files,
    bool                        wantFiles,
    bool                        includePath) {

    // if it is a .zip, prefix will specify the folder within
    // whose contents we want to see
    std::string prefix = "";
    std::string path = filenamePath(filespec);

    if ((path.size() > 0) && isSlash(path[path.size() - 1])) {
        // Strip the trailing slash
        path = path.substr(0, path.length() -1);
    }
    
    if ((path == "") || FileSystem::exists(path)) {
        if ((path != "") && FileSystem::isZipfile(path)) {
            // .zip should only work if * is specified as the Base + Ext
            // Here, we have been asked for the root's contents
            debugAssertM(filenameBaseExt(filespec) == "*", "Can only call getFiles/getDirs on zipfiles using '*' wildcard");
            getFileOrDirListZip(path, prefix, files, wantFiles, includePath);
        } else {
            // It is a normal directory
            getFileOrDirListNormal(filespec, files, wantFiles, includePath);
        }
    } else if (zipfileExists(filenamePath(filespec), path, prefix)) {
        // .zip should only work if * is specified as the Base + Ext
        // Here, we have been asked for the contents of a folder within the .zip
        debugAssertM(filenameBaseExt(filespec) == "*", "Can only call getFiles/getDirs on zipfiles using '*' wildcard");
        getFileOrDirListZip(path, prefix, files, wantFiles, includePath);
    }
}


void getFiles(const std::string&            filespec,
              Array<std::string>&            files,
              bool                    includePath) {
    
    determineFileOrDirList(filespec, files, true, includePath);
}


void getDirs(
    const std::string&            filespec,
    Array<std::string>&            files,
    bool                        includePath) {

    determineFileOrDirList(filespec, files, false, includePath);
}


std::string filenameBaseExt(const std::string& filename) {
    size_t i = filename.rfind("/");
    size_t j = filename.rfind("\\");

    if ((j > i) && (j != std::string::npos)) {
        i = j;
    }

#   ifdef G3D_WINDOWS
        j = filename.rfind(":");
        if ((i == std::string::npos) && (j != std::string::npos)) {
            i = j;
        }
#   endif

    if (i == std::string::npos) {
        return filename;
    } else {
        return filename.substr(i + 1, filename.length() - i);
    }
}


std::string filenameBase(const std::string& s) {
    std::string drive;
    std::string base;
    std::string ext;
    Array<std::string> path;

    parseFilename(s, drive, path, base, ext);
    return base;
}


std::string filenameExt(const std::string& filename) {
    size_t i = filename.rfind(".");
    if (i != std::string::npos) {
        return filename.substr(i + 1, filename.length() - i);
    } else {
        return "";
    }
}


std::string filenamePath(const std::string& filename) {
    size_t i = filename.rfind("/");
    size_t j = filename.rfind("\\");

    if ((j > i) && (j != std::string::npos)) {
        i = j;
    }

#   ifdef G3D_WINDOWS
        j = filename.rfind(":");
        if ((i == std::string::npos) && (j != std::string::npos)) {
            i = j;
        }
#   endif

    if (i == std::string::npos) {
        return "";
    } else {
        return filename.substr(0, i+1);
    }
}


bool isZipfile(const std::string& filename) {

    FILE* f = fopen(filename.c_str(), "r");
    if (f == NULL) {
        return false;
    }
    uint8 header[4];
    (void)fread(header, 4, 1, f);
    
    const uint8 zipHeader[4] = {0x50, 0x4b, 0x03, 0x04};
    for (int i = 0; i < 4; ++i) {
        if (header[i] != zipHeader[i]) {
            fclose(f);
            return false;
        }
    }

    fclose(f);
    return true;
}


bool isDirectory(const std::string& filename) {
    struct _stat st;
    bool exists = _stat(filename.c_str(), &st) != -1;
    return exists && ((st.st_mode & S_IFDIR) != 0);
}


bool filenameContainsWildcards(const std::string& filename) {
    return (filename.find('*') != std::string::npos) || (filename.find('?') != std::string::npos);
}


bool fileIsNewer(const std::string& src, const std::string& dst) {
    struct _stat sts;
    bool sexists = _stat(src.c_str(), &sts) != -1;

    struct _stat dts;
    bool dexists = _stat(dst.c_str(), &dts) != -1;

    return sexists && ((! dexists) || (sts.st_mtime > dts.st_mtime));
}

}

#ifndef G3D_WINDOWS
  #undef _stat
#endif