import type { OccluderRect, Rect, ScrollExtent } from "./types.js";

export const SCROLL_REACHABILITY_EPSILON = 1e-9;

type ClosedInterval = {
  low: number;
  high: number;
};

function strictXOverlap(targetRect: Rect, occluderRect: Rect): boolean {
  return (
    targetRect.x < occluderRect.x + occluderRect.width &&
    targetRect.x + targetRect.width > occluderRect.x
  );
}

function intersectClosed(a: ClosedInterval, b: ClosedInterval): ClosedInterval | null {
  const low = Math.max(a.low, b.low);
  const high = Math.min(a.high, b.high);
  if (low > high + SCROLL_REACHABILITY_EPSILON) {
    return null;
  }
  return { low, high };
}

function subtractOpenInterval(
  allowed: ClosedInterval,
  excludedLow: number,
  excludedHigh: number,
): ClosedInterval[] {
  if (excludedHigh <= allowed.low || excludedLow >= allowed.high) {
    return [allowed];
  }

  const remaining: ClosedInterval[] = [];
  if (allowed.low < excludedLow) {
    remaining.push({ low: allowed.low, high: excludedLow });
  }
  if (allowed.high > excludedHigh) {
    remaining.push({ low: excludedHigh, high: allowed.high });
  }
  return remaining;
}

function hasRemainingScrollOffset(intervals: ClosedInterval[]): boolean {
  return intervals.some(
    (interval) => interval.low <= interval.high + SCROLL_REACHABILITY_EPSILON,
  );
}

export function isScrollReachable(input: {
  targetRect: Rect;
  targetPosition: string;
  targetInRootScroller: boolean;
  occluders: OccluderRect[];
  fixedOccluders: OccluderRect[];
  scrollExtent: ScrollExtent;
}): boolean {
  if (input.targetPosition === "fixed" || input.targetPosition === "sticky") {
    return false;
  }
  if (!input.targetInRootScroller) {
    return false;
  }
  if (input.occluders.length === 0) {
    return false;
  }
  if (input.occluders.some((occluder) => !occluder.viewportPinned)) {
    return false;
  }
  if (input.fixedOccluders.length === 0) {
    return false;
  }

  const { targetRect, scrollExtent } = input;
  const docY = targetRect.y + scrollExtent.scrollY;
  const containment: ClosedInterval = {
    low: docY + targetRect.height - scrollExtent.viewportHeight,
    high: docY,
  };
  const scrollRange: ClosedInterval = {
    low: 0,
    high: scrollExtent.maxScrollY,
  };
  const allowed = intersectClosed(containment, scrollRange);
  if (allowed === null) {
    return false;
  }

  let candidates = [allowed];
  for (const occluder of input.fixedOccluders) {
    if (!strictXOverlap(targetRect, occluder.rect)) {
      continue;
    }

    const occluderBottom = occluder.rect.y + occluder.rect.height;
    const excludedLow = docY - occluderBottom;
    const excludedHigh = docY + targetRect.height - occluder.rect.y;
    if (excludedLow >= excludedHigh - SCROLL_REACHABILITY_EPSILON) {
      continue;
    }

    candidates = candidates.flatMap((interval) =>
      subtractOpenInterval(interval, excludedLow, excludedHigh),
    );
    if (!hasRemainingScrollOffset(candidates)) {
      return false;
    }
  }

  return hasRemainingScrollOffset(candidates);
}
