/* area.cc -- OpenGL window for plotting a 3D function.  -*- C++ -*-
   Copyright (C) 2025-2026 Gerardo Ballabio

   This program 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.

   This program 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 program.  If not, see <http://www.gnu.org/licenses/>. */

#include <cmath>
#include <gtkmm.h>
#include <epoxy/gl.h>
#include "area.h"
#include "frame.h"
#include "grid.h"

void Area::on_realize()
{
  // realize parent widget
  Gtk::GLArea::on_realize();

  // OpenGL context
  make_current();

  // background color
  glClearColor(0.0, 0.0, 0.0, 1.0);

  // depth buffer
  set_has_depth_buffer();
  glEnable(GL_DEPTH_TEST);

  // antialiasing
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glEnable(GL_LINE_SMOOTH);
  glEnable(GL_POLYGON_SMOOTH);

  // set up things
  init_model_matrix();

  frame.init();
  side = frame.get_font_size() * 6;

  grid.init();
}

bool Area::on_render(const Glib::RefPtr<Gdk::GLContext> &context)
{
  // get current time
  Glib::TimeVal timeval;
  timeval.assign_current_time();
  double t = timeval.as_double();
  static double t0 = t;
  double dt = t - t0;
  t0 = t;

  // framerate counter
  static double tf = t;
  static int n = 0;
  ++n;
  const double seconds = 2.0;
  if (t - tf >= seconds)
    {
      fps = n / (t - tf);
      tf = t;
      n = 0;
      fps_signal.emit(fps);
    }

  // clear window
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // resize if necessary
  int width_ = get_width(), height_ = get_height();
  if (width_ != width || height_ != height)
    resize(width_, height_);

  // draw things
  draw();

  // done
  glFlush();

  return true;
}

bool Area::on_button_press_event(GdkEventButton *event)
{
  x0 = event->x - get_width() / 2;
  y0 = get_height() / 2 - event->y;
  button[event->button] = true;
  press_signal.emit();
  return true;
}

bool Area::on_button_release_event(GdkEventButton *event)
{
  button[event->button] = false;
  return true;
}

bool Area::on_motion_notify_event(GdkEventMotion *event)
{
  if (button[1]) // left button
    {
      double l = (get_width() + get_height()) / 4,
	x = event->x - get_width() / 2, y = get_height() / 2 - event->y;
      rotate(x, y, x0, y0, l);
      x0 = x;
      y0 = y;
    }
  if (button[2]) // middle button
    {
      double d0 = std::sqrt(x0 * x0 + y0 * y0);
      double x = event->x - get_width() / 2, y = get_height() / 2 - event->y,
	d = std::sqrt(x * x + y * y);
      const double d1 = 100.0;
      set_scale(get_scale() * (d0 + d1) / (d + d1));
      x0 = x;
      y0 = y;
    }
  queue_draw();
  return true;
}

void Area::on_grid_signal_scale(double scale_)
{
  scale = scale_;
  frame.set_scale(scale);
}

void Area::resize(int width_, int height_)
{
  width = width_;
  height = height_;

  // projection matrix
  // rotate to set x axis pointing right, y axis forward, z axis upwards
  // then apply perspective projection
  // finally translate along axis x
  const double pi = 3.141592653589793, d2r = pi / 180;
  const double tan = std::tan(17.5 * d2r), distance = 6.0;
  int min = ((width - side) < height) ? (width - side) : height;
  double z0 = distance, z1 = -1.0, z2 = -distance * 2.0;
  double x1 = -z1 * tan * width / min, y1 = -z1 * tan * height / min,
    x2 = -(double) side / width;
  double perspective_matrix[] =
    {
      -z1/x1,    0.0,                            0.0, 0.0,
          x2,    0.0,               -(z1+z2)/(z1-z2), 1.0,
         0.0, -z1/y1,                            0.0, 0.0,
       z0*x2,    0.0, -(2*z1*z2+z1*z0+z2*z0)/(z1-z2),  z0,
    };
  for (unsigned i=0; i<16; ++i)
    view_matrix[i] = perspective_matrix[i];

  frame.set_frame(width, height);
  grid.set_window_size(width, height, frame.get_font_size());
};

void Area::draw()
{
  // multiply matrices
  for (unsigned i=0; i<4; ++i)
    for (unsigned j=0; j<4; ++j)
      matrix[i*4+j] =
	view_matrix[   j] * model_matrix[i*4  ] +
	view_matrix[ 4+j] * model_matrix[i*4+1] +
	view_matrix[ 8+j] * model_matrix[i*4+2] +
	view_matrix[12+j] * model_matrix[i*4+3];

  frame.set_matrix(matrix);
  grid.set_view_matrix(view_matrix);
  grid.set_model_matrix(model_matrix);

  frame.draw();
  grid.draw();
}

void Area::init_model_matrix()
{
  const double pi = 3.141592653589793238, d2r = pi / 180;
  const double theta_init = 18.0, phi_init = 28.0;
  const double sin_t = sin(theta_init * d2r), cos_t = cos(theta_init * d2r);
  const double sin_p = sin(phi_init * d2r), cos_p = cos(phi_init * d2r);
  const double init_matrix[] = { cos_p, -cos_t*sin_p, -sin_t*sin_p, 0,
				 sin_p, cos_t*cos_p,  sin_t*cos_p,  0,
				 0,     -sin_t,       cos_t,        0,
				 0,     0,            0,            1, };
  for (unsigned i=0; i<sizeof(init_matrix)/sizeof(init_matrix[0]); ++i)
    model_matrix[i] = init_matrix[i];
}

Area::Area()
  : frame(*this),
    width(0), height(0), side(0),
    fps(0.0)
{
  // mouse events
  add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
	     Gdk::POINTER_MOTION_MASK);
  for (unsigned i=0; i<sizeof(button)/sizeof(button[0]); ++i)
    button[i] = false;

  // connect signal
  grid.signal_scale().connect
    (sigc::mem_fun(*this, &Area::on_grid_signal_scale));

  // activate timeout
  timeout = Glib::signal_timeout().connect
    (sigc::mem_fun(*this, &Area::on_timeout), 5); // milliseconds
}

void Area::rotate(double x, double z, double x0, double z0, double l)
{
  double ll = l*l;
  double ax, ay, az, bx, by, bz, cx, cy, cz, dx, dy, dz, ex, ey, ez;
  double alpha, length;
  double m1[16], m2[16];

  if (x == x0 && z == z0)
    return;

  // stereographic projection
  alpha = ll / (x0*x0 + z0*z0 + ll);
  ax = x0 * alpha;
  ay = -l * (alpha - 0.5);
  az = z0 * alpha;
  length = std::sqrt(ax*ax + ay*ay + az*az);
  ax /= length;
  ay /= length;
  az /= length;

  alpha = ll / (x*x + z*z + ll);
  bx = x * alpha;
  by = -l * (alpha - 0.5);
  bz = z * alpha;
  length = std::sqrt(bx*bx + by*by + bz*bz);
  bx /= length;
  by /= length;
  bz /= length;

  // cross product
  cx = ay * bz - az * by;
  cy = az * bx - ax * bz;
  cz = ax * by - ay * bx;
  length = std::sqrt(cx*cx + cy*cy + cz*cz);
  cx /= length;
  cy /= length;
  cz /= length;

  dx = ay * cz - az * cy;
  dy = az * cx - ax * cz;
  dz = ax * cy - ay * cx;

  ex = by * cz - bz * cy;
  ey = bz * cx - bx * cz;
  ez = bx * cy - by * cx;

  // rotation matrix
  m1[0] = ax*bx + cx*cx + dx*ex;
  m1[1] = ax*by + cx*cy + dx*ey;
  m1[2] = ax*bz + cx*cz + dx*ez;
  m1[4] = ay*bx + cy*cx + dy*ex;
  m1[5] = ay*by + cy*cy + dy*ey;
  m1[6] = ay*bz + cy*cz + dy*ez;
  m1[8] = az*bx + cz*cx + dz*ex;
  m1[9] = az*by + cz*cy + dz*ey;
  m1[10] = az*bz + cz*cz + dz*ez;
  m1[3] = m1[7] = m1[11] = m1[12] = m1[13] = m1[14] = 0;
  m1[15] = 1;

  // multiply model matrix
  for (unsigned i=0; i<16; ++i)
    m2[i] = model_matrix[i];
  for (unsigned i=0; i<4; ++i)
    for (unsigned j=0; j<4; ++j)
      {
	model_matrix[i*4+j] = 0.0;
	for (unsigned k=0; k<4; ++k)
	  model_matrix[i*4+j] += m1[k*4+j] * m2[i*4+k];
      }
}

void Area::set_scale(double scale_)
{
  scale = scale_;
  grid.set_scale(scale);
  // frame scale is updated via on_grid_signal_scale
}

void Area::set_vscale(double vscale_)
{
  vscale = vscale_;
  grid.set_vscale(vscale);
  frame.set_vscale(vscale);
}

void Area::reset()
{
  init_model_matrix();
  set_scale(1.0);
  set_vscale(1.0);
  resize(width, height);
  grid.queue_set_values();
}
