/**
 * PROJECT:         ExectOS
 * COPYRIGHT:       See COPYING.md in the top level directory
 * FILE:            xtldr/textui.c
 * DESCRIPTION:     Text console User Interface (TUI) support for XT Boot Loader
 * DEVELOPERS:      Rafal Kupiec <belliash@codingworkshop.eu.org>
 */

#include <xtldr.h>


/**
 * Displays a simple TUI-based boot menu.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlDisplayBootMenu()
{
    XTBL_DIALOG_HANDLE Handle;
    PXTBL_BOOTMENU_ITEM MenuEntries = NULL;
    ULONG Index, NumberOfEntries, HighligtedEntryId;
    UINT_PTR EventIndex;
    EFI_EVENT Events[2];
    EFI_INPUT_KEY Key;
    EFI_EVENT TimerEvent;
    EFI_STATUS Status;
    LONG TimeOut;
    PWCHAR TimeOutString;

    /* Initialize boot menu list */
    Status = BlInitializeBootMenuList(&MenuEntries, &NumberOfEntries, &HighligtedEntryId);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to initialize boot menu list, exit into XTLDR shell */
        return;
    }

    /* Get timeout from the configuration */
    TimeOutString = BlGetConfigValue(L"TIMEOUT");
    TimeOut = -1;

    /* Check if timeout is specified */
    if(TimeOutString != NULL)
    {
        /* Convert timeout string to number */
        TimeOut = 0;
        while(*TimeOutString >= '0' && *TimeOutString <= '9')
        {
            TimeOut *= 10;
            TimeOut += *TimeOutString - '0';
            TimeOutString++;
        }
    }

    /* Infinite boot menu loop */
    while(TRUE)
    {
        /* Draw boot menu */
        BlpDrawBootMenu(&Handle);

        /* Check if there is anything to show in the boot menu */
        if(NumberOfEntries > 0) {
            /* Check if all menu entries will fit into the menu box */
            if(NumberOfEntries > Handle.Height - 2)
            {
                /* Too many menu entries, limit entries to match box height (-2 for upper and bottom borders) */
                NumberOfEntries = Handle.Height - 2;
            }

            /* Iterate through all menu entries */
            for(Index = 0; Index < NumberOfEntries; Index++)
            {
                /* Draw menu entry */
                BlpDrawBootMenuEntry(&Handle, MenuEntries[Index].EntryName, Index, Index == HighligtedEntryId);
            }
        }
        else
        {
            /* No menu entries found, show error message */
            BlDisplayErrorDialog(L"XTLDR", L"No boot menu entries found in the configuration. Falling back to shell.");

            /* Exit into XTLDR shell */
            return;
        }

        /* Create a timer event for controlling the timeout of the boot menu */
        Status = EfiSystemTable->BootServices->CreateEvent(EFI_EVENT_TIMER, EFI_TPL_CALLBACK, NULL, NULL, &TimerEvent);
        if(Status == STATUS_EFI_SUCCESS)
        {
            /* Setup new EFI timer */
            Status = EfiSystemTable->BootServices->SetTimer(TimerEvent, TimerPeriodic, 10000000);
        }

        /* Check is EFI timer was successfully created */
        if(Status != STATUS_EFI_SUCCESS)
        {
            /* Timer creation failed, disable the timer */
            TimeOut = -1;
        }

        /* Initialize EFI events */
        Events[0] = EfiSystemTable->ConIn->WaitForKey;
        Events[1] = TimerEvent;

        /* Flush keyboard buffer out of any keystrokes */
        EfiSystemTable->ConIn->Reset(EfiSystemTable->ConIn, FALSE);

        /* Infinite boot menu event loop */
        while(TRUE)
        {
            /* Wait for EFI event */
            BlWaitForEfiEvent(2, Events, &EventIndex);

            /* Check which event was received */
            if(EventIndex == 0)
            {
                /* Key pressed, check if timer is still active */
                if(TimeOut != -1)
                {
                    /* Disable the timer */
                    TimeOut = -1;

                    /* Cancel timer event */
                    EfiSystemTable->BootServices->SetTimer(TimerEvent, TimerCancel, 0);

                    /* Remove the timer message */
                    BlClearConsoleLine(Handle.PosY + Handle.Height + 4);
                }

                /* Read key stroke */
                BlReadKeyStroke(&Key);

                if(Key.ScanCode == 0x03 || Key.UnicodeChar == 0x0D)
                {
                    /* ENTER or RightArrow key pressed, boot the highlighted OS */
                    BlSetConsoleAttributes(Handle.DialogColor | Handle.TextColor);
                    BlClearConsoleLine(Handle.PosY + Handle.Height + 4);
                    BlSetCursorPosition(4, Handle.PosY + Handle.Height + 4);
                    BlConsolePrint(L"Booting '%S' now...\n", MenuEntries[HighligtedEntryId].EntryName);

                    /* Boot the highlighted (chosen) OS */
                    Status = BlInvokeBootProtocol(MenuEntries[HighligtedEntryId].ShortName,
                                                  MenuEntries[HighligtedEntryId].Options);
                    if(Status != STATUS_SUCCESS)
                    {
                        /* Failed to boot OS */
                        BlDebugPrint(L"ERROR: Failed to boot '%S' (Status Code: 0x%zX)\n",
                                     MenuEntries[HighligtedEntryId].EntryName, Status);
                        BlDisplayErrorDialog(L"XTLDR", L"Failed to startup the selected Operating System.");
                    }

                    /* Break from boot menu event loop to redraw whole boot menu */
                    break;
                }
                else if(Key.ScanCode == 0x01)
                {
                    /* UpArrow key pressed, go to previous entry if possible */
                    if(HighligtedEntryId > 0)
                    {
                        /* Highlight previous entry */
                        HighligtedEntryId--;
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[HighligtedEntryId + 1].EntryName,
                                             HighligtedEntryId + 1, FALSE);
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[HighligtedEntryId].EntryName,
                                             HighligtedEntryId, TRUE);
                    }
                }
                else if(Key.ScanCode == 0x02)
                {
                    /* DownArrow key pressed, go to next entry if possible */
                    if(HighligtedEntryId < NumberOfEntries - 1)
                    {
                        /* Highlight next entry */
                        HighligtedEntryId++;
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[HighligtedEntryId - 1].EntryName,
                                             HighligtedEntryId - 1, FALSE);
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[HighligtedEntryId].EntryName,
                                             HighligtedEntryId, TRUE);
                    }
                }
                else if(Key.ScanCode == 0x09)
                {
                    /* PageUp key pressed, go to top entry */
                    if(HighligtedEntryId != 0)
                    {
                        /* Highlight first entry */
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[HighligtedEntryId].EntryName,
                                             HighligtedEntryId, FALSE);
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[0].EntryName, 0, TRUE);

                        /* Update highlighted entry ID */
                        HighligtedEntryId = 0;
                    }
                }
                else if(Key.ScanCode == 0x0A)
                {
                    /* PageDown key pressed, go to bottom entry */
                    if(HighligtedEntryId != NumberOfEntries - 1)
                    {
                        /* Highlight last entry */
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[HighligtedEntryId].EntryName,
                                             HighligtedEntryId, FALSE);
                        BlpDrawBootMenuEntry(&Handle, MenuEntries[NumberOfEntries - 1].EntryName,
                                             NumberOfEntries - 1, TRUE);

                        /* Update highlighted entry ID */
                        HighligtedEntryId = NumberOfEntries - 1;
                    }
                }
                else if(Key.ScanCode == 0x0B)
                {
                    /* F1 key pressed, show help */
                    BlDisplayInfoDialog(L"XTLDR", L"XTLDR, the XTOS Boot Loader for UEFI and EFI-based machines.\n"
                                                  L" \n"
                                                  L"Use arrow keys (Up/Down) to change the highlighted entry and\n"
                                                  L"PgUp/PgDown keys to jump to the first/last position.\n"
                                                  L" \n"
                                                  L"Press ENTER key to boot the highlighted boot menu entry.\n"
                                                  L"Press 'e' key to edit the highlighted menu entry.\n"
                                                  L"Press 's' key to exit into XTLDR shell (enters advanced mode).\n"
                                                  L" \n"
                                                  L"F1 shows this help, F10 reboots into UEFI firmware interface,\n"
                                                  L"F11 reboots the machine and F12 turns it off.\n"
                                                  L" \n"
                                                  L" \n"
                                                  L"XTLDR is a part of the ExectOS Operating System.\n"
                                                  L"Visit https://exectos.eu.org/ for more information.");

                    /* Break from boot menu event loop to redraw whole boot menu */
                    break;
                }
                else if(Key.ScanCode == 0x14)
                {
                    /* F10 key pressed, reboot into UEFI setup interface */
                    BlEnterFirmwareSetup();
                    BlDisplayErrorDialog(L"XTLDR", L"Reboot into firmware setup interface not supported!");

                    /* Break from boot menu event loop to redraw whole boot menu */
                    break;
                }
                else if(Key.ScanCode == 0x15)
                {
                    /* F11 key pressed, reboot the machine */
                    BlRebootSystem();
                    BlDisplayErrorDialog(L"XTLDR", L"Failed to reboot the machine!");

                    /* Break from boot menu event loop to redraw whole boot menu */
                    break;
                }
                else if(Key.ScanCode == 0x16)
                {
                    /* F12 key pressed, shutdown the machine */
                    BlShutdownSystem();
                    BlDisplayErrorDialog(L"XTLDR", L"Failed to shutdown the machine!");

                    /* Break from boot menu event loop to redraw whole boot menu */
                    break;
                }
                else if(Key.UnicodeChar == 0x65)
                {
                    /* 'e' key pressed, edit the highlighted entry */
                    BlDisplayErrorDialog(L"XTLDR", L"Editing boot menu entries is not implemented yet!");

                    /* Break from boot menu event loop to redraw whole boot menu */
                    break;
                }
                else if(Key.UnicodeChar == 0x73)
                {
                    /* 's' key pressed, exit into XTLDR shell */
                    return;
                }
            }
            else
            {
                /* Timer tick, check if time out expired */
                if(TimeOut > 0)
                {
                    /* Update a message and decrease timeout value */
                    BlSetConsoleAttributes(Handle.DialogColor | Handle.TextColor);
                    BlClearConsoleLine(Handle.PosY + Handle.Height + 4);
                    BlSetCursorPosition(4, Handle.PosY + Handle.Height + 4);
                    BlConsolePrint(L"The highlighted position will be booted automatically in %ld seconds.", TimeOut);
                    TimeOut--;
                }
                else if(TimeOut == 0)
                {
                    /* Time out expired, update a message */
                    BlSetConsoleAttributes(Handle.DialogColor | Handle.TextColor);
                    BlClearConsoleLine(Handle.PosY + Handle.Height + 4);
                    BlSetCursorPosition(4, Handle.PosY + Handle.Height + 4);
                    BlConsolePrint(L"Booting '%S' now...\n", MenuEntries[HighligtedEntryId].EntryName);

                    /* Disable the timer just in case booting OS fails */
                    TimeOut = -1;

                    /* Boot the highlighted (default) OS */
                    Status = BlInvokeBootProtocol(MenuEntries[HighligtedEntryId].ShortName,
                                                  MenuEntries[HighligtedEntryId].Options);
                    if(Status != STATUS_SUCCESS)
                    {
                        /* Failed to boot OS */
                        BlDebugPrint(L"ERROR: Failed to boot '%S' (Status Code: 0x%zX)\n",
                                     MenuEntries[HighligtedEntryId].EntryName, Status);
                        BlDisplayErrorDialog(L"XTLDR", L"Failed to startup the selected Operating System.");
                    }
                    break;
                }
            }
        }
    }
}

/**
 * Displays a red error dialog box with the specified caption and message.
 *
 * @param Caption
 *        Supplies a caption string put on the dialog box.
 *
 * @param Message
 *        Supplies a message string put on the dialog box.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlDisplayErrorDialog(IN PWCHAR Caption,
                     IN PWCHAR Message)
{
    XTBL_DIALOG_HANDLE Handle;
    EFI_INPUT_KEY Key;
    UINT_PTR Index;

    /* Set dialog window attributes */
    Handle.Attributes = XTBL_TUI_DIALOG_ERROR_BOX | XTBL_TUI_DIALOG_ACTIVE_BUTTON;

    /* Determine dialog window size and position */
    BlpDetermineDialogBoxSize(&Handle, Message);

    /* Disable cursor and draw dialog box */
    BlDisableConsoleCursor();
    BlpDrawDialogBox(&Handle, Caption, Message);

    /* Draw active button */
    BlpDrawDialogButton(&Handle);

    /* Initialize key stroke */
    Key.ScanCode = 0;
    Key.UnicodeChar = 0;

    /* Wait until ENTER or ESC key is pressed */
    while(Key.ScanCode != 0x17 && Key.UnicodeChar != 0x0D)
    {
        /* Wait for key press and read key stroke */
        BlWaitForEfiEvent(1, &EfiSystemTable->ConIn->WaitForKey, &Index);
        BlReadKeyStroke(&Key);
        BlResetConsoleInputBuffer();
    }

    /* Clear screen to remove dialog box */
    BlSetConsoleAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY);
    BlClearConsoleScreen();
}

/**
 * Displays a blue informational dialog box with the specified caption and message.
 *
 * @param Caption
 *        Supplies a caption string put on the dialog box.
 *
 * @param Message
 *        Supplies a message string put on the dialog box.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlDisplayInfoDialog(IN PWCHAR Caption,
                    IN PWCHAR Message)
{
    XTBL_DIALOG_HANDLE Handle;
    EFI_INPUT_KEY Key;
    UINT_PTR Index;

    /* Set dialog window attributes */
    Handle.Attributes = XTBL_TUI_DIALOG_GENERIC_BOX | XTBL_TUI_DIALOG_ACTIVE_BUTTON;

    /* Determine dialog window size and position */
    BlpDetermineDialogBoxSize(&Handle, Message);

    /* Disable cursor and draw dialog box */
    BlDisableConsoleCursor();
    BlpDrawDialogBox(&Handle, Caption, Message);

    /* Draw active button */
    BlpDrawDialogButton(&Handle);

    /* Initialize key stroke */
    Key.ScanCode = 0;
    Key.UnicodeChar = 0;

    /* Wait until ENTER or ESC key is pressed */
    while(Key.ScanCode != 0x17 && Key.UnicodeChar != 0x0D)
    {
        /* Wait for key press and read key stroke */
        BlWaitForEfiEvent(1, &EfiSystemTable->ConIn->WaitForKey, &Index);
        BlReadKeyStroke(&Key);
        BlResetConsoleInputBuffer();
    }

    /* Clear screen to remove dialog box */
    BlSetConsoleAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY);
    BlClearConsoleScreen();
}

/**
 * Displays a blue informational dialog box with the specified caption and message and an input field.
 *
 * @param Caption
 *        Specifies a caption string put on the dialog box.
 *
 * @param Message
 *        Specifies a message string put on the dialog box.
 *
 * @param InputFieldText
 *        Specifies a pointer to the input field text that will be edited.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlDisplayInputDialog(IN PWCHAR Caption,
                     IN PWCHAR Message,
                     IN PWCHAR *InputFieldText)
{
    SIZE_T InputFieldLength, TextCursorPosition, TextIndex, TextPosition;
    XTBL_DIALOG_HANDLE Handle;
    PWCHAR InputFieldBuffer;
    SIZE_T BufferLength;
    EFI_INPUT_KEY Key;
    EFI_STATUS Status;
    UINT_PTR Index;

    /* Set dialog window attributes */
    Handle.Attributes = XTBL_TUI_DIALOG_GENERIC_BOX | XTBL_TUI_DIALOG_ACTIVE_INPUT | XTBL_TUI_DIALOG_INACTIVE_BUTTON;

    /* Determine dialog window size and position */
    BlpDetermineDialogBoxSize(&Handle, Message);

    /* Disable cursor and draw dialog box */
    BlDisableConsoleCursor();
    BlpDrawDialogBox(&Handle, Caption, Message);

    /* Draw inactive button */
    BlpDrawDialogButton(&Handle);

    /* Draw active input field */
    BlpDrawDialogInputField(&Handle, *InputFieldText);

    /* Initialize key stroke */
    Key.ScanCode = 0;
    Key.UnicodeChar = 0;

    /* Get initial input text length and allocate a buffer */
    BufferLength = RtlWideStringLength(*InputFieldText, 0);
    Status = BlAllocateMemoryPool(BufferLength * sizeof(WCHAR), (PVOID *)&InputFieldBuffer);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Memory allocation failure, print error message and return */
        BlDebugPrint(L"ERROR: Memory allocation failure (Status Code: 0x%zX)\n", Status);
        BlDisplayErrorDialog(L"XTLDR", L"Failed to allocate memory for input field buffer.");
        return;
    }

    /* Copy input text into edit buffer */
    RtlCopyMemory(InputFieldBuffer, *InputFieldText, BufferLength * sizeof(WCHAR));
    InputFieldBuffer[BufferLength] = L'\0';

    /* Determine input field length */
    InputFieldLength = BufferLength;
    if(InputFieldLength > Handle.Width - 8)
    {
        InputFieldLength = Handle.Width - 8;
    }

    /* Start at first character */
    TextPosition = 0;
    BlSetCursorPosition(Handle.PosX + 4 + TextPosition, Handle.PosY + Handle.Height - 4);

    /* Wait until ENTER or ESC key is pressed */
    while(TRUE)
    {
        /* Wait for key press and read key stroke */
        BlWaitForEfiEvent(1, &EfiSystemTable->ConIn->WaitForKey, &Index);
        BlReadKeyStroke(&Key);
        BlResetConsoleInputBuffer();

        /* Check key press scan code */
        if(Key.ScanCode == 0x17)
        {
            /* ESC key pressed, return */
            break;
        }
        else if(Key.UnicodeChar == 0x09)
        {
            /* TAB key pressed, toggle input field and button */
            Handle.Attributes ^= (XTBL_TUI_DIALOG_ACTIVE_INPUT | XTBL_TUI_DIALOG_INACTIVE_INPUT);
            Handle.Attributes ^= (XTBL_TUI_DIALOG_ACTIVE_BUTTON | XTBL_TUI_DIALOG_INACTIVE_BUTTON);
        }
        else if(Key.ScanCode == 0x03)
        {
            /* RIGHT key pressed, move cursor forward */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT && TextPosition < InputFieldLength)
            {
                TextPosition++;
            }
        }
        else if(Key.ScanCode == 0x04)
        {
            /* LEFT key pressed, move cursor back */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT && TextPosition > 0)
            {
                TextPosition--;
            }
        }
        else if(Key.ScanCode == 0x05)
        {
            /* HOME key pressed, move cursor to the beginning */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
            {
                TextPosition = 0;
            }
        }
        else if(Key.ScanCode == 0x06)
        {
            /* END key pressed, move cursor to the end */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
            {
                TextPosition = InputFieldLength;
            }
        }
        else if(Key.ScanCode == 0x08)
        {
            /* DELETE key pressed, delete character */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
            {
                if(InputFieldLength > 0 && TextPosition < InputFieldLength)
                {
                    RtlMoveMemory(InputFieldBuffer + TextPosition, InputFieldBuffer + TextPosition + 1,
                                  (InputFieldLength - TextPosition) * sizeof(WCHAR));
                    InputFieldLength--;
                    InputFieldBuffer[InputFieldLength] = 0;
                }
            }
        }
        else if(Key.UnicodeChar == 0x08)
        {
            /* BACKSPACE key pressed, delete character */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
            {
                if(InputFieldLength > 0 && TextPosition > 0 && TextPosition <= InputFieldLength)
                {
                    TextPosition--;
                    RtlMoveMemory(InputFieldBuffer + TextPosition, InputFieldBuffer + TextPosition + 1,
                                  (InputFieldLength - TextPosition) * sizeof(WCHAR));
                    InputFieldLength--;
                    InputFieldBuffer[InputFieldLength] = 0;
                }
            }
        }
        else if(Key.UnicodeChar == 0x0D)
        {
            /* ENTER key pressed, update input buffer */
            *InputFieldText = InputFieldBuffer;
            break;
        }
        else
        {
            /* Other key pressed, add character to the buffer */
            if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT && Key.UnicodeChar != 0)
            {
                RtlMoveMemory(InputFieldBuffer + TextPosition + 1, InputFieldBuffer + TextPosition,
                              (InputFieldLength - TextPosition) * sizeof(WCHAR));
                InputFieldBuffer[TextPosition] = Key.UnicodeChar;
                TextPosition++;
                InputFieldLength++;
                InputFieldBuffer[InputFieldLength] = 0;
            }
        }

        if(TextPosition > (Handle.Width - 9))
        {
            TextIndex = TextPosition - (Handle.Width - 9);
            TextCursorPosition = Handle.Width - 9;
        }
        else
        {
            TextIndex = 0;
            TextCursorPosition = TextPosition;
        }

        /* Redraw input field and button */
        BlpDrawDialogButton(&Handle);
        BlpDrawDialogInputField(&Handle, &InputFieldBuffer[TextIndex]);

        if(Handle.Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
        {
            BlSetCursorPosition(Handle.PosX + 4 + TextCursorPosition, Handle.PosY + Handle.Height - 4);
        }
    }

    /* Clear screen to remove dialog box */
    BlSetConsoleAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY);
    BlClearConsoleScreen();
}

/**
 * Displays a blue informational dialog box with the specified caption and message and a progress bar.
 *
 * @param Caption
 *        Supplies a caption string put on the dialog box.
 *
 * @param Message
 *        Supplies a message string put on the dialog box.
 *
 * @param Percentage
 *        Specifies the percentage progress of the progress bar.
 *
 * @return This routine returns a dialog box handle needed to update the progress bar.
 *
 * @since XT 1.0
 */
XTCDECL
XTBL_DIALOG_HANDLE
BlDisplayProgressDialog(IN PWCHAR Caption,
                        IN PWCHAR Message,
                        IN UCHAR Percentage)
{
    XTBL_DIALOG_HANDLE Handle;

    /* Set dialog window attributes */
    Handle.Attributes = XTBL_TUI_DIALOG_GENERIC_BOX | XTBL_TUI_DIALOG_PROGRESS_BAR;

    /* Determine dialog window size and position */
    BlpDetermineDialogBoxSize(&Handle, Message);

    /* Disable cursor and draw dialog box */
    BlDisableConsoleCursor();
    BlpDrawDialogBox(&Handle, Caption, Message);

    /* Draw active button */
    BlpDrawDialogProgressBar(&Handle, Percentage);

    /* Return dialog handle */
    return Handle;
}

/**
 * Updates the progress bar on the dialog box.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @param Message
 *        Supplies a new message that will be put on the dialog box, while updating the progress bar.
 *
 * @param Percentage
 *        Specifies the new percentage progress of the progress bar.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlUpdateProgressBar(IN PXTBL_DIALOG_HANDLE Handle,
                    IN PWCHAR Message,
                    IN UCHAR Percentage)
{
    /* Check if message needs an update */
    if(Message != NULL)
    {
        /* Update a message on the dialog box */
        BlpDrawDialogMessage(Handle, Message);
    }

    /* Update progress bar */
    BlpDrawDialogProgressBar(Handle, Percentage);
}

/**
 * Determines dialog box size based on enabled components and message length.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @param Message
 *        Supplies a pointer to the message string put on the dialog box.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDetermineDialogBoxSize(IN OUT PXTBL_DIALOG_HANDLE Handle,
                          IN PWCHAR Message)
{
    UINT_PTR Width, Height, LineLength;
    SIZE_T Index, MessageLength;
    UCHAR Attributes;
    ULONG Mask;

    /* Set minimum dialog window size */
    Height = 4;
    Width = 36;

    /* Zero line length */
    LineLength = 0;

    /* Adjust window height according to enabled components */
    Mask = 1;
    Attributes = Handle->Attributes;
    while(Mask)
    {
        /* Check enabled components that affect dialog window size */
        switch(Attributes & Mask)
        {
            case XTBL_TUI_DIALOG_ACTIVE_BUTTON:
            case XTBL_TUI_DIALOG_INACTIVE_BUTTON:
                Height += 1;
                break;
            case XTBL_TUI_DIALOG_ACTIVE_INPUT:
            case XTBL_TUI_DIALOG_INACTIVE_INPUT:
            case XTBL_TUI_DIALOG_PROGRESS_BAR:
                Height += 2;
                break;
        }

        /* Update component attributes mask */
        Attributes &= ~Mask;
        Mask <<= 1;
    }

    /* Check if input field is active */
    if(Handle->Attributes & (XTBL_TUI_DIALOG_ACTIVE_INPUT | XTBL_TUI_DIALOG_INACTIVE_INPUT))
    {
        /* Set maximum dialog window width to fit input field */
        Width = XTBL_TUI_MAX_DIALOG_WIDTH;
    }

    /* Get message length and count dialog window dimensions */
    MessageLength = RtlWideStringLength(Message, 0);
    for(Index = 0; Index < MessageLength; Index++)
    {
        /* Check if this is multiline message */
        if(Message[Index] == L'\n' || Index == MessageLength - 1)
        {
            /* Check if this line exceeds current dialog window width */
            if(LineLength > Width)
            {
                /* Update dialog window width */
                Width = LineLength;
            }

            /* Increase dialog window height to fit next line */
            Height++;
            LineLength = 0;
        }
        else
        {
            /* Increase dialog window width to fit next character */
            LineLength++;
        }
    }

    /* Add more space to dialog window to fit side borders */
    Width += 4;

    /* Get console resolution */
    BlQueryConsoleMode(&Handle->ResX, &Handle->ResY);

    /* Make sure dialog window fits in the buffer */
    if(Width > XTBL_TUI_MAX_DIALOG_WIDTH)
    {
        /* Set maximum dialog window width */
        Width = XTBL_TUI_MAX_DIALOG_WIDTH;
    }

    /* Make sure dialog window fits on the screen (X axis) and it is not too small for input field */
    if(Width > (Handle->ResX - 2))
    {
        /* Set maximum dialog window width */
        Width = Handle->ResX - 2;
    }

    /* Make sure dialog window fits on the screen (Y axis)*/
    if(Height > (Handle->ResY - 2))
    {
        /* Set maximum dialog window height */
        Height = Handle->ResY - 2;
    }

    /* Set dialog window final dimensions */
    Handle->PosX = (Handle->ResX - Width) / 2;
    Handle->PosY = (Handle->ResY - Height) / 2;
    Handle->Width = Width;
    Handle->Height = Height;
}

/**
 * Draws a text UI-based boot menu.
 *
 * @param Handle
 *        Supplies a pointer to the boot menu handle.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawBootMenu(OUT PXTBL_DIALOG_HANDLE Handle)
{
    /* Query console screen resolution */
    BlQueryConsoleMode(&Handle->ResX, &Handle->ResY);

    /* Set boot menu parameters */
    Handle->Attributes = 0;
    Handle->DialogColor = EFI_TEXT_BGCOLOR_BLACK;
    Handle->TextColor = EFI_TEXT_FGCOLOR_LIGHTGRAY;
    Handle->PosX = 3;
    Handle->PosY = 3;
    Handle->Width = Handle->ResX - 6;
    Handle->Height = Handle->ResY - 10;

    /* Clear screen and disable cursor */
    BlSetConsoleAttributes(Handle->DialogColor | Handle->TextColor);
    BlClearConsoleScreen();
    BlDisableConsoleCursor();

    /* Check if debugging enabled */
    if(DEBUG)
    {
        /* Print debug version of XTLDR banner */
        BlSetCursorPosition((Handle->ResX - 44) / 2, 1);
        BlConsolePrint(L"XTLDR Boot Loader v%d.%d (%s-%s)\n",
                       XTLDR_VERSION_MAJOR, XTLDR_VERSION_MINOR, XTOS_VERSION_DATE, XTOS_VERSION_HASH);
    }
    else
    {
        /* Print standard XTLDR banner */
        BlSetCursorPosition((Handle->ResX - 22) / 2, 1);
        BlConsolePrint(L"XTLDR Boot Loader v%d.%d\n", XTLDR_VERSION_MAJOR, XTLDR_VERSION_MINOR);
    }

    /* Draw empty dialog box for boot menu */
    BlpDrawDialogBox(Handle, NULL, NULL);

    /* Print help message below the boot menu */
    BlSetCursorPosition(0, Handle->PosY + Handle->Height);
    BlSetConsoleAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY);
    BlConsolePrint(L"    Use cursors to change the selection. Press ENTER key to boot the chosen\n"
                    "    Operating System, 'e' to edit it before booting or 's' for XTLDR shell.\n"
                    "    Additional help available after pressing F1 key.");
}

/**
 * Draws boot menu entry at the specified position.
 *
 * @param Handle
 *        Supplies a pointer to the boot menu handle.
 *
 * @param MenuEntry
 *        Supplies a pointer to the buffer containing a menu entry name.
 *
 * @param Position
 *        Specifies entry position on the list in the boot menu.
 *
 * @param Highlighted
 *        Specifies whether this entry should be highlighted or not.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawBootMenuEntry(IN PXTBL_DIALOG_HANDLE Handle,
                     IN PWCHAR MenuEntry,
                     IN UINT Position,
                     IN BOOLEAN Highlighted)
{
    UINT Index;

    /* Move cursor to the right position */
    BlSetCursorPosition(5, 4 + Position);

    /* Check whether this entry should be highlighted */
    if(Highlighted)
    {
        /* Highlight this entry */
        BlSetConsoleAttributes(EFI_TEXT_BGCOLOR_LIGHTGRAY | EFI_TEXT_FGCOLOR_BLACK);
    }
    else
    {
        /* Set default colors */
        BlSetConsoleAttributes(EFI_TEXT_BGCOLOR_BLACK | EFI_TEXT_FGCOLOR_LIGHTGRAY);
    }

    /* Clear menu entry */
    for(Index = 0; Index < Handle->Width - 4; Index++)
    {
        BlConsolePrint(L" ");
    }

    /* Print menu entry */
    BlSetCursorPosition(5, 4 + Position);
    BlConsolePrint(L"%S\n", MenuEntry);
}

/**
 * Draws dialog box with caption and message.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @param Caption
 *        Specifies a caption string put on the dialog box.
 *
 * @param Message
 *        Specifies a message string put on the dialog box.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawDialogBox(IN OUT PXTBL_DIALOG_HANDLE Handle,
                 IN PWCHAR Caption,
                 IN PWCHAR Message)
{
    WCHAR BoxLine[XTBL_TUI_MAX_DIALOG_WIDTH];
    SIZE_T CaptionLength;
    UINT_PTR PosX, PosY;

    /* Set dialog colors */
    if(Handle->Attributes & XTBL_TUI_DIALOG_ERROR_BOX)
    {
        /* Error dialog with red background and brown button */
        Handle->DialogColor = EFI_TEXT_BGCOLOR_RED;
        Handle->TextColor = EFI_TEXT_FGCOLOR_WHITE;
    }
    else if(Handle->Attributes & XTBL_TUI_DIALOG_GENERIC_BOX)
    {
        /* Generic dialog with blue background and cyan button */
        Handle->DialogColor = EFI_TEXT_BGCOLOR_BLUE;
        Handle->TextColor = EFI_TEXT_FGCOLOR_WHITE;
    }

    /* Set dialog box colors */
    BlSetConsoleAttributes(Handle->DialogColor | 0x0F);

    /* Iterate through dialog box lines */
    for(PosY = Handle->PosY; PosY < Handle->PosY + Handle->Height; PosY++)
    {
        /* Set cursor position in the appropriate place */
        BlSetCursorPosition(Handle->PosX, PosY);

        /* Draw dialog box */
        if(PosY == Handle->PosY)
        {
            /* Draw top line of the dialog box, starting from the left corner */
            BoxLine[0] = EFI_TEXT_BOX_DOWN_RIGHT;

            /* Check if there is a caption for this dialog */
            if(Caption != NULL)
            {
                /* Get caption length */
                CaptionLength = RtlWideStringLength(Caption, 0);

                /* Start caption area with vertical line */
                BoxLine[1] = EFI_TEXT_BOX_VERTICAL_LEFT;

                /* Fill caption area with spaces */
                for(PosX = 2; PosX < CaptionLength + 4; PosX++)
                {
                    BoxLine[PosX] = ' ';
                }

                /* End caption area with vertical line */
                BoxLine[CaptionLength + 4] = EFI_TEXT_BOX_VERTICAL_RIGHT;
            }
            else
            {
                /* No caption, -4 because of left and right vertical lines and corresponding spaces */
                CaptionLength = -4;
            }

            /* Draw bottom line */
            for(PosX = CaptionLength + 5; PosX < Handle->Width - 1; PosX++)
            {
                BoxLine[PosX] = EFI_TEXT_BOX_HORIZONTAL;
            }

            /* End with top right corner */
            BoxLine[Handle->Width - 1] = EFI_TEXT_BOX_DOWN_LEFT;
        }
        else if(PosY == Handle->PosY + Handle->Height - 1)
        {
            /* Draw bottom line of the dialog box, starting from the left corner */
            BoxLine[0] = EFI_TEXT_BOX_UP_LEFT;

            /* Fill bottom with horizontal line */
            for(PosX = 1; PosX < Handle->Width - 1; PosX++)
            {
                BoxLine[PosX] = EFI_TEXT_BOX_HORIZONTAL;
            }

            /* End with bottom right corner */
            BoxLine[Handle->Width - 1] = EFI_TEXT_BOX_UP_RIGHT;
        }
        else
        {
            /* Draw the middle of the dialog box */
            BoxLine[0] = EFI_TEXT_BOX_VERTICAL;

            /* Fill dialog box with spaces */
            for(PosX = 1; PosX < Handle->Width - 1; PosX++)
            {
                BoxLine[PosX] = ' ';
            }

            /* End with vertical line */
            BoxLine[Handle->Width - 1] = EFI_TEXT_BOX_VERTICAL;
        }

        /* Add null terminator to the end of the line */
        BoxLine[Handle->Width] = 0;

        /* Write the line to the console */
        BlConsoleWrite(BoxLine);
    }

    /* Make sure there is a caption to print */
    if(Caption != NULL)
    {
    /* Write dialog box caption */
        BlSetCursorPosition(Handle->PosX + 3, Handle->PosY);
        BlConsolePrint(L"%S", Caption);
    }

    /* Make sure there is a message to print */
    if(Message != NULL)
    {
        /* Write a message on the dialog box */
        BlpDrawDialogMessage(Handle, Message);
    }
}

/**
 * Draws an active or inactive button in the dialog box, depending on the attributes.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawDialogButton(IN PXTBL_DIALOG_HANDLE Handle)
{
    ULONG ButtonColor, TextColor;

    /* Set dialog button colors */
    if(Handle->Attributes & XTBL_TUI_DIALOG_ACTIVE_BUTTON)
    {
        /* This is an active button */
        if(Handle->Attributes & XTBL_TUI_DIALOG_ERROR_BOX)
        {
            /* This is an error dialog box */
            ButtonColor = EFI_TEXT_BGCOLOR_BROWN;
            TextColor = EFI_TEXT_FGCOLOR_WHITE;
        }
        else
        {
            /* This is a generic dialog box */
            ButtonColor = EFI_TEXT_BGCOLOR_CYAN;
            TextColor = EFI_TEXT_FGCOLOR_WHITE;
        }
    }
    else
    {
        /* This is an inactive button */
        ButtonColor = EFI_TEXT_BGCOLOR_LIGHTGRAY;
        TextColor = EFI_TEXT_FGCOLOR_BLACK;
    }

    /* Disable cursor and draw dialog button */
    BlDisableConsoleCursor();
    BlSetConsoleAttributes(ButtonColor | TextColor);
    BlSetCursorPosition(Handle->ResX / 2 - 4, Handle->PosY + Handle->Height - 2);
    BlConsolePrint(L"[  OK  ]");
}

/**
 * Draws an active or inactive input field in the dialog box, depending on the attributes.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @param InputFieldText
 *        Supplies a pointer to the wide char string that will be displayed in the input field.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawDialogInputField(IN PXTBL_DIALOG_HANDLE Handle,
                        IN PWCHAR InputFieldText)
{
    WCHAR InputField[XTBL_TUI_MAX_DIALOG_WIDTH];
    ULONG InputColor, TextColor;
    UINT_PTR Index, Position;
    SIZE_T Length;

    /* Set dialog button colors */
    if(Handle->Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
    {
        /* This is an active input field */
        if(Handle->Attributes & XTBL_TUI_DIALOG_ERROR_BOX)
        {
            /* This is an error dialog box */
            InputColor = EFI_TEXT_BGCOLOR_BROWN;
            TextColor = EFI_TEXT_FGCOLOR_WHITE;
        }
        else
        {
            /* This is a generic dialog box */
            InputColor = EFI_TEXT_BGCOLOR_CYAN;
            TextColor = EFI_TEXT_FGCOLOR_WHITE;
        }
    }
    else
    {
        /* This is an inactive input field */
        InputColor = EFI_TEXT_BGCOLOR_LIGHTGRAY;
        TextColor = EFI_TEXT_FGCOLOR_BLACK;
    }

    /* Set progress bar color and position */
    BlSetConsoleAttributes(InputColor | TextColor);
    Position = (Handle->Attributes & (XTBL_TUI_DIALOG_ACTIVE_BUTTON | XTBL_TUI_DIALOG_INACTIVE_BUTTON)) ? 4 : 3;
    BlSetCursorPosition(Handle->PosX + 4, Handle->PosY + Handle->Height - Position);

    /* Draw input field */
    for(Index = 0; Index < Handle->Width - 8; Index++)
    {
        /* Fill input field with spaces */
        InputField[Index] = L' ';
    }

    /* Disable cursor and write input field to console */
    BlDisableConsoleCursor();
    BlConsoleWrite(InputField);

    /* Check input field text length */
    Length = RtlWideStringLength(InputFieldText, 0);
    if(Length > (Handle->Width - 9))
    {
        /* Text longer than input field width, display only part of it */
        Length = Handle->Width - 9;
    }

    /* Copy a part of input field text to input field */
    for(Index = 0; Index < Length; Index++)
    {
        /* Write input field text */
        InputField[Index] = InputFieldText[Index];
    }

    /* Add null terminator to the end of the line */
    InputField[Handle->Width] = 0;

    /* Write input field text */
    BlSetCursorPosition(Handle->PosX + 4, Handle->PosY + Handle->Height - Position);
    BlConsoleWrite(InputField);

    /* Check if this is an active input field */
    if(Handle->Attributes & XTBL_TUI_DIALOG_ACTIVE_INPUT)
    {
        /* Enable cursor for active input field */
        BlEnableConsoleCursor();
    }
}

/**
 * Draws a message on the dialog box specified by the handle.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @param Message
 *        Supplies a message that will be displayed on the dialog box.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawDialogMessage(IN PXTBL_DIALOG_HANDLE Handle,
                     IN PWCHAR Message)
{
    PWCHAR Msg, MsgLine, LastMsgLine;
    SIZE_T Index, Length, LineLength;
    EFI_STATUS Status;
    ULONG Line;

    /* Allocate memory for dialog box message */
    Length = RtlWideStringLength(Message, 0);
    Status = BlAllocateMemoryPool(Length * sizeof(WCHAR), (PVOID *)&Msg);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Memory allocation failure, print debug message and return */
        BlDebugPrint(L"ERROR: Memory allocation failure (Status Code: 0x%zX)\n", Status);
        return;
    }

    /* Make a copy of dialog box message */
    RtlCopyMemory(Msg, Message, Length * sizeof(WCHAR));
    Msg[Length] = 0;

    /* Tokenize dialog box message */
    MsgLine = RtlTokenizeWideString(Msg, L"\n", &LastMsgLine);

    /* Iterate through message lines */
    Line = 0;
    while(MsgLine)
    {
        /* Determine line length */
        LineLength = RtlWideStringLength(MsgLine, 0);

        /* Write line in the dialog box */
        BlSetCursorPosition(Handle->PosX + 2, Handle->PosY + 2 + Line);
        BlSetConsoleAttributes(Handle->DialogColor | Handle->TextColor);
        BlConsolePrint(L"%S", MsgLine);

        /* Check if message line is shorter than the dialog box working area */
        if(LineLength < Handle->Width - 4)
        {
            /* Fill the rest of the line with spaces */
            for(Index = LineLength; Index < Handle->Width - 4; Index++)
            {
                BlConsolePrint(L" ");
            }
        }

        /* Get next line */
        MsgLine = RtlTokenizeWideString(NULL, L"\n", &LastMsgLine);
        Line++;
    }
}

/**
 * Draws a progress bar component in the dialog box.
 *
 * @param Handle
 *        Supplies a pointer to the dialog box handle.
 *
 * @param Percentage
 *        Specifies the percentage progress of the progress bar.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
BlpDrawDialogProgressBar(IN PXTBL_DIALOG_HANDLE Handle,
                         IN UCHAR Percentage)
{
    UINT_PTR Index, ProgressLength, ProgressBarLength;
    WCHAR ProgressBar[XTBL_TUI_MAX_DIALOG_WIDTH];
    UINT_PTR Position;

    /* Determine progress bar length and calculate progress */
    ProgressBarLength = Handle->Width - 8;
    ProgressLength = (ProgressBarLength * Percentage) / 100;

    /* Set progress bar color and position */
    BlSetConsoleAttributes(EFI_TEXT_FGCOLOR_YELLOW);
    Position = (Handle->Attributes & (XTBL_TUI_DIALOG_ACTIVE_BUTTON | XTBL_TUI_DIALOG_INACTIVE_BUTTON)) ? 4 : 3;
    BlSetCursorPosition(Handle->PosX + 4, Handle->PosY + Handle->Height - Position);

    /* Draw progress bar */
    for(Index = 0; Index < ProgressBarLength; Index++)
    {
        /* Fill progress bar */
        if(Index < ProgressLength)
        {
            /* Fill with full block */
            ProgressBar[Index] = EFI_TEXT_BOX_FULL_BLOCK;
        }
        else
        {
            /* Fill with light block */
            ProgressBar[Index] = EFI_TEXT_BOX_LIGHT_BLOCK;
        }
    }

    /* Terminate progress bar string */
    ProgressBar[Index] = 0;

    /* Disable cursor and write progress bar to console */
    BlDisableConsoleCursor();
    BlConsoleWrite(ProgressBar);
}
