/** @file
  This driver is a implementation of the Graphics Output Protocol
  for the QEMU ramfb device.

  Copyright (c) 2018, Red Hat Inc.

  This program and the accompanying materials are licensed and made
  available under the terms and conditions of the BSD License which
  accompanies this distribution. The full text of the license may be
  found at http://opensource.org/licenses/bsd-license.php

  THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS"
  BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER
  EXPRESS OR IMPLIED.

**/

#include <Protocol/GraphicsOutput.h>

#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/DevicePathLib.h>
#include <Library/FrameBufferBltLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/QemuFwCfgLib.h>

#include <Guid/QemuRamfb.h>

#define RAMFB_FORMAT  0x34325258 /* DRM_FORMAT_XRGB8888 */
#define RAMFB_BPP     4

#pragma pack (1)
typedef struct RAMFB_CONFIG {
  UINT64 Address;
  UINT32 FourCC;
  UINT32 Flags;
  UINT32 Width;
  UINT32 Height;
  UINT32 Stride;
} RAMFB_CONFIG;
#pragma pack ()

STATIC EFI_HANDLE                    mRamfbHandle;
STATIC EFI_HANDLE                    mGopHandle;
STATIC FRAME_BUFFER_CONFIGURE        *mQemuRamfbFrameBufferBltConfigure;
STATIC UINTN                         mQemuRamfbFrameBufferBltConfigureSize;
STATIC FIRMWARE_CONFIG_ITEM          mRamfbFwCfgItem;

STATIC EFI_GRAPHICS_OUTPUT_MODE_INFORMATION mQemuRamfbModeInfo[] = {
  {
    0,    // Version
    640,  // HorizontalResolution
    480,  // VerticalResolution
  },{
    0,    // Version
    800,  // HorizontalResolution
    600,  // VerticalResolution
  },{
    0,    // Version
    1024, // HorizontalResolution
    768,  // VerticalResolution
  }
};

STATIC EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE mQemuRamfbMode = {
  ARRAY_SIZE (mQemuRamfbModeInfo),                // MaxMode
  0,                                              // Mode
  mQemuRamfbModeInfo,                             // Info
  sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION),  // SizeOfInfo
};

STATIC
EFI_STATUS
EFIAPI
QemuRamfbGraphicsOutputQueryMode (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL          *This,
  IN  UINT32                                ModeNumber,
  OUT UINTN                                 *SizeOfInfo,
  OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  **Info
  )
{
  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  *ModeInfo;

  if (Info == NULL || SizeOfInfo == NULL ||
      ModeNumber >= mQemuRamfbMode.MaxMode) {
    return EFI_INVALID_PARAMETER;
  }
  ModeInfo = &mQemuRamfbModeInfo[ModeNumber];

  *Info = AllocateCopyPool (sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION),
            ModeInfo);
  if (*Info == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }
  *SizeOfInfo = sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION);

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
QemuRamfbGraphicsOutputSetMode (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
  IN  UINT32                       ModeNumber
  )
{
  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  *ModeInfo;
  RAMFB_CONFIG                          Config;
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL         Black;
  RETURN_STATUS                         Status;

  if (ModeNumber >= mQemuRamfbMode.MaxMode) {
    return EFI_UNSUPPORTED;
  }
  ModeInfo = &mQemuRamfbModeInfo[ModeNumber];

  DEBUG ((DEBUG_INFO, "Ramfb: SetMode %u (%ux%u)\n", ModeNumber,
    ModeInfo->HorizontalResolution, ModeInfo->VerticalResolution));

  Config.Address = SwapBytes64 (mQemuRamfbMode.FrameBufferBase);
  Config.FourCC  = SwapBytes32 (RAMFB_FORMAT);
  Config.Flags   = SwapBytes32 (0);
  Config.Width   = SwapBytes32 (ModeInfo->HorizontalResolution);
  Config.Height  = SwapBytes32 (ModeInfo->VerticalResolution);
  Config.Stride  = SwapBytes32 (ModeInfo->HorizontalResolution * RAMFB_BPP);

  Status = FrameBufferBltConfigure (
             (VOID*)(UINTN)mQemuRamfbMode.FrameBufferBase,
             ModeInfo,
             mQemuRamfbFrameBufferBltConfigure,
             &mQemuRamfbFrameBufferBltConfigureSize
             );

  if (Status == RETURN_BUFFER_TOO_SMALL) {
    if (mQemuRamfbFrameBufferBltConfigure != NULL) {
      FreePool (mQemuRamfbFrameBufferBltConfigure);
    }
    mQemuRamfbFrameBufferBltConfigure =
      AllocatePool (mQemuRamfbFrameBufferBltConfigureSize);
    if (mQemuRamfbFrameBufferBltConfigure == NULL) {
      mQemuRamfbFrameBufferBltConfigureSize = 0;
      return EFI_OUT_OF_RESOURCES;
    }

    Status = FrameBufferBltConfigure (
               (VOID*)(UINTN)mQemuRamfbMode.FrameBufferBase,
               ModeInfo,
               mQemuRamfbFrameBufferBltConfigure,
               &mQemuRamfbFrameBufferBltConfigureSize
               );
  }
  if (RETURN_ERROR (Status)) {
    ASSERT (Status == RETURN_UNSUPPORTED);
    return Status;
  }

  mQemuRamfbMode.Mode = ModeNumber;
  mQemuRamfbMode.Info = ModeInfo;

  QemuFwCfgSelectItem (mRamfbFwCfgItem);
  QemuFwCfgWriteBytes (sizeof (Config), &Config);

  //
  // clear screen
  //
  ZeroMem (&Black, sizeof (Black));
  Status = FrameBufferBlt (
             mQemuRamfbFrameBufferBltConfigure,
             &Black,
             EfiBltVideoFill,
             0,                               // SourceX -- ignored
             0,                               // SourceY -- ignored
             0,                               // DestinationX
             0,                               // DestinationY
             ModeInfo->HorizontalResolution,  // Width
             ModeInfo->VerticalResolution,    // Height
             0                                // Delta -- ignored
             );
  if (RETURN_ERROR (Status)) {
    DEBUG ((DEBUG_WARN, "%a: clearing the screen failed: %r\n",
      __FUNCTION__, Status));
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
QemuRamfbGraphicsOutputBlt (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL          *This,
  IN  EFI_GRAPHICS_OUTPUT_BLT_PIXEL         *BltBuffer, OPTIONAL
  IN  EFI_GRAPHICS_OUTPUT_BLT_OPERATION     BltOperation,
  IN  UINTN                                 SourceX,
  IN  UINTN                                 SourceY,
  IN  UINTN                                 DestinationX,
  IN  UINTN                                 DestinationY,
  IN  UINTN                                 Width,
  IN  UINTN                                 Height,
  IN  UINTN                                 Delta
  )
{
  return FrameBufferBlt (
           mQemuRamfbFrameBufferBltConfigure,
           BltBuffer,
           BltOperation,
           SourceX,
           SourceY,
           DestinationX,
           DestinationY,
           Width,
           Height,
           Delta
           );
}

STATIC EFI_GRAPHICS_OUTPUT_PROTOCOL mQemuRamfbGraphicsOutput = {
  QemuRamfbGraphicsOutputQueryMode,
  QemuRamfbGraphicsOutputSetMode,
  QemuRamfbGraphicsOutputBlt,
  &mQemuRamfbMode,
};

EFI_STATUS
EFIAPI
InitializeQemuRamfb (
  IN EFI_HANDLE           ImageHandle,
  IN EFI_SYSTEM_TABLE     *SystemTable
  )
{
  EFI_DEVICE_PATH_PROTOCOL  *RamfbDevicePath;
  EFI_DEVICE_PATH_PROTOCOL  *GopDevicePath;
  VOID                      *DevicePath;
  VENDOR_DEVICE_PATH        VendorDeviceNode;
  ACPI_ADR_DEVICE_PATH      AcpiDeviceNode;
  EFI_STATUS                Status;
  EFI_PHYSICAL_ADDRESS      FbBase;
  UINTN                     FbSize, MaxFbSize, Pages;
  UINTN                     FwCfgSize;
  UINTN                     Index;

  if (!QemuFwCfgIsAvailable ()) {
    DEBUG ((DEBUG_INFO, "Ramfb: no FwCfg\n"));
    return EFI_NOT_FOUND;
  }

  Status = QemuFwCfgFindFile ("etc/ramfb", &mRamfbFwCfgItem, &FwCfgSize);
  if (EFI_ERROR (Status)) {
    return EFI_NOT_FOUND;
  }
  if (FwCfgSize != sizeof (RAMFB_CONFIG)) {
    DEBUG ((DEBUG_ERROR, "Ramfb: FwCfg size mismatch (expected %lu, got %lu)\n",
      (UINT64)sizeof (RAMFB_CONFIG), (UINT64)FwCfgSize));
    return EFI_PROTOCOL_ERROR;
  }

  MaxFbSize = 0;
  for (Index = 0; Index < ARRAY_SIZE (mQemuRamfbModeInfo); Index++) {
    mQemuRamfbModeInfo[Index].PixelsPerScanLine =
      mQemuRamfbModeInfo[Index].HorizontalResolution;
    mQemuRamfbModeInfo[Index].PixelFormat =
      PixelBlueGreenRedReserved8BitPerColor;
    FbSize = RAMFB_BPP *
      mQemuRamfbModeInfo[Index].HorizontalResolution *
      mQemuRamfbModeInfo[Index].VerticalResolution;
    if (MaxFbSize < FbSize) {
      MaxFbSize = FbSize;
    }
    DEBUG ((DEBUG_INFO, "Ramfb: Mode %lu: %ux%u, %lu kB\n", (UINT64)Index,
      mQemuRamfbModeInfo[Index].HorizontalResolution,
      mQemuRamfbModeInfo[Index].VerticalResolution,
      (UINT64)(FbSize / 1024)));
  }

  Pages = EFI_SIZE_TO_PAGES (MaxFbSize);
  MaxFbSize = EFI_PAGES_TO_SIZE (Pages);
  FbBase = (EFI_PHYSICAL_ADDRESS)(UINTN)AllocateReservedPages (Pages);
  if (FbBase == 0) {
    DEBUG ((DEBUG_ERROR, "Ramfb: memory allocation failed\n"));
    return EFI_OUT_OF_RESOURCES;
  }
  DEBUG ((DEBUG_INFO, "Ramfb: Framebuffer at 0x%lx, %lu kB, %lu pages\n",
    (UINT64)FbBase, (UINT64)(MaxFbSize / 1024), (UINT64)Pages));
  mQemuRamfbMode.FrameBufferSize = MaxFbSize;
  mQemuRamfbMode.FrameBufferBase = FbBase;

  //
  // 800 x 600
  //
  QemuRamfbGraphicsOutputSetMode (&mQemuRamfbGraphicsOutput, 1);

  //
  // ramfb vendor devpath
  //
  VendorDeviceNode.Header.Type = HARDWARE_DEVICE_PATH;
  VendorDeviceNode.Header.SubType = HW_VENDOR_DP;
  CopyGuid (&VendorDeviceNode.Guid, &gQemuRamfbGuid);
  SetDevicePathNodeLength (&VendorDeviceNode.Header,
    sizeof (VENDOR_DEVICE_PATH));

  RamfbDevicePath = AppendDevicePathNode (NULL,
    (EFI_DEVICE_PATH_PROTOCOL *) &VendorDeviceNode);
  if (RamfbDevicePath == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto FreeFramebuffer;
  }

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &mRamfbHandle,
                  &gEfiDevicePathProtocolGuid,
                  RamfbDevicePath,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Ramfb: install Ramfb Vendor DevicePath failed: %r\n",
      Status));
    goto FreeRamfbDevicePath;
  }

  //
  // gop devpath + protocol
  //
  AcpiDeviceNode.Header.Type = ACPI_DEVICE_PATH;
  AcpiDeviceNode.Header.SubType = ACPI_ADR_DP;
  AcpiDeviceNode.ADR = ACPI_DISPLAY_ADR (
    1,                                       // DeviceIdScheme
    0,                                       // HeadId
    0,                                       // NonVgaOutput
    1,                                       // BiosCanDetect
    0,                                       // VendorInfo
    ACPI_ADR_DISPLAY_TYPE_EXTERNAL_DIGITAL,  // Type
    0,                                       // Port
    0                                        // Index
    );
  SetDevicePathNodeLength (&AcpiDeviceNode.Header,
    sizeof (ACPI_ADR_DEVICE_PATH));

  GopDevicePath = AppendDevicePathNode (RamfbDevicePath,
    (EFI_DEVICE_PATH_PROTOCOL *) &AcpiDeviceNode);
  if (GopDevicePath == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto FreeRamfbHandle;
  }

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &mGopHandle,
                  &gEfiDevicePathProtocolGuid,
                  GopDevicePath,
                  &gEfiGraphicsOutputProtocolGuid,
                  &mQemuRamfbGraphicsOutput,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Ramfb: install GOP DevicePath failed: %r\n",
      Status));
    goto FreeGopDevicePath;
  }

  Status = gBS->OpenProtocol (
                  mRamfbHandle,
                  &gEfiDevicePathProtocolGuid,
                  &DevicePath,
                  gImageHandle,
                  mGopHandle,
                  EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Ramfb: OpenProtocol failed: %r\n", Status));
    goto FreeGopHandle;
  }

  return EFI_SUCCESS;

FreeGopHandle:
  gBS->UninstallMultipleProtocolInterfaces (
         mGopHandle,
         &gEfiDevicePathProtocolGuid,
         GopDevicePath,
         &gEfiGraphicsOutputProtocolGuid,
         &mQemuRamfbGraphicsOutput,
         NULL
         );
FreeGopDevicePath:
  FreePool (GopDevicePath);
FreeRamfbHandle:
  gBS->UninstallMultipleProtocolInterfaces (
         mRamfbHandle,
         &gEfiDevicePathProtocolGuid,
         RamfbDevicePath,
         NULL
         );
FreeRamfbDevicePath:
  FreePool (RamfbDevicePath);
FreeFramebuffer:
  FreePages ((VOID*)(UINTN)mQemuRamfbMode.FrameBufferBase, Pages);
  return Status;
}
