/*
 * Copyright (c) 2021 Huawei Device Co., 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 "directory_ex.h"
#include <dirent.h>
#include <cerrno>
#include <fcntl.h>
#include <stack>
#include "securec.h"
#include "unistd.h"
#include "utils_log.h"
using namespace std;

namespace OHOS {

#ifdef UTILS_CXX_RUST
rust::String RustGetCurrentProcFullFileName()
{
    return rust::String(GetCurrentProcFullFileName());
}

rust::String RustGetCurrentProcPath()
{
    return rust::String(GetCurrentProcPath());
}

rust::String RustExtractFilePath(const rust::String& fileFullName)
{
    std::string tmpName = std::string(fileFullName);
    return rust::String(ExtractFilePath(tmpName));
}

rust::String RustExtractFileName(const rust::String& fileFullName)
{
    std::string tmpName = std::string(fileFullName);
    return rust::String(ExtractFileName(tmpName));
}

rust::String RustExtractFileExt(const rust::String& fileName)
{
    std::string tmpName = std::string(fileName);
    return rust::String(ExtractFileExt(tmpName));
}

rust::String RustExcludeTrailingPathDelimiter(const rust::String& path)
{
    std::string tmpPath = std::string(path);
    return rust::String(ExcludeTrailingPathDelimiter(tmpPath));
}

rust::String RustIncludeTrailingPathDelimiter(const rust::String& path)
{
    std::string tmpPath = std::string(path);
    return rust::String(IncludeTrailingPathDelimiter(tmpPath));
}

bool RustPathToRealPath(const rust::String& path, rust::String& realPath)
{
    std::string tmpPath = std::string(path);
    std::string tmpResolved;

    if (PathToRealPath(tmpPath, tmpResolved)) {
        realPath = tmpResolved;
        return true;
    }

    return false;
}

void RustGetDirFiles(const rust::String& path, rust::vec<rust::String>& files)
{
    std::string tmpPath(path);
    std::vector<std::string> tmpFiles(files.begin(), files.end());
    GetDirFiles(tmpPath, tmpFiles);
    std::copy(tmpFiles.begin(), tmpFiles.end(), std::back_inserter(files));
}
#endif

string GetCurrentProcFullFileName()
{
    char procFile[PATH_MAX + 1] = {0};
    int ret = readlink("/proc/self/exe", procFile, PATH_MAX);
    if (ret < 0 || ret > PATH_MAX) {
        UTILS_LOGD("Get proc name failed, ret is: %{public}d!", ret);
        return string();
    }
    procFile[ret] = '\0';
    return string(procFile);
}

string GetCurrentProcPath()
{
    return ExtractFilePath(GetCurrentProcFullFileName());
}

string ExtractFilePath(const string& fileFullName)
{
    return string(fileFullName).substr(0, fileFullName.rfind("/") + 1);
}

std::string ExtractFileName(const std::string& fileFullName)
{
    return string(fileFullName).substr(fileFullName.rfind("/") + 1, fileFullName.size());
}

string ExtractFileExt(const string& fileName)
{
    string::size_type pos = fileName.rfind(".");
    if (pos == string::npos) {
        return "";
    }

    return string(fileName).substr(pos + 1, fileName.size());
}

string ExcludeTrailingPathDelimiter(const std::string& path)
{
    if (path.rfind("/") != path.size() - 1) {
        return path;
    }

    if (!path.empty()) {
        return path.substr(0, (int)path.size() - 1);
    }

    return path;
}

string IncludeTrailingPathDelimiter(const std::string& path)
{
    if (path.rfind("/") != path.size() - 1) {
        return path + "/";
    }

    return path;
}

void GetDirFiles(const string& path, vector<string>& files)
{
    string pathStringWithDelimiter;
    DIR *dir = opendir(path.c_str());
    if (dir == nullptr) {
        return;
    }

    while (true) {
        struct dirent *ptr = readdir(dir);
        if (ptr == nullptr) {
            break;
        }

        // current dir or parent dir
        if ((strcmp(ptr->d_name, ".") == 0) || (strcmp(ptr->d_name, "..") == 0)) {
            continue;
        } else if (ptr->d_type == DT_DIR) {
            pathStringWithDelimiter = IncludeTrailingPathDelimiter(path) + string(ptr->d_name);
            GetDirFiles(pathStringWithDelimiter, files);
        } else {
            files.push_back(IncludeTrailingPathDelimiter(path) + string(ptr->d_name));
        }
    }
    closedir(dir);
}

bool ForceCreateDirectory(const string& path)
{
    string::size_type index = 0;
    do {
        string subPath;
        index = path.find('/', index + 1);
        if (index == string::npos) {
            subPath = path;
        } else {
            subPath = path.substr(0, index);
        }

        if (access(subPath.c_str(), F_OK) != 0) {
            if (mkdir(subPath.c_str(), (S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)) != 0 && errno != EEXIST) {
                return false;
            }
        }
    } while (index != string::npos);

    return access(path.c_str(), F_OK) == 0;
}

struct DirectoryNode {
    DIR *dir;
    int currentFd;
    char name[256]; // the same max char length with d_name in struct dirent
};

bool ForceRemoveDirectory(const string& path)
{
    bool ret = true;
    int strRet;
    DIR *dir = opendir(path.c_str());
    if (dir == nullptr) {
        UTILS_LOGD("Failed to open root dir: %{public}s: %{public}s ", path.c_str(), strerror(errno));
        return false;
    }
    stack<DIR *> traversStack;
    stack<DirectoryNode> removeStack;
    traversStack.push(dir);
    while (!traversStack.empty()) {
        DIR *currentDir = traversStack.top();
        traversStack.pop();
        DirectoryNode node;
        int currentFd = dirfd(currentDir);
        if (currentFd < 0) {
            UTILS_LOGD("Failed to get dirfd, fd: %{public}d: %{public}s ", currentFd, strerror(errno));
            ret = false;
            continue;
        }

        while (true) {
            struct dirent *ptr = readdir(currentDir);
            if (ptr == nullptr) {
                break;
            }
            const char *name = ptr->d_name;
            // current dir or parent dir
            if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
                continue;
            }

            if (ptr->d_type == DT_DIR) {
                int subFd = openat(currentFd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
                if (subFd < 0) {
                    UTILS_LOGD("Failed in subFd openat: %{public}s ", name);
                    ret = false;
                    continue;
                }
                DIR *subDir = fdopendir(subFd);
                if (subDir == nullptr) {
                    close(subFd);
                    UTILS_LOGD("Failed in fdopendir: %{public}s", strerror(errno));
                    ret = false;
                    continue;
                }
                node.dir = subDir;
                node.currentFd = currentFd;
                strRet = strcpy_s(node.name, sizeof(node.name), name);
                if (strRet != EOK) {
                    UTILS_LOGE("Failed to exec strcpy_s, name= %{public}s, strRet= %{public}d", name, strRet);
                }
                removeStack.push(node);
                traversStack.push(subDir);
            } else {
                if (faccessat(currentFd, name, F_OK, AT_SYMLINK_NOFOLLOW) == 0) {
                    if (unlinkat(currentFd, name, 0) < 0) {
                        UTILS_LOGD("Couldn't unlinkat subFile %{public}s: %{public}s", name, strerror(errno));
                        ret = false;
                        break;
                    }
                } else {
                    UTILS_LOGD("Access to file: %{public}s is failed", name);
                    ret = false;
                    break;
                }
            }
        }
    }
    if (!ret) {
        UTILS_LOGD("Failed to remove some subfile under path: %{public}s", path.c_str());
    }
    while (!removeStack.empty()) {
        DirectoryNode node = removeStack.top();
        removeStack.pop();
        closedir(node.dir);
        if (unlinkat(node.currentFd, node.name, AT_REMOVEDIR) < 0) {
            UTILS_LOGD("Couldn't unlinkat subDir %{public}s: %{public}s", node.name, strerror(errno));
            continue;
        }
    }
    closedir(dir);
    if (faccessat(AT_FDCWD, path.c_str(), F_OK, AT_SYMLINK_NOFOLLOW) == 0) {
        if (remove(path.c_str()) != 0) {
            UTILS_LOGD("Failed to remove root dir: %{public}s: %{public}s ", path.c_str(), strerror(errno));
            return false;
        }
    }
    return faccessat(AT_FDCWD, path.c_str(), F_OK, AT_SYMLINK_NOFOLLOW) != 0;
}

bool RemoveFile(const string& fileName)
{
    if (access(fileName.c_str(), F_OK) == 0) {
        return remove(fileName.c_str()) == 0;
    }

    return true;
}

bool IsEmptyFolder(const string& path)
{
    vector<string> files;
    GetDirFiles(path, files);
    return files.empty();
}

uint64_t GetFolderSize(const string& path)
{
    vector<string> files;
    struct stat statbuf = {0};
    GetDirFiles(path, files);
    uint64_t totalSize = 0;
    for (auto& file : files) {
        if (stat(file.c_str(), &statbuf) == 0) {
            totalSize += statbuf.st_size;
        }
    }

    return totalSize;
}

// inner function, and param is legitimate
bool ChangeMode(const string& fileName, const mode_t& mode)
{
    return (chmod(fileName.c_str(), mode) == 0);
}

bool ChangeModeFile(const string& fileName, const mode_t& mode)
{
    if (access(fileName.c_str(), F_OK) != 0) {
        return false;
    }

    return ChangeMode(fileName, mode);
}

bool ChangeModeDirectory(const string& path, const mode_t& mode)
{
    string subPath;
    bool ret = true;
    DIR *dir = opendir(path.c_str());
    if (dir == nullptr) {
        return false;
    }

    while (true) {
        struct dirent *ptr = readdir(dir);
        if (ptr == nullptr) {
            break;
        }

        // current dir or parent dir
        if (strcmp(ptr->d_name, ".") == 0 || strcmp(ptr->d_name, "..") == 0) {
            continue;
        }
        subPath = IncludeTrailingPathDelimiter(path) + string(ptr->d_name);
        if (ptr->d_type == DT_DIR) {
            ret = ChangeModeDirectory(subPath, mode);
        } else {
            if (access(subPath.c_str(), F_OK) == 0) {
                if (!ChangeMode(subPath, mode)) {
                    UTILS_LOGD("Failed to exec ChangeMode");
                    closedir(dir);
                    return false;
                }
            }
        }
    }
    closedir(dir);
    string currentPath = ExcludeTrailingPathDelimiter(path);
    if (access(currentPath.c_str(), F_OK) == 0) {
        if (!ChangeMode(currentPath, mode)) {
            UTILS_LOGD("Failed to exec ChangeMode");
            return false;
        }
    }
    return ret;
}

bool PathToRealPath(const string& path, string& realPath)
{
    if (path.empty()) {
        UTILS_LOGD("path is empty!");
        return false;
    }

    if ((path.length() >= PATH_MAX)) {
        UTILS_LOGD("path len is error, the len is: [%{public}zu]", path.length());
        return false;
    }

    char tmpPath[PATH_MAX] = {0};
    if (realpath(path.c_str(), tmpPath) == nullptr) {
        UTILS_LOGD("path to realpath error");
        return false;
    }

    realPath = tmpPath;
    if (access(realPath.c_str(), F_OK) != 0) {
        UTILS_LOGD("check realpath (%{private}s) error", realPath.c_str());
        return false;
    }
    return true;
}

#if defined(IOS_PLATFORM) || defined(_WIN32)
string TransformFileName(const string& fileName)
{
    string::size_type pos = fileName.find(".");
    string transformfileName = "";
    if (pos == string::npos) {
        transformfileName = fileName;

#ifdef _WIN32
        transformfileName = transformfileName.append(".dll");
#elif defined IOS_PLATFORM
        transformfileName = transformfileName.append(".dylib");
#endif

        return transformfileName;
    } else {
        transformfileName = string(fileName).substr(0, pos + 1);

#ifdef _WIN32
        transformfileName = transformfileName.append("dll");
#elif defined IOS_PLATFORM
        transformfileName = transformfileName.append("dylib");
#endif

        return transformfileName;
    }
}
#endif

} // OHOS