// src/cairo_widget.cc - Copyright 2005, 2006, University
//               of Padova, dept. of Pure and Applied
//               Mathematics
//
// This file is part of SGPEMv2.
//
// 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 3 of the License, or
// (at your option) any later version.
//
// SGPEMv2 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 SGPEMv2. If not, see http://www.gnu.org/licenses/.



#include "cairo_elements.hh"
#include "cairo_widget.hh"

#include <gdkmm/window.h>
#include <gtkmm/stylecontext.h>

#include <cstring>
#include <cassert>
#include <memory>

using namespace sgpem;


CairoWidget::CairoWidget()
    : Glib::ObjectBase("sgpem_CairoWidget"),
      Gtk::Widget(),
      _draw_w(1), _draw_h(1),
      _client_w(0), _client_h(0),
      _pixbuf_w(0), _pixbuf_h(0),
      _need_redraw(true), _scaling_mode(scaling_none),
      _buf(NULL)
{
  set_has_window (false);
}


CairoWidget::~CairoWidget()
{
}

/*
  Scaling mode setter+getter method.
 */
CairoWidget::scaling_mode
CairoWidget::set_scaling_mode(CairoWidget::scaling_mode scaling)
{
  CairoWidget::scaling_mode old_scaling = _scaling_mode;
  _scaling_mode = scaling;
  return old_scaling;
}

/*
  Scaling mode getter method.
 */
CairoWidget::scaling_mode
CairoWidget::get_scaling_mode() const
{
  return _scaling_mode;
}

/*
  Called by Gtk+ before creting the widget.
  Sets all values and attributes required.
 */
void
CairoWidget::on_realize()
{
  Gtk::Widget::on_realize();

  if (_ref_gdk_window) return;

  GdkWindowAttr attributes;
  memset(&attributes, 0, sizeof(attributes));

  Gtk::Allocation alloc = get_allocation();

  attributes.x = alloc.get_x();
  attributes.y = alloc.get_y();
  attributes.width = alloc.get_width();
  attributes.height = alloc.get_height();

  attributes.wclass = GDK_INPUT_OUTPUT;
  attributes.event_mask = get_events() | GDK_EXPOSURE_MASK;
  attributes.window_type = GDK_WINDOW_CHILD;

  _client_w = alloc.get_width();
  _client_h = alloc.get_height();

  static const gint attributes_mask = Gdk::WA_X | Gdk::WA_Y;

  _ref_gdk_window = Gdk::Window::create(get_window() /* parent */,
                                        &attributes, attributes_mask);

  set_has_window(true);
  set_window(_ref_gdk_window);

  _ref_gdk_window->set_user_data(gobj());
}

void
CairoWidget::on_unrealize()
{
  _ref_gdk_window.clear();
  Gtk::Widget::on_unrealize();
}

/*
  This method is called when the widget is moved/resized.
 */
void
CairoWidget::on_size_allocate(const Gtk::Allocation& allocation)
{
  set_allocation(allocation);

  if (!_ref_gdk_window) return;

  _ref_gdk_window->move_resize(allocation.get_x(), allocation.get_y(),
                               allocation.get_width(), allocation.get_height());

  // the widget's dimension are saved for futher use
  _client_w = allocation.get_width();
  _client_h = allocation.get_height();

  // the widget must be redrawn
  _need_redraw = true;
}

/*
  This method is called by Gtk to get the widget desired size.
 */
void
CairoWidget::on_size_request(Gtk::Requisition* requisition)
{
  // gets the passed values
  _client_w = requisition->width;
  _client_h = requisition->height;

  // ask the desired dimensions
  calc_widget_size(_client_w, _client_h);

  // pass values back to Gtk
  requisition->width  = _client_w;
  requisition->height = _client_h;
}

/*
  This method is called by Gtk when need to redraw widget
 */
bool
CairoWidget::on_draw (const Cairo::RefPtr<Cairo::Context>& ctx)
{
  if (!_ref_gdk_window) return true;

  // save actual size
  auto actual_w = get_width();
  auto actual_h = get_height();

  // the widget must be redrawn if
  // - there's an explicit request(_need_redraw)
  // - the size is changed
  if(_need_redraw || actual_w != _client_w || actual_h != _client_h)
  {
    // reset flag, set values with actual size
    _need_redraw = false;
    _client_w = actual_w;
    _client_h = actual_h;

    // The pixbuf must be crated or not?
    // If the pixbuf buffer size isn't enough
    // it must be allocated or reallocated.
    // Here is used a lazy technique:
    // allocate only if growing, keep if shrinking.
    // Taking a bigger pixmap saves operations when
    // many window resizing occours (i.e. mouse resizing).
    if(_client_w > _pixbuf_w || _client_h > _pixbuf_h)
    {
      // previdence in the code:
      // allocate more than actually needed
      _pixbuf_w = static_cast<int>(_client_w * 1.2);
      _pixbuf_h = static_cast<int>(_client_h * 1.2);

      // allocate the pixmap
      _buf = Gdk::Pixbuf::create (_ref_gdk_window, 0, 0, _pixbuf_w, _pixbuf_h);
    }

    // Draw the widget background as the first thing.
    // I've seen this is how it's done in the very Gtk+ toolkit
    // for the GtkProgressBar widget.
    Glib::RefPtr<Gtk::StyleContext> style = get_style_context();
    //    style->paint_box (_buf,
    //                Gtk::STATE_NORMAL, Gtk::SHADOW_ETCHED_IN,
    //                nullptr, *this, "through",
    //                0, 0, _client_w, _client_h);

    // Determine the final drawing dimensions
    // before to start drawing
    _draw_w = _draw_h = 0; // default no scaling
    calc_drawing_size(ctx, _draw_w, _draw_h);

    // calculate and apply scale factors
    calc_scale_factors();
    ctx->scale(_scale_x, _scale_y);

    // do the drawing...
    draw_widget(ctx);

  } // ~ if(_need_redraw || actual_w!=_client_w || actual_h!=_client_h)

  // Now the in-memory picture is placed on screen

  // Clip to redraw only the smallest possible area
  // Use double buffering or we're CPU-dead
  // Copy from the buffer to the screen
  if (_buf)
    _ref_gdk_window->draw_drawable(get_style()->get_black_gc(), _buf,
                                event->area.x, event->area.y,
                                event->area.x, event->area.y,
                                event->area.width, event->area.height);

  return true;
}

/*
  Here calc the scale factors to apply drawing
 */
void
CairoWidget::calc_scale_factors()
{
  // scale factor is: desired_size/effective_size
  //
  // "desired_size" here is how big can fin in window
  // "effective_size" is how big the application produce
  if(_scaling_mode != scaling_none && _draw_w != 0 && _draw_h != 0)
  {
    // there are two possible scale factors x and y
    _scale_x = (double)_client_w/(double)_draw_w;
    _scale_y = (double)_client_h/(double)_draw_h;

    // will be chosen one, another, both or none
    // depending on the active mode
    switch(_scaling_mode)
    {
      case scaling_none: // not necessary!
        // doesn't scale
        _scale_x = _scale_y = 1.0;
        break;

      case scaling_to_w:
        // scale to fit in width
        // x and y take the same x value
        _scale_y = _scale_x;
        break;

      case scaling_to_h:
        // scale to fit in height
        // x and y take the same y value
        _scale_x = _scale_y;
        break;

      case scaling_min:
        // scale to fit always in window
        // x and y take the same minimum value
        if(_scale_x <= _scale_y)
          _scale_y = _scale_x;
        else
          _scale_x = _scale_y;
        break;

      case scaling_max:
        // scale to fit never in window
        // x and y take the same mxiimum value
        if(_scale_x >= _scale_y)
          _scale_y = _scale_x;
        else
          _scale_x = _scale_y;
        break;

      case scaling_all:
        // scale to fit stretching in window
        // x and y mantain the differnt calculated values
        //
        // nothing to do...
        break;
    }
  }
  else
  {
    // can't determine scaling factors:
    // take the easy way
    _scale_x = _scale_y = 1.0;
  }
}

/*
  Utility method: force to redraw widget
 */
void
CairoWidget::redraw()
{
  // Force redraw
  _need_redraw = true;

  // invalidate all widget's surface
  queue_draw();
}

/*
  Utility method: force to resize and redraw widget
 */
void
CairoWidget::resize_redraw()
{
  // Force redraw
  _need_redraw = true;

  if(_buf)
  {
    Cairo::RefPtr<Cairo::Context> ctx = _buf->create_cairo_context();

    if(ctx)
    {
      // Determine the final drawing dimensions before to start drawing
      _draw_w = _draw_h = 0; // default no scaling
      calc_drawing_size(ctx, _draw_w, _draw_h);
      ctx.clear();

      calc_scale_factors();

      // invalidate all widget's surface
      queue_draw();

      set_size_request((int)_scale_x*_draw_w, (int)_scale_y*_draw_h);
    }
  }
  else
  {
    // invalidate all widget's surface
    queue_draw();
  }

  // invalidate all widget's surface
  // queue_draw();
}

/*
  Dummy method.
  Derived classes can reimplement it or not as desired.
 */
void
CairoWidget::calc_drawing_size(const Cairo::RefPtr<Cairo::Context>& /* ctx */, size_t& /* width */, size_t& /*height*/)
{
}


/*
  Dummy method.
  Derived classes can reimplement it or not as desired.
 */
void
CairoWidget::calc_widget_size(size_t& /*width*/, size_t& /*height*/)
{
}

