/*****************************************************************************
 * Copyright (c) 2014-2026 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD__) || defined(__NetBSD__)              \
    || defined(__HAIKU__)

    #include "Platform.h"

    #include "../Date.h"
    #include "../Diagnostic.h"
    #include "../core/Memory.hpp"
    #include "../core/Path.hpp"
    #include "../core/String.hpp"

    #include <cerrno>
    #include <clocale>
    #include <cstdlib>
    #include <cstring>
    #include <ctime>
    #include <dirent.h>
    #include <fcntl.h>
    #include <fnmatch.h>
    #include <locale>
    #include <pwd.h>
    #include <sys/stat.h>
    #include <sys/time.h>
    #include <sys/wait.h>
    #include <unistd.h>

// The name of the mutex used to prevent multiple instances of the game from running
static constexpr const utf8* kSingleInstanceMutexName = u8"openrct2.lock";

namespace OpenRCT2::Platform
{
    std::string GetEnvironmentVariable(std::string_view name)
    {
        return String::toStd(getenv(std::string(name).c_str()));
    }

    std::string GetEnvironmentPath(const char* name)
    {
        auto value = getenv(name);
        if (value == nullptr)
        {
            return std::string();
        }

        auto colon = std::strchr(value, ':');
        if (colon == nullptr)
        {
            return std::string(value);
        }

        return std::string(value, colon);
    }

    std::string GetHomePath()
    {
        std::string path;
        auto pw = getpwuid(getuid());
        if (pw != nullptr)
        {
            path = pw->pw_dir;
        }
        else
        {
            path = GetEnvironmentVariable("HOME");
        }
        if (path.empty())
        {
            path = "/";
        }
        return path;
    }

    std::string FormatShortDate(std::time_t timestamp)
    {
        setlocale(LC_TIME, "");
        char date[20];
        std::strftime(date, sizeof(date), "%x", std::localtime(&timestamp));
        return std::string(date);
    }

    std::string FormatTime(std::time_t timestamp)
    {
        setlocale(LC_TIME, "");
        char time[20];
        std::strftime(time, sizeof(time), "%X", std::localtime(&timestamp));
        return std::string(time);
    }

    bool IsColourTerminalSupported()
    {
        static bool hasChecked = false;
        static bool isSupported = false;
        if (!hasChecked)
        {
            auto term = GetEnvironmentVariable("TERM");
            isSupported = term != "cons25" && term != "dumb" && term != "emacs";
            hasChecked = true;
        }
        return isSupported;
    }

    bool IsRunningInWine()
    {
        return false;
    }

    bool FindApp(std::string_view app, std::string* output)
    {
        auto tryPath = [&](const std::string& path) -> bool {
            const char* args[] = { path.c_str(), "--version", nullptr };
            if (Execute(args, nullptr) == 0)
            {
                if (output)
                    *output = path;
                return true;
            }
            return false;
        };

        std::string appStr = std::string(app);
        if (tryPath(appStr))
            return true;

    #ifdef __APPLE__
        // Apps on macOS don't inherit the shell PATH, so check common Homebrew install locations as a fallback.
        for (const char* prefix : { "/opt/homebrew/bin/", "/usr/local/bin/" })
        {
            if (tryPath(std::string(prefix) + appStr))
                return true;
        }
    #endif

        return false;
    }

    int32_t Execute(const char* args[], std::string* output)
    {
        // Build the command string from args for logging
        std::string commandLine;
        if (args && args[0])
        {
            for (size_t i = 0; args[i]; ++i)
            {
                if (i > 0)
                    commandLine += " ";
                commandLine += args[i];
            }
        }
    #ifndef __EMSCRIPTEN__
        int status;
        pid_t pid1;

        LOG_INFO("Executing command: %s", commandLine.c_str());
        int fd_pipe[2];
        if (pipe(fd_pipe) != 0)
        { /* create a pipe */
            return -1;
        }

        pid1 = fork();
        if (pid1 == 0)
        {                      /* child process */
            close(fd_pipe[0]); /* no reading from pipe */
            /* write stdout in pipe */
            if (dup2(fd_pipe[1], STDOUT_FILENO) == -1)
            {
                _exit(128);
            }

            /* const casting argv is fine:
             * https://pubs.opengroup.org/onlinepubs/9699919799/functions/fexecve.html -> rational
             */
            execvp(args[0], const_cast<char**>(args));
            _exit(129);
        }
        else if (pid1 < 0)
        { /* fork() failed */
            return -128;
        }
        else
        {                      /* parent process */
            close(fd_pipe[1]); /* no writing to the pipe */
            if (waitpid(pid1, &status, 0) != pid1)
            {
                return -errno;
            }

            if (!WIFEXITED(status))
            {
                return -129;
            }

            if (WEXITSTATUS(status) >= 128)
            {
                return -130 - WEXITSTATUS(status);
            }

            // Optionally, read output from fd_pipe[0] if output != nullptr
            if (output != nullptr)
            {
                std::vector<char> outputBuffer;
                char buffer[1024];
                ssize_t readBytes;
                while ((readBytes = read(fd_pipe[0], buffer, sizeof(buffer))) > 0)
                {
                    outputBuffer.insert(outputBuffer.end(), buffer, buffer + readBytes);
                }
                // Trim line breaks
                size_t outputLength = outputBuffer.size();
                while (outputLength > 0 && outputBuffer[outputLength - 1] == '\n')
                {
                    --outputLength;
                }
                *output = std::string(outputBuffer.data(), outputLength);
            }
            close(fd_pipe[0]);
            return 0; /* success! */
        }
    #else
        LOG_WARNING("Emscripten cannot execute processes. The commandline was '%s'.", commandLine.c_str());
        return -1;
    #endif // __EMSCRIPTEN__
    }

    #ifndef __ANDROID__
    uint64_t GetLastModified(std::string_view path)
    {
        uint64_t lastModified = 0;
        struct stat statInfo{};
        if (stat(std::string(path).c_str(), &statInfo) == 0)
        {
            lastModified = statInfo.st_mtime;
        }
        return lastModified;
    }

    uint64_t GetFileSize(std::string_view path)
    {
        uint64_t size = 0;
        struct stat statInfo{};
        if (stat(std::string(path).c_str(), &statInfo) == 0)
        {
            size = statInfo.st_size;
        }
        return size;
    }
    #endif

    bool ShouldIgnoreCase()
    {
        return false;
    }

    bool IsPathSeparator(char c)
    {
        return c == '/';
    }

    std::string ResolveCasing(std::string_view path, bool fileExists)
    {
        std::string result;
        if (fileExists)
        {
            // Windows is case insensitive so it will exist and that is all that matters
            // for now. We can properly resolve the casing if we ever need to.
            result = path;
        }
        else
        {
            std::string fileName = Path::GetFileName(path);
            std::string directory = Path::GetDirectory(path);

            struct dirent** files;
            auto count = scandir(directory.c_str(), &files, nullptr, alphasort);
            if (count != -1)
            {
                // Find a file which matches by name (case insensitive)
                for (int32_t i = 0; i < count; i++)
                {
                    if (String::iequals(files[i]->d_name, fileName.c_str()))
                    {
                        result = Path::Combine(directory, std::string(files[i]->d_name));
                        break;
                    }
                }

                // Free memory
                for (int32_t i = 0; i < count; i++)
                {
                    free(files[i]);
                }
                free(files);
            }
        }
        return result;
    }

    bool RequireNewWindow(bool openGL)
    {
        return true;
    }

    std::string GetUsername()
    {
        std::string result;
        auto pw = getpwuid(getuid());
        if (pw != nullptr)
        {
            result = std::string(pw->pw_name);
        }
        return result;
    }

    uint8_t GetLocaleDateFormat()
    {
        const std::time_base::dateorder dateorder = std::use_facet<std::time_get<char>>(std::locale()).date_order();

        switch (dateorder)
        {
            case std::time_base::mdy:
                return DATE_FORMAT_MONTH_DAY_YEAR;

            case std::time_base::ymd:
                return DATE_FORMAT_YEAR_MONTH_DAY;

            case std::time_base::ydm:
                return DATE_FORMAT_YEAR_DAY_MONTH;

            default:
                return DATE_FORMAT_DAY_MONTH_YEAR;
        }
    }

    TemperatureUnit GetLocaleTemperatureFormat()
    {
    // LC_MEASUREMENT is GNU specific.
    #ifdef LC_MEASUREMENT
        const char* langstring = setlocale(LC_MEASUREMENT, "");
    #else
        const char* langstring = setlocale(LC_ALL, "");
    #endif

        if (langstring != nullptr)
        {
            if (!fnmatch("*_US*", langstring, 0) || !fnmatch("*_BS*", langstring, 0) || !fnmatch("*_BZ*", langstring, 0)
                || !fnmatch("*_PW*", langstring, 0))
            {
                return TemperatureUnit::Fahrenheit;
            }
        }
        return TemperatureUnit::Celsius;
    }

    bool ProcessIsElevated()
    {
    #ifndef __EMSCRIPTEN__
        return (geteuid() == 0);
    #else
        return false;
    #endif // __EMSCRIPTEN__
    }

    bool LockSingleInstance()
    {
        // We will never close this file manually. The operating system will
        // take care of that, because flock keeps the lock as long as the
        // file is open and closes it automatically on file close.
        // This is intentional.
        int32_t pidFile = open(kSingleInstanceMutexName, O_CREAT | O_RDWR, 0666);

        if (pidFile == -1)
        {
            LOG_WARNING("Cannot open lock file for writing.");
            return false;
        }

        struct flock lock;

        lock.l_start = 0;
        lock.l_len = 0;
        lock.l_type = F_WRLCK;
        lock.l_whence = SEEK_SET;

        if (fcntl(pidFile, F_SETLK, &lock) == -1)
        {
            if (errno == EWOULDBLOCK)
            {
                LOG_WARNING("Another OpenRCT2 session has been found running.");
                return false;
            }
            LOG_ERROR("flock returned an uncatched errno: %d", errno);
            return false;
        }
        return true;
    }

    int32_t GetDrives()
    {
        // POSIX systems do not know drives. Return 0.
        return 0;
    }

    #ifndef __ANDROID__
    time_t FileGetModifiedTime(u8string_view path)
    {
        struct stat buf;
        if (stat(u8string(path).c_str(), &buf) == 0)
        {
            return buf.st_mtime;
        }
        return 100;
    }
    #endif

    datetime64 GetDatetimeNowUTC()
    {
        const datetime64 epochAsTicks = 621355968000000000;

        struct timeval tv;
        gettimeofday(&tv, NULL);

        // Epoch starts from: 1970-01-01T00:00:00Z
        // Convert to ticks from 0001-01-01T00:00:00Z
        uint64_t utcEpochTicks = static_cast<uint64_t>(tv.tv_sec) * 10000000uLL + tv.tv_usec * 10;
        datetime64 utcNow = epochAsTicks + utcEpochTicks;
        return utcNow;
    }
} // namespace OpenRCT2::Platform

#endif
