/**
 * Easing Functions — Drop-in replacement for Remotion's Easing module.
 *
 * Standard easing curves using cubic Bézier and physics-based functions.
 * API is 1:1 compatible with `import { Easing } from 'remotion'`.
 */

/** Linear — no easing */
function linear(t: number): number {
  return t;
}

/**
 * Standard ease curve — equivalent to CSS `ease`.
 * Approximation of cubic-bezier(0.25, 0.1, 0.25, 1.0)
 */
function ease(t: number): number {
  return cubicBezier(0.25, 0.1, 0.25, 1.0, t);
}

/** Quadratic easing */
function quad(t: number): number {
  return t * t;
}

/** Cubic easing */
function cubic(t: number): number {
  return t * t * t;
}

/** Sinusoidal easing */
function sin(t: number): number {
  return 1 - Math.cos((t * Math.PI) / 2);
}

/** Circular easing */
function circle(t: number): number {
  return 1 - Math.sqrt(1 - t * t);
}

/** Exponential easing */
function exp(t: number): number {
  return t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
}

/** Elastic easing */
function elastic(bounciness: number = 1): (t: number) => number {
  const p = bounciness * Math.PI;
  return (t: number) => 1 - Math.pow(Math.cos((t * Math.PI) / 2), 3) * Math.cos(t * p);
}

/** Back easing — overshoots then returns */
function back(s: number = 1.70158): (t: number) => number {
  return (t: number) => t * t * ((s + 1) * t - s);
}

/** Bounce easing — simulates a bouncing ball */
function bounce(t: number): number {
  if (t < 1 / 2.75) {
    return 7.5625 * t * t;
  }
  if (t < 2 / 2.75) {
    const t2 = t - 1.5 / 2.75;
    return 7.5625 * t2 * t2 + 0.75;
  }
  if (t < 2.5 / 2.75) {
    const t2 = t - 2.25 / 2.75;
    return 7.5625 * t2 * t2 + 0.9375;
  }
  const t2 = t - 2.625 / 2.75;
  return 7.5625 * t2 * t2 + 0.984375;
}

/**
 * Custom cubic Bézier curve.
 * Equivalent to CSS cubic-bezier(mX1, mY1, mX2, mY2).
 */
function bezier(mX1: number, mY1: number, mX2: number, mY2: number): (t: number) => number {
  return (t: number) => cubicBezier(mX1, mY1, mX2, mY2, t);
}

// ═══ Composition Helpers ═══

/** Apply easing only to the "in" part (acceleration) */
function easeIn(easing: (t: number) => number): (t: number) => number {
  return easing;
}

/** Apply easing only to the "out" part (deceleration) */
function easeOut(easing: (t: number) => number): (t: number) => number {
  return (t: number) => 1 - easing(1 - t);
}

/** Apply easing to both in and out */
function easeInOut(easing: (t: number) => number): (t: number) => number {
  return (t: number) => {
    if (t < 0.5) {
      return easing(t * 2) / 2;
    }
    return 1 - easing((1 - t) * 2) / 2;
  };
}

// ═══ Cubic Bézier Implementation ═══

const NEWTON_ITERATIONS = 4;
const NEWTON_MIN_SLOPE = 0.001;
const SUBDIVISION_PRECISION = 0.0000001;
const SUBDIVISION_MAX_ITERATIONS = 10;
const TABLE_SIZE = 11;
const SAMPLE_STEP_SIZE = 1.0 / (TABLE_SIZE - 1.0);

function A(a1: number, a2: number): number {
  return 1.0 - 3.0 * a2 + 3.0 * a1;
}
function B(a1: number, a2: number): number {
  return 3.0 * a2 - 6.0 * a1;
}
function C(a1: number): number {
  return 3.0 * a1;
}

function calcBezier(t: number, a1: number, a2: number): number {
  return ((A(a1, a2) * t + B(a1, a2)) * t + C(a1)) * t;
}

function getSlope(t: number, a1: number, a2: number): number {
  return 3.0 * A(a1, a2) * t * t + 2.0 * B(a1, a2) * t + C(a1);
}

function binarySubdivide(x: number, a: number, b: number, mX1: number, mX2: number): number {
  let currentX: number;
  let currentT: number;
  let i = 0;
  do {
    currentT = a + (b - a) / 2.0;
    currentX = calcBezier(currentT, mX1, mX2) - x;
    if (currentX > 0.0) {
      b = currentT;
    } else {
      a = currentT;
    }
  } while (
    Math.abs(currentX) > SUBDIVISION_PRECISION &&
    ++i < SUBDIVISION_MAX_ITERATIONS
  );
  return currentT;
}

function newtonRaphsonIterate(x: number, guessT: number, mX1: number, mX2: number): number {
  for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
    const currentSlope = getSlope(guessT, mX1, mX2);
    if (currentSlope === 0.0) return guessT;
    const currentX = calcBezier(guessT, mX1, mX2) - x;
    guessT -= currentX / currentSlope;
  }
  return guessT;
}

function cubicBezier(mX1: number, mY1: number, mX2: number, mY2: number, x: number): number {
  if (mX1 === mY1 && mX2 === mY2) return x; // Linear

  // Precompute sample table
  const sampleValues = new Float32Array(TABLE_SIZE);
  for (let i = 0; i < TABLE_SIZE; ++i) {
    sampleValues[i] = calcBezier(i * SAMPLE_STEP_SIZE, mX1, mX2);
  }

  function getTForX(x: number): number {
    let intervalStart = 0.0;
    let currentSample = 1;
    const lastSample = TABLE_SIZE - 1;

    for (; currentSample !== lastSample && sampleValues[currentSample] <= x; ++currentSample) {
      intervalStart += SAMPLE_STEP_SIZE;
    }
    --currentSample;

    const dist =
      (x - sampleValues[currentSample]) /
      (sampleValues[currentSample + 1] - sampleValues[currentSample]);
    const guessForT = intervalStart + dist * SAMPLE_STEP_SIZE;
    const initialSlope = getSlope(guessForT, mX1, mX2);

    if (initialSlope >= NEWTON_MIN_SLOPE) {
      return newtonRaphsonIterate(x, guessForT, mX1, mX2);
    } else if (initialSlope === 0.0) {
      return guessForT;
    } else {
      return binarySubdivide(
        x,
        intervalStart,
        intervalStart + SAMPLE_STEP_SIZE,
        mX1,
        mX2
      );
    }
  }

  if (x === 0) return 0;
  if (x === 1) return 1;
  return calcBezier(getTForX(x), mY1, mY2);
}

// ═══ Public API (mirrors Remotion's Easing) ═══

export const Easing = {
  linear,
  ease,
  quad,
  cubic,
  sin,
  circle,
  exp,
  elastic,
  back,
  bounce,
  bezier,
  in: easeIn,
  out: easeOut,
  inOut: easeInOut,
};
