let camera;
let red;

let charges = [];
let faces = [];

let sphere_radius = 200;

const SURFACE_NONE = 0;
const SURFACE_CIRCLES = 1;
const SURFACE_EARTH = 2;

let surface = SURFACE_CIRCLES;
let physics = false;
let skeleton = false;
let polyhedron = false;


function preload() {
  earth_image = loadImage("atlas1.jpg");
}

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  camera = createCamera();
  red = color(0xbf, 0x00, 0x00);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

function draw() {
  orbitControl();
  background(50);

  make_lights();
  if (physics) move_charges(charges);

  draw_charges(sphere_radius);
  if (skeleton) draw_skeleton(sphere_radius);
  if (polyhedron) {
    if (physics || faces.length === 0) find_faces();
    draw_faces(sphere_radius);
  }
  draw_sphere(sphere_radius, 25);
}

function face_dist_sq([v1, v2, v3]) {
  const center = p5.Vector.add(v1, v2).add(v3).mult(1 / 3);
  return createVector(camera.eyeX, camera.eyeY, camera.eyeZ).sub(center).magSq();
};

function find_faces() {
  faces = [];
  for (let i = 2; i < charges.length; i += 1) {
    for (let j = 1; j < i; j += 1) {
      for (let k = 0; k < j; k += 1) {
        // Check if p1 p2 p3 form a face of the convex polyhedron
        // enclosing all vertices ...
        const p1 = charges[i].position;
        const p2 = charges[j].position;
        const p3 = charges[k].position;
        const normal = p5.Vector.sub(p2, p1).cross(p5.Vector.sub(p3, p1));
        // ... by checking if the other vertices are on the same
        // side of the plane generated by p1 p2 p3
        let plane_separates_vertices = false;
        let euler_formula = false;
        for (let r = 1; r < charges.length; r += 1) {
          for (let s = 0; s < r; s += 1) {
            if (
              r === i || r === j || r === k ||
              s === i || s === j || s === k
            ) continue;
            const q1 = charges[r].position;
            const q2 = charges[s].position;
            // Let l(t) := q1 + (q2 - q1) * t.
            // L := { l(t) : 0 <= t <= 1 } is the line segment
            // between q1 and q2.  L intersects the plane
            // generated by p1 p2 p3 iff l(t) intersects with
            // the plane for some 0 <= t <= 1.
            //
            // If L intersects the plane, q1 and q2 are on
            // opposite sides of the plane generated by p1 p2 p3,
            // so p1 p2 p3 can't be a face.  If we want the
            // polyhedron to be convex.  Which we do.
            //
            // A point k is on the plane generated by p1 p2 p3 iff
            // dot(k - p1, normal) = 0.  Let n := normal.
            //
            // dot(l(t) - p1, n) = 0
            // iff dot(q1 + (q2 - q1) * t - p1, n) = 0
            // iff dot(q1 - p1, n) + dot(q2 - q2, n) * t = 0
            // iff t = dot(p1 - q1, n) / dot(q2 - q2, n)
            const t = (
              p5.Vector.dot(p5.Vector.sub(p1, q1), normal) /
              p5.Vector.dot(p5.Vector.sub(q2, q1), normal)
            );
            plane_separates_vertices ||= t >= 0 && t <= 1;
            if (plane_separates_vertices) break;
            euler_formula ||= charges.length * 2 - faces.length == 4;
            if (euler_formula) break;
          }
          if (plane_separates_vertices || euler_formula) break;
        }
        if (euler_formula) return;
        if (!plane_separates_vertices) {
          faces.push([p1, p2, p3]);
        }
      }
    }
  }
}

function draw_faces(radius) {
  // fix OpenGL stacking alpha behaviour
  faces.sort((a, b) => face_dist_sq(b) - face_dist_sq(a));
  push();
  strokeWeight(2);
  stroke(0x00);
  for ([p1, p2, p3] of faces) {
    fill(0xbf, 0x7f);
    beginShape(TRIANGLES);
    vertex(p1.x * radius, p1.y * radius, p1.z * radius);
    vertex(p2.x * radius, p2.y * radius, p2.z * radius);
    vertex(p3.x * radius, p3.y * radius, p3.z * radius);
    endShape();
  }
  pop();
}

function draw_skeleton(radius) {
  push();
  noStroke();
  fill(0xff);
  sphere(4);
  stroke(0xbf);
  for (let charge of charges) {
    line(
      0,
      0,
      0,
      charge.position.x * radius,
      charge.position.y * radius,
      charge.position.z * radius,
    );
  }
  pop();
}

function make_charges(n) {
  charges = [];
  for (let i = 0; i < n; i += 1) {
    let position;
    if (i === 0) {
      position = createVector(0, -1, 0);
    } else {
      const lat = random(-TAU / 4, TAU / 4);
      const lon = random(0, TAU);
      position = createVector(
        cos(lat) * cos(lon),
        sin(lat),
        cos(lat) * sin(lon),
      );
    }
    charges.push({
      position: position,
      velocity: createVector(),
      acceleration: createVector(),
      color: red,
    });
  }
}

function draw_charges(radius) {
  push();
  noStroke();
  for (let charge of charges.values()) {
    ambientMaterial(charge.color);
    let position = charge.position.copy();
    position.mult(radius);
    push();
    translate(position.x, position.y, position.z);
    sphere(15);
    pop();
  }
  pop();
}

function draw_sphere(radius, n_axis_circles) {
  stroke(0x3f);
  noFill();

  if (surface === SURFACE_NONE) n_axis_circles = 0;
  else if (surface === SURFACE_EARTH) n_axis_circles = 2;

  push();
  rotateX(TAU / 4);
  draw_circles(
    radius,
    n_axis_circles,
    color(0x00, 0x9f, 0xff),
    color(0xff, 0x9f, 0x00),
  );
  pop();
  push();
  rotateY(TAU / 4);
  draw_circles(
    radius,
    n_axis_circles,
    color(0xff, 0x00, 0xff),
    color(0x00, 0xff, 0x00),
  );
  pop();

  if (surface === SURFACE_EARTH) {
    noStroke();
    noFill();
    tint(0xff, 0x9f);
    texture(earth_image);
    push();
    rotateY(TAU / 4);
    sphere(radius);
    pop();
  }
}

function draw_circles(radius, n_circles, pole_1_color, pole_2_color) {
  push();
  stroke(pole_1_color);
  translate(0, 0, -radius);
  point(0, 0);
  pop();
  for (let i = 1; i < n_circles; i += 1) {
    const angle = map(i, 0, n_circles - 1, -TAU / 4, TAU / 4);
    const circle_radius = radius * cos(angle);
    push();
    translate(0, 0, radius * sin(angle));
    circle(0, 0, circle_radius * 2);
    pop();
  }
  push();
  stroke(pole_2_color);
  translate(0, 0, radius);
  point(0, 0);
  pop();
}

function make_lights() {
  let light = createVector(0, 1, -1);
  light.normalize();
  directionalLight(0x1f, 0x1f, 0x1f, light);
  ambientLight(0xbf);
}

function keyPressed() {
  if (key == ' ') {
    physics = !physics;
  } else if (key == 'd') {
    surface = (surface + 1) % 3;
  } else if (key == 'f') {
    skeleton = !skeleton;
  } else if (key == 'g') {
    polyhedron = !polyhedron;
  } else if (key >= '0' && key <= '9') {
    make_charges(Math.pow(int(key), 2));
    faces = [];
  }
}

// TODO draw faces
// algorithm: choose 3 vertices until 2-partition of other vertices has one empty set
// done when V - E + F = 2. V is known. count E and F while creating faces
// when the graph is finished: E = F * 3 / 2
// V - E + F = V - F * 3 / 2 + F = V - F / 2
// V * 2 - F = 4