/*
	Terminal
	by John Ryland
	Copyright (c) 2023
*/

////////////////////////////////////////////////////////////////////////////////////
//	Terminal Process

#include "TerminalProcess.h"
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <fcntl.h>
#ifdef _WIN32
#  include <Windows.h>
#  include <ConsoleApi.h>
#  include <memory>
#else

#ifdef __LINUX__
#  include <pty.h>
#else
#  include <util.h>
#endif

#  include <unistd.h>
#endif

namespace Terminal {

TerminalProcess::TerminalProcess()
    : m_terminalFd(0)
    , m_exited(false)
{
}

// virtual
TerminalProcess::~TerminalProcess()
{
    Shutdown();
}

// virtual
void TerminalProcess::Update()
{
}

// virtual
bool TerminalProcess::HasExited()
{
    return m_terminalFd && m_exited;
}

#ifndef _WIN32

// virtual
void TerminalProcess::Initialize()
{
    if (m_terminalFd)
        printf("TerminalProcess double initialized\n");

    if (::forkpty(&m_terminalFd, NULL, NULL, NULL) == 0)
    {
        //::setenv("term", "TerminalEmulator", 1);
        ::setenv("TERM", "xterm-256color", 1);
        ::execl("/bin/bash", "/bin/bash", "-i", nullptr);
        ::exit(0);
    }

    ::fcntl(m_terminalFd, F_SETFL, fcntl(m_terminalFd, F_GETFL) | O_NONBLOCK);
}

// virtual
void TerminalProcess::Shutdown()
{
    if (m_terminalFd)
        ::close(m_terminalFd);
    m_terminalFd = 0;
}

// virtual
bool TerminalProcess::GetAvailableData(uint8_t*& buffer, uint32_t& bytesAvailable)
{
    if (!m_terminalFd)
        return false;

    ssize_t read_size = ::read(m_terminalFd, m_buffer, BUFFER_SIZE);
    if (read_size > 0)
    {
        buffer = m_buffer;
        bytesAvailable = read_size;
        return true;
    }

    if (read_size <= 0 && errno != EAGAIN)
    {
        printf("Bad read: closing\n");
        m_exited = true;
    }
    return false;
}

// virtual
bool TerminalProcess::SendBytes(const char* buffer, uint32_t bytesToWrite)
{
    return m_terminalFd && ::write(m_terminalFd, buffer, bytesToWrite) == bytesToWrite;
}

// virtual
void TerminalProcess::UpdateSize(int rows, int cols)
{
    struct winsize size { (uint16_t)rows, (uint16_t)cols, 0, 0 };
#ifdef __LINUX__
    ::ioctl(m_terminalFd, TIOCSWINSZ, &size);
#endif
}

#else

/*
typedef void* HPCON;

// Creates a "Pseudo Console" (ConPTY).
HRESULT WINAPI CreatePseudoConsole(
                                _In_ COORD size,        // ConPty Dimensions
                                _In_ HANDLE hInput,     // ConPty Input
                                _In_ HANDLE hOutput,    // ConPty Output
                                _In_ DWORD dwFlags,     // ConPty Flags
                                _Out_ HPCON* phPC);     // ConPty Reference

// Resizes the given ConPTY to the specified size, in characters.
HRESULT WINAPI ResizePseudoConsole(_In_ HPCON hPC, _In_ COORD size);

// Closes the ConPTY and all associated handles. Client applications attached 
// to the ConPTY will also terminated. 
VOID WINAPI ClosePseudoConsole(_In_ HPCON hPC);

*/

// Initializes the specified startup info struct with the required properties and
// updates its thread attribute list with the specified ConPTY handle
HRESULT InitializeStartupInfoAttachedToConPTY(STARTUPINFOEX* siEx, HPCON hPC)
{
    HRESULT hr = E_UNEXPECTED;
    size_t size;

    siEx->StartupInfo.cb = sizeof(STARTUPINFOEX);
    
    // Create the appropriately sized thread attribute list
    InitializeProcThreadAttributeList(NULL, 1, 0, &size);
    std::unique_ptr<BYTE[]> attrList = std::make_unique<BYTE[]>(size);
    
    // Set startup info's attribute list & initialize it
    siEx->lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(
        attrList.get());
    bool fSuccess = InitializeProcThreadAttributeList(
        siEx->lpAttributeList, 1, 0, (PSIZE_T)&size);
        
    if (fSuccess)
    {
        // Set thread attribute list's Pseudo Console to the specified ConPTY
        fSuccess = UpdateProcThreadAttribute(
                        lpAttributeList,
                        0,
                        PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
                        hPC,
                        sizeof(HPCON),
                        NULL,
                        NULL);
        return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError());
    }
    else
    {
        hr = HRESULT_FROM_WIN32(GetLastError());
    }
    return hr;
}

// virtual
void TerminalProcess::Initialize()
{
    if (m_terminalFd)
        printf("TerminalProcess double initialized\n");

    HANDLE hOut, hIn;
    HANDLE outPipeOurSide, inPipeOurSide;
    HANDLE outPipePseudoConsoleSide, inPipePseudoConsoleSide;
    HPCON m_terminalFd = 0;

    // Create the in/out pipes:
    CreatePipe(&inPipePseudoConsoleSide, &inPipeOurSide, NULL, 0);
    CreatePipe(&outPipeOurSide, &outPipePseudoConsoleSide, NULL, 0);

    // Create the Pseudo Console, using the pipes
    CreatePseudoConsole(
        {80, 32}, 
        inPipePseudoConsoleSide, 
        outPipePseudoConsoleSide, 
        0, 
        &m_terminalFd);

    // Prepare the StartupInfoEx structure attached to the ConPTY.
    STARTUPINFOEX siEx{};
    InitializeStartupInfoAttachedToConPTY(&siEx, m_terminalFd);

    // Create the client application, using startup info containing ConPTY info
    wchar_t* commandline = L"c:\\windows\\system32\\cmd.exe";
    PROCESS_INFORMATION piClient{};
    fSuccess = CreateProcessW(
                    nullptr,
                    commandline,
                    nullptr,
                    nullptr,
                    TRUE,
                    EXTENDED_STARTUPINFO_PRESENT,
                    nullptr,
                    nullptr,
                    &siEx->StartupInfo,
                    &piClient);
}

// virtual
void TerminalProcess::Shutdown()
{
    if (m_terminalFd)
        ClosePseudoConsole(m_terminalFd);
    m_terminalFd = 0;
}

// virtual
bool TerminalProcess::GetAvailableData(uint8_t*& buffer, uint32_t& bytesAvailable)
{
    if (!m_terminalFd)
        return false;

/*
    DWORD dwRead;
    if (!PeekNamedPipe(
  __in       HANDLE hNamedPipe,
  __out_opt  LPVOID lpBuffer,
  __in       DWORD nBufferSize,
  __out_opt  LPDWORD lpBytesRead,
  __out_opt  LPDWORD lpTotalBytesAvail,
  __out_opt  LPDWORD lpBytesLeftThisMessage
);

if(TotalBytesAvail > 0)
*/

    DWORD dwRead;
    if (ReadFile(hOut, m_buffer, (DWORD)BUFFER_SIZE, &dwRead, nullptr))
    {
        buffer = m_buffer;
        bytesAvailable = dwRead;
        return true;
    }

/*
    if (read_size <= 0 && errno != EAGAIN)
    {
        printf("Bad read: closing\n");
        m_exited = true;
    }
*/
    return false;
}

// virtual
bool TerminalProcess::SendBytes(const char* buffer, uint32_t bytesToWrite)
{
    DWORD dwWritten;
    WriteFile(hIn, buffer, (DWORD)bytesToWrite, &dwWritten, nullptr);
    return m_terminalFd && dwWritten == bytesToWrite;
}

// virtual
void TerminalProcess::UpdateSize(int rows, int cols)
{
    COORD coord { (uint16_t)rows, (uint16_t)cols };
    ResizePseudoConsole(m_terminalFd, coord);
}

#endif

} // Terminal namespace
