type GapBoundsInput = {
  arenaHeight: number;
  gapHeight: number;
  scale: number;
};

type ReachableGapInput = GapBoundsInput & {
  desiredGapY: number;
  portrait: boolean;
  previousGapY: number;
  previousPreviousGapY?: number;
};

type ChooseGapInput = Omit<ReachableGapInput, "desiredGapY"> & {
  random?: () => number;
};

const clamp = (value: number, minimum: number, maximum: number) =>
  Math.max(minimum, Math.min(maximum, value));

export const getGapBounds = ({
  arenaHeight,
  gapHeight,
  scale,
}: GapBoundsInput) => {
  const floor = arenaHeight - 19;
  const margin = gapHeight / 2 + 48 * scale;
  const minY = margin;
  const maxY = Math.max(minY, floor - margin);
  return { minY, maxY };
};

export const getReachableGapStep = ({
  arenaHeight,
  gapHeight,
  scale,
  portrait,
}: GapBoundsInput & { portrait: boolean }) => {
  const { minY, maxY } = getGapBounds({ arenaHeight, gapHeight, scale });
  const availableHeight = maxY - minY;
  const overlapLimit = gapHeight * (portrait ? 0.82 : 0.95);
  return Math.max(0, Math.min(availableHeight * 0.44, overlapLimit));
};

export const constrainReachableGapY = ({
  arenaHeight,
  gapHeight,
  scale,
  desiredGapY,
  portrait,
  previousGapY,
  previousPreviousGapY,
}: ReachableGapInput) => {
  const { minY, maxY } = getGapBounds({ arenaHeight, gapHeight, scale });
  const previous = clamp(previousGapY, minY, maxY);
  const maxStep = getReachableGapStep({
    arenaHeight,
    gapHeight,
    scale,
    portrait,
  });

  let next = clamp(
    desiredGapY,
    Math.max(minY, previous - maxStep),
    Math.min(maxY, previous + maxStep),
  );

  if (previousPreviousGapY !== undefined && maxStep > 0) {
    const previousPrevious = clamp(previousPreviousGapY, minY, maxY);
    const priorMove = previous - previousPrevious;
    const nextMove = next - previous;
    const continuesSameDirection =
      Math.sign(priorMove) !== 0 && Math.sign(priorMove) === Math.sign(nextMove);

    if (continuesSameDirection && Math.abs(priorMove) > maxStep * 0.78) {
      const continuationLimit = maxStep * 0.55;
      next =
        previous +
        Math.sign(nextMove) * Math.min(Math.abs(nextMove), continuationLimit);
    }
  }

  return clamp(next, minY, maxY);
};

export const chooseReachableGapY = ({
  arenaHeight,
  gapHeight,
  scale,
  portrait,
  previousGapY,
  previousPreviousGapY,
  random = Math.random,
}: ChooseGapInput) => {
  const { minY, maxY } = getGapBounds({ arenaHeight, gapHeight, scale });
  const previous = clamp(previousGapY, minY, maxY);
  const maxStep = getReachableGapStep({
    arenaHeight,
    gapHeight,
    scale,
    portrait,
  });
  const target = minY + random() * (maxY - minY);
  const controlledDrift = (target - previous) * 0.68;
  const naturalVariation = (random() - 0.5) * maxStep * 0.9;

  return constrainReachableGapY({
    arenaHeight,
    gapHeight,
    scale,
    desiredGapY: previous + controlledDrift + naturalVariation,
    portrait,
    previousGapY: previous,
    previousPreviousGapY,
  });
};
