/* Copyright 2016 Pierre Ossman <ossman@cendio.se> for Cendio AB
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <math.h>
#include <sys/time.h>

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/fl_draw.H>
#include <FL/x.H>

#include <rdr/Exception.h>
#include <rfb/util.h>

#include "../vncviewer/PlatformPixelBuffer.h"

#include "util.h"

class TestWindow: public Fl_Window {
public:
  TestWindow();
  ~TestWindow();

  virtual void start(int width, int height);
  virtual void stop();

  void draw() override;

protected:
  void flush() override;

  void update();
  virtual void changefb();

  static void timer(void* data);

public:
  unsigned long long pixels, frames;
  double time;

protected:
  PlatformPixelBuffer* fb;
};

class PartialTestWindow: public TestWindow {
protected:
  void changefb() override;
};

class OverlayTestWindow: public PartialTestWindow {
public:
  OverlayTestWindow();

  void start(int width, int height) override;
  void stop() override;

  void draw() override;

protected:
  Surface* overlay;
  Surface* offscreen;
};

TestWindow::TestWindow() :
  Fl_Window(0, 0, "Framebuffer Performance Test"),
  fb(nullptr)
{
}

TestWindow::~TestWindow()
{
  stop();
}

void TestWindow::start(int width, int height)
{
  uint32_t pixel;

  stop();

  resize(x(), y(), width, height);

  pixels = 0;
  frames = 0;
  time = 0;

  fb = new PlatformPixelBuffer(w(), h());

  pixel = 0;
  fb->fillRect(fb->getRect(), &pixel);

  show();
}

void TestWindow::stop()
{
  hide();

  delete fb;
  fb = nullptr;

  Fl::remove_idle(timer, this);
}

void TestWindow::draw()
{
  int X, Y, W, H;

  // We cannot update the damage region from inside the draw function,
  // so delegate this to an idle function
  Fl::add_idle(timer, this);

  // Check what actually needs updating
  fl_clip_box(0, 0, w(), h(), X, Y, W, H);
  if ((W == 0) || (H == 0))
    return;

  fb->draw(X, Y, X, Y, W, H);

  pixels += W*H;
  frames++;
}

void TestWindow::flush()
{
  startTimeCounter();
  Fl_Window::flush();
#if !defined(WIN32) && !defined(__APPLE__)
  // Make sure we measure any work we queue up
  XSync(fl_display, False);
#endif
  endTimeCounter();

  time += getTimeCounter();
}

void TestWindow::update()
{
  rfb::Rect r;

  startTimeCounter();

  changefb();

  r = fb->getDamage();
  damage(FL_DAMAGE_USER1, r.tl.x, r.tl.y, r.width(), r.height());

#if !defined(WIN32) && !defined(__APPLE__)
  // Make sure we measure any work we queue up
  XSync(fl_display, False);
#endif

  endTimeCounter();

  time += getTimeCounter();
}

void TestWindow::changefb()
{
  uint32_t pixel;

  pixel = rand();
  fb->fillRect(fb->getRect(), &pixel);
}

void TestWindow::timer(void* data)
{
  TestWindow* self;

  Fl::remove_idle(timer, data);

  self = (TestWindow*)data;
  self->update();
}

void PartialTestWindow::changefb()
{
  rfb::Rect r;
  uint32_t pixel;

  r = fb->getRect();
  r.tl.x += w() / 4;
  r.tl.y += h() / 4;
  r.br.x -= w() / 4;
  r.br.y -= h() / 4;

  pixel = rand();
  fb->fillRect(r, &pixel);
}

OverlayTestWindow::OverlayTestWindow() :
  overlay(nullptr), offscreen(nullptr)
{
}

void OverlayTestWindow::start(int width, int height)
{
  PartialTestWindow::start(width, height);

  overlay = new Surface(400, 200);
  overlay->clear(0xff, 0x80, 0x00, 0xcc);

  // X11 needs an off screen buffer for compositing to avoid flicker,
  // and alpha blending doesn't work for windows on Win32
#if !defined(__APPLE__)
  offscreen = new Surface(w(), h());
#else
  offscreen = nullptr;
#endif
}

void OverlayTestWindow::stop()
{
  PartialTestWindow::stop();

  delete offscreen;
  offscreen = nullptr;
  delete overlay;
  overlay = nullptr;
}

void OverlayTestWindow::draw()
{
  int ox, oy, ow, oh;
  int X, Y, W, H;

  // We cannot update the damage region from inside the draw function,
  // so delegate this to an idle function
  Fl::add_idle(timer, this);

  // Check what actually needs updating
  fl_clip_box(0, 0, w(), h(), X, Y, W, H);
  if ((W == 0) || (H == 0))
    return;

  // We might get a redraw before we are fully ready
  if (!overlay)
    return;

  // Simplify the clip region to a simple rectangle in order to
  // properly draw all the layers even if they only partially overlap
  fl_push_no_clip();
  fl_push_clip(X, Y, W, H);

  if (offscreen)
    fb->draw(offscreen, X, Y, X, Y, W, H);
  else
    fb->draw(X, Y, X, Y, W, H);

  pixels += W*H;
  frames++;

  ox = (w() - overlay->width()) / 2;
  oy = h() / 4 - overlay->height() / 2;
  ow = overlay->width();
  oh = overlay->height();
  fl_clip_box(ox, oy, ow, oh, X, Y, W, H);
  if ((W != 0) && (H != 0)) {
    if (offscreen)
      overlay->blend(offscreen, X - ox, Y - oy, X, Y, W, H);
    else
      overlay->blend(X - ox, Y - oy, X, Y, W, H);
  }

  fl_pop_clip();
  fl_pop_clip();

  if (offscreen) {
    fl_clip_box(0, 0, w(), h(), X, Y, W, H);
    offscreen->draw(X, Y, X, Y, W, H);
  }
}

static void dosubtest(TestWindow* win, int width, int height,
                      unsigned long long* pixels,
		      unsigned long long* frames,
		      double* time)
{
  struct timeval start;

  win->start(width, height);

  gettimeofday(&start, nullptr);
  while (rfb::msSince(&start) < 3000)
    Fl::wait();

  win->stop();

  *pixels = win->pixels;
  *frames = win->frames;
  *time = win->time;
}

static bool is_constant(double a, double b)
{
    return (fabs(a - b) / a) < 0.1;
}

static void dotest(TestWindow* win)
{
  unsigned long long pixels[3];
  unsigned long long frames[3];
  double time[3];

  double delay, rate;

  // Run the test several times at different resolutions...
  dosubtest(win, 800, 600, &pixels[0], &frames[0], &time[0]);
  dosubtest(win, 1024, 768, &pixels[1], &frames[1], &time[1]);
  dosubtest(win, 1280, 960, &pixels[2], &frames[2], &time[2]);

  // ...in order to compute how much of the rendering time is static,
  // and how much depends on the number of pixels
  // (i.e. solve: time = delay * frames + rate * pixels)
  delay = (((time[0] - (double)pixels[0] / pixels[1] * time[1]) /
            (frames[0] - (double)pixels[0] / pixels[1] * frames[1])) +
           ((time[1] - (double)pixels[1] / pixels[2] * time[2]) /
            (frames[1] - (double)pixels[1] / pixels[2] * frames[2]))) / 2.0;
  rate = (((time[0] - (double)frames[0] / frames[1] * time[1]) /
           (pixels[0] - (double)frames[0] / frames[1] * pixels[1])) +
          ((time[1] - (double)frames[1] / frames[2] * time[2]) /
           (pixels[1] - (double)frames[1] / frames[2] * pixels[2]))) / 2.0;

  // However, we have some corner cases:

  // We are restricted by some delay, e.g. refresh rate
  if (is_constant(frames[0]/time[0], frames[2]/time[2])) {
    fprintf(stderr, "Warning: Fixed delay dominating updates.\n\n");
    delay = time[2]/frames[2];
    rate = 0.0;
  }

  // There isn't any fixed delay, we are only restricted by pixel
  // throughput
  if (fabs(delay) < 0.001) {
    delay = 0.0;
    rate = time[2]/pixels[2];
  }

  // We can hit cache limits that causes performance to drop
  // with increasing update size, screwing up our calculations
  if ((pixels[2] / time[2]) < (pixels[0] / time[0] * 0.9)) {
    fprintf(stderr, "Warning: Unexpected behaviour. Measurement unreliable.\n\n");

    // We can't determine the proportions between these, so divide the
    // time spent evenly
    delay = time[2] / 2.0 / frames[2];
    rate = time[2] / 2.0 / pixels[2];
  }

  fprintf(stderr, "Rendering delay: %g ms/frame\n", delay * 1000.0);
  fprintf(stderr, "Rendering rate: %s\n",
          (rate == 0.0) ? "N/A pixels/s" :
                          rfb::siPrefix(1.0 / rate, "pixels/s").c_str());
  fprintf(stderr, "Maximum FPS: %g fps @ 1920x1080\n",
          1.0 / (delay + rate * 1920 * 1080));
}

int main(int /*argc*/, char** /*argv*/)
{
  TestWindow* win;

  fprintf(stderr, "Full window update:\n\n");
  win = new TestWindow();
  dotest(win);
  delete win;
  fprintf(stderr, "\n");

  fprintf(stderr, "Partial window update:\n\n");
  win = new PartialTestWindow();
  dotest(win);
  delete win;
  fprintf(stderr, "\n");

  fprintf(stderr, "Partial window update with overlay:\n\n");
  win = new OverlayTestWindow();
  dotest(win);
  delete win;
  fprintf(stderr, "\n");

  return 0;
}
