/* frame.cc -- code for drawing frame.  -*- 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 <functional>
#include <sstream>
#include <string>
#include <vector>
#include <gtkmm.h>
#include <epoxy/gl.h>
#include "frame.h"

const int tex_size = 1024;
const GLuint restart = static_cast<GLuint>(-1);
const GLuint max_ticks = 87, max_ticks2 = 29;

void gl_frame::init_lines()
{
  // build vertex shader
  const GLchar *vertex_source =
    "#version 330                                     \n"
    "                                                   "
    "uniform mat4 u_matrix;                             "
    "                                                   "
    "in vec3 in_position;                               "
    "                                                   "
    "out vec4 color;                                    "
    "                                                   "
    "void main()                                        "
    "{                                                  "
    "  gl_Position = u_matrix * vec4(in_position, 1.0); "
    "  color = vec4(1.0, 1.0, 1.0, 1.0);                "
    "}                                                  ";
  h_vertex_shader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(h_vertex_shader, 1, &vertex_source, NULL);
  glCompileShader(h_vertex_shader);

  // build fragment shader
  const GLchar *fragment_source =
    "#version 330       \n"
    "                     "
    "in vec4 color;       "
    "                     "
    "out vec4 out_color;  "
    "                     "
    "void main()          "
    "{                    "
    "  out_color = color; "
    "}                    ";
  h_fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(h_fragment_shader, 1, &fragment_source, NULL);
  glCompileShader(h_fragment_shader);

  // build shader program
  h_program = glCreateProgram();
  glAttachShader(h_program, h_fragment_shader);
  glAttachShader(h_program, h_vertex_shader);
  glLinkProgram(h_program);

  // uniform variables
  u_matrix = glGetUniformLocation(h_program, "u_matrix");

  glGenVertexArrays(1, &h_vertex_array_axes);
  glGenBuffers(1, &h_vertex_buffer_axes);
  glGenVertexArrays(1, &h_vertex_array_bar);
  glGenBuffers(1, &h_vertex_buffer_bar);
}

void gl_frame::init_textures()
{
  // build vertex shader
  const GLchar *vertex_source_tex =
    "#version 330                                     \n"
    "                                                   "
    "uniform mat4 u_matrix;                             "
    "                                                   "
    "in vec3 in_position;                               "
    "in vec2 in_coord;                                  "
    "                                                   "
    "out vec2 tex_coord;                                "
    "                                                   "
    "void main()                                        "
    "{                                                  "
    "  gl_Position = u_matrix * vec4(in_position, 1.0); "
    "  tex_coord = in_coord;                            "
    "}                                                  ";
  h_vertex_shader_tex = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(h_vertex_shader_tex, 1, &vertex_source_tex, NULL);
  glCompileShader(h_vertex_shader_tex);

  // build fragment shader
  const GLchar *fragment_source_tex =
    "#version 330                                                \n"
    "                                                              "
    "uniform sampler2D u_sampler;                                  "
    "                                                              "
    "in vec2 tex_coord;                                            "
    "                                                              "
    "out vec4 color;                                               "
    "                                                              "
    "void main()                                                   "
    "{                                                             "
    "  /* texture contains alpha channel, color is white */        "
    "  color = vec4(1.0, 1.0, 1.0, texture(u_sampler, tex_coord)); "
    "  /* don't write on depth buffer if alpha is about zero */    "
    "  if (color.a <= 0.2)                                         "
    "    discard;                                                  "
    "}                                                             ";
  h_fragment_shader_tex = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(h_fragment_shader_tex, 1, &fragment_source_tex, NULL);
  glCompileShader(h_fragment_shader_tex);

  // build shader program
  h_program_tex = glCreateProgram();
  glAttachShader(h_program_tex, h_fragment_shader_tex);
  glAttachShader(h_program_tex, h_vertex_shader_tex);
  glLinkProgram(h_program_tex);

  // uniform variables
  u_matrix_tex = glGetUniformLocation(h_program_tex, "u_matrix");
  u_sampler_tex = glGetUniformLocation(h_program_tex, "u_sampler");

  // Gtk+ seems to go crazy if I try to rewrite textures
  // so draw in advance all the characters that may appear in numbers
  // and build ticks labels from them
  glGenTextures(1, &h_texture);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, h_texture);
  glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, tex_size, tex_size);
  char text[] = {
    'x', 'y', 'z', // axis labels
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // numbers
    '.', '+', '-', 'e',
  };
  for (int i=0; i<sizeof(text)/sizeof(text[0]); ++i)
    init_subtexture(text[i]);

  // four coordinates per vertex
  // 3 vertices for axis labels plus 20 for ticks labels
  tvertices.resize(4 * (3 + 20));

  // build vertex array
  glGenVertexArrays(1, &h_vertex_array_tex);
  glBindVertexArray(h_vertex_array_tex);
  glGenBuffers(1, &h_buffer_tex);
  glBindBuffer(GL_ARRAY_BUFFER, h_buffer_tex);
  GLfloat quad[] =
    {
      0.0, 0.0,
      1.0, 0.0,
      1.0, 1.0,
      0.0, 1.0,
    };
  glBufferData(GL_ARRAY_BUFFER, 12 * sizeof(GLfloat) + sizeof(quad), NULL,
	       GL_DYNAMIC_DRAW);
  glBufferSubData(GL_ARRAY_BUFFER, 12 * sizeof(GLfloat), sizeof(quad), quad);

  // vertex attributes
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void *) 0);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0,
			(void *) (12 * sizeof(float)));
  glEnableVertexAttribArray(0);
  glEnableVertexAttribArray(1);
}

void gl_frame::init_subtexture(char text)
{
  // write text on Cairo surface
  // use the same font and settings as text drawn in other widgets
  char str[] = { text, '\0', }; // build string with text as only character
  Glib::RefPtr<Pango::Layout> layout = widget.create_pango_layout(str);
  Pango::Rectangle rectangle =
    layout->get_line(0)->get_pixel_logical_extents();
  const int margin = 1, align = 4; // pango requires alignment
  tchar.push_back(text);
  if (tx.size() == 0)
    tx.push_back(0);
  else
    tx.push_back(tx.back() + twidth.back());
  ty.push_back(0);
  twidth.push_back(rectangle.get_width() + margin * 2);
  theight.push_back(rectangle.get_height() + margin * 2);
  int lbearing = -rectangle.get_lbearing() + margin,
    descent = rectangle.get_descent() + margin,
    swidth = ((twidth.back() + align - 1) / align) * align;
  std::vector<unsigned char> surfacealpha(swidth * theight.back(), 0);
  Cairo::RefPtr<Cairo::ImageSurface> surface =
    Cairo::ImageSurface::create(&surfacealpha[0], Cairo::FORMAT_A8,
				twidth.back(), theight.back(), swidth);
  Cairo::RefPtr<Cairo::Context> context = Cairo::Context::create(surface);
  context->move_to(lbearing, descent);
  context->scale(1.0, -1.0);
  layout->get_line(0)->show_in_cairo_context(context);
  context->stroke();

  // create subtexture from Cairo surface
  glTexSubImage2D(GL_TEXTURE_2D, 0,
		  tx.back(), ty.back(), twidth.back(), theight.back(),
		  GL_RED, GL_UNSIGNED_BYTE, (void *) &surfacealpha[0]);
}

void gl_frame::init_axes()
{
  // build index array
  GLuint indices[] =
    {
      // x axis
      0, 1, restart,    // axis
      2, 1, 3, restart, // arrowhead
      4, 1, 5, restart, // arrowhead
      // y axis
      6, 7, restart,
      8, 7, 9, restart,
      10, 7, 11, restart,
      // z axis
      12, 13, restart,
      14, 13, 15, restart,
      16, 13, 17, restart,
    };
  i_axes = sizeof(indices) / sizeof(indices[0]);
  glGenBuffers(1, &h_index_buffer_axes);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer_axes);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
	       GL_STATIC_DRAW);
}

void gl_frame::init_cube()
{
  // build vertex array
  GLfloat vertices[] =
    {
      // cube
      -1.0, -1.0, -1.0,
      -1.0, -1.0,  1.0,
      -1.0,  1.0, -1.0,
      -1.0,  1.0,  1.0,
       1.0, -1.0, -1.0,
       1.0, -1.0,  1.0,
       1.0,  1.0, -1.0,
       1.0,  1.0,  1.0,
    };
  v_cube = sizeof(vertices) / sizeof(vertices[0]) / 3;
  glGenVertexArrays(1, &h_vertex_array_cube);
  glBindVertexArray(h_vertex_array_cube);
  glGenBuffers(1, &h_vertex_buffer_cube);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer_cube);
  glBufferData(GL_ARRAY_BUFFER,
	       sizeof(vertices) + 6 * sizeof(vertices[0]) * max_ticks,
	       NULL, GL_DYNAMIC_DRAW);
  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void *) 0);

  // build index array
  GLuint indices_cube[] =
    {
      // cube
      0, 1, 5, 7, restart,
      1, 3, 7, 6, restart,
      3, 2, 6, 4, restart,
      2, 0, 4, 5, restart,
    };
  i_cube = sizeof(indices_cube) / sizeof(indices_cube[0]);
  GLuint indices_tick[] =
    {
      v_cube, v_cube + 1, restart,
    };
  i_tick = sizeof(indices_tick) / sizeof(indices_tick[0]);
  v_tick = 2;
  glGenBuffers(1, &h_index_buffer_cube);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer_cube);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER,
	       sizeof(indices_cube) + sizeof(indices_tick) * max_ticks,
	       NULL, GL_DYNAMIC_DRAW);
  glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(indices_cube),
		  indices_cube);
  for (int i=0; i<max_ticks; ++i)
    {
      // add tick
      glBufferSubData(GL_ELEMENT_ARRAY_BUFFER,
		      sizeof(indices_cube) + sizeof(indices_tick) * i,
		      sizeof(indices_tick), indices_tick);
      // increment indices for next tick
      indices_tick[0] += v_tick;
      indices_tick[1] = indices_tick[0] + 1;
    }

  s_tick_xy = s_tick_xz = s_tick_yz = s_tick_yx = s_tick_zx = s_tick_zy = -1;
}

void gl_frame::init_bar()
{
  v_bar = 4;
  glGenVertexArrays(1, &h_vertex_array_bar);
  glBindVertexArray(h_vertex_array_bar);
  glGenBuffers(1, &h_vertex_buffer_bar);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer_bar);
  glBufferData(GL_ARRAY_BUFFER,
	       3 * v_bar * sizeof(GLfloat) + 6 * sizeof(GLfloat) * max_ticks2,
	       NULL, GL_DYNAMIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *) 0);

  // build index array
  GLuint indices_bar[] =
    {
      0, 1, 2, 3, 0, restart,
    };
  v_bar = 4;
  i_bar = sizeof(indices_bar) / sizeof(indices_bar[0]);
  GLuint indices_tick2[] =
    {
      v_bar, v_bar + 1, restart,
    };
  i_tick2 = sizeof(indices_tick2) / sizeof(indices_tick2[0]);
  v_tick2 = 2;
  glGenBuffers(1, &h_index_buffer_bar);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer_bar);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER,
	       sizeof(indices_bar) + sizeof(indices_tick2) * max_ticks2,
	       NULL, GL_DYNAMIC_DRAW);
  glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(indices_bar),
		  indices_bar);
  for (int i=0; i<max_ticks2; ++i)
    {
      // add tick
      glBufferSubData(GL_ELEMENT_ARRAY_BUFFER,
		      sizeof(indices_bar) + sizeof(indices_tick2) * i,
		      sizeof(indices_tick2), indices_tick2);
      // increment indices for next tick
      indices_tick2[0] += v_tick2;
      indices_tick2[1] = indices_tick2[0] + 1;
    }
}

void gl_frame::set_axes()
{
  // arrow head dimensions
  GLfloat hwidth = 0.5 * font_size * pixel_scale,
    hlength = 1.0 - font_size * pixel_scale;

  // build vertex array
  GLfloat vertices[] =
    {
      // x axis
      -1.0,      0.0,     0.0, // axis
       1.0,      0.0,     0.0,
       hlength, -hwidth,  0.0, // arrow head
       hlength,  hwidth,  0.0,
       hlength,  0.0,    -hwidth,
       hlength,  0.0,     hwidth,
      // y axis
       0.0,    -1.0,      0.0,
       0.0,     1.0,      0.0,
      -hwidth,  hlength,  0.0,
       hwidth,  hlength,  0.0,
       0.0,     hlength, -hwidth,
       0.0,     hlength,  hwidth,
      // z axis
       0.0,     0.0,    -1.0,
       0.0,     0.0,     1.0,
      -hwidth,  0.0,     hlength,
       hwidth,  0.0,     hlength,
       0.0,    -hwidth,  hlength,
       0.0,     hwidth,  hlength,
    };
  glBindVertexArray(h_vertex_array_axes);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer_axes);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void *) 0);

  // draw axis labels at fixed pixel distance from axis tip
  GLfloat length = 1.0 + 1.5 * font_size * pixel_scale;
  GLfloat in[] = {
    length, 0.0,    0.0,    1.0,
    0.0,    length, 0.0,    1.0,
    0.0,    0.0,    length, 1.0,
  };
  for (int i=0; i<sizeof(in)/sizeof(in[0]); ++i)
    tvertices[i] = in[i];
}

void gl_frame::set_ticks()
{
  // recompute ticks for current size and scale

  // temporary workaround
  if (scale < 1e-100 || vscale < 1e-100)
    return;

  // compute ticks positions
  tlabel.resize(3); // redo labels, first 3 are reserved to axis labels
  std::vector<double> ticks_x, ticks_y, ticks_z, ticks_f;
  int start_x, start_y, start_z, start_f,
    stride_x, stride_y, stride_z, stride_f;
  set_ticks_scale(ticks_x, start_x, stride_x, -scale, scale);
  set_ticks_scale(ticks_y, start_y, stride_y, -scale, scale);
  set_ticks_scale(ticks_z, start_z, stride_z, -scale, scale);
  set_ticks_scale(ticks_f, start_f, stride_f, -1.0 / vscale, 1.0 / vscale);

  // build vertex array
  GLfloat tbig = 1.0 + 0.8 * font_size * pixel_scale,
    tsmall = 1.0 + 0.4 * font_size * pixel_scale,
    tlab = 1.0 + 2.0 * font_size * pixel_scale;
  GLfloat big_x[] =
    {
      0.0, 1.0f * s_tick_xy, 1.0f * s_tick_xz,
      0.0, tbig * s_tick_xy, tbig * s_tick_xz,
    },
    big_y[] =
    {
      1.0f * s_tick_yx, 0.0, 1.0f * s_tick_yz,
      tbig * s_tick_yx, 0.0, tbig * s_tick_yz,
    },
    big_z[] =
    {
      1.0f * s_tick_zx, 1.0f * s_tick_zy, 0.0,
      tbig * s_tick_zx, tbig * s_tick_zy, 0.0,
    },
    small_x[] =
    {
      0.0, 1.0f   * s_tick_xy, 1.0f   * s_tick_xz,
      0.0, tsmall * s_tick_xy, tsmall * s_tick_xz,
    },
    small_y[] =
    {
      1.0f   * s_tick_yx, 0.0, 1.0f   * s_tick_yz,
      tsmall * s_tick_yx, 0.0, tsmall * s_tick_yz,
    },
    small_z[] =
    {
      1.0f   * s_tick_zx, 1.0f   * s_tick_zy, 0.0,
      tsmall * s_tick_zx, tsmall * s_tick_zy, 0.0,
    },
    lab_x[] =
    {
      0.0, tlab * s_tick_xy, tlab * s_tick_xz,
    },
    lab_y[] =
    {
      tlab * s_tick_yx, 0.0, tlab * s_tick_yz,
    },
    lab_z[] =
    {
      tlab * s_tick_zx, tlab * s_tick_zy, 0.0,
    };
  glBindVertexArray(h_vertex_array_cube);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer_cube);
  GLuint offset = 3 * v_cube * sizeof(GLfloat);
  n_ticks = big_ticks = 0;
  // ticks on x axis
  for (int i=0; i<ticks_x.size() && n_ticks<max_ticks; ++i, ++n_ticks)
    {
      if (i % stride_x == start_x)
	{
	  // big tick
	  big_x[0] = big_x[3] = lab_x[0] = ticks_x[i];
	  glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(big_x), big_x);
	  offset += sizeof(big_x);

	  // first 12 elements (4 coordinates * 3 vectors) used for axis labels
	  for (int l=0; l<3; ++l)
	    tvertices[12 + big_ticks * 4 + l] = lab_x[l];
	  tvertices[12 + big_ticks * 4 + 3] = 1.0;

	  ++big_ticks;
	}
      else
	{
	  // small tick
	  small_x[0] = small_x[3] = ticks_x[i];
	  glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(small_x), small_x);
	  offset += sizeof(small_x);
	}
    }
  // ticks on y axis
  for (int j=0; j<ticks_y.size() && n_ticks<max_ticks; ++j, ++n_ticks)
    {
      if (j % stride_y == start_y)
	{
	  // big tick
	  big_y[1] = big_y[4] = lab_y[1] = ticks_y[j];
	  glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(big_y), big_y);
	  offset += sizeof(big_y);

	  // first 12 elements (4 coordinates * 3 vectors) used for axis labels
	  for (int l=0; l<3; ++l)
	    tvertices[12 + big_ticks * 4 + l] = lab_y[l];
	  tvertices[12 + big_ticks * 4 + 3] = 1.0;

	  ++big_ticks;
	}
      else
	{
	  // small tick
	  small_y[1] = small_y[4] = ticks_y[j];
	  glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(small_y), small_y);
	  offset += sizeof(small_y);
	}
    }
  // ticks on z axis
  for (int k=0; k<ticks_z.size() && n_ticks<max_ticks; ++k, ++n_ticks)
    {
      if (k % stride_z == start_z)
	{
	  // big tick
	  big_z[2] = big_z[5] = lab_z[2] = ticks_z[k];
	  glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(big_z), big_z);
	  offset += sizeof(big_z);

	  // first 12 elements (4 coordinates * 3 vectors) used for axis labels
	  for (int l=0; l<3; ++l)
	    tvertices[12 + big_ticks * 4 + l] = lab_z[l];
	  tvertices[12 + big_ticks * 4 + 3] = 1.0;

	  ++big_ticks;
	}
      else
	{
	  // small tick
	  small_z[2] = small_z[5] = ticks_z[k];
	  glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(small_z), small_z);
	  offset += sizeof(small_z);
	}
    }

  // build vertex array
  GLfloat vertices[] =
    {
      (GLfloat) width / 2 - font_size * 2,  0.8f * height / 2,
      (GLfloat) width / 2 - font_size,      0.8f * height / 2,
      (GLfloat) width / 2 - font_size,     -0.8f * height / 2,
      (GLfloat) width / 2 - font_size * 2, -0.8f * height / 2,
    };
  GLfloat tzero = 2.0 * font_size, tbig2 = 2.6 * font_size,
    tsmall2 = 2.3 * font_size, tlab2 = 3.0 * font_size;
  GLfloat big_f[] =
    {
      width / 2 - tzero, 0.0,
      width / 2 - tbig2, 0.0,
    },
    small_f[] =
    {
      width / 2 - tzero,   0.0,
      width / 2 - tsmall2, 0.0,
    },
    lab_f[] =
    {
      width / 2 - tlab2, 0.0,
    };
  glBindVertexArray(h_vertex_array_bar);
  glBindBuffer(GL_ARRAY_BUFFER, h_vertex_buffer_bar);
  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void *) 0);
  GLuint offset2 = 2 * v_bar * sizeof(GLfloat);
  n_ticks2 = big_ticks2 = 0;
  // ticks on scale bar
  for (int i=0; i<ticks_f.size() && n_ticks2<max_ticks2; ++i, ++n_ticks2)
    {
      if (i % stride_f == start_f)
	{
	  // big tick
	  big_f[1] = big_f[3] = lab_f[1] = ticks_f[i] * vertices[1];
	  glBufferSubData(GL_ARRAY_BUFFER, offset2, sizeof(big_f), big_f);
	  offset2 += sizeof(big_f);

	  // first 12 elements (4 coordinates * 3 vectors) used for axis labels
	  // big_ticks * 4 more elements used for ticks labels on cube
	  for (int l=0; l<2; ++l)
	    tvertices[12 + big_ticks * 4 + big_ticks2 * 4 + l] = lab_f[l];
	  tvertices[12 + big_ticks * 4 + big_ticks2 * 4 + 2] = 0.0;
	  tvertices[12 + big_ticks * 4 + big_ticks2 * 4 + 3] = 1.0;
	  ++big_ticks2;
	}
      else
	{
	  // small tick
	  small_f[1] = small_f[3] = ticks_f[i] * vertices[1];
	  glBufferSubData(GL_ARRAY_BUFFER, offset2, sizeof(small_f), small_f);
	  offset2 += sizeof(small_f);
	}
    }
}

void gl_frame::set_ticks_sides()
{
  // choose on which sides of the cube to draw ticks
  bool update = false;

  // store old values
  GLint s_xy = s_tick_xy, s_xz = s_tick_xz, s_yz = s_tick_yz,
    s_yx = s_tick_yx, s_zx = s_tick_zx, s_zy = s_tick_zy;

  // map cube vertices to screen coordinates
  double d0 = -matrix[3] - matrix[7] - matrix[11] + matrix[15],
    x0 = (-matrix[0] - matrix[4] - matrix[8] + matrix[12]) / d0,
    y0 = (-matrix[1] - matrix[5] - matrix[9] + matrix[13]) / d0;
  double d1 = matrix[3] - matrix[7] - matrix[11] + matrix[15],
    x1 = (matrix[0] - matrix[4] - matrix[8] + matrix[12]) / d1,
    y1 = (matrix[1] - matrix[5] - matrix[9] + matrix[13]) / d1;
  double d2 = -matrix[3] + matrix[7] - matrix[11] + matrix[15],
    x2 = (-matrix[0] + matrix[4] - matrix[8] + matrix[12]) / d2,
    y2 = (-matrix[1] + matrix[5] - matrix[9] + matrix[13]) / d2;
  double d3 = matrix[3] + matrix[7] - matrix[11] + matrix[15],
    x3 = (matrix[0] + matrix[4] - matrix[8] + matrix[12]) / d3,
    y3 = (matrix[1] + matrix[5] - matrix[9] + matrix[13]) / d3;
  double d4 = -matrix[3] - matrix[7] + matrix[11] + matrix[15],
    x4 = (-matrix[0] - matrix[4] + matrix[8] + matrix[12]) / d4,
    y4 = (-matrix[1] - matrix[5] + matrix[9] + matrix[13]) / d4;
  double d5 = matrix[3] - matrix[7] + matrix[11] + matrix[15],
    x5 = (matrix[0] - matrix[4] + matrix[8] + matrix[12]) / d5,
    y5 = (matrix[1] - matrix[5] + matrix[9] + matrix[13]) / d5;
  double d6 = -matrix[3] + matrix[7] + matrix[11] + matrix[15],
    x6 = (-matrix[0] + matrix[4] + matrix[8] + matrix[12]) / d6,
    y6 = (-matrix[1] + matrix[5] + matrix[9] + matrix[13]) / d6;
  double d7 = matrix[3] + matrix[7] + matrix[11] + matrix[15],
    x7 = (matrix[0] + matrix[4] + matrix[8] + matrix[12]) / d7,
    y7 = (matrix[1] + matrix[5] + matrix[9] + matrix[13]) / d7;

  // among parallel sides, choose the farthest from the center
  // on the bottom-left half of the screen

  // axis x
  double c = -1e+100;
  double c0 = (x1*y0 - x0*y1) * (x1*y0 - x0*y1) /
    ((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0) + 1e-100);
  if (c0 > c && (x0 + x1) + 2.0 * (y0 + y1) < 0.5)
    {
      c = c0;
      s_tick_xy = s_tick_xz = -1;
    }
  double c1 = (x3*y2 - x2*y3) * (x3*y2 - x2*y3) /
    ((x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2) + 1e-100);
  if (c1 > c && (x2 + x3) + 2.0 * (y2 + y3) < 0.5)
    {
      c = c1;
      s_tick_xy = 1;
      s_tick_xz = -1;
    }
  double c2 = (x5*y4 - x4*y5) * (x5*y4 - x4*y5) /
    ((x5 - x4) * (x5 - x4) + (y5 - y4) * (y5 - y4) + 1e-100);
  if (c2 > c && (x4 + x5) + 2.0 * (y4 + y5) < 0.5)
    {
      c = c2;
      s_tick_xy = -1;
      s_tick_xz = 1;
    }
  double c3 = (x7*y6 - x6*y7) * (x7*y6 - x6*y7) /
    ((x7 - x6) * (x7 - x6) + (y7 - y6) * (y7 - y6) + 1e-100);
  if (c3 > c && (x6 + x7) + 2.0 * (y6 + y7) < 0.5)
    s_tick_xy = s_tick_xz = 1;

  // axis y
  double cc = -1e+100;
  double cc0 = (x2*y0 - x0*y2) * (x2*y0 - x0*y2) /
    ((x2 - x0) * (x2 - x0) + (y2 - y0) * (y2 - y0) + 1e-100);
  if (cc0 > cc && (x0 + x2) + 2.0 * (y0 + y2) < 0.5)
    {
      cc = cc0;
      s_tick_yz = s_tick_yx = -1;
    }
  double cc1 = (x3*y1 - x1*y3) * (x3*y1 - x1*y3) /
    ((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1) + 1e-100);
  if (cc1 > cc && (x1 + x3) + 2.0 * (y1 + y3) < 0.5)
    {
      cc = cc1;
      s_tick_yx = 1;
    }
  double cc2 = (x6*y4 - x4*y6) * (x6*y4 - x4*y6) /
    ((x6 - x4) * (x6 - x4) + (y6 - y4) * (y6 - y4) + 1e-100);
  if (cc2 > cc && (x4 + x6) + 2.0 * (y4 + y6) < 0.5)
    {
      cc = cc2;
      s_tick_yz = 1;
      s_tick_yx = -1;
    }
  double cc3 = (x7*y5 - x5*y7) * (x7*y5 - x5*y7) /
    ((x7 - x5) * (x7 - x5) + (y7 - y5) * (y7 - y5) + 1e-100);
  if (cc3 > cc && (x5 + x7) + 2.0 * (y5 + y7) < 0.5)
    s_tick_yz = s_tick_yx = 1;

  // axis z
  double ccc = -1e+100;
  double ccc0 = (x4*y0 - x0*y4) * (x4*y0 - x0*y4) /
    ((x4 - x0) * (x4 - x0) + (y4 - y0) * (y4 - y0) + 1e-100);
  if (ccc0 > ccc && (x0 + x4) + 2.0 * (y0 + y4) < 0.5)
    {
      ccc = ccc0;
      s_tick_zx = s_tick_zy = -1;
    }
  double ccc1 = (x5*y1 - x1*y5) * (x5*y1 - x1*y5) /
    ((x5 - x1) * (x5 - x1) + (y5 - y1) * (y5 - y1) + 1e-100);
  if (ccc1 > ccc && (x1 + x5) + 2.0 * (y1 + y5) < 0.5)
    {
      ccc = ccc1;
      s_tick_zx = 1;
      s_tick_zy = -1;
    }
  double ccc2 = (x6*y2 - x2*y6) * (x6*y2 - x2*y6) /
    ((x6 - x2) * (x6 - x2) + (y6 - y2) * (y6 - y2) + 1e-100);
  if (ccc2 > ccc && (x2 + x6) + 2.0 * (y2 + y6) < 0.5)
    {
      ccc = ccc2;
      s_tick_zy = 1;
      s_tick_zx = -1;
    }
  double ccc3 = (x7*y3 - x3*y7) * (x7*y3 - x3*y7) /
    ((x7 - x3) * (x7 - x3) + (y7 - y3) * (y7 - y3) + 1e-100);
  if (ccc3 > ccc && (x3 + x7) + 2.0 * (y3 + y7) < 0.5)
    s_tick_zx = s_tick_zy = 1;

  // need updating if any changed
  if (s_tick_xy != s_xy || s_tick_xz != s_xz || s_tick_yz != s_yz ||
      s_tick_yx != s_yx || s_tick_zx != s_zx || s_tick_zy != s_zy)
    set_ticks();
}

void gl_frame::set_ticks_scale
(std::vector<double> &ticks, int &start, int &stride, double min, double max)
{
  // set ticks to numbers as round as possible
  // units are always 1e+n, 2e+n or 5e+n
  // big ticks can be from 2 to 5
  // small ticks can be from 9 to 24

  // do not draw ticks too close to corners
  const double margin = 0.02;
  double mid = (min + max) / 2.0,
    min2 = mid - (mid - min) * (1.0 - margin),
    max2 = mid + (max - mid) * (1.0 - margin);

  // first compute big ticks
  int magnitude = std::floor(std::log10(max - min));
  double unit = std::pow(10.0, magnitude);
  double base = std::ceil(min2 / unit / 10.0) * unit * 10.0;
  stride = 5;
  if (base > max2)
    {
      base = std::ceil(min2 / unit / 2.0) * unit * 2.0;
      if (base > max2)
	base = std::ceil(min2 / unit) * unit;
    }
  double first = base - std::floor((base - min2) / unit) * unit,
    last = base + std::floor((max2 - base) / unit) * unit;
  int n = static_cast<int>(std::floor((last - first) / unit + 0.5)) + 1;
  // initially require at least four ticks
  // if they would become more than five then accept fewer (even two)
  if (n < 4)
    {
      unit *= 0.5;
      stride = 5;
      first = base - std::floor((base - min2) / unit) * unit;
      last = base + std::floor((max2 - base) / unit) * unit;
      n = static_cast<int>(std::floor((last - first) / unit + 0.5)) + 1;
      if (n < 4)
	{
	  unit *= 0.4; // original unit * 0.2
	  stride = 4;
	  first = base - std::floor((base - min2) / unit) * unit;
	  last = base + std::floor((max2 - base) / unit) * unit;
	  n = static_cast<int>(std::floor((last - first) / unit + 0.5)) + 1;
	  if (n > 5)
	    {
	      unit *= 2.5; // roll back to original unit * 0.5
	      stride = 5;
	      first = base - std::floor((base - min2) / unit) * unit;
	      last = base + std::floor((max2 - base) / unit) * unit;
	      n = static_cast<int>
		(std::floor((last - first) / unit + 0.5)) + 1;
	    }
	}
      else if (n > 5)
	{
	  unit *= 2.0; // roll back to original unit
	  stride = 5;
	  first = base - std::floor((base - min2) / unit) * unit;
	  last = base + std::floor((max2 - base) / unit) * unit;
	  n = static_cast<int>(std::floor((last - first) / unit + 0.5)) + 1;
	}
    }
  else if (n > 5)
    {
      unit *= 2.0;
      stride = 4;
      first = base - std::floor((base - min2) / unit) * unit;
      last = base + std::floor((max2 - base) / unit) * unit;
      n = static_cast<int>(std::floor((last - first) / unit + 0.5)) + 1;
    }

  // then compute small ticks
  double unit2 = unit / stride,
    first2 = base - std::floor((base - min2) / unit2) * unit2,
    last2 = base + std::floor((max2 - base) / unit2) * unit2;
  int n2 = static_cast<int>(std::floor((last2 - first2) / unit2 + 0.5)) + 1;
  // big ticks are at positions start + stride * i
  start = static_cast<int>(std::floor((first - first2) / unit2 + 0.5));

  // compute scaled coordinates
  double begin = -1.0 + (first2 - min) / (max - min) * 2.0,
    step = unit2 / (max - min) * 2.0;
  ticks.resize(n2);
  for (int i=0; i<n2; ++i)
    {
      ticks[i] = begin + step * i;
      if (i % stride == start)
	{
	  // big tick, create label
	  double coord = first2 + unit2 * i; // unscaled coordinate
	  // avoid numerical issue
	  const double epsilon = 1e-10;
	  if (std::fabs(coord / unit2) < epsilon)
	    coord = 0.0;
	  std::ostringstream o;
	  o << coord; // unscaled coordinate
	  tlabel.push_back(o.str());
	}
    }
}

void gl_frame::set_font_size()
{
  // use font height as unit size
  Glib::RefPtr<Pango::Layout> layout = widget.create_pango_layout("");
  Pango::Rectangle rectangle =
    layout->get_line(0)->get_pixel_logical_extents();
  font_size = rectangle.get_ascent();
}

void gl_frame::draw_axes()
{
  glUseProgram(h_program);
  glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix);

  glEnable(GL_PRIMITIVE_RESTART);
  glPrimitiveRestartIndex(restart);
  glLineWidth(1.0);

  glBindVertexArray(h_vertex_array_axes);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer_axes);

  glDrawElements(GL_LINE_STRIP, i_axes, GL_UNSIGNED_INT, NULL);

  glUseProgram(0);
}

void gl_frame::draw_axes_labels()
{
  glUseProgram(h_program_tex);

  // draw textures at their actual pixel size
  // to look right, their coordinates must correspond to whole pixels
  // so the origin of the axes is kept at the center of the window
  // but if width or height are odd, it is shifted by half pixel
  GLfloat scalex = 2.0 / width, scaley = 2.0 / height,
    oddx = (GLfloat) (width - (width / 2) * 2) / width, // half pixel or 0
    oddy = (GLfloat) (height - (height / 2) * 2) / height;
  GLfloat matrix_tex[] = {
    scalex, 0.0,    0.0, 0.0,
    0.0,    scaley, 0.0, 0.0,
    0.0,    0.0,    1.0, 0.0,
    oddx,   oddy,   0.0, 1.0,
  };
  glUniformMatrix4fv(u_matrix_tex, 1, GL_FALSE, matrix_tex);

  for (int i=0; i<3; ++i)
    draw_texture(i, &tvertices[i*4], scalex, scaley, -twidth[i] / 2);

  glUseProgram(0);
}

void gl_frame::draw_cube()
{
  glUseProgram(h_program);
  glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix);

  glEnable(GL_PRIMITIVE_RESTART);
  glPrimitiveRestartIndex(restart);
  glLineWidth(1.0);

  glBindVertexArray(h_vertex_array_cube);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer_cube);

  glDrawElements(GL_LINE_STRIP, i_cube + i_tick * n_ticks,
		 GL_UNSIGNED_INT, NULL);

  glUseProgram(0);
}

void gl_frame::draw_ticks_labels()
{
  glUseProgram(h_program_tex);

  // draw textures at their actual pixel size
  // to look right, their coordinates must correspond to whole pixels
  // so the origin of the axes is kept at the center of the window
  // but if width or height are odd, it is shifted by half pixel
  GLfloat scalex = 2.0 / width, scaley = 2.0 / height,
    oddx = (GLfloat) (width - (width / 2) * 2) / width, // half pixel or 0
    oddy = (GLfloat) (height - (height / 2) * 2) / height;
  GLfloat matrix_tex[] = {
    scalex, 0.0,    0.0, 0.0,
    0.0,    scaley, 0.0, 0.0,
    0.0,    0.0,    1.0, 0.0,
    oddx,   oddy,   0.0, 1.0,
  };
  glUniformMatrix4fv(u_matrix_tex, 1, GL_FALSE, matrix_tex);

  // draw ticks labels as sequences of characters
  const int margin = 1;
  for (int i=3; i<3+big_ticks; ++i) // first 3 reserved to axis labels
    {
      std::vector<unsigned> nchar(tlabel[i].size());
      int offset = 0;
      // compute full label width
      for (unsigned j=0; j<nchar.size(); ++j)
	{
	  nchar[j] = std::find(tchar.begin(), tchar.end(), tlabel[i][j]) -
	    tchar.begin();
	  offset += twidth[nchar[j]] - margin * 2;
	}
      // draw label centered
      offset = -offset / 2;
      for (unsigned j=0; j<tlabel[i].size(); ++j)
	{
	  draw_texture(nchar[j], &tvertices[i*4], scalex, scaley, offset);
	  offset += twidth[nchar[j]] - margin * 2;
	}
    }

  glUseProgram(0);
}

void gl_frame::draw_bar()
{
  glUseProgram(h_program);

  GLfloat scalex = 2.0 / width, scaley = 2.0 / height,
    oddx = (GLfloat) (width - (width / 2) * 2) / width, // half pixel or 0
    oddy = (GLfloat) (height - (height / 2) * 2) / height;
  GLfloat matrix_bar[] = {
    scalex, 0.0,    0.0, 0.0,
    0.0,    scaley, 0.0, 0.0,
    0.0,    0.0,    1.0, 0.0,
    oddx,   oddy,   0.0, 1.0,
  };
  glUniformMatrix4fv(u_matrix, 1, GL_FALSE, matrix_bar);

  glEnable(GL_PRIMITIVE_RESTART);
  glPrimitiveRestartIndex(restart);
  glLineWidth(1.0);

  glBindVertexArray(h_vertex_array_bar);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, h_index_buffer_bar);

  glDrawElements(GL_LINE_STRIP, i_bar + i_tick2 * n_ticks2,
		 GL_UNSIGNED_INT, NULL);

  glUseProgram(0);
}

void gl_frame::draw_bar_labels()
{
  glUseProgram(h_program_tex);

  // draw textures at their actual pixel size
  // to look right, their coordinates must correspond to whole pixels
  // so the origin of the axes is kept at the center of the window
  // but if width or height are odd, it is shifted by half pixel
  GLfloat scalex = 2.0 / width, scaley = 2.0 / height,
    oddx = (GLfloat) (width - (width / 2) * 2) / width, // half pixel or 0
    oddy = (GLfloat) (height - (height / 2) * 2) / height;
  GLfloat matrix_tex[] = {
    scalex, 0.0,    0.0, 0.0,
    0.0,    scaley, 0.0, 0.0,
    0.0,    0.0,    1.0, 0.0,
    oddx,   oddy,   0.0, 1.0,
  };
  glUniformMatrix4fv(u_matrix_tex, 1, GL_FALSE, matrix_tex);

  // draw ticks labels as sequences of characters
  const int margin = 1;
  for (int i=3+big_ticks; i<3+big_ticks+big_ticks2; ++i)
    {
      std::vector<unsigned> nchar(tlabel[i].size());
      int offset = 0;
      // compute full label width
      for (unsigned j=0; j<nchar.size(); ++j)
	{
	  nchar[j] = std::find(tchar.begin(), tchar.end(), tlabel[i][j]) -
	    tchar.begin();
	  offset += twidth[nchar[j]] - margin * 2;
	}
      // draw label right aligned
      offset = -offset;
      for (unsigned j=0; j<tlabel[i].size(); ++j)
	{
	  draw_texture(nchar[j], &tvertices[i*4],
		       scalex, scaley, offset, false);
	  offset += twidth[nchar[j]] - margin * 2;
	}
    }

  glUseProgram(0);
}

void gl_frame::draw_texture(int n, const GLfloat *in,
			    GLfloat scalex, GLfloat scaley, int offset,
			    bool cube)
{
  glActiveTexture(GL_TEXTURE0);
  glBindVertexArray(h_vertex_array_tex);
  glBindBuffer(GL_ARRAY_BUFFER, h_buffer_tex);

  GLfloat out[4];
  if (cube)
    {
      // multiply by current matrix
      for (int i=0; i<4; ++i)
	{
	  out[i] = 0.0;
	  for (int j=0; j<4; ++j)
	    out[i] += in[j] * matrix[i+j*4];
	}
      // normalize
      for (int i=0; i<4; ++i)
	out[i] /= out[3];
      // scale and round to integer
      out[0] = std::floor(out[0] / scalex + 0.5);
      out[1] = std::floor(out[1] / scaley + 0.5);
    }
  else
    {
      // just round to integer
      out[0] = std::floor(in[0] + 0.5);
      out[1] = std::floor(in[1] + 0.5);
      out[2] = out[3] = 0.0;
    }
  // texture coordinates in pixel units
  const int margin = 1;
  GLfloat left = offset - margin, right = left + twidth[n],
    bottom = -theight[n] / 2, top = bottom + theight[n];
  GLfloat rectangle[] =
    {
      left,  bottom, 0.0,
      right, bottom, 0.0,
      right, top,    0.0,
      left,  top,    0.0,
    };
  for (int i=0; i<4; ++i)
    for (int j=0; j<3; ++j)
      rectangle[i*3+j] += out[j];
  GLfloat origin_x = (GLfloat) tx[n] / tex_size,
    origin_y = (GLfloat) ty[n] / tex_size,
    scale_x = (GLfloat) twidth[n] / tex_size,
    scale_y = (GLfloat) theight[n] / tex_size;
  GLfloat trectangle[] =
    {
      origin_x,           origin_y,
      origin_x + scale_x, origin_y,
      origin_x + scale_x, origin_y + scale_y,
      origin_x,           origin_y + scale_y,
    };
  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(rectangle), rectangle);
  glBufferSubData(GL_ARRAY_BUFFER, 12 * sizeof(GLfloat), sizeof(trectangle),
		  trectangle);

  glUniform1i(u_sampler_tex, 0);
  glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}

gl_frame::gl_frame(Gtk::Widget &widget_)
  : widget(widget_), big_ticks(0), big_ticks2(0),
    d_axes(true), d_cube(true), d_bar(true)
{ }

void gl_frame::init()
{
  init_lines();
  init_textures();
  init_axes();
  init_cube();
  init_bar();
  set_font_size();
}

void gl_frame::set_frame(int width_, int height_)
{
  width = width_;
  height = height_;
  pixel_scale = (width < height) ? (2.0 / width) : (2.0 / height);

  set_font_size();
  set_axes();
  set_ticks();
}

void gl_frame::set_scale(double scale_)
{
  bool update = (scale != scale_);
  scale = scale_;
  if (update)
    set_ticks();
}

void gl_frame::set_vscale(double vscale_)
{
  bool update = (vscale != vscale_);
  vscale = vscale_;
  if (update)
    set_ticks();
}

void gl_frame::set_matrix(const GLfloat *matrix_)
{
  for (GLuint i=0; i<16; ++i)
    matrix[i] = matrix_[i];
  set_ticks_sides();
}

void gl_frame::draw()
{
  if (d_axes)
    {
      draw_axes();
      draw_axes_labels();
    }
  if (d_cube)
    {
      draw_cube();
      draw_ticks_labels();
    }
  if (d_bar)
    {
      draw_bar();
      draw_bar_labels();
    }
}
