// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/exo/wayland/clients/blur.h"

#include <algorithm>
#include <vector>

#include "base/command_line.h"
#include "components/exo/wayland/clients/client_helper.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/effects/SkBlurImageFilter.h"
#include "third_party/skia/include/gpu/GrContext.h"
#include "ui/gl/gl_bindings.h"

namespace exo {
namespace wayland {
namespace clients {
namespace {

// Rotation speed (degrees/second).
const double kRotationSpeed = 360.0;

// The opacity of the foreground.
const double kForegroundOpacity = 0.7;

// Rects grid size.
const int kGridSize = 4;

// Create grid image for |size| and |cell_size|.
sk_sp<SkImage> CreateGridImage(const gfx::Size& size,
                               const gfx::Size& cell_size) {
  sk_sp<SkSurface> surface(SkSurface::MakeRaster(
      SkImageInfo::MakeN32(size.width(), size.height(), kOpaque_SkAlphaType)));
  SkCanvas* canvas = surface->getCanvas();
  canvas->clear(SK_ColorWHITE);
  for (int y = 0; y < kGridSize; ++y) {
    for (int x = 0; x < kGridSize; ++x) {
      if ((y + x) % 2)
        continue;
      SkPaint paint;
      paint.setColor(SK_ColorLTGRAY);
      canvas->save();
      canvas->translate(x * cell_size.width(), y * cell_size.height());
      canvas->drawIRect(SkIRect::MakeWH(cell_size.width(), cell_size.height()),
                        paint);
      canvas->restore();
    }
  }
  return surface->makeImageSnapshot();
}

// Adjust sigma by increasing the scale factor until less than |max_sigma|.
// Returns the adjusted sigma value.
double AdjustSigma(double sigma, double max_sigma, int* scale_factor) {
  *scale_factor = 1;
  while (sigma > max_sigma) {
    *scale_factor *= 2;
    sigma /= 2.0;
  }
  return sigma;
}

// Draw background contents to canvas.
void DrawContents(SkImage* background_grid_image,
                  const gfx::Size& cell_size,
                  base::TimeDelta elapsed_time,
                  SkCanvas* canvas) {
  // Draw background grid.
  SkPaint paint;
  paint.setBlendMode(SkBlendMode::kSrc);
  canvas->drawImage(background_grid_image, 0, 0, &paint);

  // Draw rotated rectangles.
  SkScalar rect_size =
      SkScalarHalf(std::min(cell_size.width(), cell_size.height()));
  SkIRect rect = SkIRect::MakeXYWH(
      -SkScalarHalf(rect_size), -SkScalarHalf(rect_size), rect_size, rect_size);
  SkScalar rotation = elapsed_time.InMilliseconds() * kRotationSpeed / 1000;
  for (int y = 0; y < kGridSize; ++y) {
    for (int x = 0; x < kGridSize; ++x) {
      const SkColor kColors[] = {SK_ColorBLUE, SK_ColorGREEN,
                                 SK_ColorRED,  SK_ColorYELLOW,
                                 SK_ColorCYAN, SK_ColorMAGENTA};
      SkPaint paint;
      paint.setColor(kColors[(y * kGridSize + x) % arraysize(kColors)]);
      canvas->save();
      canvas->translate(
          x * cell_size.width() + SkScalarHalf(cell_size.width()),
          y * cell_size.height() + SkScalarHalf(cell_size.height()));
      canvas->rotate(rotation / (y * kGridSize + x + 1));
      canvas->drawIRect(rect, paint);
      canvas->restore();
    }
  }
}

void FrameCallback(void* data, wl_callback* callback, uint32_t time) {
  bool* callback_pending = static_cast<bool*>(data);
  *callback_pending = false;
}

}  // namespace

Blur::Blur() = default;

Blur::~Blur() = default;

void Blur::Run(double sigma_x,
               double sigma_y,
               double max_sigma,
               bool offscreen,
               int frames) {
  Buffer* buffer = buffers_.front().get();
  gfx::Size cell_size(size_.width() / kGridSize, size_.height() / kGridSize);

  // Create grid image. Simulates a wallpaper.
  if (!grid_image_)
    grid_image_ = CreateGridImage(size_, cell_size);

  // Create blur surfaces if needed.
  sk_sp<SkImageFilter> blur_filter;
  std::vector<sk_sp<SkSurface>> blur_surfaces;
  std::vector<sk_sp<SkSurface>> content_surfaces;
  int blur_scale_factor_x = 1;
  int blur_scale_factor_y = 1;
  if (sigma_x > 0.0 || sigma_y > 0.0) {
    sigma_x = AdjustSigma(sigma_x, max_sigma, &blur_scale_factor_x);
    sigma_y = AdjustSigma(sigma_y, max_sigma, &blur_scale_factor_y);
    blur_filter = SkBlurImageFilter::Make(sigma_x, sigma_y, nullptr, nullptr,
                                          SkBlurImageFilter::kClamp_TileMode);
    auto size = SkISize::Make(size_.width() / blur_scale_factor_x,
                              size_.height() / blur_scale_factor_y);
    do {
      blur_surfaces.push_back(
          buffer->sk_surface->makeSurface(SkImageInfo::MakeN32(
              size.width(), size.height(), kOpaque_SkAlphaType)));
      size = SkISize::Make(std::min(size_.width(), size.width() * 2),
                           std::min(size_.height(), size.height() * 2));
    } while (size.width() < size_.width() || size.height() < size_.height());
  }

  bool callback_pending = false;
  std::unique_ptr<wl_callback> frame_callback;
  wl_callback_listener frame_listener = {FrameCallback};
  int frame_count = 0;
  base::TimeTicks initial_time = base::TimeTicks::Now();
  while (frame_count < frames) {
    base::TimeDelta elapsed_time = base::TimeTicks::Now() - initial_time;
    if (blur_filter) {
      // Create contents surfaces.
      while (!blur_surfaces.empty()) {
        sk_sp<SkSurface> surface = blur_surfaces.back();
        blur_surfaces.pop_back();

        SkSize size = SkSize::Make(surface->width(), surface->height());
        SkCanvas* canvas = surface->getCanvas();
        canvas->save();

        // Draw contents if this is the first surface.
        if (content_surfaces.empty()) {
          canvas->scale(size.width() / size_.width(),
                        size.height() / size_.height());
          DrawContents(grid_image_.get(), cell_size, elapsed_time, canvas);
        } else {
          // Otherwise, scale larger surface to produce surface.
          canvas->scale(size.width() / content_surfaces.back()->width(),
                        size.height() / content_surfaces.back()->height());
          SkPaint paint;
          paint.setBlendMode(SkBlendMode::kSrc);
          paint.setFilterQuality(SkFilterQuality::kLow_SkFilterQuality);
          content_surfaces.back()->draw(canvas, 0, 0, &paint);
        }

        canvas->restore();
        content_surfaces.push_back(surface);
      }

      // Make blur image from last content surface.
      SkIRect subset;
      SkIPoint offset;
      sk_sp<SkImage> blur_image = content_surfaces.back()->makeImageSnapshot();
      sk_sp<SkImage> blurred_image =
          blur_image->makeWithFilter(blur_filter.get(), blur_image->bounds(),
                                     blur_image->bounds(), &subset, &offset);
      SkCanvas* canvas = buffer->sk_surface->getCanvas();
      canvas->save();
      SkSize size = SkSize::Make(size_.width(), size_.height());
      canvas->scale(size.width() / blur_image->width(),
                    size.height() / blur_image->height());
      SkPaint paint;
      paint.setBlendMode(SkBlendMode::kSrc);
      paint.setFilterQuality(SkFilterQuality::kLow_SkFilterQuality);
      // Simulate multi-texturing by adding foreground opacity.
      int alpha = (1.0 - kForegroundOpacity) * 255.0 + 0.5;
      paint.setColor(SkColorSetA(SK_ColorBLACK, alpha));
      canvas->drawImage(blurred_image, offset.x() - subset.x(),
                        offset.y() - subset.y(), &paint);
      canvas->restore();

      // Restore blur surfaces for next frame.
      std::swap(content_surfaces, blur_surfaces);
      std::reverse(blur_surfaces.begin(), blur_surfaces.end());
    } else {  // !blur_filter
      SkCanvas* canvas = buffer->sk_surface->getCanvas();
      DrawContents(grid_image_.get(), cell_size, elapsed_time, canvas);
      SkPaint paint;
      int alpha = kForegroundOpacity * 255.0 + 0.5;
      paint.setColor(SkColorSetA(SK_ColorBLACK, alpha));
      canvas->drawIRect(SkIRect::MakeWH(size_.width(), size_.height()), paint);
    }

    if (gr_context_) {
      gr_context_->flush();
      glFinish();
    }

    // Submit 1 of 50 frames for onscreen display when in offscreen mode.
    if ((frame_count++ % 50) && offscreen)
      continue;

    while (callback_pending)
      wl_display_dispatch(display_.get());

    callback_pending = true;

    wl_surface_set_buffer_scale(surface_.get(), scale_);
    wl_surface_set_buffer_transform(surface_.get(), transform_);
    wl_surface_damage(surface_.get(), 0, 0, surface_size_.width(),
                      surface_size_.height());
    wl_surface_attach(surface_.get(), buffer->buffer.get(), 0, 0);

    frame_callback.reset(wl_surface_frame(surface_.get()));
    wl_callback_add_listener(frame_callback.get(), &frame_listener,
                             &callback_pending);
    wl_surface_commit(surface_.get());
    wl_display_flush(display_.get());
  }
}

}  // namespace clients
}  // namespace wayland
}  // namespace exo
