/* plot.cc -- main window for three-dimensional plot.  -*- 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 <cstdlib>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <gtkmm.h>
#include <epoxy/gl.h>
#include "area.h"
#include "plot.h"
#include "sidebar.h"

bool MainWindow::on_key_press_event(GdkEventKey *event)
{
  double r = (get_width() + get_height()) / 4.0;

  if (!focus_area)
    // pass key to standard handler (pass to active widget)
    if (Gtk::ApplicationWindow::on_key_press_event(event))
      return true;

  switch (event->keyval)
    {
      // quit program
    case GDK_KEY_q: case GDK_KEY_Q: hide(); break;

      // resize grid
    case GDK_KEY_s: spin_step1_0.spin(Gtk::SPIN_STEP_FORWARD, 1);  break;
    case GDK_KEY_t: spin_step2_0.spin(Gtk::SPIN_STEP_FORWARD, 1);  break;
    case GDK_KEY_S: spin_step1_0.spin(Gtk::SPIN_STEP_BACKWARD, 1); break;
    case GDK_KEY_T: spin_step2_0.spin(Gtk::SPIN_STEP_BACKWARD, 1); break;

      // rotate
    case GDK_KEY_Up:        area.rotate(  0,  5, 0, 0, r); break;
    case GDK_KEY_Down:      area.rotate(  0, -5, 0, 0, r); break;
    case GDK_KEY_Left:      area.rotate( -5,  0, 0, 0, r); break;
    case GDK_KEY_Right:     area.rotate(  5,  0, 0, 0, r); break;
    case GDK_KEY_Page_Up:   area.rotate(-10,  r, 0, r, r); break;
    case GDK_KEY_Page_Down: area.rotate( 10,  r, 0, r, r); break;

      // zoom
    case GDK_KEY_Home: area.set_scale(area.get_scale() * 0.98); break;
    case GDK_KEY_End:  area.set_scale(area.get_scale() / 0.98); break;

      // reset view
    case GDK_KEY_0: area.reset(); break;

      // toggle display visual elements
    case GDK_KEY_v: case GDK_KEY_V:
      check_volume.set_active(!check_volume.get_active());
      break;
    case GDK_KEY_i: case GDK_KEY_I:
      check_surfaces.set_active(!check_surfaces.get_active());
      break;
    case GDK_KEY_a: case GDK_KEY_A:
      check_axes.set_active(!check_axes.get_active());
      break;
    case GDK_KEY_f: case GDK_KEY_F:
      check_cube.set_active(!check_cube.get_active());
      break;
    case GDK_KEY_b: case GDK_KEY_B:
      check_bar.set_active(!check_bar.get_active());
      break;
    case GDK_KEY_c: case GDK_KEY_C:
      check_clip.set_active(!check_clip.get_active());
      break;

      // change number of isosurfaces
    case GDK_KEY_u: spin_nsurfaces.spin(Gtk::SPIN_STEP_FORWARD, 10);  break;
    case GDK_KEY_U: spin_nsurfaces.spin(Gtk::SPIN_STEP_BACKWARD, 10); break;

      // change number of threads
    case GDK_KEY_h: spin_nthreads.spin(Gtk::SPIN_STEP_FORWARD, 1);  break;
    case GDK_KEY_H: spin_nthreads.spin(Gtk::SPIN_STEP_BACKWARD, 1); break;

      // help
    case GDK_KEY_F1: on_button_help(); break;

    default: break;
    }

  queue_draw();
  return true;
}

bool MainWindow::on_timeout()
{
  Glib::TimeVal timeval;
  timeval.assign_current_time();
  double t = timeval.as_double();

  const double delay = 0.5; // wait this time before activating progress bar
  if (t - t0 > delay)
    progressbar.pulse();

  return true;
}

void MainWindow::on_area_signal_working(bool working_)
{
  if (working_)
    {
      // activate timeout (if not already active)
      if (!working)
	{
	  // record start time
	  Glib::TimeVal timeval;
	  timeval.assign_current_time();
	  t0 = timeval.as_double();

	  timeout = Glib::signal_timeout().connect
	    (sigc::mem_fun(*this, &MainWindow::on_timeout), 20); // millisecs
	  working = true;
	}
    }
  else
    {
      // suppress timeout, clear progressbar
      timeout.disconnect();
      progressbar.set_fraction(0.0);
      working = false;
    }
}

void MainWindow::on_area_signal_press()
{
  focus_area = true;
}

void MainWindow::on_area_signal_fps(double fps_)
{
  // update fps counter
  std::ostringstream os;
  os << std::fixed << std::setprecision(1) << "Frame rate: " << fps_;
  label_fps.set_label(os.str());
}

void MainWindow::on_sidebar_signal_press()
{
  focus_area = false;
}

void MainWindow::on_check_surfaces_signal_toggled()
{
  area.set_draw_surfaces(check_surfaces.get_active());
}

void MainWindow::on_check_volume_signal_toggled()
{
  area.set_draw_volume(check_volume.get_active());
}

void MainWindow::on_check_axes_signal_toggled()
{
  area.set_draw_axes(check_axes.get_active());
}

void MainWindow::on_check_cube_signal_toggled()
{
  area.set_draw_cube(check_cube.get_active());
}

void MainWindow::on_check_bar_signal_toggled()
{
  area.set_draw_bar(check_bar.get_active());
}

void MainWindow::on_check_clip_signal_toggled()
{
  area.set_clip(check_clip.get_active());
}

void MainWindow::on_spin_step_signal_value_changed()
{
  // update grid parameters
  GLuint step1[3], step2[3];
  step1[0] = spin_step1_0.get_value_as_int();
  step1[1] = spin_step1_1.get_value_as_int();
  step1[2] = spin_step1_2.get_value_as_int();
  step2[0] = spin_step2_0.get_value_as_int();
  step2[1] = spin_step2_1.get_value_as_int();
  step2[2] = spin_step2_2.get_value_as_int();
  area.set_grid(area.get_size(), step1, step2);
}

void MainWindow::on_spin_nsurfaces_signal_value_changed()
{
  // update number of isosurfaces
  area.set_nsurfaces(spin_nsurfaces.get_value_as_int());
}

void MainWindow::on_spin_nthreads_signal_value_changed()
{
  // update number of threads
  area.set_nthreads(spin_nthreads.get_value_as_int());
}

void MainWindow::on_button_about()
{
  Gtk::AboutDialog dialog;
  dialog.set_transient_for(*this);

  // don't show missing icon, I don't like how it looks
  dialog.set_logo(Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, 8, 1, 1));

  dialog.set_program_name("GPlot");
  dialog.set_version("0.1");
  dialog.set_comments("A program for visualizing 3D functions");
  dialog.set_website("http://www.nongnu.org/gwaves/");
  dialog.set_copyright("Copyright © 2024-2026 Gerardo Ballabio");
  dialog.set_license
    ("GPlot is free software; you can redistribute it and/or modify\n"
     "it under the terms of the GNU General Public License as published by\n"
     "the Free Software Foundation; either version 3 of the License, or\n"
     "(at your option) any later version.\n"
     "\n"
     "GPlot is distributed in the hope that it will be useful, but\n"
     "WITHOUT ANY WARRANTY; without even the implied warranty of\n"
     "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
     "GNU General Public License for more details.\n"
     "\n"
     "You should have received a copy of the GNU General Public License\n"
     "along with GPlot.  If not, see <http://www.gnu.org/licenses/>.");

  dialog.run();
}

void MainWindow::on_button_help()
{
  // launch help browser
  if (!gtk_show_uri_on_window(static_cast<Gtk::Window *>(this)->gobj(),
			      "ghelp:gwaves", GDK_CURRENT_TIME, 0))
    // fall back to html manual (requires network connection)
    gtk_show_uri_on_window(static_cast<Gtk::Window *>(this)->gobj(),
			   "http://www.nongnu.org/gwaves/gwaves.html",
			   GDK_CURRENT_TIME, 0);
}

MainWindow::MainWindow(const Glib::ustring &file,
		       GLuint x, GLuint y, GLuint z)
{
  set_default_size(900, 600);

  // build widget hierarchy
  add(toplevel);

  // display window
  toplevel.attach(area, 0, 0, 1, 1);
  area.set_hexpand(true);
  area.set_vexpand(true);
  area.signal_working().connect
    (sigc::mem_fun(*this, &MainWindow::on_area_signal_working));
  area.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_area_signal_press));
  area.signal_fps().connect
    (sigc::mem_fun(*this, &MainWindow::on_area_signal_fps));

  // progress bar
  toplevel.attach(progressbar, 0, 1, 1, 1);
  progressbar.set_pulse_step(0.05);

  // side bar
  toplevel.attach(grid_sidebar, 1, 0, 1, 2);

  // notebook
  grid_sidebar.attach(notebook, 0, 0, 1, 1);
  notebook.set_hexpand(false);
  notebook.set_vexpand(true);

  // first tab
  notebook.append_page(grid_controls, "Controls");
  grid_controls.set_border_width(12);
  grid_controls.set_row_spacing(18);

  grid_controls.attach(grid_elements, 0, 1, 1, 1);
  grid_elements.set_row_spacing(6);
  grid_elements.attach(label_elements, 0, 0, 1, 1);
  label_elements.set_text("<b>Visual elements</b>");
  label_elements.set_use_markup(true);
  label_elements.set_alignment(Gtk::ALIGN_START);
  grid_elements.attach(grid_modes, 0, 1, 1, 1);
  grid_modes.set_margin_start(12);
  grid_modes.set_row_spacing(6);
  grid_modes.attach(check_surfaces, 0, 0, 1, 1);
  check_surfaces.set_label("Isosurfaces");
  check_surfaces.set_active(true);
  check_surfaces.signal_toggled().connect
    (sigc::mem_fun(*this, &MainWindow::on_check_surfaces_signal_toggled));
  check_surfaces.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_modes.attach(check_volume, 0, 1, 1, 1);
  check_volume.set_label("Volume rendering");
  check_volume.set_active(true);
  check_volume.signal_toggled().connect
    (sigc::mem_fun(*this, &MainWindow::on_check_volume_signal_toggled));
  check_volume.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_modes.attach(check_axes, 0, 2, 1, 1);
  check_axes.set_label("Cartesian axes");
  check_axes.set_active(true);
  check_axes.signal_toggled().connect
    (sigc::mem_fun(*this, &MainWindow::on_check_axes_signal_toggled));
  check_axes.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_modes.attach(check_cube, 0, 3, 1, 1);
  check_cube.set_label("Coordinate frame");
  check_cube.set_active(true);
  check_cube.signal_toggled().connect
    (sigc::mem_fun(*this, &MainWindow::on_check_cube_signal_toggled));
  check_cube.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_modes.attach(check_bar, 0, 4, 1, 1);
  check_bar.set_label("Scale bar");
  check_bar.set_active(true);
  check_bar.signal_toggled().connect
    (sigc::mem_fun(*this, &MainWindow::on_check_bar_signal_toggled));
  check_bar.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_modes.attach(check_clip, 0, 5, 1, 1);
  check_clip.set_label("Clip viewing region");
  check_clip.set_active(true);
  check_clip.signal_toggled().connect
    (sigc::mem_fun(*this, &MainWindow::on_check_clip_signal_toggled));
  check_clip.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));

  // second tab
  notebook.append_page(grid_settings, "Settings");
  grid_settings.set_border_width(12);
  grid_settings.set_row_spacing(18);

  grid_settings.attach(grid_grid, 0, 0, 1, 1);
  grid_grid.set_row_spacing(6);
  grid_grid.attach(label_grid, 0, 0, 1, 1);
  label_grid.set_text("<b>Grid dimensions</b>");
  label_grid.set_use_markup(true);
  label_grid.set_alignment(Gtk::ALIGN_START);
  grid_grid.attach(grid_size, 0, 1, 1, 1);
  grid_size.set_margin_start(12);
  grid_size.set_column_spacing(6);
  grid_size.attach(label_step1, 0, 0, 1, 1);
  label_step1.set_text("step1:");
  grid_size.attach(label_step2, 0, 3, 1, 1);
  label_step2.set_text("step2:");
  grid_size.attach(spin_step1_0, 1, 0, 1, 1);
  spin_step1_0.set_adjustment(Gtk::Adjustment::create(1, 1, 5));
  spin_step1_0.signal_value_changed().connect
    (sigc::mem_fun(*this, &MainWindow::on_spin_step_signal_value_changed));
  spin_step1_0.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_size.attach(spin_step1_1, 1, 1, 1, 1);
  spin_step1_1.set_adjustment(Gtk::Adjustment::create(1, 1, 5));
  spin_step1_1.signal_value_changed().connect
    (sigc::mem_fun(*this, &MainWindow::on_spin_step_signal_value_changed));
  spin_step1_1.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_size.attach(spin_step1_2, 1, 2, 1, 1);
  spin_step1_2.set_adjustment(Gtk::Adjustment::create(1, 1, 5));
  spin_step1_2.signal_value_changed().connect
    (sigc::mem_fun(*this, &MainWindow::on_spin_step_signal_value_changed));
  spin_step1_2.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_size.attach(spin_step2_0, 1, 3, 1, 1);
  spin_step2_0.set_adjustment(Gtk::Adjustment::create(1, 1, 5));
  spin_step2_0.signal_value_changed().connect
    (sigc::mem_fun(*this, &MainWindow::on_spin_step_signal_value_changed));
  spin_step2_0.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_size.attach(spin_step2_1, 1, 4, 1, 1);
  spin_step2_1.set_adjustment(Gtk::Adjustment::create(1, 1, 5));
  spin_step2_1.signal_value_changed().connect
    (sigc::mem_fun(*this, &MainWindow::on_spin_step_signal_value_changed));
  spin_step2_1.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_size.attach(spin_step2_2, 1, 5, 1, 1);
  spin_step2_2.set_adjustment(Gtk::Adjustment::create(1, 1, 5));
  spin_step2_2.signal_value_changed().connect
    (sigc::mem_fun(*this, &MainWindow::on_spin_step_signal_value_changed));
  spin_step2_2.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));

  grid_settings.attach(grid_surfaces, 0, 1, 1, 1);
  grid_surfaces.set_row_spacing(6);
  grid_surfaces.attach(label_surfaces, 0, 0, 1, 1);
  label_surfaces.set_text("<b>Isosurfaces</b>");
  label_surfaces.set_use_markup(true);
  label_surfaces.set_alignment(Gtk::ALIGN_START);
  grid_surfaces.attach(grid_nsurfaces, 0, 1, 1, 1);
  grid_nsurfaces.set_margin_start(12);
  grid_nsurfaces.set_column_spacing(6);
  grid_nsurfaces.attach(label_nsurfaces, 0, 0, 1, 1);
  label_nsurfaces.set_text("levels:");
  grid_nsurfaces.attach(spin_nsurfaces, 1, 0, 1, 1);
  spin_nsurfaces.set_adjustment(Gtk::Adjustment::create(20, 10, 50, 10, 10));
  spin_nsurfaces.set_snap_to_ticks();
  spin_nsurfaces.signal_value_changed().connect
    (sigc::mem_fun
     (*this, &MainWindow::on_spin_nsurfaces_signal_value_changed));
  spin_nsurfaces.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));

  grid_settings.attach(grid_threads, 0, 2, 1, 1);
  grid_threads.set_row_spacing(6);
  grid_threads.attach(label_threads, 0, 0, 1, 1);
  label_threads.set_text("<b>Multithreading</b>");
  label_threads.set_use_markup(true);
  label_threads.set_alignment(Gtk::ALIGN_START);
  grid_threads.attach(grid_nthreads, 0, 1, 1, 1);
  grid_nthreads.set_margin_start(12);
  grid_nthreads.set_column_spacing(6);
  grid_nthreads.attach(label_nthreads, 0, 0, 1, 1);
  label_nthreads.set_text("threads:");
  grid_nthreads.attach(spin_nthreads, 1, 0, 1, 1);
  spin_nthreads.set_adjustment(Gtk::Adjustment::create(4, 1, 8));
  spin_nthreads.signal_value_changed().connect
    (sigc::mem_fun
     (*this, &MainWindow::on_spin_nthreads_signal_value_changed));
  area.set_nthreads(spin_nthreads.get_value_as_int());
  spin_nthreads.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));

  grid_settings.attach(frame_fps, 0, 3, 1, 1);
  frame_fps.set_hexpand(true);
  frame_fps.add(label_fps);
  label_fps.set_label("Frame rate: ");

  // end of notebook

  grid_sidebar.attach(grid_buttons, 0, 1, 1, 1);
  grid_buttons.set_border_width(6);
  grid_buttons.set_column_spacing(6);
  grid_buttons.set_row_spacing(6);
  grid_buttons.set_column_homogeneous(true);
  grid_buttons.attach(button_help, 0, 0, 1, 1);
  button_help.set_label("Help");
  button_help.signal_clicked().connect
    (sigc::mem_fun(*this, &MainWindow::on_button_help));
  button_help.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_buttons.attach(button_about, 1, 0, 1, 1);
  button_about.set_label("About");
  button_about.signal_clicked().connect
    (sigc::mem_fun(*this, &MainWindow::on_button_about));
  button_about.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));
  grid_buttons.attach(button_close, 2, 0, 1, 1); // 0, 1, 2, 1);
  button_close.set_label("Close");
  button_close.signal_clicked().connect
    (sigc::mem_fun(*this, &MainWindow::hide));
  button_close.signal_press().connect
    (sigc::mem_fun(*this, &MainWindow::on_sidebar_signal_press));

  GLuint size[] = { x, y, z, }, step[] = { 1, 1, 1, };
  area.set_grid(size, step, step);

  // read data from file
  if (!area.read_values(file))
    {
      std::cerr << "invalid file (bad format or not enough values)"
		<< std::endl;
      std::exit(1);
    }
}

void MainWindow::on_startup(Glib::RefPtr<Gtk::Application> &app)
{
  app->add_window(*this);
  show_all();

  area.set_scale(1.0); // this needs to be called after area has been realized
  area.set_vscale(1.0 / area.get_vmax());
  area.set_new_vscale(1.0 / area.get_vmax());

  GLuint step1_[3], step2_[3];
  step1_[0] = spin_step1_0.get_value_as_int();
  step1_[1] = spin_step1_1.get_value_as_int();
  step1_[2] = spin_step1_2.get_value_as_int();
  step2_[0] = spin_step2_0.get_value_as_int();
  step2_[1] = spin_step2_1.get_value_as_int();
  step2_[2] = spin_step2_2.get_value_as_int();
  area.init_step(step1_, step2_);
}

int main(int argc, char *argv[])
{
  // read command line arguments
  Glib::ustring file;
  GLuint x = 0, y = 0, z = 0;
  switch (argc)
    {
    case 1: break;
    case 5: default:
      x = std::atoi(argv[2]);
      y = std::atoi(argv[3]);
      z = std::atoi(argv[4]);
      // fall through
    case 4: case 3:
    case 2:
      file = argv[1];
      argv += argc - 1;
      argc = 1;
      break;
    }

  // create main window
  Glib::RefPtr<Gtk::Application> app = Gtk::Application::create(argc, argv);

  MainWindow window(file, x, y, z);
  app->signal_startup().connect
    (sigc::bind<Glib::RefPtr<Gtk::Application> &>
     (sigc::mem_fun(window, &MainWindow::on_startup), app));

  return app->run(window);
}
