/*
 * PROJECT:     ReactOS Tasklist Command
 * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
 * PURPOSE:     Displays a list of currently running processes on the computer.
 * COPYRIGHT:   Copyright 2021 He Yang <1160386205@qq.com>
 */

#include "tasklist.h"

// the strings in OptionList are the command-line options.
// should always correspond with the defines below, in sequence (except OPTION_INVALID)
static PCWSTR OptionList[] = { L"?", L"nh" };

#define OPTION_INVALID    -1
#define OPTION_HELP       0
#define OPTION_NOHEADER    1

// the max string length PrintResString can handle
#define RES_STR_MAXLEN 64

// Print split line
VOID PrintSplitLine(UINT Length)
{
    for (; Length; Length--)
    {
        ConPuts(StdOut, L"=");
    }
}

// Print spaces
VOID PrintSpace(UINT Length)
{
    ConPrintf(StdOut, L"%*ls", (INT)Length, L"");
}

// Print a string.
// if bAlignLeft == TRUE then aligned to left, otherwise aligned to right
// MaxWidth is the width for printing.
VOID PrintString(LPCWSTR String, UINT MaxWidth, BOOL bAlignLeft)
{
    ConPrintf(StdOut, bAlignLeft ? L"%-*.*ls" : L"%*.*ls", MaxWidth, MaxWidth, String);
}

// Print a string from resource
// if bAlignLeft == TRUE then aligned to left, otherwise aligned to right
// MaxWidth is the width for printing.
// The string WILL be truncated if it's longer than RES_STR_MAXLEN
VOID PrintResString(UINT uID, UINT MaxWidth, BOOL bAlignLeft)
{
    WCHAR StringBuffer[RES_STR_MAXLEN];
    LoadStringW(NULL, uID, StringBuffer, _countof(StringBuffer));
    PrintString(StringBuffer, MaxWidth, bAlignLeft);
}

// Print a number, aligned to right.
// MaxWidth is the width for printing.
// the number WILL NOT be truncated if it's longer than MaxWidth
VOID PrintNum(LONGLONG Number, UINT MaxWidth)
{
    ConPrintf(StdOut, L"%*lld", MaxWidth, Number);
}

// Print memory size using KB as unit, with comma-separated number, aligned to right.
// MaxWidth is the width for printing.
// the number WILL be truncated if it's longer than MaxWidth
BOOL PrintMemory(SIZE_T MemorySizeByte, UINT MaxWidth)
{
    SIZE_T MemorySize = MemorySizeByte >> 10;

    WCHAR NumberString[27] = { 0 }; // length 26 is enough to display ULLONG_MAX in decimal with comma, one more for zero-terminated.
    C_ASSERT(sizeof(SIZE_T) <= 8);

    PWCHAR pNumberStr = NumberString;

    // calculate the length
    UINT PrintLength = 0;
    SIZE_T Tmp = MemorySize;
    UINT Mod = 1;

    do
    {
        Tmp /= 10;
        PrintLength++;
        Mod *= 10;
    } while (Tmp);

    for (UINT i = PrintLength; i; i--)
    {
        Mod /= 10;
        *pNumberStr = L'0' + (MemorySize / Mod);
        MemorySize %= Mod;
        pNumberStr++;

        if (i != 1 && i % 3 == 1)
        {
            *pNumberStr = L',';
            pNumberStr++;
        }
    }

    WCHAR FormatStr[RES_STR_MAXLEN];
    LoadStringW(NULL, IDS_MEMORY_STR, FormatStr, _countof(FormatStr));

    WCHAR String[RES_STR_MAXLEN + _countof(NumberString)] = { 0 };
    StringCchPrintfW(String, _countof(String), FormatStr, NumberString);
    PrintString(String, MaxWidth, FALSE);

    return TRUE;
}

VOID PrintHeader(VOID)
{
    PrintResString(IDS_HEADER_IMAGENAME, COLUMNWIDTH_IMAGENAME, TRUE);
    PrintSpace(1);
    PrintResString(IDS_HEADER_PID,       COLUMNWIDTH_PID,       FALSE);
    PrintSpace(1);
    PrintResString(IDS_HEADER_SESSION,   COLUMNWIDTH_SESSION,   FALSE);
    PrintSpace(1);
    PrintResString(IDS_HEADER_MEMUSAGE,  COLUMNWIDTH_MEMUSAGE,  FALSE);

    ConPuts(StdOut, L"\n");

    PrintSplitLine(COLUMNWIDTH_IMAGENAME);
    PrintSpace(1);
    PrintSplitLine(COLUMNWIDTH_PID);
    PrintSpace(1);
    PrintSplitLine(COLUMNWIDTH_SESSION);
    PrintSpace(1);
    PrintSplitLine(COLUMNWIDTH_MEMUSAGE);

    ConPuts(StdOut, L"\n");
}

BOOL EnumProcessAndPrint(BOOL bNoHeader)
{
    // Call NtQuerySystemInformation for the process information
    ULONG ProcessInfoBufferLength = 0;
    ULONG ResultLength = 0;
    PBYTE ProcessInfoBuffer = NULL;

    // Get the buffer size we need
    NTSTATUS Status = NtQuerySystemInformation(SystemProcessInformation, NULL, 0, &ResultLength);

    // New process/thread might appear before we call for the actual data.
    // Try to avoid this by retrying several times.
    for (UINT Retry = 0; Retry < NT_SYSTEM_QUERY_MAX_RETRY; Retry++)
    {
        // (Re)allocate buffer
        ProcessInfoBufferLength = ResultLength;
        ResultLength = 0;
        if (ProcessInfoBuffer)
        {
            PBYTE NewProcessInfoBuffer = HeapReAlloc(GetProcessHeap(), 0,
                                                     ProcessInfoBuffer,
                                                     ProcessInfoBufferLength);
            if (NewProcessInfoBuffer)
            {
                ProcessInfoBuffer = NewProcessInfoBuffer;
            }
            else
            {
                // out of memory
                ConResMsgPrintf(StdErr, 0, IDS_OUT_OF_MEMORY);
                HeapFree(GetProcessHeap(), 0, ProcessInfoBuffer);
                return FALSE;
            }
        }
        else
        {
            ProcessInfoBuffer = HeapAlloc(GetProcessHeap(), 0, ProcessInfoBufferLength);
            if (!ProcessInfoBuffer)
            {
                // out of memory
                ConResMsgPrintf(StdErr, 0, IDS_OUT_OF_MEMORY);
                return FALSE;
            }
        }

        // Query information
        Status = NtQuerySystemInformation(SystemProcessInformation,
                                          ProcessInfoBuffer,
                                          ProcessInfoBufferLength,
                                          &ResultLength);
        if (Status != STATUS_INFO_LENGTH_MISMATCH)
        {
            break;
        }
    }

    if (!NT_SUCCESS(Status))
    {
        // tried NT_SYSTEM_QUERY_MAX_RETRY times, or failed with some other reason
        ConResMsgPrintf(StdErr, 0, IDS_ENUM_FAILED);
        HeapFree(GetProcessHeap(), 0, ProcessInfoBuffer);
        return FALSE;
    }

    ConPuts(StdOut, L"\n");

    if (!bNoHeader)
        PrintHeader();

    PSYSTEM_PROCESS_INFORMATION pSPI;
    pSPI = (PSYSTEM_PROCESS_INFORMATION)ProcessInfoBuffer;
    while (pSPI)
    {
        PrintString(pSPI->UniqueProcessId ? pSPI->ImageName.Buffer : L"System Idle Process", COLUMNWIDTH_IMAGENAME, TRUE);
        PrintSpace(1);
        PrintNum((LONGLONG)(INT_PTR)pSPI->UniqueProcessId, COLUMNWIDTH_PID);
        PrintSpace(1);
        PrintNum((ULONGLONG)pSPI->SessionId, COLUMNWIDTH_SESSION);
        PrintSpace(1);
        PrintMemory(pSPI->WorkingSetSize, COLUMNWIDTH_MEMUSAGE);

        ConPuts(StdOut, L"\n");

        if (pSPI->NextEntryOffset == 0)
            break;
        pSPI = (PSYSTEM_PROCESS_INFORMATION)((LPBYTE)pSPI + pSPI->NextEntryOffset);
    }

    HeapFree(GetProcessHeap(), 0, ProcessInfoBuffer);
    return TRUE;
}

INT GetOptionType(LPCWSTR szOption)
{
    if (szOption[0] != L'/' && szOption[0] != L'-')
    {
        return OPTION_INVALID;
    }
    szOption++;

    for (UINT i = 0; i < _countof(OptionList); i++)
    {
        if (!_wcsicmp(OptionList[i], szOption))
        {
            return i;
        }
    }
    return OPTION_INVALID;
}

BOOL ProcessArguments(INT argc, WCHAR **argv)
{
    BOOL bHasHelp = FALSE, bHasNoHeader = FALSE;

    for (INT i = 1; i < argc; i++)
    {
        INT Option = GetOptionType(argv[i]);

        switch (Option)
        {
            case OPTION_HELP:
            {
                if (bHasHelp)
                {
                    // -? already specified
                    ConResMsgPrintf(StdErr, 0, IDS_OPTION_TOO_MUCH, argv[i], 1);
                    ConResMsgPrintf(StdErr, 0, IDS_USAGE);
                    return FALSE;
                }
                bHasHelp = TRUE;
                break;
            }
            case OPTION_NOHEADER:
            {
                if (bHasNoHeader)
                {
                    // -nh already specified
                    ConResMsgPrintf(StdErr, 0, IDS_OPTION_TOO_MUCH, argv[i], 1);
                    ConResMsgPrintf(StdErr, 0, IDS_USAGE);
                    return FALSE;
                }
                bHasNoHeader = TRUE;
                break;
            }
            case OPTION_INVALID:
            default:
            {
                ConResMsgPrintf(StdErr, 0, IDS_INVALID_OPTION);
                ConResMsgPrintf(StdErr, 0, IDS_USAGE);
                return FALSE;
            }
        }
    }

    if (bHasHelp)
    {
        if (argc > 2) // any arguments other than -? exists
        {
            ConResMsgPrintf(StdErr, 0, IDS_INVALID_SYNTAX);
            ConResMsgPrintf(StdErr, 0, IDS_USAGE);
            return FALSE;
        }
        else
        {
            ConResMsgPrintf(StdOut, 0, IDS_USAGE);
            ConResMsgPrintf(StdOut, 0, IDS_DESCRIPTION);
            return FALSE;
        }
    }
    else
    {
        EnumProcessAndPrint(bHasNoHeader);
    }
    return TRUE;
}

int wmain(int argc, WCHAR **argv)
{
    /* Initialize the Console Standard Streams */
    ConInitStdStreams();

    if (!ProcessArguments(argc, argv))
    {
        return 1;
    }
    return 0;
}
