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

/*
 * Loosely based on CrScreenShotDxe (https://github.com/LongSoft/CrScreenshotDxe).
 *
 * Copyright (c) 2016, Nikolaj Schlej, All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "DisplayDxe.h"
#include <Protocol/SimpleFileSystem.h>
#include <Library/PrintLib.h>
#include <Library/BmpSupportLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>

/*
 * ShowStatus defs.
 */
#define STATUS_SQUARE_SIDE 5
#define STATUS_YELLOW 0xff, 0xff, 0x00
#define STATUS_GREEN  0x00, 0xff, 0x00
#define STATUS_BLUE   0x00, 0x00, 0xff
#define STATUS_RED    0xff, 0x00, 0x00

EFI_STATUS
ShowStatus (
  IN EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput,
  IN UINT8 Red,
  IN UINT8 Green,
  IN UINT8 Blue
  )
{
  UINTN Index;
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL Square[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL Backup[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];

  for (Index = 0; Index < STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE; Index++) {
    Square[Index].Blue = Blue;
    Square[Index].Green = Green;
    Square[Index].Red = Red;
    Square[Index].Reserved = 0x00;
  }

  // Backup current image.
  GraphicsOutput->Blt (GraphicsOutput, Backup,
                    EfiBltVideoToBltBuffer, 0, 0, 0, 0,
                    STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);

  // Draw the status square.
  GraphicsOutput->Blt (GraphicsOutput, Square,
                    EfiBltBufferToVideo, 0, 0, 0, 0,
                    STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);

  // Wait 500ms.
  gBS->Stall (500 * 1000);

  // Restore the backup.
  GraphicsOutput->Blt (GraphicsOutput, Backup,
                    EfiBltBufferToVideo, 0, 0, 0, 0,
                    STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
FindWritableFs (
  OUT EFI_FILE_PROTOCOL **WritableFs
  )
{
  EFI_FILE_PROTOCOL *Fs = NULL;
  EFI_HANDLE *HandleBuffer = NULL;
  UINTN      HandleCount;
  UINTN      Index;

  EFI_STATUS Status = gBS->LocateHandleBuffer (ByProtocol,
                             &gEfiSimpleFileSystemProtocolGuid,
                             NULL, &HandleCount, &HandleBuffer);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  for (Index = 0; Index < HandleCount; Index++) {
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFs = NULL;
    EFI_FILE_PROTOCOL *File = NULL;

    Status = gBS->HandleProtocol (HandleBuffer[Index],
                    &gEfiSimpleFileSystemProtocolGuid,
                    (VOID**)&SimpleFs);
    if (EFI_ERROR (Status)) {
      ASSERT_EFI_ERROR (Status);
      /*
       * Not supposed to happen.
       */
      continue;
    }

    Status = SimpleFs->OpenVolume (SimpleFs, &Fs);
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a OpenVolume[%u] returned %r\n", __FUNCTION__, Index, Status));
      continue;
    }

    Status = Fs->Open (Fs, &File, L"--------.---",
                   EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ |
                   EFI_FILE_MODE_WRITE, 0);
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a Open[%u] returned %r\n", __FUNCTION__, Index, Status));
      continue;
    }

    /*
     * Okay, we have a writable filesystem!
     */
    Fs->Delete (File);
    *WritableFs = Fs;
    Status = EFI_SUCCESS;
    break;
  }

  if (HandleBuffer) {
    FreePool (HandleBuffer);
  }

  return Status;
}

STATIC
VOID
TakeScreenshot (
  VOID
  )
{
  VOID *BmpImage = NULL;
  EFI_FILE_PROTOCOL *Fs = NULL;
  EFI_FILE_PROTOCOL *File = NULL;
  EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = &gDisplayProto;
  EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Image = NULL;
  EFI_STATUS Status;
  CHAR16 FileName[8 + 1 + 3 + 1];
  UINT32 ScreenWidth;
  UINT32 ScreenHeight;
  UINTN ImageSize;
  UINTN BmpSize;
  UINTN Index;
  EFI_TIME Time;

  Status = FindWritableFs (&Fs);
  if (EFI_ERROR (Status)) {
    ShowStatus (GraphicsOutput, STATUS_YELLOW);
  }

  ScreenWidth = GraphicsOutput->Mode->Info->HorizontalResolution;
  ScreenHeight = GraphicsOutput->Mode->Info->VerticalResolution;
  ImageSize = ScreenWidth * ScreenHeight;

  Status = gRT->GetTime (&Time, NULL);
  if (!EFI_ERROR (Status)) {
    UnicodeSPrint (FileName, sizeof (FileName), L"%02d%02d%02d%02d.bmp",
      Time.Day, Time.Hour, Time.Minute, Time.Second);
  } else {
    UnicodeSPrint (FileName, sizeof (FileName), L"scrnshot.bmp");
  }

  Image = AllocatePool (ImageSize * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL));
  if (Image == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    ShowStatus (GraphicsOutput, STATUS_RED);
    goto Done;
  }

  Status = GraphicsOutput->Blt (GraphicsOutput, Image,
                             EfiBltVideoToBltBuffer, 0, 0, 0, 0,
                             ScreenWidth, ScreenHeight, 0);
  if (EFI_ERROR (Status)) {
    ShowStatus (GraphicsOutput, STATUS_RED);
    goto Done;
  }

  for (Index = 0; Index < ImageSize; Index++) {
    if (Image[Index].Red != 0x00 ||
        Image[Index].Green != 0x00 ||
        Image[Index].Blue != 0x00) {
      break;
    }
  }

  if (Index == ImageSize) {
    ShowStatus (GraphicsOutput, STATUS_BLUE);
    goto Done;
  }

  Status = TranslateGopBltToBmp (Image, ScreenHeight, ScreenWidth,
             &BmpImage, (UINT32*)&BmpSize);
  if (EFI_ERROR (Status)) {
    ShowStatus (GraphicsOutput, STATUS_RED);
    goto Done;
  }

  Status = Fs->Open (Fs, &File, FileName, EFI_FILE_MODE_CREATE |
                 EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
  if (EFI_ERROR (Status)) {
    ShowStatus (GraphicsOutput, STATUS_RED);
    goto Done;
  }

  Status = File->Write (File, &BmpSize, BmpImage);
  File->Close (File);
  if (EFI_ERROR (Status)) {
    ShowStatus (GraphicsOutput, STATUS_RED);
    goto Done;
  }

  ShowStatus (GraphicsOutput, STATUS_GREEN);

Done:
  if (BmpImage != NULL) {
    FreePool (BmpImage);
  }

  if (Image != NULL) {
    FreePool (Image);
  }
}

STATIC
EFI_STATUS
EFIAPI
ScreenshotKeyHandler (
  IN EFI_KEY_DATA *KeyData
  )
{
  TakeScreenshot ();
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
ProcessScreenshotHandler (
  IN EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleTextInEx
  )
{
  EFI_STATUS Status;
  EFI_HANDLE Handle;
  EFI_KEY_DATA ScreenshotKey;

  /*
   * LCtrl+LAlt+F12
   */
  ScreenshotKey.Key.ScanCode = SCAN_F12;
  ScreenshotKey.Key.UnicodeChar = 0;
  ScreenshotKey.KeyState.KeyShiftState = EFI_SHIFT_STATE_VALID |
    EFI_LEFT_CONTROL_PRESSED | EFI_LEFT_ALT_PRESSED;
  ScreenshotKey.KeyState.KeyToggleState = 0;

  Status = SimpleTextInEx->RegisterKeyNotify (
                             SimpleTextInEx,
                             &ScreenshotKey,
                             ScreenshotKeyHandler,
                             &Handle
                           );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a: couldn't register key notification: %r\n", __FUNCTION__, Status));
    return Status;
  }

  return EFI_SUCCESS;
}

STATIC
VOID
ProcessScreenshotHandlers (
  VOID
  )
{
  UINTN Index;
  EFI_STATUS Status;
  UINTN HandleCount;
  EFI_HANDLE *HandleBuffer;
  EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleTextInEx;

  Status = gBS->LocateHandleBuffer (ByProtocol, &gEfiSimpleTextInputExProtocolGuid,
                  NULL, &HandleCount, &HandleBuffer);
  if (EFI_ERROR (Status)) {
    return;
  }

  for (Index = 0; Index < HandleCount; Index++) {
    Status = gBS->HandleProtocol (HandleBuffer[Index],
                    &gEfiSimpleTextInputExProtocolGuid,
                    (VOID**)&SimpleTextInEx);
    if (EFI_ERROR (Status)) {
      ASSERT_EFI_ERROR (Status);
      /*
       * Not supposed to happen.
       */
      continue;
    }

    Status = ProcessScreenshotHandler (SimpleTextInEx);
    if (EFI_ERROR (Status)) {
      continue;
    }
  }
}

STATIC
VOID
EFIAPI
OnTextInExInstall (
  IN EFI_EVENT Event,
  IN VOID *Context
  )
{
  ProcessScreenshotHandlers ();
}

VOID
RegisterScreenshotHandlers (
  VOID
)
{
  EFI_STATUS Status;
  EFI_EVENT TextInExInstallEvent;
  VOID *TextInExInstallRegistration;

  ProcessScreenshotHandlers ();

  Status = gBS->CreateEvent (EVT_NOTIFY_SIGNAL, TPL_CALLBACK,
                  OnTextInExInstall, NULL,
                  &TextInExInstallEvent);
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a: couldn't create protocol install event: %r\n", __FUNCTION__, Status));
    return;
  }

  Status = gBS->RegisterProtocolNotify (&gEfiSimpleTextInputExProtocolGuid,
                  TextInExInstallEvent,
                  &TextInExInstallRegistration);
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a: couldn't register protocol install notify: %r\n", __FUNCTION__, Status));
    return;
  }
}
