/**
 * PROJECT:         ExectOS
 * COPYRIGHT:       See COPYING.md in the top level directory
 * FILE:            xtldr/debug.cc
 * DESCRIPTION:     XT Boot Loader debugging support
 * DEVELOPERS:      Rafal Kupiec <belliash@codingworkshop.eu.org>
 */

#include <xtldr.hh>


/**
 * Enables I/O space access to all serial controllers found on the PCI(E) root bridge.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
Debug::ActivateSerialIOController()
{
    EFI_GUID PciGuid = EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_GUID;
    PEFI_PCI_ROOT_BRIDGE_IO_PROTOCOL PciDev;
    USHORT Bus, Device, Function, Command;
    UINT_PTR Index, PciHandleSize;
    PEFI_HANDLE PciHandle = NULLPTR;
    PCI_COMMON_HEADER PciHeader;
    EFI_STATUS Status;
    ULONGLONG Address;

    /* Allocate memory for single EFI_HANDLE, what should be enough in most cases */
    PciHandleSize = sizeof(EFI_HANDLE);
    Status = Memory::AllocatePool(PciHandleSize, (PVOID*)&PciHandle);
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Memory allocation failure */
        return Status;
    }

    /* Get all instances of PciRootBridgeIo */
    Status = XtLoader::GetEfiSystemTable()->BootServices->LocateHandle(ByProtocol, &PciGuid, NULLPTR,
                                                                       &PciHandleSize, PciHandle);
    if(Status == STATUS_EFI_BUFFER_TOO_SMALL)
    {
        /* Reallocate more memory as requested by UEFI */
        Memory::FreePool(PciHandle);
        Status = Memory::AllocatePool(PciHandleSize, (PVOID*)&PciHandle);
        if(Status != STATUS_EFI_SUCCESS)
        {
            /* Memory reallocation failure */
            return Status;
        }

        /* Second attempt to get instances of PciRootBridgeIo */
        Status = XtLoader::GetEfiSystemTable()->BootServices->LocateHandle(ByProtocol, &PciGuid, NULLPTR,
                                                                           &PciHandleSize, PciHandle);
    }

    /* Make sure successfully obtained PciRootBridgeIo instances */
    if(Status != STATUS_EFI_SUCCESS)
    {
        /* Failed to get PciRootBridgeIo instances */
        return Status;
    }

    /* Enumerate all devices for each handle, which decides a segment and a bus number range */
    for(Index = 0; Index < (PciHandleSize / sizeof(EFI_HANDLE)); Index++)
    {
        /* Get inferface from the protocol */
        Status = XtLoader::GetEfiSystemTable()->BootServices->HandleProtocol(PciHandle[Index], &PciGuid, (PVOID*)&PciDev);
        if(Status != STATUS_EFI_SUCCESS)
        {
            /* Failed to get interface */
            return Status;
        }

        /* Enumerate whole PCI bridge */
        for(Bus = 0; Bus <= PCI_MAX_BRIDGE_NUMBER; Bus++)
        {
            /* Enumerate all devices for each bus */
            for(Device = 0; Device < PCI_MAX_DEVICES; Device++)
            {
                /* Enumerate all functions for each devices */
                for(Function = 0; Function < PCI_MAX_FUNCTION; Function++)
                {
                    /* Read configuration space */
                    Address = ((ULONGLONG)((((UINT_PTR) Bus) << 24) + (((UINT_PTR) Device) << 16) +
                                           (((UINT_PTR) Function) << 8) + ((UINT_PTR) 0)));
                    PciDev->Pci.Read(PciDev, EfiPciIoWidthUint32, Address, sizeof (PciHeader) / sizeof (UINT), &PciHeader);

                    /* Check if device exists */
                    if(PciHeader.VendorId == PCI_INVALID_VENDORID)
                    {
                        /* Skip non-existen device */
                        continue;
                    }

                    /* Check if device is serial controller or multiport serial controller */
                    if(PciHeader.BaseClass == 0x07 && (PciHeader.SubClass == 0x00 || PciHeader.SubClass == 0x02))
                    {
                        /* Enable I/O space access */
                        Address |= 0x4;
                        Command = PCI_ENABLE_IO_SPACE;
                        Status = PciDev->Pci.Write(PciDev, EfiPciIoWidthUint16, Address, 1, &Command);
                    }
                }
            }
        }
    }

    /* Return SUCCESS */
    return STATUS_EFI_SUCCESS;
}

/**
 * This routine initializes the XTLDR debug console.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
Debug::InitializeDebugConsole()
{
    ULONG PortAddress, PortNumber, BaudRate;
    PWCHAR DebugConfiguration, DebugPort, LastPort;
    EFI_STATUS Status;

    /* Set default serial port options */
    PortAddress = 0;
    PortNumber = 0;
    BaudRate = 0;

    /* Get debug configuration */
    Configuration::GetValue(L"DEBUG", &DebugConfiguration);

    /* Make sure any debug options are provided and debug console is not initialized yet */
    if(DebugConfiguration && EnabledDebugPorts == 0)
    {
        /* Find all debug ports */
        DebugPort = RTL::WideString::TokenizeWideString(DebugConfiguration, L";", &LastPort);

        /* Iterate over all debug ports */
        while(DebugPort != NULLPTR)
        {
            /* Check what port is set for debugging */
            if(RTL::WideString::CompareWideStringInsensitive(DebugPort, L"COM", 3) == 0)
            {
                /* Read COM port number */
                DebugPort += 3;
                while(*DebugPort >= '0' && *DebugPort <= '9')
                {
                    /* Get port number */
                    PortNumber *= 10;
                    PortNumber += *DebugPort - '0';
                    DebugPort++;
                }

                /* Check if custom COM port address supplied */
                if(PortNumber == 0 && RTL::WideString::CompareWideStringInsensitive(DebugPort, L":0x", 3) == 0)
                {
                    /* COM port address provided */
                    DebugPort += 3;
                    while((*DebugPort >= '0' && *DebugPort <= '9') ||
                          (*DebugPort >= 'A' && *DebugPort <= 'F') ||
                          (*DebugPort >= 'a' && *DebugPort <= 'f'))
                    {
                        /* Get port address */
                        PortAddress *= 16;
                        if(*DebugPort >= '0' && *DebugPort <= '9')
                        {
                            PortAddress += *DebugPort - '0';
                        }
                        else if(*DebugPort >= 'A' && *DebugPort <= 'F')
                        {
                            PortAddress += *DebugPort - 'A' + 10;
                        }
                        else if(*DebugPort >= 'a' && *DebugPort <= 'f')
                        {
                            PortAddress += *DebugPort - 'a' + 10;
                        }
                        DebugPort++;
                    }
                }

                /* Look for additional COM port parameters */
                if(*DebugPort == ',')
                {
                    /* Baud rate provided */
                    DebugPort++;
                    while(*DebugPort >= '0' && *DebugPort <= '9')
                    {
                        /* Get baud rate */
                        BaudRate *= 10;
                        BaudRate += *DebugPort - '0';
                        DebugPort++;
                    }
                }

                /* Enable debug port */
                EnabledDebugPorts |= XTBL_DEBUGPORT_SERIAL;
            }
            else if(RTL::WideString::CompareWideStringInsensitive(DebugPort, L"SCREEN", 5) == 0)
            {
                /* Enable debug port */
                EnabledDebugPorts |= XTBL_DEBUGPORT_SCREEN;
            }
            else
            {
                /* Unsupported debug port specified */
                Console::Print(L"ERROR: Unsupported debug port ('%S') specified\n", DebugPort);
                EfiUtils::SleepExecution(3000);
            }

            /* Take next debug port */
            DebugPort = RTL::WideString::TokenizeWideString(NULLPTR, L";", &LastPort);
        }

        /* Check if serial debug port is enabled */
        if(EnabledDebugPorts & XTBL_DEBUGPORT_SERIAL)
        {
            /* Try to initialize COM port */
            Status = InitializeSerialPort(PortNumber, PortAddress, BaudRate);
            if(Status != STATUS_EFI_SUCCESS)
            {
                /* Remove serial debug port, as COM port initialization failed and return */
                EnabledDebugPorts &= ~XTBL_DEBUGPORT_SERIAL;
                return Status;
            }
        }
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * This routine initializes the serial debug console.
 *
 * @param PortNumber
 *        Supplies a port number.
 *
 * @param PortAddress
 *        Supplies an address of the COM port.
 *
 * @param BaudRate
 *        Supplies an optional port baud rate.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
EFI_STATUS
Debug::InitializeSerialPort(IN ULONG PortNumber,
                            IN ULONG PortAddress,
                            IN ULONG BaudRate)
{
    EFI_STATUS EfiStatus;
    XTSTATUS Status;

    /* Check if custom COM port address supplied */
    if(!PortAddress)
    {
        /* We support only a pre-defined number of ports */
        if(PortNumber > COMPORT_COUNT)
        {
            /* Fail if wrong/unsupported port used */
            return STATUS_INVALID_PARAMETER;
        }

        /* Check if serial port is set */
        if(PortNumber == 0)
        {
            /* Use COM1 by default */
            PortNumber = 1;
        }

        /* Set custom port address based on the port number and print debug message */
        PortAddress = ComPortList[PortNumber - 1];
        Console::Print(L"Initializing serial console at port COM%d\n", PortNumber);
    }
    else
    {
        /* Custom port address supplied, print debug message */
        Console::Print(L"Initializing serial console at COM port address: 0x%lX\n", PortAddress);
    }

    /* Initialize COM port */
    Status = HL::ComPort::InitializeComPort(&SerialPort, (PUCHAR)UlongToPtr(PortAddress), BaudRate);

    /* Port not found under supplied address */
    if(Status == STATUS_NOT_FOUND && PortAddress)
    {
        /* This might be PCI(E) serial controller, try to activate I/O space access first */
        EfiStatus = ActivateSerialIOController();
        if(EfiStatus == STATUS_EFI_SUCCESS)
        {
            /* Try to reinitialize COM port */
            Console::Print(L"Enabled I/O space access for all PCI(E) serial controllers found\n");
            Status = HL::ComPort::InitializeComPort(&SerialPort, (PUCHAR)UlongToPtr(PortAddress), BaudRate);
        }
    }

    /* Check COM port initialization status code */
    if(Status != STATUS_SUCCESS)
    {
        /* Serial port initialization failed, mark as not ready */
        return STATUS_EFI_NOT_READY;
    }

    /* Return success */
    return STATUS_EFI_SUCCESS;
}

/**
 * This routine formats the input string and prints it out to the debug ports.
 *
 * @param Format
 *        The formatted string that is to be written to the output.
 *
 * @param ...
 *        Depending on the format string, this routine might expect a sequence of additional arguments.
 *
 * @return This routine does not return any value.
 *
 * @since XT 1.0
 */
XTCDECL
VOID
Debug::Print(IN PCWSTR Format,
             IN ...)
{
    RTL_PRINT_CONTEXT ConsolePrintContext, SerialPrintContext;
    VA_LIST Arguments;

    /* Check if debugging enabled and if EFI serial port is fully initialized */
    if(DEBUG)
    {
        /* Initialize the print contexts */
        ConsolePrintContext.WriteWideCharacter = Console::PutChar;
        SerialPrintContext.WriteWideCharacter = PutChar;

        /* Initialise the va_list */
        VA_START(Arguments, Format);

        /* Check if serial debug port is enabled */
        if((EnabledDebugPorts & XTBL_DEBUGPORT_SERIAL) && (SerialPort.Flags & COMPORT_FLAG_INIT))
        {
            /* Format and print the string to the serial console */
            RTL::WideString::FormatWideString(&SerialPrintContext, (PWCHAR)Format, Arguments);
        }

        /* Check if screen debug port is enabled and Boot Services are still available */
        if((EnabledDebugPorts & XTBL_DEBUGPORT_SCREEN) && (XtLoader::GetBootServicesStatus() == TRUE))
        {
            /* Format and print the string to the screen */
            RTL::WideString::FormatWideString(&ConsolePrintContext, (PWCHAR)Format, Arguments);
        }

        /* Clean up the va_list */
        VA_END(Arguments);
    }
}

/**
 * Writes a character to the serial console.
 *
 * @param Character
 *        The integer promotion of the character to be written.
 *
 * @return This routine returns a status code.
 *
 * @since XT 1.0
 */
XTCDECL
XTSTATUS
Debug::PutChar(IN WCHAR Character)
{
    WCHAR Buffer[2];

    /* Write character to the serial console */
    Buffer[0] = Character;
    Buffer[1] = 0;
    return HL::ComPort::WriteComPort(&SerialPort, Buffer[0]);
}

/**
 * Determines if the serial port has been successfully initialized and is ready for communication.
 *
 * @return This routine returns TRUE if the serial port is initialized and ready, FALSE otherwise.
 *
 * @since XT 1.0
 */
XTCDECL
BOOLEAN
Debug::SerialPortReady()
{
    return (SerialPort.Flags & COMPORT_FLAG_INIT);
}
