// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "vfs_api.h"

using namespace fs;

#define DEFAULT_FILE_BUFFER_SIZE 4096

FileImplPtr VFSImpl::open(const char* fpath, const char* mode, const bool create)
{
    if(!_mountpoint) {
        log_e("File system is not mounted");
        return FileImplPtr();
    }

    if(!fpath || fpath[0] != '/') {
        log_e("%s does not start with /", fpath);
        return FileImplPtr();
    }

    char * temp = (char *)malloc(strlen(fpath)+strlen(_mountpoint)+2);
    if(!temp) {
        log_e("malloc failed");
        return FileImplPtr();
    }

    strcpy(temp, _mountpoint);
    strcat(temp, fpath);

    struct stat st;
    //file found
    if(!stat(temp, &st)) {
        free(temp);
        if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode)) {
            return std::make_shared<VFSFileImpl>(this, fpath, mode);
        }
        log_e("%s has wrong mode 0x%08X", fpath, st.st_mode);
        return FileImplPtr();
    }

    //try to open this as directory (might be mount point)
    DIR * d = opendir(temp);
    if(d) {
        closedir(d);
        free(temp);
        return std::make_shared<VFSFileImpl>(this, fpath, mode);
    }

    //file not found but mode permits file creation without folder creation
    if((mode && mode[0] != 'r') && (!create)){
        free(temp);
        return std::make_shared<VFSFileImpl>(this, fpath, mode);
    }

    ////file not found but mode permits file creation and folder creation
    if((mode && mode[0] != 'r') && create){

        char *token;
        char *folder = (char *)malloc(strlen(fpath));

        int start_index = 0;
        int end_index = 0;

        token = strchr(fpath+1,'/');
        end_index = (token-fpath);

        while (token != NULL)
        {
            memcpy(folder,fpath + start_index, end_index-start_index);
            folder[end_index-start_index] = '\0';
            
            if(!VFSImpl::mkdir(folder))
            {
                log_e("Creating folder: %s failed!",folder);
                return FileImplPtr();
            }

            token=strchr(token+1,'/');
            if(token != NULL)
            {
                end_index = (token-fpath);
                memset(folder, 0, strlen(folder));
            }
            
        }

        free(folder);
        free(temp);
        return std::make_shared<VFSFileImpl>(this, fpath, mode);

    }

    log_e("%s does not exist, no permits for creation", temp);
    free(temp);
    return FileImplPtr();
}

bool VFSImpl::exists(const char* fpath)
{
    if(!_mountpoint) {
        log_e("File system is not mounted");
        return false;
    }

    VFSFileImpl f(this, fpath, "r");
    if(f) {
        f.close();
        return true;
    }
    return false;
}

bool VFSImpl::rename(const char* pathFrom, const char* pathTo)
{
    if(!_mountpoint) {
        log_e("File system is not mounted");
        return false;
    }

    if(!pathFrom || pathFrom[0] != '/' || !pathTo || pathTo[0] != '/') {
        log_e("bad arguments");
        return false;
    }
    if(!exists(pathFrom)) {
        log_e("%s does not exists", pathFrom);
        return false;
    }
    size_t mountpointLen = strlen(_mountpoint);
    char * temp1 = (char *)malloc(strlen(pathFrom)+mountpointLen+1);
    if(!temp1) {
        log_e("malloc failed");
        return false;
    }
    char * temp2 = (char *)malloc(strlen(pathTo)+mountpointLen+1);
    if(!temp2) {
        free(temp1);
        log_e("malloc failed");
        return false;
    }

    strcpy(temp1, _mountpoint);
    strcat(temp1, pathFrom);

    strcpy(temp2, _mountpoint);
    strcat(temp2, pathTo);

    auto rc = ::rename(temp1, temp2);
    free(temp1);
    free(temp2);
    return rc == 0;
}

bool VFSImpl::remove(const char* fpath)
{
    if(!_mountpoint) {
        log_e("File system is not mounted");
        return false;
    }

    if(!fpath || fpath[0] != '/') {
        log_e("bad arguments");
        return false;
    }

    VFSFileImpl f(this, fpath, "r");
    if(!f || f.isDirectory()) {
        if(f) {
            f.close();
        }
        log_e("%s does not exists or is directory", fpath);
        return false;
    }
    f.close();

    char * temp = (char *)malloc(strlen(fpath)+strlen(_mountpoint)+1);
    if(!temp) {
        log_e("malloc failed");
        return false;
    }

    strcpy(temp, _mountpoint);
    strcat(temp, fpath);

    auto rc = unlink(temp);
    free(temp);
    return rc == 0;
}

bool VFSImpl::mkdir(const char *fpath)
{
    if(!_mountpoint) {
        log_e("File system is not mounted");
        return false;
    }

    VFSFileImpl f(this, fpath, "r");
    if(f && f.isDirectory()) {
        f.close();
        //log_w("%s already exists", fpath);
        return true;
    } else if(f) {
        f.close();
        log_e("%s is a file", fpath);
        return false;
    }

    char * temp = (char *)malloc(strlen(fpath)+strlen(_mountpoint)+1);
    if(!temp) {
        log_e("malloc failed");
        return false;
    }

    strcpy(temp, _mountpoint);
    strcat(temp, fpath);

    auto rc = ::mkdir(temp, ACCESSPERMS);
    free(temp);
    return rc == 0;
}

bool VFSImpl::rmdir(const char *fpath)
{
    if(!_mountpoint) {
        log_e("File system is not mounted");
        return false;
    }

    if (strcmp(_mountpoint, "/spiffs") == 0) {
        log_e("rmdir is unnecessary in SPIFFS");
        return false;
    }

    VFSFileImpl f(this, fpath, "r");
    if(!f || !f.isDirectory()) {
        if(f) {
            f.close();
        }
        log_e("%s does not exists or is a file", fpath);
        return false;
    }
    f.close();

    char * temp = (char *)malloc(strlen(fpath)+strlen(_mountpoint)+1);
    if(!temp) {
        log_e("malloc failed");
        return false;
    }

    strcpy(temp, _mountpoint);
    strcat(temp, fpath);

    auto rc = ::rmdir(temp);
    free(temp);
    return rc == 0;
}




VFSFileImpl::VFSFileImpl(VFSImpl* fs, const char* fpath, const char* mode)
    : _fs(fs)
    , _f(NULL)
    , _d(NULL)
    , _path(NULL)
    , _isDirectory(false)
    , _written(false)
{
    char * temp = (char *)malloc(strlen(fpath)+strlen(_fs->_mountpoint)+1);
    if(!temp) {
        return;
    }

    strcpy(temp, _fs->_mountpoint);
    strcat(temp, fpath);

    _path = strdup(fpath);
    if(!_path) {
        log_e("strdup(%s) failed", fpath);
        free(temp);
        return;
    }

    if(!stat(temp, &_stat)) {
        //file found
        if (S_ISREG(_stat.st_mode)) {
            _isDirectory = false;
            _f = fopen(temp, mode);
            if(!_f) {
                log_e("fopen(%s) failed", temp);
            }
            if(_f && (_stat.st_blksize == 0))
            {
                setvbuf(_f,NULL,_IOFBF,DEFAULT_FILE_BUFFER_SIZE);
            } 
        } else if(S_ISDIR(_stat.st_mode)) {
            _isDirectory = true;
            _d = opendir(temp);
            if(!_d) {
                log_e("opendir(%s) failed", temp);
            }
        } else {
            log_e("Unknown type 0x%08X for file %s", ((_stat.st_mode)&_IFMT), temp);
        }
    } else {
        //file not found
        if(!mode || mode[0] == 'r') {
            //try to open as directory
            _d = opendir(temp);
            if(_d) {
                _isDirectory = true;
            } else {
                _isDirectory = false;
                //log_w("stat(%s) failed", temp);
            }
        } else {
            //lets create this new file
            _isDirectory = false;
            _f = fopen(temp, mode);
            if(!_f) {
                log_e("fopen(%s) failed", temp);
            }
            if(_f && (_stat.st_blksize == 0))
            {
                setvbuf(_f,NULL,_IOFBF,DEFAULT_FILE_BUFFER_SIZE);
            } 
        }
    }
    free(temp);
}

VFSFileImpl::~VFSFileImpl()
{
    close();
}

void VFSFileImpl::close()
{
    if(_path) {
        free(_path);
        _path = NULL;
    }
    if(_isDirectory && _d) {
        closedir(_d);
        _d = NULL;
        _isDirectory = false;
    } else if(_f) {
        fclose(_f);
        _f = NULL;
    }
}

VFSFileImpl::operator bool()
{
    return (_isDirectory && _d != NULL) || _f != NULL;
}

time_t VFSFileImpl::getLastWrite() {
    _getStat() ;
    return _stat.st_mtime;
}

void VFSFileImpl::_getStat() const
{
    if(!_path) {
        return;
    }
    char * temp = (char *)malloc(strlen(_path)+strlen(_fs->_mountpoint)+1);
    if(!temp) {
        return;
    }

    strcpy(temp, _fs->_mountpoint);
    strcat(temp, _path);

    if(!stat(temp, &_stat)) {
        _written = false;
    }
    free(temp);
}

size_t VFSFileImpl::write(const uint8_t *buf, size_t size)
{
    if(_isDirectory || !_f || !buf || !size) {
        return 0;
    }
    _written = true;
    return fwrite(buf, 1, size, _f);
}

size_t VFSFileImpl::read(uint8_t* buf, size_t size)
{
    if(_isDirectory || !_f || !buf || !size) {
        return 0;
    }

    return fread(buf, 1, size, _f);
}

void VFSFileImpl::flush()
{
    if(_isDirectory || !_f) {
        return;
    }
    fflush(_f);
    // workaround for https://github.com/espressif/arduino-esp32/issues/1293
    fsync(fileno(_f));
}

bool VFSFileImpl::seek(uint32_t pos, SeekMode mode)
{
    if(_isDirectory || !_f) {
        return false;
    }
    auto rc = fseek(_f, pos, mode);
    return rc == 0;
}

size_t VFSFileImpl::position() const
{
    if(_isDirectory || !_f) {
        return 0;
    }
    return ftell(_f);
}

size_t VFSFileImpl::size() const
{
    if(_isDirectory || !_f) {
        return 0;
    }
    if (_written) {
        _getStat();
    }
    return _stat.st_size;
}

/*
* Change size of files internal buffer used for read / write operations.
* Need to be called right after opening file before any other operation!
*/
bool VFSFileImpl::setBufferSize(size_t size)
{
    if(_isDirectory || !_f) {
        return 0;
    }
    int res = setvbuf(_f,NULL,_IOFBF,size);
    return res == 0;
}

const char* VFSFileImpl::path() const
{
    return (const char*) _path;
}

const char* VFSFileImpl::name() const
{
    return pathToFileName(path());
}

//to implement
boolean VFSFileImpl::isDirectory(void)
{
    return _isDirectory;
}

FileImplPtr VFSFileImpl::openNextFile(const char* mode)
{
    if(!_isDirectory || !_d) {
        return FileImplPtr();
    }
    struct dirent *file = readdir(_d);
    if(file == NULL) {
        return FileImplPtr();
    }
    if(file->d_type != DT_REG && file->d_type != DT_DIR) {
        return openNextFile(mode);
    }

    size_t pathLen = strlen(_path);
    size_t fileNameLen = strlen(file->d_name);
    char * name = (char *)malloc(pathLen+fileNameLen+2);

    if(name == NULL) {
        return FileImplPtr();
    }

    strcpy(name, _path);

    if ((file->d_name[0] != '/') && (_path[pathLen - 1] != '/'))
    {
        strcat(name, "/");
    }

    strcat(name, file->d_name);

    FileImplPtr fileImplPtr = std::make_shared<VFSFileImpl>(_fs, name, mode);
    free(name);
    return fileImplPtr;
}

boolean VFSFileImpl::seekDir(long position){
    if(!_d){
        return false;
    }
    seekdir(_d, position);
    return true;
}


String VFSFileImpl::getNextFileName()
{
    if (!_isDirectory || !_d) {
        return "";
    }
    struct dirent *file = readdir(_d);
    if (file == NULL) {
        return "";
    }
    if (file->d_type != DT_REG && file->d_type != DT_DIR) {
        return "";
    }
    String fname = String(file->d_name);
    String name = String(_path);
    if (!fname.startsWith("/") && !name.endsWith("/")) {
        name += "/";
    }
    name += fname;
    return name;
}

String VFSFileImpl::getNextFileName(bool *isDir)
{
    if (!_isDirectory || !_d) {
        return "";
    }
    struct dirent *file = readdir(_d);
    if (file == NULL) {
        return "";
    }
    if (file->d_type != DT_REG && file->d_type != DT_DIR) {
        return "";
    }
    String fname = String(file->d_name);
    String name = String(_path);
    if (!fname.startsWith("/") && !name.endsWith("/")) {
        name += "/";
    }
    name += fname;

    // check entry is a directory
    if (isDir) {
        *isDir = (file->d_type == DT_DIR);
    }
    return name;
}

void VFSFileImpl::rewindDirectory(void)
{
    if(!_isDirectory || !_d) {
        return;
    }
    rewinddir(_d);
}