/*
 *  Copyright 1994-2022 Olivier Girondel
 *
 *  This file is part of lebiniou.
 *
 *  lebiniou 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  lebiniou 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 lebiniou. If not, see <http://www.gnu.org/licenses/>.
 */

#include "params3d.h"
#include "brandom.h"

/* 3D scale factor */
#define SCALE_FACTOR_MIN  1.03
#define SCALE_FACTOR_MULT 0.9 // OSD changes via mouse wheel

/* 3D rotations */
#define DEFAULT_ROT_AMOUNT 0.001
#define DEFAULT_ROT_FACTOR 50

#define MAX_3D    10.0
#define NSAMPLES  15 /* for the sphere */
#define NSAMPLES2 ((float)(NSAMPLES))
#define TSTEP     (2.0 * M_PI / NSAMPLES2)
#define PSTEP     (M_PI / (NSAMPLES2 - 1))


/* These are the Quadrants also ;) */

/* oliv3 FIXME reorder so that we can traverse easily ?
 * like inverting a value in draw sphere optimized
 */
Point3d_t Cube3d[8] = {
  { { -1.0, +1.0, +1.0 } }, /* 0 */
  { { +1.0, +1.0, +1.0 } }, /* 1 */
  { { +1.0, -1.0, +1.0 } }, /* 2 */
  { { -1.0, -1.0, +1.0 } }, /* 3 */
  { { -1.0, +1.0, -1.0 } }, /* 4 */
  { { +1.0, +1.0, -1.0 } }, /* 5 */
  { { +1.0, -1.0, -1.0 } }, /* 6 */
  { { -1.0, -1.0, -1.0 } }  /* 7 */
};

Point3d_t Sphere3d[NSAMPLES][NSAMPLES];

/* #define RHO (sqrt (3.0)) */
#define RHO 1.732051


static void
init_sphere_3d(void)
{
  uint8_t it = 0, ip = 0;

  for (float theta = 0; it < NSAMPLES; theta += TSTEP, it++) {
    ip = 0;
    for (float phi = 0; ip < NSAMPLES; phi += PSTEP, ip++) {
      Point3d_t *p = &Sphere3d[it][ip];

      p->pos.x = RHO * cosf(theta) * sinf(phi);
      p->pos.y = RHO * sinf(theta) * sinf(phi);
      p->pos.z = RHO * cosf(phi);
    }
  }
}


static void
Params3d_set_scale_factor(Params3d_t *p)
{
  p->scale_factor = p->scale_factor0 * pow(SCALE_FACTOR_MULT, p->scale_factor_coeff);
#ifdef DEBUG
  VERBOSE(printf("[i] 3D scale factor coeff: %d => %.2f\n", p->scale_factor_coeff, p->scale_factor));
#endif
}


void
Params3d_set_defaults(Params3d_t *p)
{
  zero_3d(p);
  p->scale_factor0 = HEIGHT / 3.0;
  p->scale_factor_coeff = 1;
  Params3d_set_scale_factor(p);
  p->boundary = BOUNDARY_NONE;
}


void
Params3d_init(Params3d_t *p)
{
  Params3d_set_defaults(p);

  p->rotation_factor = DEFAULT_ROT_FACTOR;
  p->rotate_amount = DEFAULT_ROT_AMOUNT;

  p->xs = p->xe = HWIDTH;
  p->ys = p->ye = HHEIGHT;
#ifdef WITH_GL
  p->gl_xs = p->gl_xe = HWIDTH;
  p->gl_ys = p->gl_ye = HHEIGHT;
#endif

  init_sphere_3d();

#ifdef WITH_GL
  p->gl_fov = 45; /* FIXME empirical value */
#endif
}


uint8_t
Params3d_is_rotating(const Params3d_t *p)
{
  return (p->rotate_factor[X_AXIS]
          || p->rotate_factor[Y_AXIS]
          || p->rotate_factor[Z_AXIS]);
}

void
Params3d_randomize(Params3d_t *p)
{
  do {
    p->rotate_factor[X_AXIS] = b_rand_int32_range(-p->rotation_factor, p->rotation_factor);
    p->rotate_factor[Y_AXIS] = b_rand_int32_range(-p->rotation_factor, p->rotation_factor);
    p->rotate_factor[Z_AXIS] = b_rand_int32_range(-p->rotation_factor, p->rotation_factor);
  } while (!Params3d_is_rotating(p));
}


inline void
draw_sphere_3d(const Params3d_t *params3d, Buffer8_t *b, const Pixel_t color)
{
  for (uint8_t it = 0; it < NSAMPLES; it++)
    for (uint8_t ip = 0; ip < NSAMPLES; ip++) {
      set_pixel_3d(params3d, b, &Sphere3d[it][ip], color);
    }
}


inline void
draw_sphere_wireframe_3d(const Params3d_t *params3d, Buffer8_t *b, const Pixel_t color)
{
  for (uint8_t it = 0; it < NSAMPLES - 1; it++) {
    for (uint8_t ip = 0; ip < NSAMPLES; ip++) {
      draw_line_3d(params3d, b, &Sphere3d[it][ip], &Sphere3d[it+1][ip], color);
    }
  }
  for (uint8_t ip = 0; ip < NSAMPLES; ip++) {
    draw_line_3d(params3d, b, &Sphere3d[0][ip], &Sphere3d[NSAMPLES-1][ip], color);
  }
  for (uint8_t it = 0; it < NSAMPLES; it++) {
    for (uint8_t ip = 0; ip < NSAMPLES-1; ip++) {
      draw_line_3d(params3d, b, &Sphere3d[it][ip], &Sphere3d[it][ip+1], color);
    }
  }
}


inline void
draw_cube_3d(const Params3d_t *p, Buffer8_t *b, const Pixel_t color)
{
  draw_line_3d(p, b, &Cube3d[7], &Cube3d[6], color);
  draw_line_3d(p, b, &Cube3d[7], &Cube3d[4], color);
  draw_line_3d(p, b, &Cube3d[7], &Cube3d[3], color);

  draw_line_3d(p, b, &Cube3d[5], &Cube3d[6], color);
  draw_line_3d(p, b, &Cube3d[5], &Cube3d[4], color);
  draw_line_3d(p, b, &Cube3d[5], &Cube3d[1], color);

  draw_line_3d(p, b, &Cube3d[0], &Cube3d[4], color);
  draw_line_3d(p, b, &Cube3d[0], &Cube3d[3], color);
  draw_line_3d(p, b, &Cube3d[0], &Cube3d[1], color);

  draw_line_3d(p, b, &Cube3d[2], &Cube3d[6], color);
  draw_line_3d(p, b, &Cube3d[2], &Cube3d[3], color);
  draw_line_3d(p, b, &Cube3d[2], &Cube3d[1], color);
}


void
draw_line_3d(const Params3d_t *p, Buffer8_t *b, const Point3d_t *p1, const Point3d_t *p2, const Pixel_t color)
{
  Point2d_t pa, a1;
  Point2d_t pb, b1;

  pa = projection_perspective(p, p1);
  pb = projection_perspective(p, p2);
  a1 = pixel_ecran(p, &pa);
  b1 = pixel_ecran(p, &pb);

  draw_line(b, a1.x, a1.y, b1.x, b1.y, color);
}


inline void
Params3d_rotate(Params3d_t *p)
{
  float dx = (p->xe-p->xs)/(float)(WIDTH/8);
  float dy = (p->ye-p->ys)/(float)(HEIGHT/8);

  p->rotations[Y_AXIS] += dx;
  p->rotations[X_AXIS] += dy;

  if (p->rotations[X_AXIS] > 2 * M_PI) {
    p->rotations[X_AXIS] -= 2 * M_PI;
  } else if (p->rotations[X_AXIS] < -2 * M_PI) {
    p->rotations[X_AXIS] += 2 * M_PI;
  }

  if (p->rotations[Z_AXIS] > 2 * M_PI) {
    p->rotations[Z_AXIS] -= 2 * M_PI;
  } else if (p->rotations[Z_AXIS] < -2 * M_PI) {
    p->rotations[Z_AXIS] += 2 * M_PI;
  }

  p->xs = p->xe;
  p->ys = p->ye;

  update(p);
}


#ifdef WITH_GL
void
Params3d_rotate_GL(Params3d_t *p)
{
  float dx = (p->gl_xe-p->gl_xs)/(float)(WIDTH/8);
  float dy = (p->gl_ye-p->gl_ys)/(float)(HEIGHT/8);

  // printf ("dx= %f, dy= %f\n", dx, dy);

  p->gl_rotations[Y_AXIS] += dx*30;
  p->gl_rotations[X_AXIS] += dy*30;

  if (p->gl_rotations[Y_AXIS] > 360) {
    p->gl_rotations[Y_AXIS] -= 360;
  } else if (p->gl_rotations[Y_AXIS] < 0) {
    p->gl_rotations[Y_AXIS] += 360;
  }

  if (p->rotations[X_AXIS] > 360) {
    p->rotations[X_AXIS] -= 360;
  } else if (p->rotations[X_AXIS] < 0) {
    p->rotations[X_AXIS] += 360;
  }

  p->gl_xs = p->gl_xe;
  p->gl_ys = p->gl_ye;
}
#endif


inline void
Params3d_change_rotations(Params3d_t *p)
{
  for (uint8_t i = X_AXIS; i <= Z_AXIS; i++) {
    p->rotations[i] += (p->rotate_factor[i] * p->rotate_amount);
  }
  update(p);
}


void
update_x(Params3d_t *p)
{
  p->Cos[X_AXIS] = cosf(p->rotations[X_AXIS]);
  p->Sin[X_AXIS] = sinf(p->rotations[X_AXIS]);
}


void
update_y(Params3d_t *p)
{
  p->Cos[Y_AXIS] = cosf(p->rotations[Y_AXIS]);
  p->Sin[Y_AXIS] = sinf(p->rotations[Y_AXIS]);
}


void
update_z(Params3d_t *p)
{
  p->Cos[Z_AXIS] = cosf(p->rotations[Z_AXIS]);
  p->Sin[Z_AXIS] = sinf(p->rotations[Z_AXIS]);
}


Boundaries_t
Params3d_str2boundary(const char *str)
{
  if (is_equal(str, "none")) {
    return BOUNDARY_NONE;
  } else if (is_equal(str, "cube")) {
    return BOUNDARY_CUBE;
  } else if (is_equal(str, "sphere_dots")) {
    return BOUNDARY_SPHERE_DOTS;
  } else if (is_equal(str, "sphere_wireframe")) {
    return BOUNDARY_SPHERE_WIREFRAME;
  } else {
    return BOUNDARY_NONE;
  }
}


const char *
Params3d_boundary2str(const Boundaries_t b)
{
  assert((b >= BOUNDARY_NONE) && (b < NB_BOUNDARIES));
  switch (b) {
    case BOUNDARY_NONE:
      return "none";

    case BOUNDARY_CUBE:
      return "cube";

    case BOUNDARY_SPHERE_DOTS:
      return "sphere_dots";

    case BOUNDARY_SPHERE_WIREFRAME:
      return "sphere_wireframe";

    default:
      return NULL;
  }
}


void
Params3d_from_json(Params3d_t *p, const json_t *json)
{
  if (NULL != json) {
    // we suppose that json comes from Params3d_to_json
    json_t *array;

    zero_3d(p);
    p->rotate_amount = json_real_value(json_object_get(json, "rotateAmount"));
    array = json_object_get(json, "rotateFactor");
    for (uint8_t i = X_AXIS; i <= Z_AXIS; i++) {
      p->rotate_factor[i] = json_integer_value(json_array_get(array, i));
    }
    array = json_object_get(json, "rotations");
    for (uint8_t i = X_AXIS; i <= Z_AXIS; i++) {
      p->rotations[i] = json_real_value(json_array_get(array, i));
    }
    p->scale_factor_coeff = json_integer_value(json_object_get(json, "scaleFactorCoeff"));
    p->boundary = Params3d_str2boundary(json_string_value(json_object_get(json, "boundary")));
    Params3d_set_scale_factor(p);
  } else {
    Params3d_set_defaults(p);
  }
}


json_t *
Params3d_to_json(const Params3d_t *p)
{
  return json_pack("{sf s[iii] s[fff] si ss}",
                   "rotateAmount", p->rotate_amount,
                   "rotateFactor",
                   p->rotate_factor[X_AXIS],
                   p->rotate_factor[Y_AXIS],
                   p->rotate_factor[Z_AXIS],
                   "rotations",
                   p->rotations[X_AXIS],
                   p->rotations[Y_AXIS],
                   p->rotations[Z_AXIS],
                   "scaleFactorCoeff", p->scale_factor_coeff,
                   "boundary", Params3d_boundary2str(p->boundary));
}


void
Params3d_zoom_in(Params3d_t *p)
{
  p->scale_factor_coeff--;
  Params3d_set_scale_factor(p);
}


void
Params3d_zoom_out(Params3d_t *p)
{
  p->scale_factor_coeff++;
  Params3d_set_scale_factor(p);
}


void
Params3d_set_rotate_factor(Params3d_t *p, const enum Axes axis, const int8_t value)
{
  p->rotate_factor[axis] = value;
}
