/** @file
 *
 *  Copyright (c) 2020, ARM Limited. All rights reserved.
 *  Copyright (c) 2017-2018, Andrei Warkentin <andrey.warkentin@gmail.com>
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *
 *  SPDX-License-Identifier: BSD-2-Clause-Patent
 *
 **/

#include <Base.h>
#include "DisplayDxe.h"

#define MODE_800_ENABLED      BIT0
#define MODE_640_ENABLED      BIT1
#define MODE_1024_ENABLED     BIT2
#define MODE_720P_ENABLED     BIT3
#define MODE_1080P_ENABLED    BIT4
#define MODE_NATIVE_ENABLED   BIT5
#define JUST_NATIVE_ENABLED   MODE_NATIVE_ENABLED
#define ALL_MODES             (BIT6 - 1)
#define POS_TO_FB(posX, posY) ((UINT8*)                                 \
                               ((UINTN)This->Mode->FrameBufferBase +    \
                                (posY) * This->Mode->Info->PixelsPerScanLine * \
                                PI3_BYTES_PER_PIXEL +                   \
                                (posX) * PI3_BYTES_PER_PIXEL))

STATIC
EFI_STATUS
EFIAPI
DriverSupported (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  Controller,
  IN EFI_DEVICE_PATH_PROTOCOL    *RemainingDevicePath
  );

STATIC
EFI_STATUS
EFIAPI
DriverStart (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  Controller,
  IN EFI_DEVICE_PATH_PROTOCOL    *RemainingDevicePath
  );

STATIC
EFI_STATUS
EFIAPI
DriverStop (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  Controller,
  IN UINTN                       NumberOfChildren,
  IN EFI_HANDLE                  *ChildHandleBuffer
  );

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

STATIC
EFI_STATUS
EFIAPI
DisplaySetMode (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
  IN  UINT32                       ModeNumber
  );

STATIC
EFI_STATUS
EFIAPI
DisplayBlt (
  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         OPTIONAL
  );

EFI_DRIVER_BINDING_PROTOCOL mDriverBinding = {
  DriverSupported,
  DriverStart,
  DriverStop,
  0xa,
  NULL,
  NULL
};

typedef struct {
  VENDOR_DEVICE_PATH DisplayDevicePath;
  EFI_DEVICE_PATH EndDevicePath;
} DISPLAY_DEVICE_PATH;

typedef struct {
  UINT32 Width;
  UINT32 Height;
} GOP_MODE_DATA;

STATIC UINT32 mBootWidth;
STATIC UINT32 mBootHeight;
STATIC EFI_HANDLE mDevice;
STATIC RASPBERRY_PI_FIRMWARE_PROTOCOL *mFwProtocol;
STATIC EFI_CPU_ARCH_PROTOCOL *mCpu;

STATIC UINTN mLastMode;
STATIC GOP_MODE_DATA mGopModeTemplate[] = {
  { 800,  600  }, /* Legacy */
  { 640,  480  }, /* Legacy */
  { 1024, 768  }, /* Legacy */
  { 1280, 720  }, /* 720p */
  { 1920, 1080 }, /* 1080p */
  { 0,    0    }, /* Physical */
};

STATIC UINTN mLastMode;
STATIC GOP_MODE_DATA mGopModeData[ARRAY_SIZE (mGopModeTemplate)];

STATIC DISPLAY_DEVICE_PATH mDisplayProtoDevicePath =
{
  {
    {
      HARDWARE_DEVICE_PATH,
      HW_VENDOR_DP,
      {
        (UINT8)(sizeof (VENDOR_DEVICE_PATH)),
        (UINT8)((sizeof (VENDOR_DEVICE_PATH)) >> 8),
      }
    },
    EFI_CALLER_ID_GUID,
  },
  {
    END_DEVICE_PATH_TYPE,
    END_ENTIRE_DEVICE_PATH_SUBTYPE,
    {
      sizeof (EFI_DEVICE_PATH_PROTOCOL),
      0
    }
  }
};

#define PI3_BITS_PER_PIXEL              (32)
#define PI3_BYTES_PER_PIXEL             (PI3_BITS_PER_PIXEL / 8)

EFI_GRAPHICS_OUTPUT_PROTOCOL gDisplayProto = {
  DisplayQueryMode,
  DisplaySetMode,
  DisplayBlt,
  NULL
};

STATIC
EFI_STATUS
EFIAPI
DisplayQueryMode (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL          *This,
  IN  UINT32                                ModeNumber,
  OUT UINTN                                 *SizeOfInfo,
  OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  **Info
  )
{
  EFI_STATUS Status;
  GOP_MODE_DATA *Mode;

  if (Info == NULL || SizeOfInfo == NULL || ModeNumber >= This->Mode->MaxMode) {
    return EFI_INVALID_PARAMETER;
  }

  Status = gBS->AllocatePool (
                  EfiBootServicesData,
                  sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION),
                  (VOID**)Info
                );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Mode = &mGopModeData[ModeNumber];

  *SizeOfInfo = sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION);
  (*Info)->Version = This->Mode->Info->Version;
  (*Info)->HorizontalResolution = Mode->Width;
  (*Info)->VerticalResolution = Mode->Height;
  (*Info)->PixelFormat = This->Mode->Info->PixelFormat;
  (*Info)->PixelInformation.RedMask = This->Mode->Info->PixelInformation.RedMask;
  (*Info)->PixelInformation.GreenMask = This->Mode->Info->PixelInformation.GreenMask;
  (*Info)->PixelInformation.BlueMask = This->Mode->Info->PixelInformation.BlueMask;
  (*Info)->PixelInformation.ReservedMask = This->Mode->Info->PixelInformation.ReservedMask;
  (*Info)->PixelsPerScanLine = Mode->Width;

  return EFI_SUCCESS;
}

STATIC
VOID
ClearScreen (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL *This
  )
{
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL Fill;

  Fill.Red = 0x00;
  Fill.Green = 0x00;
  Fill.Blue = 0x00;
  This->Blt (This, &Fill, EfiBltVideoFill,
          0, 0, 0, 0, This->Mode->Info->HorizontalResolution,
          This->Mode->Info->VerticalResolution,
          This->Mode->Info->HorizontalResolution *
          sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL));
}

STATIC
EFI_STATUS
EFIAPI
DisplaySetMode (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
  IN  UINT32                       ModeNumber
  )
{
  UINTN FbSize;
  UINTN FbPitch;
  EFI_STATUS Status;
  EFI_PHYSICAL_ADDRESS FbBase;
  GOP_MODE_DATA *Mode = &mGopModeData[ModeNumber];

 if (ModeNumber >= This->Mode->MaxMode) {
    return EFI_UNSUPPORTED;
  }

  DEBUG ((DEBUG_INFO, "Setting mode %u from %u: %u x %u\n",
    ModeNumber, This->Mode->Mode, Mode->Width, Mode->Height));
  Status = mFwProtocol->GetFB (Mode->Width, Mode->Height,
                          PI3_BITS_PER_PIXEL, &FbBase,
                          &FbSize, &FbPitch);
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Could not set mode %u\n", ModeNumber));
    return EFI_DEVICE_ERROR;
  }

  DEBUG ((DEBUG_INFO, "Mode %u: %u x %u framebuffer is %u bytes at %p\n",
    ModeNumber, Mode->Width, Mode->Height, FbSize, FbBase));

  if (FbPitch / PI3_BYTES_PER_PIXEL != Mode->Width) {
    DEBUG ((DEBUG_ERROR, "Error: Expected width %u, got width %u\n",
      Mode->Width, FbPitch / PI3_BYTES_PER_PIXEL));
    return EFI_DEVICE_ERROR;
  }

  /*
   * WT, because certain OS loaders access the frame buffer directly
   * and we don't want to see corruption due to missing WB cache
   * maintenance. Performance with WT is good.
   */
  Status = mCpu->SetMemoryAttributes (mCpu, FbBase,
                   ALIGN_VALUE (FbSize, EFI_PAGE_SIZE),
                   EFI_MEMORY_WT);
  if (Status != EFI_SUCCESS) {
    DEBUG ((DEBUG_ERROR, "Couldn't set framebuffer attributes: %r\n", Status));
    return Status;
  }

  This->Mode->Mode = ModeNumber;
  This->Mode->Info->Version = 0;
  This->Mode->Info->HorizontalResolution = Mode->Width;
  This->Mode->Info->VerticalResolution = Mode->Height;
  /*
   * NOTE: Windows REQUIRES BGR in 32 or 24 bit format.
   */
  This->Mode->Info->PixelFormat = PixelBlueGreenRedReserved8BitPerColor;
  This->Mode->Info->PixelsPerScanLine = Mode->Width;
  This->Mode->SizeOfInfo = sizeof (*This->Mode->Info);
  This->Mode->FrameBufferBase = FbBase;
  This->Mode->FrameBufferSize = Mode->Width * Mode->Height * PI3_BYTES_PER_PIXEL;
  DEBUG((DEBUG_INFO, "Reported Mode->FrameBufferSize is %u\n", This->Mode->FrameBufferSize));

  ClearScreen (This);
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
DisplayBlt (
  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         OPTIONAL
  )
{
  UINT8 *VidBuf, *BltBuf, *VidBuf1;
  UINTN i;

  if ((UINTN)BltOperation >= EfiGraphicsOutputBltOperationMax) {
    return EFI_INVALID_PARAMETER;
  }

  if (Width == 0 || Height == 0) {
    return EFI_INVALID_PARAMETER;
  }

  switch (BltOperation) {
  case EfiBltVideoFill:
    BltBuf = (UINT8*)BltBuffer;

    for (i = 0; i < Height; i++) {
      VidBuf = POS_TO_FB (DestinationX, DestinationY + i);

      SetMem32 (VidBuf, Width * PI3_BYTES_PER_PIXEL, *(UINT32*)BltBuf);
    }
    break;

  case EfiBltVideoToBltBuffer:
    if (Delta == 0) {
      Delta = Width * PI3_BYTES_PER_PIXEL;
    }

    for (i = 0; i < Height; i++) {
      VidBuf = POS_TO_FB (SourceX, SourceY + i);

      BltBuf = (UINT8*)((UINTN)BltBuffer + (DestinationY + i) * Delta +
        DestinationX * PI3_BYTES_PER_PIXEL);

      gBS->CopyMem ((VOID*)BltBuf, (VOID*)VidBuf, PI3_BYTES_PER_PIXEL * Width);
    }
    break;

  case EfiBltBufferToVideo:
    if (Delta == 0) {
      Delta = Width * PI3_BYTES_PER_PIXEL;
    }

    for (i = 0; i < Height; i++) {
      VidBuf = POS_TO_FB (DestinationX, DestinationY + i);
      BltBuf = (UINT8*)((UINTN)BltBuffer + (SourceY + i) * Delta +
        SourceX * PI3_BYTES_PER_PIXEL);

      gBS->CopyMem ((VOID*)VidBuf, (VOID*)BltBuf, Width * PI3_BYTES_PER_PIXEL);
    }
    break;

  case EfiBltVideoToVideo:
    for (i = 0; i < Height; i++) {
      VidBuf = POS_TO_FB (SourceX, SourceY + i);
      VidBuf1 = POS_TO_FB (DestinationX, DestinationY + i);

      gBS->CopyMem ((VOID*)VidBuf1, (VOID*)VidBuf, Width * PI3_BYTES_PER_PIXEL);
    }
    break;

  default:
    return EFI_INVALID_PARAMETER;
    break;
  }

  return EFI_SUCCESS;
}

/**
   Initialize the state information for the Display Dxe

   @param  ImageHandle   of the loaded driver
   @param  SystemTable   Pointer to the System Table

   @retval EFI_SUCCESS           Protocol registered
   @retval EFI_OUT_OF_RESOURCES  Cannot allocate protocol data structure
   @retval EFI_DEVICE_ERROR      Hardware problems

**/
EFI_STATUS
EFIAPI
DisplayDxeInitialize (
  IN EFI_HANDLE         ImageHandle,
  IN EFI_SYSTEM_TABLE   *SystemTable
  )
{
  EFI_STATUS Status;

  Status = gBS->LocateProtocol (&gRaspberryPiFirmwareProtocolGuid, NULL,
                  (VOID**)&mFwProtocol);
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = gBS->LocateProtocol (&gEfiCpuArchProtocolGuid, NULL,
                  (VOID**)&mCpu);
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Query the current display resolution from mailbox
  Status = mFwProtocol->GetFBSize (&mBootWidth, &mBootHeight);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  DEBUG ((DEBUG_INFO, "Display boot mode is %u x %u\n",
    mBootWidth, mBootHeight));

  Status = gBS->InstallMultipleProtocolInterfaces (
    &mDevice, &gEfiDevicePathProtocolGuid,
    &mDisplayProtoDevicePath, &gEfiCallerIdGuid,
    NULL, NULL);
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = EfiLibInstallDriverBindingComponentName2 (
             ImageHandle,
             SystemTable,
             &mDriverBinding,
             ImageHandle,
             &gComponentName,
             &gComponentName2
           );
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  return Status;
}

STATIC
EFI_STATUS
EFIAPI
DriverSupported (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  Controller,
  IN EFI_DEVICE_PATH_PROTOCOL    *RemainingDevicePath
  )
{
  VOID *Temp;

  if (Controller != mDevice) {
    return EFI_UNSUPPORTED;
  }

  if (gBS->HandleProtocol (Controller, &gEfiGraphicsOutputProtocolGuid,
             (VOID**)&Temp) == EFI_SUCCESS) {
    return EFI_ALREADY_STARTED;
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
DriverStart (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  Controller,
  IN EFI_DEVICE_PATH_PROTOCOL    *RemainingDevicePath
  )
{
  UINTN Index;
  UINTN TempIndex;
  EFI_STATUS Status;
  VOID *Dummy;

  Status = gBS->OpenProtocol (
                  Controller,
                  &gEfiCallerIdGuid,
                  (VOID**)&Dummy,
                  This->DriverBindingHandle,
                  Controller,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  gDisplayProto.Mode = AllocateZeroPool (sizeof (EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE));
  if (gDisplayProto.Mode == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  gDisplayProto.Mode->Info = AllocateZeroPool (sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION));
  if (gDisplayProto.Mode->Info == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  Status = PcdSet8S (PcdDisplayEnableScaledVModes,
             PcdGet8 (PcdDisplayEnableScaledVModes) & ALL_MODES);
  ASSERT_EFI_ERROR (Status);

  if (PcdGet8 (PcdDisplayEnableScaledVModes) == 0) {
    Status = PcdSet8S (PcdDisplayEnableScaledVModes, JUST_NATIVE_ENABLED);
    ASSERT_EFI_ERROR (Status);
  }

  mLastMode = 0;
  for  (TempIndex = 0, Index = 0;
        TempIndex < ARRAY_SIZE (mGopModeTemplate); TempIndex++) {
    if ((PcdGet8 (PcdDisplayEnableScaledVModes) & (1 << TempIndex)) != 0) {
      DEBUG ((DEBUG_ERROR, "Mode %u: %u x %u present\n",
        TempIndex, mGopModeTemplate[TempIndex].Width,
        mGopModeTemplate[TempIndex].Height));

      CopyMem (&mGopModeData[Index], &mGopModeTemplate[TempIndex],
        sizeof (GOP_MODE_DATA));
      mLastMode = Index;
      Index++;
    }
  }

  if (PcdGet8 (PcdDisplayEnableScaledVModes) == JUST_NATIVE_ENABLED) {
    /*
     * mBootWidth x mBootHeight may not be sensible,
     * so clean it up, since we won't be adding
     * any other extra vmodes.
     */
    if (mBootWidth < 640 || mBootHeight < 480) {
      mBootWidth = 640;
      mBootHeight = 480;
    } else if (mBootWidth == 800 && mBootHeight == 480) {
      /* The Pi 7" screen is close to 800x600, just pretend it is. */
      mBootHeight = 600;
    }
  }

  if ((PcdGet8(PcdDisplayEnableScaledVModes) & MODE_NATIVE_ENABLED) != 0) {
     /*
      * Adjust actual native res only if native res is enabled
      * (so last mode is native res).
      */
     mGopModeData[mLastMode].Width = mBootWidth;
     mGopModeData[mLastMode].Height = mBootHeight;
  }

  for (Index = 0; Index <= mLastMode; Index++) {
    UINTN FbSize;
    UINTN FbPitch;
    EFI_PHYSICAL_ADDRESS FbBase;

    GOP_MODE_DATA *Mode = &mGopModeData[Index];

    Status = mFwProtocol->GetFB (Mode->Width, Mode->Height,
                            PI3_BITS_PER_PIXEL, &FbBase,
                            &FbSize, &FbPitch);
    if (EFI_ERROR (Status)) {
      goto Done;
    }

    //
    // There is no way to communicate pitch back to OS. OS and even UEFI
    // expect a fully linear frame buffer. So the width should
    // be based on the frame buffer's pitch value. In some cases VC
    // firmware would allocate ao frame buffer with some padding
    // presumably to be 8 byte align.
    //
    Mode->Width = FbPitch / PI3_BYTES_PER_PIXEL;

    DEBUG ((DEBUG_INFO, "Mode %u: %u x %u framebuffer is %u bytes at %p\n",
      Index, Mode->Width, Mode->Height, FbSize, FbBase));

    ASSERT (FbPitch != 0);
    ASSERT (FbBase != 0);
    ASSERT (FbSize != 0);
  }

  // Both set the mode and initialize current mode information.
  gDisplayProto.Mode->MaxMode = mLastMode + 1;
  DisplaySetMode (&gDisplayProto, 0);

  Status = gBS->InstallMultipleProtocolInterfaces (
    &Controller, &gEfiGraphicsOutputProtocolGuid,
    &gDisplayProto, NULL);
  if (EFI_ERROR (Status)) {
    goto Done;
  }

  if (PcdGet32 (PcdDisplayEnableSShot)) {
    RegisterScreenshotHandlers ();
  } else {
    DEBUG ((DEBUG_INFO, "Screenshot capture disabled\n"));
  }

Done:
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Could not start DisplayDxe: %r\n", Status));
    if (gDisplayProto.Mode->Info != NULL) {
      FreePool (gDisplayProto.Mode->Info);
      gDisplayProto.Mode->Info = NULL;
    }

    if (gDisplayProto.Mode != NULL) {
      FreePool (gDisplayProto.Mode);
      gDisplayProto.Mode = NULL;
    }

    gBS->CloseProtocol (
           Controller,
           &gEfiCallerIdGuid,
           This->DriverBindingHandle,
           Controller
         );
  }
  return Status;
}

STATIC
EFI_STATUS
EFIAPI
DriverStop (
  IN EFI_DRIVER_BINDING_PROTOCOL *This,
  IN EFI_HANDLE                  Controller,
  IN UINTN                       NumberOfChildren,
  IN EFI_HANDLE                  *ChildHandleBuffer
  )
{
  EFI_STATUS Status;

  ClearScreen (&gDisplayProto);

  Status = gBS->UninstallMultipleProtocolInterfaces (
    Controller, &gEfiGraphicsOutputProtocolGuid,
    &gDisplayProto, NULL);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  FreePool (gDisplayProto.Mode->Info);
  gDisplayProto.Mode->Info = NULL;
  FreePool (gDisplayProto.Mode);
  gDisplayProto.Mode = NULL;

  gBS->CloseProtocol (
         Controller,
         &gEfiCallerIdGuid,
         This->DriverBindingHandle,
         Controller
       );

  return Status;
}
