#include "il2cpp-config.h"

#if IL2CPP_TARGET_POSIX && !IL2CPP_USE_PLATFORM_SPECIFIC_CONSOLE

#include "os/Console.h"
#include "os/File.h"

#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>

namespace il2cpp
{
namespace os
{
namespace Console
{
    static bool setupComplete = false;
    static int32_t s_terminalSize;
    static struct termios s_initialAttr;
    static struct termios s_il2cppAttr;
    static std::string s_keypadXmit;
    static std::string s_teardown;
    static struct sigaction s_saveSigcont, s_saveSigint, s_saveSigwinch;

    static int32_t GetTerminalSize()
    {
        struct winsize ws;

        if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0)
            return (ws.ws_col << 16) | ws.ws_row;

        return -1;
    }

    static bool SetProperty(int32_t property, bool value)
    {
        struct termios attr;
        bool callset = false;
        bool check;

        if (tcgetattr(STDIN_FILENO, &attr) == -1)
            return false;

        check = (attr.c_lflag & property) != 0;
        if ((value || check) && !(value && check))
        {
            callset = true;
            if (value)
                attr.c_lflag |= property;
            else
                attr.c_lflag &= ~property;
        }

        if (!callset)
            return true;

        if (tcsetattr(STDIN_FILENO, TCSANOW, &attr) == -1)
            return true;

        s_il2cppAttr = attr;

        return true;
    }

    static void SetControlChars(uint8_t* control_chars, const uint8_t *cc)
    {
        // The index into the array comes from corlib/System/ControlCharacters.cs
#ifdef VINTR
        control_chars[0] = cc[VINTR];
#endif
#ifdef VQUIT
        control_chars[1] = cc[VQUIT];
#endif
#ifdef VERASE
        control_chars[2] = cc[VERASE];
#endif
#ifdef VKILL
        control_chars[3] = cc[VKILL];
#endif
#ifdef VEOF
        control_chars[4] = cc[VEOF];
#endif
#ifdef VTIME
        control_chars[5] = cc[VTIME];
#endif
#ifdef VMIN
        control_chars[6] = cc[VMIN];
#endif
#ifdef VSWTC
        control_chars[7] = cc[VSWTC];
#endif
#ifdef VSTART
        control_chars[8] = cc[VSTART];
#endif
#ifdef VSTOP
        control_chars[9] = cc[VSTOP];
#endif
#ifdef VSUSP
        control_chars[10] = cc[VSUSP];
#endif
#ifdef VEOL
        control_chars[11] = cc[VEOL];
#endif
#ifdef VREPRINT
        control_chars[12] = cc[VREPRINT];
#endif
#ifdef VDISCARD
        control_chars[13] = cc[VDISCARD];
#endif
#ifdef VWERASE
        control_chars[14] = cc[VWERASE];
#endif
#ifdef VLNEXT
        control_chars[15] = cc[VLNEXT];
#endif
#ifdef VEOL2
        control_chars[16] = cc[VEOL2];
#endif
    }

    static void CallDoConsoleCancelEvent()
    {
        // TODO: Call Console.cancel_handler delegate from another thread.
    }

    static void SigintHandler(int signo)
    {
        static bool insideSigint = false;

        if (insideSigint)
            return;

        insideSigint = true;
        CallDoConsoleCancelEvent();
        insideSigint = false;
    }

    static void SigcontHandler(int signo, siginfo_t *the_siginfo, void *data)
    {
        // Ignore error, there is not much we can do in the sigcont handler.
        tcsetattr(STDIN_FILENO, TCSANOW, &s_il2cppAttr);

        if (!s_keypadXmit.empty())
            write(STDOUT_FILENO, s_keypadXmit.c_str(), s_keypadXmit.length());

        // Call previous handler
        if (s_saveSigcont.sa_sigaction != NULL &&
            s_saveSigcont.sa_sigaction != (void*)SIG_DFL &&
            s_saveSigcont.sa_sigaction != (void*)SIG_IGN)
            (*s_saveSigcont.sa_sigaction)(signo, the_siginfo, data);
    }

    static void SigwinchHandler(int signo, siginfo_t *the_siginfo, void *data)
    {
        const int32_t size = GetTerminalSize();

        if (size != -1)
            s_terminalSize = size;

        // Call previous handler
        if (s_saveSigwinch.sa_sigaction != NULL &&
            s_saveSigwinch.sa_sigaction != (void*)SIG_DFL &&
            s_saveSigwinch.sa_sigaction != (void*)SIG_IGN)
            (*s_saveSigwinch.sa_sigaction)(signo, the_siginfo, data);
    }

    static void ConsoleSetupSignalHandler()
    {
        struct sigaction sigcont, sigint, sigwinch;

        memset(&sigcont, 0, sizeof(struct sigaction));
        memset(&sigint, 0, sizeof(struct sigaction));
        memset(&sigwinch, 0, sizeof(struct sigaction));

        // Continuing
        sigcont.sa_sigaction = SigcontHandler;
        sigcont.sa_flags = SA_SIGINFO;
        sigemptyset(&sigcont.sa_mask);
        sigaction(SIGCONT, &sigcont, &s_saveSigcont);

        // Interrupt handler
        sigint.sa_handler = SigintHandler;
        sigint.sa_flags = 0;
        sigemptyset(&sigint.sa_mask);
        sigaction(SIGINT, &sigint, &s_saveSigint);

        // Window size changed
        sigwinch.sa_sigaction = SigwinchHandler;
        sigwinch.sa_flags = SA_SIGINFO;
        sigemptyset(&sigwinch.sa_mask);
        sigaction(SIGWINCH, &sigwinch, &s_saveSigwinch);
    }

// Exists in Mono, but is unused.
    static void ConsoleRestoreSignalHandlers()
    {
        sigaction(SIGCONT, &s_saveSigcont, NULL);
        sigaction(SIGINT, &s_saveSigint, NULL);
        sigaction(SIGWINCH, &s_saveSigwinch, NULL);
    }

    int32_t InternalKeyAvailable(int32_t ms_timeout)
    {
        fd_set rfds;
        struct timeval tv;
        struct timeval *tvptr;
        div_t divvy;
        int32_t ret, nbytes;

        do
        {
            FD_ZERO(&rfds);
            FD_SET(STDIN_FILENO, &rfds);

            if (ms_timeout >= 0)
            {
                divvy = div(ms_timeout, 1000);
                tv.tv_sec = divvy.quot;
                tv.tv_usec = divvy.rem;
                tvptr = &tv;
            }
            else
            {
                tvptr = NULL;
            }

            ret = select(STDIN_FILENO + 1, &rfds, NULL, NULL, tvptr);
        }
        while (ret == -1 && errno == EINTR);

        if (ret > 0)
        {
            nbytes = 0;

            ret = ioctl(STDIN_FILENO, FIONREAD, &nbytes);

            if (ret >= 0)
                ret = nbytes;
        }

        return (ret > 0) ? ret : 0;
    }

    bool SetBreak(bool wantBreak)
    {
        return SetProperty(IGNBRK, !wantBreak);
    }

    bool SetEcho(bool wantEcho)
    {
        return SetProperty(ECHO, wantEcho);
    }

    static void TtyShutdown()
    {
        if (!setupComplete)
            return;

        if (!s_teardown.empty())
            write(STDOUT_FILENO, s_teardown.c_str(), s_teardown.length());

        tcflush(STDIN_FILENO, TCIFLUSH);
        tcsetattr(STDIN_FILENO, TCSANOW, &s_initialAttr);

        SetProperty(ECHO, true);

        setupComplete = false;
    }

    bool TtySetup(const std::string& keypadXmit, const std::string& teardown, uint8_t* control_characters, int32_t** size)
    {
        s_terminalSize = GetTerminalSize();

        if (s_terminalSize == -1)
        {
            int32_t cols = 0, rows = 0;

            const char *colsValue = getenv("COLUMNS");

            if (colsValue != NULL)
                cols = atoi(colsValue);

            const char *linesValue = getenv("LINES");

            if (linesValue != NULL)
                rows = atoi(linesValue);

            if (cols != 0 && rows != 0)
                s_terminalSize = (cols << 16) | rows;
            else
                s_terminalSize = -1;
        }

        *size = &s_terminalSize;

        if (tcgetattr(STDIN_FILENO, &s_initialAttr) == -1)
            return false;

        s_il2cppAttr = s_initialAttr;

        s_il2cppAttr.c_lflag &= ~(ICANON);
        s_il2cppAttr.c_iflag &= ~(IXON | IXOFF);
        s_il2cppAttr.c_cc[VMIN] = 1;
        s_il2cppAttr.c_cc[VTIME] = 0;

        if (tcsetattr(STDIN_FILENO, TCSANOW, &s_il2cppAttr) == -1)
            return false;

        s_keypadXmit = keypadXmit;

        SetControlChars(control_characters, s_il2cppAttr.c_cc);

        if (setupComplete)
            return true;

        ConsoleSetupSignalHandler();

        setupComplete = true;

        s_teardown = teardown;
        atexit(TtyShutdown);

        return true;
    }

    const char* NewLine()
    {
        return "\n";
    }
}
}
}

#endif
