/*
 * Copyright 2020 Bloomberg Finance LP
 *
 * 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 <buildboxcommon_direntwrapper.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <utility>

using namespace buildboxcommon;

DirentWrapper::DirentWrapper(std::string path)
    : d_dir(nullptr), d_entry(nullptr), d_path(std::move(path)), d_fd(-1),
      d_p_fd(-1)
{
    openDir();
}

DirentWrapper::DirentWrapper(const int dirfd, const std::string &path,
                             int flags)
    : d_dir(nullptr), d_entry(nullptr), d_path(path),
      d_fd(openat(dirfd, path.c_str(), flags | O_DIRECTORY)), d_p_fd(-1)
{

    if (d_fd < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening directory at path \""
                << d_path << "\" (dirfd=" << dirfd << ")");
    }

    d_dir = fdopendir(d_fd);
    if (d_dir == nullptr) {
        close(d_fd);
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening directory from file descriptor at path \"" << d_path
                                                                      << "\"");
    }
    next();
}

DirentWrapper::DirentWrapper(const int fd, const int p_fd, std::string path)
    : d_dir(fdopendir(fd)), d_entry(nullptr), d_path(std::move(path)),
      d_fd(fd), d_p_fd(p_fd)
{

    if (d_dir == nullptr) {
        close(fd);
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening directory from file descriptor at path \""
                << d_path + "\"");
    }
    next();
}

DirentWrapper::DirentWrapper(DIR *dir, std::string path)
    : d_dir(dir), d_entry(nullptr), d_path(std::move(path)), d_fd(-1)
{
    openDir();
}

bool DirentWrapper::currentEntryIsFile() const
{
    struct stat statResult{};
    bool ret_val = false;
    if (d_entry == nullptr) {
        return ret_val;
    }
    if (fstatat(d_fd, d_entry->d_name, &statResult, AT_SYMLINK_NOFOLLOW) ==
        0) {
        ret_val = S_ISREG(statResult.st_mode);
    }
    else {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Unable to stat entity \"" << d_entry->d_name << "\"");
    }
    return ret_val;
}

bool DirentWrapper::currentEntryIsDirectory() const
{
    struct stat statResult{};
    bool ret_val = false;
    if (d_entry == nullptr) {
        return ret_val;
    }
    if (fstatat(d_fd, d_entry->d_name, &statResult, AT_SYMLINK_NOFOLLOW) ==
        0) {
        ret_val = S_ISDIR(statResult.st_mode);
    }
    else {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Unable to stat entity \"" << d_entry->d_name << "\"");
    }
    return ret_val;
}

bool DirentWrapper::currentEntryIsSymlink() const
{
    struct stat statResult{};
    bool ret_val = false;
    if (d_entry == nullptr) {
        return ret_val;
    }
    if (fstatat(d_fd, d_entry->d_name, &statResult, AT_SYMLINK_NOFOLLOW) ==
        0) {
        ret_val = S_ISLNK(statResult.st_mode);
    }
    else {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Unable to stat entity \"" << d_entry->d_name << "\"");
    }
    return ret_val;
}

DirentWrapper DirentWrapper::nextDir() const
{
    int next_fd = this->openEntry(O_DIRECTORY | O_CLOEXEC);
    if (next_fd == -1) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Error getting dir from non-directory");
    }

    return DirentWrapper(next_fd, this->fd(), this->currentEntryPath());
}

const dirent *DirentWrapper::entry() const { return d_entry; }

int DirentWrapper::fd() const { return d_fd; }

int DirentWrapper::pfd() const { return d_p_fd; }

int DirentWrapper::openEntry(int flag) const
{
    if (d_entry == nullptr) {
        return -1;
    }

    int dfd = dirfd(d_dir);
    int fd = -1;
    if (dfd >= 0) {
        fd = openat(dfd, d_entry->d_name, flag);
    }
    if (fd < 0) {
        const std::string error_message =
            "Warning: when trying to open file descriptor representing path "
            "with "
            "openat: [" +
            this->d_path + "/" + d_entry->d_name + "] " + strerror(errno);
        BUILDBOX_LOG_WARNING(error_message);
    }
    return fd;
}

std::string DirentWrapper::path() const { return d_path; }

std::string DirentWrapper::currentEntryPath() const
{
    if (d_entry == nullptr) {
        return "";
    }
    return d_path + "/" + std::string(d_entry->d_name);
}

void DirentWrapper::operator++() { next(); }

DirentWrapper::~DirentWrapper()
{
    if (d_dir != nullptr) {
        // Close the directory entry;
        const int ret_val = closedir(d_dir);
        if (ret_val != 0) {
            const int errsv = errno;
            const std::string error_message = "Error closing directory: [" +
                                              d_path + "]: " + strerror(errsv);
            BUILDBOX_LOG_WARNING(error_message);
        }
    }
}

void DirentWrapper::openDir()
{
    // Get DIR pointer.
    if (d_dir == nullptr) {
        d_dir = opendir(d_path.c_str());
        if (d_dir == nullptr) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error opening directory \"" << d_path + "\"");
        }
    }
    // Get directory file descriptor.
    if (d_fd < 0) {
        d_fd = dirfd(d_dir);
        if (d_fd < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error opening directory file descriptor at path \"" << d_path
                                                                     << "\"");
        }
        this->next();
    }
}

void DirentWrapper::next()
{
    // Skip "." and ".." if d_entry is defined.
    do {
        errno = 0;
        d_entry = readdir(d_dir);
        if (errno != 0 && d_entry == nullptr) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error reading from directory \"" << d_path << "\"");
        }
    } while (d_entry != nullptr && (strcmp(d_entry->d_name, ".") == 0 ||
                                    strcmp(d_entry->d_name, "..") == 0));
}
