let p = [],
  v = [];
let lines = [];
let theta = (3 / 2) * Math.PI,
  t = 0;
let paused = false;
let showLabels = false;
let showInterPoints = true;

let hoverIndex = -1;
let selectedIndex = -1;
let prevMouse = { x: 0, y: 0 };
let draggingAll = false;
const HIT_R = 10;
const MAX_TRAIL = 1000;

function setup() {
  createCanvas(1000, 800);
  initCubicCurve(true);
}

function draw() {
  background(32);

  stroke(120, 120, 120, 180);
  strokeWeight(2);
  noFill();
  beginShape();
  vertex(p[0].x, p[0].y);
  vertex(p[1].x, p[1].y);
  vertex(p[2].x, p[2].y);
  vertex(p[3].x, p[3].y);
  endShape();

  if (!paused) {
    if (theta > TWO_PI) theta = 0;
    theta += 0.01;
    t = (sin(theta) + 1) / 2; // 0..1
  }

  lerpCubicCurve();

  for (let i = 0; i < lines.length; i++) {
    lines[i].drawLine();
  }

  drawCurve();
  drawPointsAndLabels();
  drawHUD();
}

function drawCurve() {
  if (v.length > MAX_TRAIL) v.splice(0, v.length - MAX_TRAIL);

  stroke(0, 255, 0, 200);
  strokeWeight(5);
  noFill();
  beginShape();
  for (let i = 0; i < v.length; i++) {
    vertex(v[i].x, v[i].y);
  }
  endShape();
}

function drawPointsAndLabels() {
  push();
  noStroke();
  fill(255, 70, 70, 230);
  circle(p[9].x, p[9].y, 14);
  pop();

  for (let i = 0; i < 4; i++) {
    const hovered = i === hoverIndex;
    const selected = i === selectedIndex;
    p[i].drawPoint(hovered, selected);
    if (showLabels) {
      noStroke();
      fill(255);
      textSize(14);
      textAlign(LEFT, BOTTOM);
      text(`P${i}`, p[i].x + 10, p[i].y - 10);
    }
  }

  if (showLabels) {
    noStroke();
    fill(220);
    textSize(12);
    textAlign(LEFT, BOTTOM);
    text(`P4`, p[4].x + 8, p[4].y - 8);
  }

  if (showInterPoints) {
    const styleMid = {
      stroke: [80, 180, 255, 100],
      fill: [80, 180, 255, 50],
    };
    for (let i = 4; i <= 8; i++) {
      p[i].drawPoint(false, false, styleMid);
      if (showLabels) {
        noStroke();
        fill(200);
        textSize(12);
        textAlign(LEFT, BOTTOM);
        text(`P${i}`, p[i].x + 8, p[i].y - 8);
      }
    }
  }
}

function drawHUD() {
  noStroke();
  fill(255);
  textSize(14);
  textAlign(LEFT, TOP);
  const hud = [
    `t = ${t.toFixed(3)}`,
    `Space = ${paused ? 'Resume' : 'Pause'}`,
    `H = Toggle points 5..8  |  L = Toggle labels  |  R = Reset`,
    `Drag a handle (P0..P3). Hold Shift to move the entire curve.`,
  ];
  for (let i = 0; i < hud.length; i++) {
    text(hud[i], 12, 12 + i * 18);
  }
}

function initCubicCurve(resetPositions = true) {
  p.length = 0;
  v.length = 0;
  lines.length = 0;
  theta = (3 / 2) * Math.PI;
  t = 0;

  if (resetPositions) {
    p[0] = new cPoint(width * 0.15, height * 0.75, 'end');
    p[1] = new cPoint(width * 0.35, height * 0.2, 'control');
    p[2] = new cPoint(width * 0.65, height * 0.2, 'control');
    p[3] = new cPoint(width * 0.85, height * 0.75, 'end');
  } else {
    for (let i = 0; i < 4; i++) {
      if (!p[i])
        p[i] = new cPoint(
          width * (0.15 + i * 0.2),
          height / 2,
          i === 0 || i === 3 ? 'end' : 'control'
        );
    }
  }

  // Derived points
  p[4] = new cPoint(0, 0, 'derived');
  p[5] = new cPoint(0, 0, 'derived');
  p[6] = new cPoint(0, 0, 'derived');
  p[7] = new cPoint(0, 0, 'derived');
  p[8] = new cPoint(0, 0, 'derived');
  p[9] = new cPoint(0, 0, 'derived');

  lines[0] = new cLine(p[4], p[5], color(160, 160, 160, 200), 2);
  lines[1] = new cLine(p[5], p[6], color(160, 160, 160, 200), 2);

  lines[2] = new cLine(p[7], p[8], color(200, 200, 200, 220), 3);
}

function lerpCubicCurve() {
  // Level 1
  p[4].changeX = (1 - t) * p[0].x + t * p[1].x;
  p[4].changeY = (1 - t) * p[0].y + t * p[1].y;

  p[5].changeX = (1 - t) * p[1].x + t * p[2].x;
  p[5].changeY = (1 - t) * p[1].y + t * p[2].y;

  p[6].changeX = (1 - t) * p[2].x + t * p[3].x;
  p[6].changeY = (1 - t) * p[2].y + t * p[3].y;

  // Level 2
  p[7].changeX = (1 - t) * p[4].x + t * p[5].x;
  p[7].changeY = (1 - t) * p[4].y + t * p[5].y;

  p[8].changeX = (1 - t) * p[5].x + t * p[6].x;
  p[8].changeY = (1 - t) * p[5].y + t * p[6].y;

  // Final on-curve
  p[9].changeX = (1 - t) * p[7].x + t * p[8].x;
  p[9].changeY = (1 - t) * p[7].y + t * p[8].y;

  v.push(new cPoint(p[9].x, p[9].y, 'derived'));
}

function mouseMoved() {
  hoverIndex = -1;
  for (let i = 0; i < 4; i++) {
    if (p[i].isHit(mouseX, mouseY)) {
      hoverIndex = i;
      break;
    }
  }
}

function mousePressed() {
  prevMouse.x = mouseX;
  prevMouse.y = mouseY;

  selectedIndex = -1;
  for (let i = 0; i < 4; i++) {
    if (p[i].isHit(mouseX, mouseY)) {
      selectedIndex = i;
      break;
    }
  }

  draggingAll = keyIsDown(SHIFT) && selectedIndex === -1;
}

function mouseDragged() {
  const dx = mouseX - prevMouse.x;
  const dy = mouseY - prevMouse.y;

  if (selectedIndex >= 0) {
    p[selectedIndex].changeX = mouseX;
    p[selectedIndex].changeY = mouseY;
    v.length = 0;
  } else if (draggingAll) {
    for (let i = 0; i < 4; i++) {
      p[i].changeX = p[i].x + dx;
      p[i].changeY = p[i].y + dy;
    }
    v.length = 0;
  }

  prevMouse.x = mouseX;
  prevMouse.y = mouseY;
  return false;
}

function mouseReleased() {
  selectedIndex = -1;
  draggingAll = false;
}

function keyPressed() {
  if (key === ' ') {
    paused = !paused;
  } else if (key === 'R' || key === 'r') {
    initCubicCurve(true);
  } else if (key === 'H' || key === 'h') {
    showInterPoints = !showInterPoints;
  } else if (key === 'L' || key === 'l') {
    showLabels = !showLabels;
  }
}
