import {
  prepare,
  layout,
  prepareWithSegments,
  layoutWithLines,
} from "@chenglou/pretext";

type LayoutOptions = Record<string, unknown>;
type PreparedFactory = (
  text: string,
  font: string,
  options?: LayoutOptions,
) => unknown;
type ClampResult = ReturnType<typeof layoutWithLines> & {
  visibleLineCount: number;
  text: string;
  didTruncate: boolean;
};

// Cache prepared text to avoid re-measuring.
// prepare() is expensive, layout() is the cheap hot path.
const prepareCache = new Map();
const segmentedPrepareCache = new Map();
const MAX_CACHE_ENTRIES = 400;

function touch<T>(cache: Map<string, T>, key: string, value: T): T {
  if (cache.has(key)) cache.delete(key);
  cache.set(key, value);
  if (cache.size > MAX_CACHE_ENTRIES) {
    const oldestKey = cache.keys().next().value;
    if (typeof oldestKey === "string") cache.delete(oldestKey);
  }
  return value;
}

function cacheKey(text: string, font: string, options: LayoutOptions = {}): string {
  return `${text}\0${font}\0${JSON.stringify(options)}`;
}

function getPrepared(
  cache: Map<string, unknown>,
  factory: PreparedFactory,
  text: string,
  font: string,
  options: LayoutOptions = {},
): unknown {
  const key = cacheKey(text, font, options);
  if (cache.has(key)) {
    const cached = cache.get(key);
    if (cached !== undefined) return touch(cache, key, cached);
  }
  return touch(cache, key, factory(text, font, options));
}

export function prepareText(text: string, font: string, options: LayoutOptions = {}): unknown {
  return getPrepared(prepareCache, prepare, text, font, options);
}

export function prepareTextWithSegments(
  text: string,
  font: string,
  options: LayoutOptions = {},
): unknown {
  return getPrepared(
    segmentedPrepareCache,
    prepareWithSegments,
    text,
    font,
    options,
  );
}

/**
 * Measure the rendered height of text without DOM reflow.
 * Caches the prepare() result per unique text+font combination.
 *
 * @param {string} text - The text content to measure
 * @param {string} font - CSS font shorthand (e.g. "16px Lexend")
 * @param {number} maxWidth - Container width in pixels
 * @param {number} lineHeight - Line height in pixels
 * @param {object} [options] - Additional options (e.g. { whiteSpace: 'pre-wrap' })
 * @returns {{ height: number, lineCount: number }}
 */
export function measureTextHeight(
  text: string,
  font: string,
  maxWidth: number,
  lineHeight: number,
  options: LayoutOptions = {},
) {
  const prepared = prepareText(text, font, options);
  return layout(prepared, maxWidth, lineHeight);
}

/**
 * Measure height using pre-wrap mode (textarea-like behaviour).
 * Spaces, tabs, and hard breaks are preserved.
 */
export function measurePreWrap(
  text: string,
  font: string,
  maxWidth: number,
  lineHeight: number,
) {
  return measureTextHeight(text, font, maxWidth, lineHeight, { whiteSpace: "pre-wrap" });
}

/**
 * Return full line layout information for custom rendering or clipping.
 */
export function layoutTextLines(
  text: string,
  font: string,
  maxWidth: number,
  lineHeight: number,
  options: LayoutOptions = {},
) {
  const prepared = prepareTextWithSegments(text, font, options);
  return layoutWithLines(prepared, maxWidth, lineHeight);
}

/**
 * Clamp text to a maximum number of laid-out lines without DOM reads.
 */
export function clampTextLines(
  text: string,
  font: string,
  maxWidth: number,
  lineHeight: number,
  maxLines: number,
  options: LayoutOptions = {},
): ClampResult {
  const result = layoutTextLines(text, font, maxWidth, lineHeight, options);
  const visibleLines = result.lines.slice(0, maxLines);
  const didTruncate = result.lineCount > maxLines;
  if (!didTruncate) {
    return {
      ...result,
      visibleLineCount: result.lineCount,
      text,
      didTruncate: false,
    };
  }

  const trimmedLines = visibleLines.map((line) => line.text);
  const lastLine = trimmedLines[trimmedLines.length - 1] || "";
  trimmedLines[trimmedLines.length - 1] =
    `${lastLine.replace(/\s+$/g, "")}\u2026`;

  return {
    ...result,
    visibleLineCount: visibleLines.length,
    text: trimmedLines.join("\n"),
    height: visibleLines.length * lineHeight,
    didTruncate: true,
  };
}

/**
 * Invalidate all cached measurements.
 * Call when font family, size, or line height changes.
 */
export function invalidateCache() {
  prepareCache.clear();
  segmentedPrepareCache.clear();
}
