/*++

Copyright (c) 2014 Minoca Corp. All Rights Reserved

Module Name:

    bdscon.c

Abstract:

    This module implements BDS console support functions.

Author:

    Evan Green 17-Mar-2014

Environment:

    Firmware

--*/

//
// ------------------------------------------------------------------- Includes
//

#include "ueficore.h"
#include "bds.h"

//
// ---------------------------------------------------------------- Definitions
//

//
// ------------------------------------------------------ Data Type Definitions
//

//
// ----------------------------------------------- Internal Function Prototypes
//

EFI_STATUS
EfipBdsConnectConsoleVariable (
    CHAR16 *ConsoleVariableName
    );

EFI_STATUS
EfipBdsUpdateConsoleVariable (
    CHAR16 *VariableName,
    EFI_DEVICE_PATH_PROTOCOL *CustomizedDevicePath,
    EFI_DEVICE_PATH_PROTOCOL *ExclusiveDevicePath
    );

BOOLEAN
EfipBdsUpdateSystemTableConsole (
    CHAR16 *VariableName,
    EFI_GUID *ConsoleGuid,
    EFI_HANDLE *ConsoleHandle,
    VOID **ProtocolInterface
    );

BOOLEAN
EfipBdsIsConsoleVariableNonVolatile (
    CHAR16 *Name
    );

//
// -------------------------------------------------------------------- Globals
//

EFI_GUID EfiSimpleTextInputProtocolGuid = EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;

//
// ------------------------------------------------------------------ Functions
//

EFI_STATUS
EfipBdsConnectAllDefaultConsoles (
    VOID
    )

/*++

Routine Description:

    This routine connects the console device based on the console variables.

Arguments:

    None.

Return Value:

    None.

--*/

{

    EFI_STATUS Status;
    BOOLEAN SystemTableUpdated;
    BOOLEAN Updated;

    Status = EfipBdsConnectConsoleVariable(L"ConOut");
    if (EFI_ERROR(Status)) {
        return Status;
    }

    EfipBdsConnectConsoleVariable(L"ConIn");
    EfipBdsConnectConsoleVariable(L"ErrOut");
    SystemTableUpdated = FALSE;

    //
    // Fill console handles in the system table if no console device is
    // assigned.
    //

    Updated = EfipBdsUpdateSystemTableConsole(
                                            L"ConIn",
                                            &EfiSimpleTextInputProtocolGuid,
                                            &(EfiSystemTable->ConsoleInHandle),
                                            (VOID **)&(EfiSystemTable->ConIn));

    if (Updated != FALSE) {
        SystemTableUpdated = TRUE;
    }

    Updated = EfipBdsUpdateSystemTableConsole(
                                           L"ConOut",
                                           &EfiSimpleTextOutputProtocolGuid,
                                           &(EfiSystemTable->ConsoleOutHandle),
                                           (VOID **)&(EfiSystemTable->ConOut));

    if (Updated != FALSE) {
        SystemTableUpdated = TRUE;
    }

    Updated = EfipBdsUpdateSystemTableConsole(
                                        L"ErrOut",
                                        &EfiSimpleTextOutputProtocolGuid,
                                        &(EfiSystemTable->StandardErrorHandle),
                                        (VOID **)&(EfiSystemTable->StdErr));

    if (Updated != FALSE) {
        SystemTableUpdated = TRUE;
    }

    //
    // Recompute the CRC of the system table if it changed.
    //

    if (SystemTableUpdated != FALSE) {
        EfiSystemTable->Hdr.CRC32 = 0;
        EfiCalculateCrc32((UINT8 *)(&EfiSystemTable->Hdr),
                          EfiSystemTable->Hdr.HeaderSize,
                          &(EfiSystemTable->Hdr.CRC32));
    }

    return EFI_SUCCESS;
}

//
// --------------------------------------------------------- Internal Functions
//

EFI_STATUS
EfipBdsConnectConsoleVariable (
    CHAR16 *ConsoleVariableName
    )

/*++

Routine Description:

    This routine connects the console device named by the given variable name.
    If the device path is multi-instance any any instance is connected, this
    routine returns success.

Arguments:

    ConsoleVariableName - Supplies a pointer to a string containing the name
        of the variable of the console. Valid values are ConIn, ConOut, and
        ErrOut.

Return Value:

    EFI status code.

--*/

{

    BOOLEAN DeviceExists;
    EFI_DEVICE_PATH_PROTOCOL *DevicePathCopy;
    EFI_DEVICE_PATH_PROTOCOL *Instance;
    EFI_DEVICE_PATH_PROTOCOL *Next;
    UINTN Size;
    EFI_DEVICE_PATH_PROTOCOL *StartDevicePath;
    EFI_STATUS Status;
    UINTN VariableSize;

    Status = EFI_SUCCESS;
    DeviceExists = FALSE;
    StartDevicePath = EfipBdsGetVariable(ConsoleVariableName,
                                         &EfiGlobalVariableGuid,
                                         &VariableSize);

    if (StartDevicePath == NULL) {
        return EFI_UNSUPPORTED;
    }

    //
    // Loop across every instance in the variable.
    //

    DevicePathCopy = StartDevicePath;
    do {
        Instance = EfiCoreGetNextDevicePathInstance(&DevicePathCopy, &Size);
        if (Instance == NULL) {
            EfiCoreFreePool(StartDevicePath);
            return EFI_UNSUPPORTED;
        }

        Next = Instance;
        while (EfiCoreIsDevicePathEndType(Next) == FALSE) {
            Next = EfiCoreGetNextDevicePathNode(Next);
        }

        EfiCoreSetDevicePathEndNode(Next);

        //
        // This would be the place to check for a USB short form device path
        // and connect it directly.
        //

        Status = EfipBdsConnectDevicePath(Instance);
        if (EFI_ERROR(Status)) {
            EfipBdsUpdateConsoleVariable(ConsoleVariableName, NULL, Instance);

        } else {
            DeviceExists = TRUE;
        }

        EfiCoreFreePool(Instance);

    } while (DevicePathCopy != NULL);

    EfiCoreFreePool(StartDevicePath);
    if (DeviceExists == FALSE) {
        return EFI_NOT_FOUND;
    }

    return EFI_SUCCESS;
}

EFI_STATUS
EfipBdsUpdateConsoleVariable (
    CHAR16 *VariableName,
    EFI_DEVICE_PATH_PROTOCOL *CustomizedDevicePath,
    EFI_DEVICE_PATH_PROTOCOL *ExclusiveDevicePath
    )

/*++

Routine Description:

    This routine updates the console variable, adding or removing a device
    path from the variable.

Arguments:

    VariableName - Supplies a pointer to a string containing the name of the
        variable of the console. Valid values are ConIn, ConOut, and ErrOut.

    CustomizedDevicePath - Supplies an optional pointer to the device path to
        be added to the variable. This parameter cannot be multi-instance.

    ExclusiveDevicePath - Supplies an optional pointer to the device path to
        remove from the variable. This cannot be multi-instance.

Return Value:

    EFI status code.

--*/

{

    UINT32 Attributes;
    EFI_DEVICE_PATH_PROTOCOL *Console;
    UINTN DevicePathSize;
    EFI_DEVICE_PATH_PROTOCOL *NewDevicePath;
    EFI_DEVICE_PATH_PROTOCOL *OldNewDevicePath;
    EFI_STATUS Status;

    Console = NULL;
    DevicePathSize = 0;
    if (CustomizedDevicePath == ExclusiveDevicePath) {
        return EFI_UNSUPPORTED;
    }

    Console = EfipBdsGetVariable(VariableName,
                                 &EfiGlobalVariableGuid,
                                 &DevicePathSize);

    NewDevicePath = Console;

    //
    // If the exclusive device path is part of the variable, delete it.
    //

    if ((ExclusiveDevicePath != NULL) && (Console != NULL)) {
        NewDevicePath = EfipBdsDeletePartialMatchInstance(Console,
                                                          ExclusiveDevicePath);
    }

    //
    // Try to append the customized device path.
    //

    if (CustomizedDevicePath != NULL) {
        if (EfipBdsMatchDevicePaths(NewDevicePath, CustomizedDevicePath) ==
            FALSE) {

            //
            // If there is a part of the customized path in the new device
            // path, delete it.
            //

            NewDevicePath = EfipBdsDeletePartialMatchInstance(
                                                         NewDevicePath,
                                                         CustomizedDevicePath);

            OldNewDevicePath = NewDevicePath;
            NewDevicePath = EfiCoreAppendDevicePathInstance(
                                                         NewDevicePath,
                                                         CustomizedDevicePath);

            if (OldNewDevicePath != NULL) {
                EfiCoreFreePool(OldNewDevicePath);
            }
        }
    }

    //
    // The attributes for ConInDev, ConOutDev, and ErrOutDev is not
    // non-volatile.
    //

    Attributes = EFI_VARIABLE_BOOTSERVICE_ACCESS |
                 EFI_VARIABLE_RUNTIME_ACCESS;

    if (EfipBdsIsConsoleVariableNonVolatile(VariableName) != FALSE) {
        Attributes |= EFI_VARIABLE_NON_VOLATILE;
    }

    //
    // Finally, update the variable of the default console.
    //

    DevicePathSize = EfiCoreGetDevicePathSize(NewDevicePath);
    Status = EfiSetVariable(VariableName,
                            &EfiGlobalVariableGuid,
                            Attributes,
                            DevicePathSize,
                            NewDevicePath);

    if ((DevicePathSize == 0) || (Status == EFI_NOT_FOUND)) {
        Status = EFI_SUCCESS;
    }

    ASSERT(!EFI_ERROR(Status));

    if (Console != NULL) {
        EfiCoreFreePool(Console);
    }

    if ((Console != NewDevicePath) && (NewDevicePath != NULL)) {
        EfiCoreFreePool(NewDevicePath);
    }

    return Status;
}

BOOLEAN
EfipBdsUpdateSystemTableConsole (
    CHAR16 *VariableName,
    EFI_GUID *ConsoleGuid,
    EFI_HANDLE *ConsoleHandle,
    VOID **ProtocolInterface
    )

/*++

Routine Description:

    This routine fills in the console handle in the system table if there are
    no valid console handles.

Arguments:

    VariableName - Supplies a pointer to the string containing the standard
        console variable name.

    ConsoleGuid - Supplies a pointer to the console protocol GUID.

    ConsoleHandle - Supplies a pointer that on input points to the console
        handle in the system table to be checked. On output, this value may be
        updated.

    ProtocolInterface - Supplies a pointer that on input points to the console
        handle interface in the system table to be checked. On output, this
        value may be updated.

Return Value:

    TRUE if the system tablw as updated.

    FALSE if the table did not change.

--*/

{

    EFI_DEVICE_PATH_PROTOCOL *Console;
    UINTN DevicePathSize;
    EFI_DEVICE_PATH_PROTOCOL *FullDevicePath;
    EFI_DEVICE_PATH_PROTOCOL *Instance;
    VOID *Interface;
    BOOLEAN IsTextOut;
    EFI_HANDLE NewHandle;
    EFI_STATUS Status;
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *TextOut;

    ASSERT((VariableName != NULL) && (ConsoleHandle != NULL) &&
           (ConsoleGuid != NULL) && (ProtocolInterface != NULL));

    if (*ConsoleHandle != NULL) {
        Status = EfiHandleProtocol(*ConsoleHandle,
                                   ConsoleGuid,
                                   &Interface);

        if ((Status == EFI_SUCCESS) && (Interface == *ProtocolInterface)) {
            return FALSE;
        }
    }

    //
    // Get all possible device paths from the variable.
    //

    Console = EfipBdsGetVariable(VariableName,
                                 &EfiGlobalVariableGuid,
                                 &DevicePathSize);

    if (Console == NULL) {
        return FALSE;
    }

    //
    // Loop over every instance path in the device path.
    //

    FullDevicePath = Console;
    do {
        Instance = EfiCoreGetNextDevicePathInstance(&Console, &DevicePathSize);
        if (Instance == NULL) {
            EfiCoreFreePool(FullDevicePath);

            ASSERT(FALSE);
        }

        //
        // Find the console device handle with the instance.
        //

        Status = EfiLocateDevicePath(ConsoleGuid, &Instance, &NewHandle);
        if (!EFI_ERROR(Status)) {

            //
            // Get the console protocol on this handle.
            //

            Status = EfiHandleProtocol(NewHandle, ConsoleGuid, &Interface);
            if (!EFI_ERROR(Status)) {
                *ConsoleHandle = NewHandle;
                *ProtocolInterface = Interface;

                //
                // If it's a console out device, set the mode if the mode is
                // not valid.
                //

                IsTextOut = EfiCoreCompareGuids(
                                             ConsoleGuid,
                                             &EfiSimpleTextOutputProtocolGuid);

                if (IsTextOut != FALSE) {
                    TextOut = (EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *)Interface;
                    if (TextOut->Mode->Mode == -1) {
                        TextOut->SetMode(TextOut, 0);
                    }
                }

                return TRUE;
            }
        }

    } while (Instance != NULL);

    //
    // No available console device was found.
    //

    return FALSE;
}

BOOLEAN
EfipBdsIsConsoleVariableNonVolatile (
    CHAR16 *Name
    )

/*++

Routine Description:

    This routine returns whether or not a given console variable name should be
    set with the non-volatile flag. If the device ends in Dev, then it returns
    FALSE, otherwise it returns TRUE.

Arguments:

    Name - Supplies a pointer to the string containing the standard console
        variable name.

Return Value:

    TRUE if the variable is non-volatile.

    FALSE if the variable is volatile.

--*/

{

    CHAR16 *Character;

    Character = Name;
    while (*Character != L'\0') {
        Character += 1;
    }

    if (((INTN)((UINTN)Character - (UINTN)Name) / sizeof(CHAR16)) <= 3) {
        return TRUE;
    }

    if ((*(Character - 3) == L'D') && (*(Character - 2) == L'e') &&
        (*(Character - 1) == L'v')) {

        return FALSE;
    }

    return TRUE;
}

