// Minimal server-authoritative pong physics.
// Units are normalized to [0..1] for width/height.

export function createInitialState(seed = 0) {
  const ballSpeed = 0.55;
  const dir = (seed % 2 === 0) ? 1 : -1;
  return {
    seed,
    startedAtMs: Date.now(),
    t: 0,
    scoreL: 0,
    scoreR: 0,
    rallyHits: 0,
    ball: { x: 0.5, y: 0.5, vx: dir * ballSpeed, vy: 0.18 },
    paddleL: { y: 0.5, vy: 0 },
    paddleR: { y: 0.5, vy: 0 },
    ended: false,
    winner: null,
  };
}

export function step(state, dt, inputL, inputR) {
  const paddleMaxSpeed = 0.95;
  const aimResponsiveness = 18;
  const paddleHalf = 0.12;
  const ballRadius = 0.015;
  const minBounceAngleDeg = 12;
  const maxBounceAngleDeg = 50;
  const hitsToMaxAngle = 3;
  const bounceSpeedGain = 1.04;
  const minBounceSpeed = 0.55;
  const maxBounceSpeed = 1.2;
  const minBallY = ballRadius;
  const maxBallY = 1 - ballRadius;
  const prevBallX = state.ball.x;
  const bounceAngleMax = getProgressiveBounceAngle(state.rallyHits, minBounceAngleDeg, maxBounceAngleDeg, hitsToMaxAngle);

  state.paddleL.vy = resolvePaddleVelocity(state.paddleL.y, inputL, paddleHalf, paddleMaxSpeed, aimResponsiveness);
  state.paddleR.vy = resolvePaddleVelocity(state.paddleR.y, inputR, paddleHalf, paddleMaxSpeed, aimResponsiveness);

  state.paddleL.y = clamp(state.paddleL.y + state.paddleL.vy * dt, paddleHalf, 1 - paddleHalf);
  state.paddleR.y = clamp(state.paddleR.y + state.paddleR.vy * dt, paddleHalf, 1 - paddleHalf);

  state.ball.x += state.ball.vx * dt;
  state.ball.y += state.ball.vy * dt;

  // top/bottom bounce
  ({ position: state.ball.y, velocity: state.ball.vy } = reflectInsideBounds(state.ball.y, state.ball.vy, minBallY, maxBallY));

  // paddle collisions
  const paddleXLeft = 0.06;
  const paddleXRight = 0.94;

  // left paddle
  if (state.ball.vx < 0 && prevBallX - ballRadius >= paddleXLeft && state.ball.x - ballRadius <= paddleXLeft) {
    if (Math.abs(state.ball.y - state.paddleL.y) <= paddleHalf) {
      state.ball.x = paddleXLeft + ballRadius + 0.001;
      applyPaddleBounce(state.ball, state.paddleL.y, paddleHalf, 1, bounceAngleMax, bounceSpeedGain, minBounceSpeed, maxBounceSpeed);
      state.rallyHits += 1;
    }
  }

  // right paddle
  if (state.ball.vx > 0 && prevBallX + ballRadius <= paddleXRight && state.ball.x + ballRadius >= paddleXRight) {
    if (Math.abs(state.ball.y - state.paddleR.y) <= paddleHalf) {
      state.ball.x = paddleXRight - ballRadius - 0.001;
      applyPaddleBounce(state.ball, state.paddleR.y, paddleHalf, -1, bounceAngleMax, bounceSpeedGain, minBounceSpeed, maxBounceSpeed);
      state.rallyHits += 1;
    }
  }

  // scoring
  if (state.ball.x < -0.05) {
    state.scoreR += 1;
    resetBall(state, +1);
  } else if (state.ball.x > 1.05) {
    state.scoreL += 1;
    resetBall(state, -1);
  }

  state.t += dt;
}

export function isSetOver(state, pointsToWin = 11, minLead = 2) {
  const topScore = Math.max(state.scoreL, state.scoreR);
  const lead = Math.abs(state.scoreL - state.scoreR);
  return topScore >= pointsToWin && lead >= minLead;
}

export function resetBall(state, dir) {
  state.ball.x = 0.5;
  state.ball.y = 0.5;
  state.ball.vx = dir * 0.55;
  state.ball.vy = (Math.random() * 0.4 - 0.2);
  state.rallyHits = 0;
}

function clamp(v, a, b) {
  return Math.max(a, Math.min(b, v));
}

function reflectInsideBounds(position, velocity, min, max) {
  let nextPosition = position;
  let nextVelocity = velocity;
  let safety = 0;

  while ((nextPosition < min || nextPosition > max) && safety < 4) {
    if (nextPosition < min) {
      nextPosition = min + (min - nextPosition);
      nextVelocity = Math.abs(nextVelocity);
    } else if (nextPosition > max) {
      nextPosition = max - (nextPosition - max);
      nextVelocity = -Math.abs(nextVelocity);
    }
    safety += 1;
  }

  return {
    position: clamp(nextPosition, min, max),
    velocity: nextVelocity,
  };
}

function applyPaddleBounce(ball, paddleCenterY, paddleHalf, direction, maxAngle, speedGain, minSpeed, maxSpeed) {
  const hitOffset = clamp((ball.y - paddleCenterY) / paddleHalf, -1, 1);
  const softenedOffset = Math.sign(hitOffset) * Math.pow(Math.abs(hitOffset), 1.35);
  const bounceAngle = softenedOffset * maxAngle;
  const nextSpeed = clamp(Math.hypot(ball.vx, ball.vy) * speedGain, minSpeed, maxSpeed);

  ball.vx = Math.cos(bounceAngle) * nextSpeed * direction;
  ball.vy = Math.sin(bounceAngle) * nextSpeed;
}

function resolvePaddleVelocity(currentY, input, paddleHalf, paddleMaxSpeed, aimResponsiveness) {
  if (input && typeof input === 'object' && Number.isFinite(input.targetY)) {
    const targetY = clamp(input.targetY, paddleHalf, 1 - paddleHalf);
    const delta = targetY - currentY;
    if (Math.abs(delta) <= 0.0035) {
      return 0;
    }
    return clamp(delta * aimResponsiveness, -paddleMaxSpeed, paddleMaxSpeed);
  }

  const move = Number(input?.move ?? input ?? 0);
  if (![ -1, 0, 1 ].includes(move)) {
    return 0;
  }

  return move * paddleMaxSpeed;
}

function getProgressiveBounceAngle(rallyHits, minAngleDeg, maxAngleDeg, hitsToMaxAngle) {
  const clampedHits = clamp(rallyHits, 0, hitsToMaxAngle);
  const progress = hitsToMaxAngle <= 0 ? 1 : (clampedHits / hitsToMaxAngle);
  const angleDeg = minAngleDeg + ((maxAngleDeg - minAngleDeg) * progress);
  return angleDeg * (Math.PI / 180);
}
