export class RoutePathAnimator {
  private routePath: Float32Array = new Float32Array();
  private routeDistances: Float64Array = new Float64Array();
  private totalRouteLength: number = 0;

  private targetDistance: number = 0;
  private currentDistance: number = 0;
  private currentSegIndex: number = -1;

  private isAnimating: boolean = false;
  private animationId: number | null = null;
  private lastFrameTime: number | null = null;
  private duration: number;

  private fallbackCurX: number = 0;
  private fallbackCurY: number = 0;
  private fallbackTgtX: number = 0;
  private fallbackTgtY: number = 0;
  private fallbackInitialized: boolean = false;

  private onUpdate: (pos: { x: number; y: number }) => void;
  private onComplete: (() => void) | null;

  private static readonly BASE_DURATION = 800;
  private static readonly MAX_DURATION = 2500;
  private static readonly MIN_SMOOTHING_MS = 400;
  private static readonly NORMAL_DISTANCE = 30;
  private static readonly SETTLE_EPS = 0.02;
  private static readonly MAX_DT_MS = 100;
  private static readonly MAX_SPEED_UNITS_PER_SEC = 140;

  constructor(
    onUpdate: (pos: { x: number; y: number }) => void,
    onComplete?: () => void,
    duration: number = 800,
  ) {
    this.onUpdate = onUpdate;
    this.onComplete = onComplete ?? null;
    this.duration = duration;
  }

  setRoutePath(routePath: Float32Array, syncMapXY?: { x: number; y: number }): void {
    this.routePath = routePath;
    const n = routePath.length / 2;
    if (n < 2) {
      this.routeDistances = new Float64Array();
      this.totalRouteLength = 0;
      return;
    }
    this.routeDistances = new Float64Array(n);
    this.routeDistances[0] = 0;
    for (let i = 1; i < n; i++) {
      const dx = routePath[i * 2] - routePath[(i - 1) * 2];
      const dy = routePath[i * 2 + 1] - routePath[(i - 1) * 2 + 1];
      this.routeDistances[i] = this.routeDistances[i - 1] + Math.hypot(dx, dy);
    }
    this.totalRouteLength = this.routeDistances[n - 1];

    if (syncMapXY) {
      const proj = this.projectToRoute(syncMapXY.x, syncMapXY.y);
      this.currentDistance = proj.distance;
      this.targetDistance = proj.distance;
      const pt = this.distanceToPoint(this.currentDistance);
      this.currentSegIndex = pt.segIndex;
      this.onUpdate({ x: pt.x, y: pt.y });
    }
  }

  projectToRoute(
    x: number,
    y: number,
  ): { segIndex: number; t: number; distance: number } {
    const path = this.routePath;
    const n = path.length / 2;
    if (n < 2) return { segIndex: 0, t: 0, distance: 0 };

    let bestIndex = 0;
    let bestT = 0;
    let bestDist = Infinity;

    for (let i = 0; i < n - 1; i++) {
      const p1x = path[i * 2],
        p1y = path[i * 2 + 1];
      const p2x = path[(i + 1) * 2],
        p2y = path[(i + 1) * 2 + 1];
      const dx = p2x - p1x,
        dy = p2y - p1y;
      const len2 = dx * dx + dy * dy;
      if (len2 === 0) continue;

      const t = Math.max(0, Math.min(1, ((x - p1x) * dx + (y - p1y) * dy) / len2));
      const px = p1x + t * dx,
        py = p1y + t * dy;
      const d = Math.hypot(x - px, y - py);

      if (d < bestDist) {
        bestDist = d;
        bestIndex = i;
        bestT = t;
      }
    }

    const segLength =
      this.routeDistances[bestIndex + 1] - this.routeDistances[bestIndex];
    const distance = this.routeDistances[bestIndex] + bestT * segLength;

    return { segIndex: bestIndex, t: bestT, distance };
  }

  distanceToPoint(distance: number): { x: number; y: number; segIndex: number } {
    const dists = this.routeDistances;
    const path = this.routePath;
    const n = dists.length;

    if (n < 2) return { x: 0, y: 0, segIndex: -1 };

    distance = Math.max(0, Math.min(distance, this.totalRouteLength));

    let lo = 0,
      hi = n - 1;
    while (lo < hi - 1) {
      const mid = (lo + hi) >> 1;
      if (dists[mid] <= distance) lo = mid;
      else hi = mid;
    }

    const segStart = dists[lo];
    const segEnd = dists[hi];
    const segLen = segEnd - segStart;
    const t = segLen > 0 ? (distance - segStart) / segLen : 0;

    const x = path[lo * 2] + t * (path[hi * 2] - path[lo * 2]);
    const y = path[lo * 2 + 1] + t * (path[hi * 2 + 1] - path[lo * 2 + 1]);

    return { x, y, segIndex: lo };
  }

  private computeSmoothingTau(routeDistDelta: number, durationOverride?: number): number {
    if (durationOverride !== undefined) {
      return Math.max(durationOverride, RoutePathAnimator.MIN_SMOOTHING_MS);
    }
    return Math.min(
      Math.max(
        RoutePathAnimator.BASE_DURATION *
          Math.max(1, routeDistDelta / RoutePathAnimator.NORMAL_DISTANCE),
        RoutePathAnimator.MIN_SMOOTHING_MS,
      ),
      RoutePathAnimator.MAX_DURATION,
    );
  }

  animateTo(x: number, y: number, duration?: number): void {
    if (this.routeDistances.length < 2) {
      this.fallbackTgtX = x;
      this.fallbackTgtY = y;
      if (!this.fallbackInitialized) {
        this.fallbackCurX = x;
        this.fallbackCurY = y;
        this.fallbackInitialized = true;
        this.onUpdate({ x, y });
      }
      this.duration = this.computeSmoothingTau(
        Math.hypot(this.fallbackTgtX - this.fallbackCurX, this.fallbackTgtY - this.fallbackCurY),
        duration,
      );
      this.isAnimating = true;
      this.ensureTickLoop();
      return;
    }

    const target = this.projectToRoute(x, y);
    this.targetDistance = target.distance;

    const routeDistDelta = Math.abs(this.targetDistance - this.currentDistance);
    this.duration = this.computeSmoothingTau(routeDistDelta, duration);

    this.isAnimating = true;
    this.ensureTickLoop();
  }

  private ensureTickLoop(): void {
    if (!this.animationId) {
      this.lastFrameTime = null;
      this.animationId = requestAnimationFrame(this.tick);
    }
  }

  setPosition(x: number, y: number): void {
    this.stop();
    if (this.routeDistances.length >= 2) {
      const proj = this.projectToRoute(x, y);
      this.currentDistance = proj.distance;
      this.targetDistance = proj.distance;
      this.currentSegIndex = proj.segIndex;
      const pt = this.distanceToPoint(proj.distance);
      this.onUpdate({ x: pt.x, y: pt.y });
    } else {
      this.fallbackCurX = x;
      this.fallbackCurY = y;
      this.fallbackTgtX = x;
      this.fallbackTgtY = y;
      this.fallbackInitialized = true;
      this.onUpdate({ x, y });
    }
  }

  stop(): void {
    this.isAnimating = false;
    this.lastFrameTime = null;
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
      this.animationId = null;
    }
  }

  getCurrentPosition(): { x: number; y: number } {
    if (this.routeDistances.length < 2) {
      return { x: this.fallbackCurX, y: this.fallbackCurY };
    }
    const pt = this.distanceToPoint(this.currentDistance);
    return { x: pt.x, y: pt.y };
  }

  getCurrentSegIndex(): number {
    return this.currentSegIndex;
  }

  private stepRoute(alpha: number, dt: number): boolean {
    const delta = this.targetDistance - this.currentDistance;
    let step = Math.abs(delta) < RoutePathAnimator.SETTLE_EPS ? 0 : delta * alpha;
    const maxStep =
      RoutePathAnimator.MAX_SPEED_UNITS_PER_SEC * Math.max(dt / 1000, 1e-6);
    if (Math.abs(step) > maxStep) {
      step = Math.sign(step) * maxStep;
    }
    if (Math.abs(delta) < RoutePathAnimator.SETTLE_EPS) {
      this.currentDistance = this.targetDistance;
    } else {
      this.currentDistance += step;
    }
    const pt = this.distanceToPoint(this.currentDistance);
    this.currentSegIndex = pt.segIndex;
    this.onUpdate({ x: pt.x, y: pt.y });
    return Math.abs(this.targetDistance - this.currentDistance) < RoutePathAnimator.SETTLE_EPS;
  }

  private stepFallback(alpha: number, dt: number): boolean {
    const dx = this.fallbackTgtX - this.fallbackCurX;
    const dy = this.fallbackTgtY - this.fallbackCurY;
    const dist = Math.hypot(dx, dy);
    if (dist < RoutePathAnimator.SETTLE_EPS) {
      this.fallbackCurX = this.fallbackTgtX;
      this.fallbackCurY = this.fallbackTgtY;
    } else {
      let sx = dx * alpha;
      let sy = dy * alpha;
      const maxStep =
        RoutePathAnimator.MAX_SPEED_UNITS_PER_SEC * Math.max(dt / 1000, 1e-6);
      const stepLen = Math.hypot(sx, sy);
      if (stepLen > maxStep && stepLen > 0) {
        const k = maxStep / stepLen;
        sx *= k;
        sy *= k;
      }
      this.fallbackCurX += sx;
      this.fallbackCurY += sy;
    }
    this.onUpdate({ x: this.fallbackCurX, y: this.fallbackCurY });
    return (
      Math.hypot(this.fallbackTgtX - this.fallbackCurX, this.fallbackTgtY - this.fallbackCurY) <
      RoutePathAnimator.SETTLE_EPS
    );
  }

  private tick = (timestamp: number): void => {
    if (!this.isAnimating) return;

    let dt: number;
    if (this.lastFrameTime === null) {
      dt = 16.67;
      this.lastFrameTime = timestamp;
    } else {
      dt = Math.min(Math.max(timestamp - this.lastFrameTime, 0), RoutePathAnimator.MAX_DT_MS);
      this.lastFrameTime = timestamp;
    }

    const tau = Math.max(this.duration, 50);
    const alpha = 1 - Math.exp(-dt / tau);

    const settled =
      this.routeDistances.length >= 2
        ? this.stepRoute(alpha, dt)
        : this.stepFallback(alpha, dt);

    if (settled) {
      this.finishTickLoop();
    } else {
      this.animationId = requestAnimationFrame(this.tick);
    }
  };

  private finishTickLoop(): void {
    this.isAnimating = false;
    this.animationId = null;
    this.lastFrameTime = null;
    this.onComplete?.();
  }
}
