// glibc feature test macro for clock_gettime()
#define _POSIX_C_SOURCE 199309L

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <complex.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

static const double twopi = 6.283185307179586;

#define GLSL(s) "#version 400 core\n" #s

const char *julia_vert_src = GLSL(
  layout(location = 0) in vec4 p;
  smooth out vec2 z0;
  void main() {
    z0 = p.zw;
    gl_Position = vec4(p.xy, 0.0, 1.0);
  }
);

const char *julia_frag_src = GLSL(
  uniform int maxiters;
  uniform int period;
  uniform float eps2;
  uniform float er2;
  uniform vec2 c;
  uniform vec2 w;
  smooth in vec2 z0;
  layout(location = 0) out float julia;
  vec2 csqr(vec2 z) {
    return vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
  }
  vec2 cmul(vec2 a, vec2 b) {
    return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
  }
  void main() {
    vec2 z = z0;
    vec2 dz = vec2(0.5 * (length(dFdx(z0)) + length(dFdy(z0))), 0.0);
    float fatou = -3.0;
    for (int n = 0; n < min(maxiters, 4096); ++n) {
      float z2 = dot(z, z);
      if (z2 > er2) {
        float de = 0.5 * sqrt(z2) * log(z2) / length(dz);
        if (de < 4) {
          fatou = -2.0 + tanh(de);
        } else {
          fatou = -1.0;
        }
        break;
      }
      if (period > 0) {
        vec2 zw = z - w;
        float zw2 = dot(zw, zw);
        if (zw2 < eps2) {
          fatou = float(n % max(1, period));
          break;
        }
      }
      dz = 2.0 * cmul(z, dz);
      z = csqr(z) + c;
    }
    julia = fatou;
  }
);

const char *post_vert_src = GLSL(
  layout(location = 0) in vec4 p;
  smooth out vec2 uv;
  void main() {
    uv = p.zw;
    gl_Position = vec4(p.xy, 0.0, 1.0);
  }
);

/*

  a   b
    x
  c   d

a' = case a of
  -2 -> -2
  <= -1 -> -1
  _ -> a
ceil(a)  

x
  a' <= -2                          -> 0 (iteration count exceeded)
  a' >= -1 && d' >= -1 && a' != d'  -> 0 (escaped to different components)
  c' >= -1 && b' >= -1 && c' != b'  -> 0 (escaped to different components)
  a' == b' == c' == d'   == -1      -> pow(product(all + 2), 0.25)  (geometric mean of de)
                         >  -1      -> 1 (escaped to same component)
  _                                 -> 1 (huh?)
*/

const char *post_frag_src = GLSL(
  uniform sampler2D t;
  smooth in vec2 uv;
  layout(location = 0) out float colour;
  void main() {
    ivec2 ij = ivec2(floor(uv));
    float a = texelFetch(t, ij, 0).x;
    float b = ij.x + 1 < textureSize(t, 0).x ? texelFetch(t, ij + ivec2(1, 0), 0).x : a;
    float c = ij.y + 1 < textureSize(t, 0).y ? texelFetch(t, ij + ivec2(0, 1), 0).x : a;
    float d = ij.x + 1 < textureSize(t, 0).x && ij.y + 1 < textureSize(t, 0).y ? texelFetch(t, ij + ivec2(1, 1), 0).x : a;
    float aa = ceil(a);
    float bb = ceil(b);
    float cc = ceil(c);
    float dd = ceil(d);
    float edge = 1.0;
    if (aa <= -2.0) { edge = 0.0; } else
    if (aa >= -1.0 && dd >= -1.0 && aa != dd) { edge = 0.0; } else
    if (bb >= -1.0 && cc >= -1.0 && bb != cc) { edge = 0.0; } else
    if (aa == bb && aa == cc && aa == dd && aa == -1.0) {
      edge = pow((a + 2.0) * (b + 2.0) * (c + 2.0) * (d + 2.0), 0.25);
    }
    colour = edge;
  }
);

const char *dim1_vert_src = GLSL(
  layout(location = 0) in vec4 p;
  smooth out vec2 uv;
  void main() {
    uv = p.zw;
    gl_Position = vec4(p.xy, 0.25, 1.0);
  }
);

const char *dim1_frag_src = GLSL(
  uniform sampler2D t;
  uniform float threshold;
  smooth in vec2 uv;
  layout(location = 0) out float colour;
  void main() {
    float value = texture(t, uv).x;
    if (value <= threshold) {
      discard;
    }
    colour = value;
  }
);

const char *dim2_vert_src = GLSL(
  layout(location = 0) in vec4 p;
  void main() {
    gl_Position = vec4(p.xy, 0.75, 1.0);
  }
);

const char *dim2_frag_src = GLSL(
  layout(location = 0) out float colour;
  void main() {
    colour = 0.0;
  }
);

const char *display_vert_src = GLSL(
  layout(location = 0) in vec4 p;
  smooth out vec2 uv;
  void main() {
    uv = p.zw;
    gl_Position = vec4(p.xy, 0.0, 1.0);
  }
);

const char *display_frag_src = GLSL(
  uniform sampler2D t;
  uniform sampler2D t2;
  smooth in vec2 uv;
  layout(location = 0) out vec4 colour;
  void main() {
    colour = vec4(texture(t, uv).x * texture(t2, uv).xyz, 1.0);
  }
);

void debug_program(GLuint program, const char *name) {
  if (program) {
    GLint linked = GL_FALSE;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (linked != GL_TRUE) {
      printf("%s: OpenGL shader program link failed\n", name);
    }
    GLint length = 0;
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
    char *buffer = (char *) malloc(length + 1);
    glGetProgramInfoLog(program, length, NULL, buffer);
    buffer[length] = 0;
    if (buffer[0]) {
      printf("%s: OpenGL shader program info log\n", name);
      printf("%s\n", buffer);
    }
    free(buffer);
  } else {
    printf("%s: OpenGL shader program creation failed\n", name);
  }
}

void debug_shader(GLuint shader, GLenum type, const char *name) {
  const char *tname = 0;
  switch (type) {
    case GL_VERTEX_SHADER:   tname = "vertex";   break;
    case GL_FRAGMENT_SHADER: tname = "fragment"; break;
    default:                 tname = "unknown";  break;
  }
  if (shader) {
    GLint compiled = GL_FALSE;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (compiled != GL_TRUE) {
      printf("%s: OpenGL %s shader compile failed\n", name, tname);
    }
    GLint length = 0;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
    char *buffer = (char *) malloc(length + 1);
    glGetShaderInfoLog(shader, length, NULL, buffer);
    buffer[length] = 0;
    if (buffer[0]) {
      printf("%s: OpenGL %s shader info log\n", name, tname);
      printf("%s\n", buffer);
    }
    free(buffer);
  } else {
    printf("%s: OpenGL %s shader creation failed\n", name, tname);
  }
}

void compile_shader(GLint program, GLenum type, const char *name, const GLchar *source) {
  GLuint shader = glCreateShader(type);
  glShaderSource(shader, 1, &source, NULL);
  glCompileShader(shader);
  debug_shader(shader, type, name);
  glAttachShader(program, shader);
  glDeleteShader(shader);
}

GLint compile_program(const char *name, const GLchar *vert, const GLchar *frag) {
  GLint program = glCreateProgram();
  if (vert) { compile_shader(program, GL_VERTEX_SHADER  , name, vert); }
  if (frag) { compile_shader(program, GL_FRAGMENT_SHADER, name, frag); }
  glLinkProgram(program);
  debug_program(program, name);
  return program;
}

complex double julia_attractor(complex double c, int maxiters, int *period) {
  double epsilon = nextafter(2, 4) - 2;
  complex double z = c;
  double mzp = 1.0/0.0;
  int p = 0;
  for (int n = 1; n < maxiters; ++n) {
    double mzn = cabs(z);
    if (mzn > 1024) {
      break;
    }
    if (mzn < mzp) {
      mzp = mzn;
      p = n;
      complex double z0 = z;
      for (int i = 0; i < 64; ++i) {
        complex double f = z0;
        complex double df = 1;
        for (int j = 0; j < p; ++j) {
          df = 2 * f * df;
          f = f * f + c;
        }
        complex double z1 = z0 - (f - z0) / (df - 1);
        if (cabs(z1 - z0) <= epsilon) {
          z0 = z1;
          break;
        }
        if (isinf(creal(z1)) || isinf(cimag(z1)) || isnan(creal(z1)) || isnan(cimag(z1))) {
          break;
        }
        z0 = z1;
      }
      complex double w = z0;
      complex double dw = 1;
      for (int i = 0; i < p; ++i) {
        dw = 2 * w * dw;
        w = w * w + c;
      }
      if (cabs(dw) <= 1) {
        *period = p;
        return z0;
      }
    }
    z = z * z + c;
  }
  *period = 0;
  return 0;
}

double simple_linear_regression(const double *ys, int n) {
  double xbar = 0;
  double ybar = 0;
  for (int x = 0; x < n; ++x) {
    xbar += x;
    ybar += ys[x];
  }
  xbar /= n;
  ybar /= n;
  double num = 0;
  double den = 0;
  for (int x = 0; x < n; ++x) {
    double dx = x - xbar;
    double dy = ys[x] - ybar;
    num += dx * dy;
    den += dx * dx;
  }
  return num / den;
}

void hsv2rgb(double h, double s, double v, int *red, int *grn, int *blu) {
  double i, f, p, q, t, r, g, b;
  int ii;
  if (s == 0.0) { r = g = b = v; } else {
    h = 6 * (h - floor(h));
    ii = i = floor(h);
    f = h - i;
    p = v * (1 - s);
    q = v * (1 - (s * f));
    t = v * (1 - (s * (1 - f)));
    switch(ii) {
      case 0: r = v; g = t; b = p; break;
      case 1: r = q; g = v; b = p; break;
      case 2: r = p; g = v; b = t; break;
      case 3: r = p; g = q; b = v; break;
      case 4: r = t; g = p; b = v; break;
      default:r = v; g = p; b = q; break;
    }
  }
  *red = fmin(fmax(255 * r + 0.5, 0), 255);
  *grn = fmin(fmax(255 * g + 0.5, 0), 255);
  *blu = fmin(fmax(255 * b + 0.5, 0), 255);
}

int main(int argc, char **argv) {
  int wsize = 512;
  int mlevel = 5;
  int level = 5;
  bool julia_save = false;
  if (argc > 1) {
    mlevel = atoi(argv[1]);
  }
  if (argc > 2) {
    level = atoi(argv[2]);
  }
  if (argc > 3) {
    julia_save = 0 == strcmp("julia", argv[3]);
  }
  printf("{ j-dim-%02d-%02d {\n", mlevel, level);
  fflush(stdout);

  int msize = 1 << mlevel;
  int tsize = 1 << level;
  int isize = tsize >> 1;

  float *julia_fatou = 0;
  float *julia_edge = 0;
  unsigned char *julia_ppm = 0;
  if (julia_save) {
    julia_fatou = malloc(tsize * tsize * sizeof(float));
    julia_edge = malloc(isize * isize * sizeof(float));
    julia_ppm = malloc(3 * isize * isize);
  }

  if (! glfwInit()) { return 1; }
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
  GLFWwindow *window = glfwCreateWindow(wsize, wsize, "Julia", 0, 0);
  if (! window) { glfwTerminate(); return 1; }
  glfwMakeContextCurrent(window);
  glewExperimental = GL_TRUE;
  if (GLEW_OK != glewInit()) {
    return 1;
  } 
  glGetError();

  GLint julia_prog = compile_program("julia", julia_vert_src, julia_frag_src);
  GLuint julia_maxiters = glGetUniformLocation(julia_prog, "maxiters");
  GLuint julia_period = glGetUniformLocation(julia_prog, "period");
  GLuint julia_eps2 = glGetUniformLocation(julia_prog, "eps2");
  GLuint julia_er2 = glGetUniformLocation(julia_prog, "er2");
  GLuint julia_c = glGetUniformLocation(julia_prog, "c");
  GLuint julia_w = glGetUniformLocation(julia_prog, "w");

  GLint post_prog = compile_program("post", post_vert_src, post_frag_src);
  GLuint post_t = glGetUniformLocation(post_prog, "t");

  GLint dim1_prog = compile_program("dim1", dim1_vert_src, dim1_frag_src);
  GLuint dim1_t = glGetUniformLocation(dim1_prog, "t");
  GLuint dim1_threshold = glGetUniformLocation(dim1_prog, "threshold");

  GLint dim2_prog = compile_program("dim2", dim2_vert_src, dim2_frag_src);

  GLint display_prog = compile_program("display", display_vert_src, display_frag_src);
  GLuint display_t = glGetUniformLocation(display_prog, "t");
  GLuint display_t2 = glGetUniformLocation(display_prog, "t2");

  GLuint tex[4];
  glGenTextures(4, tex);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, tex[0]);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, tsize, tsize, 0, GL_RED, GL_FLOAT, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, tex[1]);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, tsize, tsize, 0, GL_RED, GL_FLOAT, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  glGenerateMipmap(GL_TEXTURE_2D);
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, tex[2]);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, tsize, tsize, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
  glActiveTexture(GL_TEXTURE3);
  glBindTexture(GL_TEXTURE_2D, tex[3]);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, msize, msize, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

  GLenum bufs[1] = { GL_COLOR_ATTACHMENT0 };
  GLenum bufs2[2] = { GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT };
  GLuint fbo[3];
  glGenFramebuffers(3, fbo);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
  glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[0], 0);
  glDrawBuffers(1, bufs);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);
  glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[1], 0);
  glDrawBuffers(1, bufs);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[2]);
  glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[0], 0);
  glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, tex[2], 0);
  glDrawBuffers(2, bufs2);

  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  GLfloat r = 2.5;
  GLfloat vbo_data[3 * 4 * 4] =
    { -1, -1, -r, -r
    , -1,  1, -r,  r
    ,  1, -1,  r, -r
    ,  1,  1,  r,  r
    , -1, -1,  0, 0
    , -1,  1,  0, tsize
    ,  1, -1,  tsize, 0
    ,  1,  1,  tsize, tsize
    , -1, -1,  0, 0
    , -1,  1,  0, 1
    ,  1, -1,  1, 0
    ,  1,  1,  1, 1
    };
  glBufferData(GL_ARRAY_BUFFER, 3 * 4 * 4 * sizeof(GLfloat), vbo_data, GL_STATIC_DRAW);

  GLuint julia_vao;
  glGenVertexArrays(1, &julia_vao);
  glBindVertexArray(julia_vao);
  glVertexAttribPointer(0, 4, GL_FLOAT, 0, 0, ((char *) 0) + 0 * 4 * 4 * sizeof(GLfloat));
  glEnableVertexAttribArray(0);

  GLuint post_vao;
  glGenVertexArrays(1, &post_vao);
  glBindVertexArray(post_vao);
  glVertexAttribPointer(0, 4, GL_FLOAT, 0, 0, ((char *) 0) + 1 * 4 * 4 * sizeof(GLfloat));
  glEnableVertexAttribArray(0);

  GLuint display_vao;
  glGenVertexArrays(1, &display_vao);
  glBindVertexArray(display_vao);
  glVertexAttribPointer(0, 4, GL_FLOAT, 0, 0, ((char *) 0) + 2 * 4 * 4 * sizeof(GLfloat));
  glEnableVertexAttribArray(0);

  GLuint dim_vao;
  glGenVertexArrays(1, &dim_vao);
  glBindVertexArray(dim_vao);
  glVertexAttribPointer(0, 4, GL_FLOAT, 0, 0, ((char *) 0) + 2 * 4 * 4 * sizeof(GLfloat));
  glEnableVertexAttribArray(0);

  GLuint query[level + 1];
  glGenQueries(level + 1, query);

  glDisable(GL_DEPTH_TEST);

  unsigned char *ppm = malloc(3 * msize * msize);
  memset(ppm, -1, 3 * msize * msize);
  double *raw = calloc(1, msize * msize/2 * (level + 1) * sizeof(*raw));

  char filename[1024];
  snprintf(filename, 1000, "j-dim-%02d-%02d.log", mlevel, level);
  FILE *f = fopen(filename, "wb");

  struct timespec t0, t1;
  clock_gettime(CLOCK_MONOTONIC, &t0);

  glGetError();
  for (int j = 0; j < msize / 2; ++j) {
    for (int i = 0; i < msize; ++i) {

      double cx = -0.75 + 4.0 * ((i + 0.5) / msize - 0.5);
      double cy = +0.00 + 4.0 * ((j + 0.5) / msize - 0.5);
      complex double c = cx + I * cy;

      int period = 0;
      complex double z0 = julia_attractor(c, 4096, &period);
      complex double z = z0;
      complex double dz = 1;
      if (period) {
        for (int n = 0; n < period; ++n) {
          dz = 2 * z * dz;
          z = z * z + c;
        }
      }

      glViewport(0, 0, tsize, tsize);

      glUseProgram(julia_prog);
      glUniform1i(julia_maxiters, 4096);
      glUniform1i(julia_period, period);
      glUniform1f(julia_eps2, 5.0 / tsize);
      glUniform1f(julia_er2, 1024 * 1024);
      glUniform2f(julia_c, creal(c), cimag(c));
      glUniform2f(julia_w, creal(z0), cimag(z0));
      glBindVertexArray(julia_vao);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[0]);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

      if (julia_save) {
        glActiveTexture(GL_TEXTURE0);
        glGetTexImage(GL_TEXTURE_2D, 0, GL_RED, GL_FLOAT, julia_fatou);
      }

      glUseProgram(post_prog);
      glBindVertexArray(post_vao);
      glUniform1i(post_t, 0);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[1]);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
      glActiveTexture(GL_TEXTURE1);
      glGenerateMipmap(GL_TEXTURE_2D);

      if (julia_save) {
        glGetTexImage(GL_TEXTURE_2D, 1, GL_RED, GL_FLOAT, julia_edge);
        #pragma omp parallel for
        for (int jj = 0; jj < isize; ++jj) {
          for (int ii = 0; ii < isize; ++ii) {
            int fatou = ceilf(julia_fatou[(jj << 1) * tsize + (ii << 1)]);
            bool f = 0 <= fatou && fatou < period;
            double hue = f ? fatou / (double) period : 0;
            double sat = f / 3.0;
            double val = julia_edge[jj * isize + ii];
            int red, grn, blu;
            hsv2rgb(hue, sat, val, &red, &grn, &blu);
            int kk = jj * isize + ii;
            julia_ppm[3 * kk + 0] = red;
            julia_ppm[3 * kk + 1] = grn;
            julia_ppm[3 * kk + 2] = blu;
          }
        }
        char julia_filename[1000];
        snprintf(julia_filename, 1000, "j-dim-%02d-%02d-julia-%02d-%02d.ppm", mlevel, level, j, i);
        FILE *julia_file = fopen(julia_filename, "wb");
        fprintf(julia_file, "P6\n%d %d\n255\n", isize, isize);
        fwrite(julia_ppm, 3 * isize * isize, 1, julia_file);
        fclose(julia_file);
      }

      // http://stackoverflow.com/questions/99906/count-image-similarity-on-gpu-opengl-occlusionquery
      glEnable(GL_DEPTH_TEST);
      glDepthFunc(GL_LEQUAL);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[2]);
      glBindVertexArray(dim_vao);
      for (int l = level; l >= 0; --l) { // 11's threshold is less than 1.0; 12's equals 1.0
        glViewport(0, 0, 1 << l, 1 << l);
        glClear(GL_DEPTH_BUFFER_BIT);
        glUseProgram(dim1_prog);
        glUniform1i(dim1_t, 1);
        glUniform1f(dim1_threshold, 1.0 - 0.5 * pow(0.25, level - l));
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glUseProgram(dim2_prog);
        glBeginQuery(GL_SAMPLES_PASSED, query[l]);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        glEndQuery(GL_SAMPLES_PASSED);
      }
      glDisable(GL_DEPTH_TEST);
      double logcount[level + 1];
      for (int l = level; l >= 0; --l) {
        GLuint count = 0;
        glGetQueryObjectuiv(query[l], GL_QUERY_RESULT, &count);
        logcount[l] = log2(count);
      }
      double dim = fabs(simple_linear_regression(logcount + (level - 3), 3));

      fprintf(f, "j = %4d ; i = %4d ; c = %+f%+fi ; p = %3d ; a = %f ; d = %f\n", j, i, creal(c), cimag(c), period, cabs(dz), dim);

      double hue = dim < 1
        ? -1.0/12.0 * (1 - dim) +  dim      *  1.0/ 6.0
        :  1.0/ 6.0 * (2 - dim) + (dim - 1) * 11.0/12.0;
      double sat = dim / 2;
      double val = 0 <= dim && dim <= 2;
      int red, grn, blu;
      hsv2rgb(hue, sat, val, &red, &grn, &blu);
      int k = j * msize + i;
      memcpy(&raw[k * (level + 1)], logcount, (level + 1) * sizeof(double));
      ppm[3 * k + 0] = red;
      ppm[3 * k + 1] = grn;
      ppm[3 * k + 2] = blu;
      k = (msize - 1 - j) * msize + i;
      ppm[3 * k + 0] = red;
      ppm[3 * k + 1] = grn;
      ppm[3 * k + 2] = blu;

      clock_gettime(CLOCK_MONOTONIC, &t1);
      double dt = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1000000000.0;
      if (dt > 0.5) {

        glActiveTexture(GL_TEXTURE3);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, msize, msize, GL_RGB, GL_UNSIGNED_BYTE, ppm);
  
        glViewport(0, 0, wsize, wsize);
        glUseProgram(display_prog);
        glBindVertexArray(display_vao);
        glUniform1i(display_t, 1);
        glUniform1i(display_t2, 3);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  
        glfwSwapBuffers(window);
        glfwPollEvents();

        clock_gettime(CLOCK_MONOTONIC, &t0);
      }

      GLuint e = glGetError();
      if (e) {
        fprintf(stderr, "\nOpenGL error %d\n", e);
      }

    }
  }
  fclose(f);

  snprintf(filename, 1000, "j-dim-%02d-%02d.ppm", mlevel, level);
  f = fopen(filename, "wb");
  fprintf(f, "P6\n%d %d\n255\n", msize, msize);
  fwrite(ppm, 3 * msize * msize, 1, f);
  fclose(f);

  snprintf(filename, 1000, "j-dim-%02d-%02d.raw", mlevel, level);
  f = fopen(filename, "wb");
  fwrite(raw, msize * msize/2 * (level + 1) * sizeof(double), 1, f);
  fclose(f);

  glfwTerminate();
  printf("} j-dim-%02d-%02d }\n", mlevel, level);
  fflush(stdout);
  return 0;
}
