﻿// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "ScreenshotCommandHandler.h"
#include <atlenc.h>
#include <atlimage.h>
#include "errorcodes.h"
#include "logging.h"
#include "../Browser.h"
#include "../IECommandExecutor.h"

namespace webdriver {

ScreenshotCommandHandler::ScreenshotCommandHandler(void) {
  this->image_ = NULL;
}

ScreenshotCommandHandler::~ScreenshotCommandHandler(void) {
}

void ScreenshotCommandHandler::ExecuteInternal(
    const IECommandExecutor& executor,
    const ParametersMap& command_parameters,
    Response* response) {
  LOG(TRACE) << "Entering ScreenshotCommandHandler::ExecuteInternal";

  BrowserHandle browser_wrapper;
  int status_code = executor.GetCurrentBrowser(&browser_wrapper);
  if (status_code != WD_SUCCESS) {
    response->SetErrorResponse(status_code, "Unable to get browser");
    return;
  }

  status_code = this->GenerateScreenshotImage(browser_wrapper);
  if (status_code != WD_SUCCESS) {
    // TODO: Return a meaningful error here.
    response->SetSuccessResponse("");
    return;
  }

  // now either correct or single color image is got
  std::string base64_screenshot = "";
  HRESULT hr = this->GetBase64Data(base64_screenshot);
  if (FAILED(hr)) {
    // TODO: Return a meaningful error here.
    LOGHR(WARN, hr) << "Unable to transform browser image to Base64 format";
    this->ClearImage();
    response->SetSuccessResponse("");
    return;
  }

  this->ClearImage();
  response->SetSuccessResponse(base64_screenshot);
}

int ScreenshotCommandHandler::GenerateScreenshotImage(BrowserHandle browser_wrapper) {
  bool is_same_colour = true;
  HRESULT hr;
  int i = 0;
  int tries = 4;
  do {
    this->ClearImage();

    this->image_ = new CImage();
    hr = this->CaptureViewport(browser_wrapper);
    if (FAILED(hr)) {
      LOGHR(WARN, hr) << "Failed to capture browser image at " << i << " try";
      this->ClearImage();
      return EUNHANDLEDERROR;
    }

    is_same_colour = IsSameColour();
    if (is_same_colour) {
      ::Sleep(2000);
      LOG(DEBUG) << "Failed to capture non single color browser image at " << i << " try";
    }

    ++i;
  } while ((i < tries) && is_same_colour);
  return WD_SUCCESS;
}

void ScreenshotCommandHandler::ClearImage() {
  if (this->image_ != NULL) {
    delete this->image_;
    this->image_ = NULL;
  }
}

HRESULT ScreenshotCommandHandler::CaptureViewport(BrowserHandle browser) {
  LOG(TRACE) << "Entering ScreenshotCommandHandler::CaptureViewport";

  HWND content_window_handle = browser->GetContentWindowHandle();
  int content_width = 0, content_height = 0;

  this->GetWindowDimensions(content_window_handle, &content_width, &content_height);
  this->CaptureWindow(content_window_handle, content_width, content_height);

  return S_OK;
}

void ScreenshotCommandHandler::CaptureWindow(HWND window_handle,
                                              long width,
                                              long height) {
  // Capture the window's canvas to a DIB.
  // If there are any scroll bars in the window, they should
  // be explicitly cropped out of the image, because of the
  // size of image we are creating..
  BOOL created = this->image_->Create(width, height, /*numbers of bits per pixel = */ 32);
  if (!created) {
    LOG(WARN) << "Unable to initialize image object";
  }
  HDC device_context_handle = this->image_->GetDC();

  BOOL print_result = ::PrintWindow(window_handle,
                                    device_context_handle,
                                    PW_CLIENTONLY);
  if (!print_result) {
    LOG(WARN) << "PrintWindow API is not able to get content window screenshot";
  }

  this->image_->ReleaseDC();
}

bool ScreenshotCommandHandler::IsSameColour() {
  int bytes_per_pixel = this->image_->GetBPP() / 8;
  if (bytes_per_pixel >= 3) {
    BYTE* root_pixel_pointer = reinterpret_cast<BYTE*>(this->image_->GetBits());
    int pitch = this->image_->GetPitch();
    int first_pixel_red = *(root_pixel_pointer);
    int first_pixel_green = *(root_pixel_pointer + 1);
    int first_pixel_blue = *(root_pixel_pointer + 2);

    for (int i = 0; i < this->image_->GetWidth(); ++i) {
      for (int j = 0; j < this->image_->GetHeight(); ++j) {
        int current_pixel_offset = (pitch * j) + (bytes_per_pixel * i);
        int current_pixel_red = *(root_pixel_pointer + current_pixel_offset);
        int current_pixel_green = *(root_pixel_pointer + current_pixel_offset + 1);
        int current_pixel_blue = *(root_pixel_pointer + current_pixel_offset + 2);
        if (first_pixel_red != current_pixel_red ||
            first_pixel_green != current_pixel_green ||
            first_pixel_blue != current_pixel_blue) {
          return false;
        }
      }
    }
  } else {
    COLORREF firstPixelColour = this->image_->GetPixel(0, 0);
    for (int i = 0; i < this->image_->GetWidth(); ++i) {
      for (int j = 0; j < this->image_->GetHeight(); ++j) {
        if (this->image_->GetPixel(i, j)) {
          return false;
        }
      }
    }
  }

  return true;
}

HRESULT ScreenshotCommandHandler::GetBase64Data(std::string& data) {
  LOG(TRACE) << "Entering ScreenshotCommandHandler::GetBase64Data";

  if (this->image_ == NULL) {
    LOG(DEBUG) << "CImage was not initialized.";
    return E_POINTER;
  }

  CComPtr<IStream> stream;
  HRESULT hr = ::CreateStreamOnHGlobal(NULL, TRUE, &stream);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Error is occured during creating IStream";
    return hr;
  }

  GUID image_format = Gdiplus::ImageFormatPNG /*Gdiplus::ImageFormatJPEG*/;
  hr = this->image_->Save(stream, image_format);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "Saving screenshot image is failed";
    return hr;
  }

  // Get the size of the stream.
  STATSTG statstg;
  hr = stream->Stat(&statstg, STATFLAG_DEFAULT);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "No stat on stream is got";
    return hr;
  }

  HGLOBAL global_memory_handle = NULL;
  hr = ::GetHGlobalFromStream(stream, &global_memory_handle);
  if (FAILED(hr)) {
    LOGHR(WARN, hr) << "No HGlobal in stream";
    return hr;
  }

  // TODO: What if the file is bigger than max_int?
  int stream_size = static_cast<int>(statstg.cbSize.QuadPart);
  LOG(DEBUG) << "Size of screenshot image stream is " << stream_size;

  int length = ::Base64EncodeGetRequiredLength(stream_size, ATL_BASE64_FLAG_NOCRLF);
  if (length <= 0) {
    LOG(WARN) << "Got zero or negative length from base64 required length";
    return E_FAIL;
  }

  BYTE* global_lock = reinterpret_cast<BYTE*>(::GlobalLock(global_memory_handle));
  if (global_lock == NULL) {
    LOGERR(WARN) << "Unable to lock memory for base64 encoding";
    ::GlobalUnlock(global_memory_handle);      
    return E_FAIL;
  }

  char* data_array = new char[length + 1];
  if (!::Base64Encode(global_lock,
                      stream_size,
                      data_array,
                      &length,
                      ATL_BASE64_FLAG_NOCRLF)) {
    delete[] data_array;
    ::GlobalUnlock(global_memory_handle);
    LOG(WARN) << "Unable to encode image stream to base64";
    return E_FAIL;
  }
  data_array[length] = '\0';
  data = data_array;

  delete[] data_array;
  ::GlobalUnlock(global_memory_handle);

  return S_OK;
}

void ScreenshotCommandHandler::GetWindowDimensions(HWND window_handle,
                                                    int* width,
                                                    int* height) {
  RECT window_rect;
  ::GetWindowRect(window_handle, &window_rect);
  *width = window_rect.right - window_rect.left;
  *height = window_rect.bottom - window_rect.top;
}

void ScreenshotCommandHandler::CropImage(HWND content_window_handle,
                                         LocationInfo element_location) {
  RECT viewport_rect;
  ::GetWindowRect(content_window_handle, &viewport_rect);
  ::OffsetRect(&viewport_rect,
               -1 * viewport_rect.left,
               -1 * viewport_rect.top);

  POINT element_rect_origin;
  element_rect_origin.x = element_location.x;
  element_rect_origin.y = element_location.y;

  RECT element_rect = { element_rect_origin.x, 
                        element_rect_origin.y,
                        element_rect_origin.x + element_location.width,
                        element_rect_origin.y + element_location.height };

  RECT screenshot_rect;
  ::IntersectRect(&screenshot_rect, &viewport_rect, &element_rect);

  long width = screenshot_rect.right - screenshot_rect.left;
  long height = screenshot_rect.bottom - screenshot_rect.top;

  RECT destination_rect = { 0, 0, width, height };

  CImage* image = new CImage();
  image->Create(width, height, 32);
  HDC device_context = image->GetDC();
  this->image_->Draw(device_context, destination_rect, screenshot_rect);
  image->ReleaseDC();

  this->ClearImage();
  this->image_ = new CImage();
  this->image_->Create(width, height, 32);
  HDC dc = this->image_->GetDC();
  image->BitBlt(dc, 0, 0);
  this->image_->ReleaseDC();
  image->Destroy();
  delete image;
}

} // namespace webdriver
