/* -*- mode: c++ -*-
 * Copyright 2025 Dracal Technologies Inc. All rights reserved.
 */

#include <dracal/common/fs.hpp>
#include <dracal/common/log.hpp>
#include <dracal/common/platform.hpp>
#include <dracal/common/string.hpp>

#include <algorithm>
#include <fstream>

#if defined(DRACAL_LINUX) || defined(DRACAL_APPLE)
    #include <cerrno>
    #include <cstring>
    #include <fcntl.h>
    #include <unistd.h>
#endif

namespace dracal::common {

bool touch(const std::string &filename, const bool overwrite, const bool quiet) noexcept {
    try {
        const fs::path complete_filename = filename;
        std::ofstream f;
        f.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        const auto mode = overwrite ? (std::ios::out | std::ios::trunc) : (std::ios::out | std::ios::app);
        f.open(complete_filename, mode | std::ios::binary);
        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (const fs::filesystem_error &err) {
        if (!quiet) {
            dracal_warn("filesystem error touch file({}): {}", filename, err.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool touch(const fs::path &path, const bool overwrite, const bool quiet) noexcept {
    try {
        std::ofstream f;
        f.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        const auto mode = overwrite ? (std::ios::out | std::ios::trunc) : (std::ios::out | std::ios::app);
        f.open(path, mode | std::ios::binary);
        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", path.string(), fail.what());
        }
        return false;
    } catch (const fs::filesystem_error &err) {
        if (!quiet) {
            dracal_warn("filesystem error touch file({}): {}", path.string(), err.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool touch(std::ofstream &file, const std::string &filename, const bool overwrite, const bool quiet) noexcept {
    try {
        file.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        const auto mode = overwrite ? (std::ios::out | std::ios::trunc) : (std::ios::out | std::ios::app);
        file.open(filename, mode | std::ios::binary);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool touch(std::ofstream &file, const fs::path &path, const bool overwrite, const bool quiet) noexcept {
    try {
        file.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        const auto mode = overwrite ? (std::ios::out | std::ios::trunc) : (std::ios::out | std::ios::app);
        file.open(path, mode | std::ios::binary);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", path.string(), fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool touch(std::ifstream &file, const std::string &filename, const bool quiet) noexcept {
    try {
        file.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        file.open(filename, std::ios::in | std::ios::binary);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool open(std::ifstream &file, size_t &size, const std::string &filename, const bool quiet) noexcept {
    try {
        fs::path path = fs::path(filename);
        file.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        file.open(path, std::ios::in | std::ios::binary);
        size = fs::file_size(path);

    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool open(std::ifstream &file, size_t &size, const fs::path &path, const bool quiet) noexcept {
    try {
        file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        file.open(path, std::ios::in | std::ios::binary);
        size = fs::file_size(path);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to touch file({}) with error: ({})", path.string(), fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool write_append(std::ofstream &file, const std::vector<std::string> &lines, const bool quiet) noexcept {
    try {
#if defined(DRACAL_WINDOWS)
        const std::string all_lines = str::join(lines, "\r\n", true);
#else
        const std::string all_lines = str::join(lines, "\n", true);
#endif

        file.write(all_lines.c_str(), static_cast<std::streamsize>(all_lines.size()));
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to append to file with error: ({})", fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

template <typename buffer>
bool read_buffer(std::ifstream &file, size_t &read_size, buffer &data, const bool quiet) noexcept {
    try {
        static_assert(std::is_trivially_copyable_v<buffer>, "buffer type must be plain old data (trivially copyable)");

        if (!file.is_open()) {
            if (!quiet) {
                dracal_warn("read_append: file is not open");
            }
            return false;
        }

        const auto bytes_needed = static_cast<std::streamsize>(sizeof(buffer));

        file.read(reinterpret_cast<char *>(&data), bytes_needed);

        if (file.gcount() != bytes_needed) {
            if (!quiet) {
                dracal_warn("read_append: size mismatch: expected {} bytes, read {}", bytes_needed, file.gcount());
            }
            return false;
        }

        read_size += static_cast<size_t>(bytes_needed);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("read_append: failed with error: ({})", fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("read_append: an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

template bool read_buffer<int8_t>(std::ifstream &file, size_t &read_size, int8_t &data, bool quiet) noexcept;
template bool read_buffer<uint8_t>(std::ifstream &file, size_t &read_size, uint8_t &data, bool quiet) noexcept;
template bool read_buffer<uint16_t>(std::ifstream &file, size_t &read_size, uint16_t &data, bool quiet) noexcept;
template bool read_buffer<uint32_t>(std::ifstream &file, size_t &read_size, uint32_t &data, bool quiet) noexcept;
template bool read_buffer<char>(std::ifstream &file, size_t &read_size, std::vector<char> &vec, bool quiet) noexcept;
template bool read_buffer<int8_t>(std::ifstream &file, size_t &read_size, std::vector<int8_t> &vec,
                                  bool quiet) noexcept;

template <> bool read_buffer(std::ifstream &file, size_t &read_size, std::string &str, const bool quiet) noexcept {
    try {
        if (!file.is_open()) {
            if (!quiet)
                dracal_warn("read_buffer: file is not open");
            return false;
        }

        const auto bytes_needed = static_cast<std::streamsize>(str.size());

        file.read(str.data(), bytes_needed);

        if (file.gcount() != bytes_needed) {
            if (!quiet)
                dracal_warn("read_buffer: size mismatch: expected {}, read {}", bytes_needed, file.gcount());
            return false;
        }

        read_size += static_cast<size_t>(bytes_needed);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet)
            dracal_warn("read_buffer: failed with error: ({})", fail.what());
        return false;
    } catch (...) {
        if (!quiet)
            dracal_warn("read_buffer: an unexpected exception was thrown");
        return false;
    }

    return true;
}

template <typename T>
bool read_buffer(std::ifstream &file, size_t &read_size, std::vector<T> &vec, const bool quiet) noexcept {
    static_assert(std::is_trivially_copyable_v<T>, "T must be plain old data (trivially copyable)");

    try {
        if (!file.is_open()) {
            if (!quiet)
                dracal_warn("read_buffer: file is not open");
            return false;
        }

        const auto bytes_needed = static_cast<std::streamsize>(vec.size() * sizeof(T));

        file.read(reinterpret_cast<char *>(vec.data()), bytes_needed);

        if (file.gcount() != bytes_needed) {
            if (!quiet)
                dracal_warn("read_buffer: size mismatch: expected {}, read {}", bytes_needed, file.gcount());
            return false;
        }

        read_size += static_cast<size_t>(bytes_needed);
    } catch (const std::ios_base::failure &fail) {
        if (!quiet)
            dracal_warn("read_buffer: failed with error: ({})", fail.what());
        return false;
    } catch (...) {
        if (!quiet)
            dracal_warn("read_buffer: an unexpected exception was thrown");
        return false;
    }

    return true;
}

bool close(std::ofstream &file, const std::string &filename, const bool quiet) noexcept {
    try {
        file.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to close file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool close(std::ofstream &file, const fs::path &filename, const bool quiet) noexcept {
    try {
        file.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to close file({}) with error: ({})", filename.string(), fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool close(std::ifstream &file, const std::string &filename, const bool quiet) noexcept {
    try {
        file.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to close file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool close(std::ifstream &file, const fs::path &filename, const bool quiet) noexcept {
    try {
        file.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to close file({}) with error: ({})", filename.string(), fail.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool read_file(const std::string &filename, std::string &text, const bool quiet) noexcept {
    try {
        const fs::path complete_filename = filename;

        std::ifstream file;
        file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        file.open(complete_filename, std::ios::in | std::ios::binary);

        const auto size = fs::file_size(complete_filename);
        text.resize(static_cast<size_t>(size));
        file.read(text.data(), static_cast<std::streamsize>(size));

        if (file.gcount() != static_cast<std::streamsize>(size)) {
            if (!quiet) {
                dracal_warn("file({}) size mismatch: expected {} bytes, read {}", complete_filename.string(), size,
                            file.gcount());
            }
            return false;
        }

        file.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to read file({}) with error: ({})", filename, fail.what());
        }
        return false;
    } catch (const fs::filesystem_error &err) {
        if (!quiet) {
            dracal_warn("filesystem error reading file({}): {}", filename, err.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool read_file(const fs::path &file, std::string &text, const bool quiet) noexcept {
    try {
        std::ifstream f;
        f.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        f.open(file, std::ios::in | std::ios::binary);

        const auto size = fs::file_size(file);
        text.resize(static_cast<size_t>(size));
        f.read(text.data(), static_cast<std::streamsize>(size));

        if (f.gcount() != static_cast<std::streamsize>(size)) {
            if (!quiet) {
                dracal_warn("file({}) size mismatch: expected {} bytes, read {}", file.string(), size, f.gcount());
            }
            return false;
        }

        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to read file({}) with error: ({})", file.string(), fail.what());
        }
        return false;
    } catch (const fs::filesystem_error &err) {
        if (!quiet) {
            dracal_warn("filesystem error reading file({}): {}", file.string(), err.what());
        }
        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }
        return false;
    }

    return true;
}

bool write_file(const std::string &filename, const std::string &text, const bool quiet) noexcept {
    try {
        fs::path file(filename);
        std::ofstream f;
        f.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        f.open(file, std::ios::out);
        f.write(text.c_str(), static_cast<std::streamsize>(text.size()));
        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to write to file({}) with error: ({})", filename, fail.what());
        }

        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }

        return false;
    }

    return true;
}

bool write_file(const std::string &filename, const std::vector<std::string> &lines, const bool add_newlines,
                const bool quiet) noexcept {
    try {
        fs::path file(filename);
        std::ofstream f;
        f.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        f.open(file, std::ios::out);

        for (size_t i = 0; i < lines.size(); ++i) {
            f.write(lines[i].c_str(), static_cast<std::streamsize>(lines[i].size()));
            if (add_newlines) {
                f.put('\n');
            }
        }

        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to write to file({}) with error: ({})", filename, fail.what());
        }

        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }

        return false;
    }

    return true;
}

bool write_file(const fs::path &file, const std::string &text, const bool quiet) noexcept {
    try {
        std::ofstream f;
        f.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        f.open(file, std::ios::out);
        f.write(text.c_str(), static_cast<std::streamsize>(text.size()));
        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to write to file({}) with error: ({})", file.string(), fail.what());
        }

        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }

        return false;
    }

    return true;
}

bool write_file(const fs::path &file, const std::vector<std::string> &lines, const bool add_newlines,
                const bool quiet) noexcept {
    try {
        std::ofstream f;
        f.exceptions(std::ofstream::failbit | std::ofstream::badbit);
        f.open(file, std::ios::out);

        for (size_t i = 0; i < lines.size(); ++i) {
            f.write(lines[i].c_str(), static_cast<std::streamsize>(lines[i].size()));
            if (add_newlines) {
                f.put('\n');
            }
        }

        f.close();
    } catch (const std::ios_base::failure &fail) {
        if (!quiet) {
            dracal_warn("failed to write to file({}) with error: ({})", file.string(), fail.what());
        }

        return false;
    } catch (...) {
        if (!quiet) {
            dracal_warn("an unexpected exception was thrown");
        }

        return false;
    }

    return true;
}

#if defined(DRACAL_LINUX) || defined(DRACAL_APPLE)

bool write_printer(const std::string &device_path, const std::vector<std::string> &lines, const bool quiet) noexcept {
    int fd = ::open(device_path.c_str(), O_RDWR);
    if (fd == -1) {
        if (!quiet) {
            dracal_warn("failed to open printer device({}) with error: {}", device_path, strerror(errno));
        }
        return false;
    }

    bool success = true;

    for (const auto &line : lines) {
        if (!line.empty()) {
            ssize_t written = ::write(fd, line.c_str(), line.size());
            if (written == -1) {
                if (!quiet) {
                    dracal_warn("failed to write to printer device({}) with error: {}", device_path, strerror(errno));
                }
                success = false;
                break;
            }

            if (static_cast<size_t>(written) != line.size()) {
                if (!quiet) {
                    dracal_warn("partial write to printer device({}): wrote {} of {} bytes", device_path, written,
                                line.size());
                }
                success = false;
                break;
            }
        }
    }

    if (::close(fd) == -1) {
        if (!quiet) {
            dracal_warn("failed to close printer device({}) with error: {}", device_path, strerror(errno));
        }
    }

    return success;
}

#elif defined(DRACAL_WINDOWS)

bool write_printer(const std::string &device_path, const std::vector<std::string> &lines, const bool quiet) noexcept {
    dracal_warn("not implemented");
    return false;
}

#endif

} // namespace dracal::common
