/*
 * PROJECT:     ReactOS Boot Video Driver for VGA-compatible cards
 * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
 * PURPOSE:     Main file
 * COPYRIGHT:   Copyright 2007 Alex Ionescu <alex.ionescu@reactos.org>
 *              Copyright 2019 Hermès Bélusca-Maïto <hermes.belusca-maito@reactos.org>
 */

#include "precomp.h"

/* PRIVATE FUNCTIONS *********************************************************/

static BOOLEAN
VgaInterpretCmdStream(
    _In_ PUSHORT CmdStream)
{
    USHORT Cmd;
    UCHAR Major, Minor;
    USHORT Port;
    USHORT Count;
    UCHAR Index;
    UCHAR Value;
    USHORT ShortValue;

    /* First make sure that we have a Command Stream */
    if (!CmdStream) return TRUE;

    /* Loop as long as we have commands */
    while (*CmdStream != EOD)
    {
        /* Get the next command and its Major and Minor functions */
        Cmd = *CmdStream++;
        Major = Cmd & 0xF0;
        Minor = Cmd & 0x0F;

        /* Check which major function this is */
        if (Major == INOUT)
        {
            /* Check the minor function */
            if (Minor & IO /* CMD_STREAM_READ */)
            {
                /* Check the sub-type */
                if (Minor & BW /* CMD_STREAM_USHORT */)
                {
                    /* Get the port and read an USHORT from it */
                    Port = *CmdStream++;
                    ShortValue = __inpw(Port);
                }
                else // if (Minor & CMD_STREAM_WRITE)
                {
                    /* Get the port and read an UCHAR from it */
                    Port = *CmdStream++;
                    Value = __inpb(Port);
                }
            }
            else if (Minor & MULTI /* CMD_STREAM_WRITE_ARRAY */)
            {
                /* Check the sub-type */
                if (Minor & BW /* CMD_STREAM_USHORT */)
                {
                    /* Get the port and the count of elements */
                    Port = *CmdStream++;
                    Count = *CmdStream++;

                    /* Write the USHORT to the port; the buffer is what's in the command stream */
                    WRITE_PORT_BUFFER_USHORT((PUSHORT)(VgaRegisterBase + Port), CmdStream, Count);

                    /* Move past the buffer in the command stream */
                    CmdStream += Count;
                }
                else // if (Minor & CMD_STREAM_WRITE)
                {
                    /* Get the port and the count of elements */
                    Port = *CmdStream++;
                    Count = *CmdStream++;

                    /* Loop the command array */
                    for (; Count; --Count, ++CmdStream)
                    {
                        /* Get the UCHAR and write it to the port */
                        Value = (UCHAR)*CmdStream;
                        __outpb(Port, Value);
                    }
                }
            }
            else if (Minor & BW /* CMD_STREAM_USHORT */)
            {
                /* Get the port */
                Port = *CmdStream++;

                /* Get the USHORT and write it to the port */
                ShortValue = *CmdStream++;
                __outpw(Port, ShortValue);
            }
            else // if (Minor & CMD_STREAM_WRITE)
            {
                /* Get the port */
                Port = *CmdStream++;

                /* Get the UCHAR and write it to the port */
                Value = (UCHAR)*CmdStream++;
                __outpb(Port, Value);
            }
        }
        else if (Major == METAOUT)
        {
            /* Check the minor function. Note these are not flags. */
            switch (Minor)
            {
                case INDXOUT:
                {
                    /* Get the port, the count of elements and the start index */
                    Port = *CmdStream++;
                    Count = *CmdStream++;
                    Index = (UCHAR)*CmdStream++;

                    /* Loop the command array */
                    for (; Count; --Count, ++Index, ++CmdStream)
                    {
                        /* Get the USHORT and write it to the port */
                        ShortValue = (USHORT)Index + ((*CmdStream) << 8);
                        __outpw(Port, ShortValue);
                    }
                    break;
                }

                case ATCOUT:
                {
                    /* Get the port, the count of elements and the start index */
                    Port = *CmdStream++;
                    Count = *CmdStream++;
                    Index = (UCHAR)*CmdStream++;

                    /* Loop the command array */
                    for (; Count; --Count, ++Index, ++CmdStream)
                    {
                        /* Write the index */
                        __outpb(Port, Index);

                        /* Get the UCHAR and write it to the port */
                        Value = (UCHAR)*CmdStream;
                        __outpb(Port, Value);
                    }
                    break;
                }

                case MASKOUT:
                {
                    /* Get the port */
                    Port = *CmdStream++;

                    /* Read the current value and add the stream data */
                    Value = __inpb(Port);
                    Value &= *CmdStream++;
                    Value ^= *CmdStream++;

                    /* Write the value */
                    __outpb(Port, Value);
                    break;
                }

                default:
                    /* Unknown command, fail */
                    return FALSE;
            }
        }
        else if (Major != NCMD)
        {
            /* Unknown major function, fail */
            return FALSE;
        }
    }

    /* If we got here, return success */
    return TRUE;
}

static BOOLEAN
VgaIsPresent(VOID)
{
    UCHAR OrgGCAddr, OrgReadMap, OrgBitMask;
    UCHAR OrgSCAddr, OrgMemMode;
    UCHAR i;

    /* Remember the original state of the Graphics Controller Address register */
    OrgGCAddr = __inpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT);

    /*
     * Write the Read Map register with a known state so we can verify
     * that it isn't changed after we fool with the Bit Mask. This ensures
     * that we're dealing with indexed registers, since both the Read Map and
     * the Bit Mask are addressed at GRAPH_DATA_PORT.
     */
    __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_READ_MAP);

    /*
     * If we can't read back the Graphics Address register setting we just
     * performed, it's not readable and this isn't a VGA.
     */
    if ((__inpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT) & GRAPH_ADDR_MASK) != IND_READ_MAP)
        return FALSE;

    /*
     * Set the Read Map register to a known state.
     */
    OrgReadMap = __inpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT);
    __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, READ_MAP_TEST_SETTING);

    /* Read it back... it should be the same */
    if (__inpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT) != READ_MAP_TEST_SETTING)
    {
        /*
         * The Read Map setting we just performed can't be read back; not a
         * VGA. Restore the default Read Map state and fail.
         */
        __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, READ_MAP_DEFAULT);
        return FALSE;
    }

    /* Remember the original setting of the Bit Mask register */
    __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_BIT_MASK);

    /* Read it back... it should be the same */
    if ((__inpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT) & GRAPH_ADDR_MASK) != IND_BIT_MASK)
    {
        /*
         * The Graphics Address register setting we just made can't be read
         * back; not a VGA. Restore the default Read Map state and fail.
         */
        __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_READ_MAP);
        __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, READ_MAP_DEFAULT);
        return FALSE;
    }

    /* Read the VGA Data Register */
    OrgBitMask = __inpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT);

    /*
     * Set up the initial test mask we'll write to and read from the Bit Mask,
     * and loop on the bitmasks.
     */
    for (i = 0xBB; i; i >>= 1)
    {
        /* Write the test mask to the Bit Mask */
        __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, i);

        /* Read it back... it should be the same */
        if (__inpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT) != i)
        {
            /*
             * The Bit Mask is not properly writable and readable; not a VGA.
             * Restore the Bit Mask and Read Map to their default states and fail.
             */
            __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, BIT_MASK_DEFAULT);
            __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_READ_MAP);
            __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, READ_MAP_DEFAULT);
            return FALSE;
        }
    }

    /*
     * There's something readable at GRAPH_DATA_PORT; now switch back and
     * make sure that the Read Map register hasn't changed, to verify that
     * we're dealing with indexed registers.
     */
    __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_READ_MAP);

    /* Read it back */
    if (__inpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT) != READ_MAP_TEST_SETTING)
    {
        /*
         * The Read Map is not properly writable and readable; not a VGA.
         * Restore the Bit Mask and Read Map to their default states, in case
         * this is an EGA, so subsequent writes to the screen aren't garbled.
         * Then fail.
         */
        __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, READ_MAP_DEFAULT);
        __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_BIT_MASK);
        __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, BIT_MASK_DEFAULT);
        return FALSE;
    }

    /*
     * We've pretty surely verified the existence of the Bit Mask register.
     * Put the Graphics Controller back to the original state.
     */
    __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, OrgReadMap);
    __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, IND_BIT_MASK);
    __outpb(VGA_BASE_IO_PORT + GRAPH_DATA_PORT, OrgBitMask);
    __outpb(VGA_BASE_IO_PORT + GRAPH_ADDRESS_PORT, OrgGCAddr);

    /*
     * Now, check for the existence of the Chain4 bit.
     */

    /*
     * Remember the original states of the Sequencer Address and Memory Mode
     * registers.
     */
    OrgSCAddr = __inpb(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT);
    __outpb(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT, IND_MEMORY_MODE);

    /* Read it back... it should be the same */
    if ((__inpb(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT) & SEQ_ADDR_MASK) != IND_MEMORY_MODE)
    {
        /*
         * Couldn't read back the Sequencer Address register setting
         * we just performed, fail.
         */
        return FALSE;
    }

    /* Read sequencer Data */
    OrgMemMode = __inpb(VGA_BASE_IO_PORT + SEQ_DATA_PORT);

    /*
     * Toggle the Chain4 bit and read back the result. This must be done during
     * sync reset, since we're changing the chaining state.
     */

    /* Begin sync reset */
    __outpw(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT, (IND_SYNC_RESET + (START_SYNC_RESET_VALUE << 8)));

    /* Toggle the Chain4 bit */
    __outpb(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT, IND_MEMORY_MODE);
    __outpb(VGA_BASE_IO_PORT + SEQ_DATA_PORT, OrgMemMode ^ CHAIN4_MASK);

    /* Read it back... it should be the same */
    if (__inpb(VGA_BASE_IO_PORT + SEQ_DATA_PORT) != (OrgMemMode ^ CHAIN4_MASK))
    {
        /*
         * Chain4 bit is not there, not a VGA.
         * Set text mode default for Memory Mode register.
         */
        __outpb(VGA_BASE_IO_PORT + SEQ_DATA_PORT, MEMORY_MODE_TEXT_DEFAULT);

        /* End sync reset */
        __outpw(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT, (IND_SYNC_RESET + (END_SYNC_RESET_VALUE << 8)));

        /* Fail */
        return FALSE;
    }

    /*
     * It's a VGA.
     */

    /* Restore the original Memory Mode setting */
    __outpb(VGA_BASE_IO_PORT + SEQ_DATA_PORT, OrgMemMode);

    /* End sync reset */
    __outpw(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT, (IND_SYNC_RESET + (END_SYNC_RESET_VALUE << 8)));

    /* Restore the original Sequencer Address setting */
    __outpb(VGA_BASE_IO_PORT + SEQ_ADDRESS_PORT, OrgSCAddr);

    /* VGA is present! */
    return TRUE;
}

/* PUBLIC FUNCTIONS **********************************************************/

BOOLEAN
NTAPI
VidInitialize(
    _In_ BOOLEAN SetMode)
{
    ULONG_PTR Context = 0;
    PHYSICAL_ADDRESS TranslatedAddress;
    PHYSICAL_ADDRESS NullAddress = {{0, 0}}, VgaAddress;
    ULONG AddressSpace;
    BOOLEAN Result;
    ULONG_PTR Base;

    /* Make sure that we have a bus translation function */
    if (!HalFindBusAddressTranslation) return FALSE;

    /* Loop trying to find possible VGA base addresses */
    while (TRUE)
    {
        /* Get the VGA Register address */
        AddressSpace = 1;
        Result = HalFindBusAddressTranslation(NullAddress,
                                              &AddressSpace,
                                              &TranslatedAddress,
                                              &Context,
                                              TRUE);
        if (!Result) return FALSE;

        /* See if this is I/O Space, which we need to map */
        if (!AddressSpace)
        {
            /* Map it */
            Base = (ULONG_PTR)MmMapIoSpace(TranslatedAddress, 0x400, MmNonCached);
        }
        else
        {
            /* The base is the translated address, no need to map I/O space */
            Base = TranslatedAddress.LowPart;
        }

        /* Try to see if this is VGA */
        VgaRegisterBase = Base;
        if (VgaIsPresent())
        {
            /* Translate the VGA Memory Address */
            VgaAddress.LowPart = MEM_VGA;
            VgaAddress.HighPart = 0;
            AddressSpace = 0;
            Result = HalFindBusAddressTranslation(VgaAddress,
                                                  &AddressSpace,
                                                  &TranslatedAddress,
                                                  &Context,
                                                  FALSE);
            if (Result) break;
        }
        else
        {
            /* It's not, so unmap the I/O space if we mapped it */
            if (!AddressSpace) MmUnmapIoSpace((PVOID)VgaRegisterBase, 0x400);
        }

        /* Continue trying to see if there's any other address */
    }

    /* Success! See if this is I/O Space, which we need to map */
    if (!AddressSpace)
    {
        /* Map it */
        Base = (ULONG_PTR)MmMapIoSpace(TranslatedAddress,
                                       MEM_VGA_SIZE,
                                       MmNonCached);
    }
    else
    {
        /* The base is the translated address, no need to map I/O space */
        Base = TranslatedAddress.LowPart;
    }

    /* Set the VGA Memory Base */
    VgaBase = Base;

    /* Now check if we have to set the mode */
    if (SetMode)
    {
        /* Clear the current position */
        VidpCurrentX = 0;
        VidpCurrentY = 0;

        /* Reset the display and initialize it */
        if (HalResetDisplay())
        {
            /* The HAL handled the display, re-initialize only the AC registers */
            VgaInterpretCmdStream(AT_Initialization);
        }
        else
        {
            /* The HAL didn't handle the display, fully re-initialize the VGA */
            VgaInterpretCmdStream(VGA_640x480);
        }
    }

    /* VGA is ready */
    return TRUE;
}

VOID
NTAPI
VidResetDisplay(
    _In_ BOOLEAN HalReset)
{
    /* Clear the current position */
    VidpCurrentX = 0;
    VidpCurrentY = 0;

    /* Clear the screen with HAL if we were asked to */
    if (HalReset)
    {
        if (!HalResetDisplay())
        {
            /* The HAL didn't handle the display, fully re-initialize the VGA */
            VgaInterpretCmdStream(VGA_640x480);
        }
    }

    /* Always re-initialize the AC registers */
    VgaInterpretCmdStream(AT_Initialization);

    /* Re-initialize the palette and fill the screen black */
    InitializePalette();
    VidSolidColorFill(0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1, BV_COLOR_BLACK);
}
