'use client';

import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { motion } from 'motion/react';

const FADE_FLOOR = 0.16;
const FADE_PEAK = 0.62;
const FADE_LEAD = 6;
const SETTLE = 'color 0.7s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1)';

const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

const isSpace = char => /\s/.test(char);

const prefersReducedMotion = () =>
  typeof window !== 'undefined' && (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false);

const glyphWidths = new Map();
let measureContext = null;

const readFont = node => {
  const style = window.getComputedStyle(node);
  return {
    font: `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`,
    spacing: parseFloat(style.letterSpacing) || 0
  };
};

const advance = (font, glyph) => {
  let widths = glyphWidths.get(font);
  if (!widths) {
    widths = new Map();
    glyphWidths.set(font, widths);
  }
  let width = widths.get(glyph);
  if (width === undefined) {
    measureContext = measureContext || document.createElement('canvas').getContext('2d');
    if (!measureContext) return 0;
    measureContext.font = font;
    width = measureContext.measureText(glyph).width;
    widths.set(glyph, width);
  }
  return width;
};

const flipFrames = (angle, opacity) => [
  { transform: `perspective(4em) translateZ(-0.42em) rotateX(${angle}deg) translateZ(0.42em)`, opacity },
  { transform: 'perspective(4em) translateZ(-0.42em) rotateX(0deg) translateZ(0.42em)', opacity: 1 }
];

export default function DecryptedText({
  text,
  speed = 50,
  maxIterations = 10,
  sequential = false,
  revealDirection = 'start',
  useOriginalCharsOnly = false,
  characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+',
  className = '',
  parentClassName = '',
  encryptedClassName = '',
  animateOn = 'hover',
  clickMode = 'once',
  effect = 'scramble',
  scrambleColor = '',
  loop = false,
  loopDelay = 2000,
  hoverRadius = 0,
  ...props
}) {
  const chars = useMemo(() => Array.from(String(text ?? '')), [text]);

  const pool = useMemo(() => {
    const letters = chars.filter(char => !isSpace(char));
    const source = useOriginalCharsOnly ? Array.from(new Set(letters)) : Array.from(characters);
    return source.length ? source : letters;
  }, [useOriginalCharsOnly, chars, characters]);

  const startsHidden = animateOn === 'click' || animateOn === 'view' || animateOn === 'inViewHover';
  const hoverable = animateOn === 'hover' || animateOn === 'inViewHover';
  const local = hoverable && hoverRadius > 0;
  const step = Math.max(16, speed);

  const [frame, setFrame] = useState(() => {
    const revealed = chars.map(char => !startsHidden || isSpace(char));
    return {
      shown: chars.map((char, index) =>
        revealed[index] ? char : pool[(char.codePointAt(0) + index * 7) % pool.length]
      ),
      revealed,
      tint: revealed.map(shown => (shown ? 0 : 1)),
      dim: revealed.map(shown => (shown ? 1 : FADE_FLOOR)),
      squeeze: revealed.map(() => 1)
    };
  });

  const containerRef = useRef(null);
  const cellsRef = useRef([]);
  const centersRef = useRef([]);
  const maskRef = useRef(frame.revealed);
  const heatRef = useRef([]);
  const warmRef = useRef(false);
  const pointerRef = useRef(null);
  const runRef = useRef(null);
  const timerRef = useRef(null);
  const holdRef = useRef(null);
  const pendingRef = useRef(null);
  const visibleRef = useRef(true);
  const targetRef = useRef(!startsHidden);
  const playedRef = useRef(false);
  const previousRef = useRef(frame);
  const engineRef = useRef(null);
  const fontRef = useRef(null);

  const revealOrder = () => {
    const order = [];
    chars.forEach((char, index) => {
      if (!isSpace(char)) order.push(index);
    });
    if (revealDirection === 'end') return order.reverse();
    if (revealDirection === 'center') {
      const middle = Math.floor(chars.length / 2);
      return order.sort((a, b) => Math.abs(a - middle) - Math.abs(b - middle));
    }
    if (revealDirection === 'random') {
      for (let i = order.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [order[i], order[j]] = [order[j], order[i]];
      }
    }
    return order;
  };

  const fit = (index, glyph) => {
    const node = containerRef.current;
    if (!fontRef.current && node) fontRef.current = readFont(node);
    const metrics = fontRef.current;
    if (!metrics) return 1;
    const room = advance(metrics.font, chars[index] ?? glyph) + metrics.spacing;
    const need = advance(metrics.font, glyph) + metrics.spacing;
    return need > room + 0.01 ? Math.round(Math.max(0.05, room / need) * 1000) / 1000 : 1;
  };

  const paint = () => {
    const mask = maskRef.current;
    const heat = heatRef.current;
    const run = runRef.current;
    const ranks = run?.forward && sequential ? new Map(run.queue.map((index, rank) => [index, rank])) : null;
    const ramp = run?.forward && !sequential ? Math.min(1, run.iteration / Math.max(1, maxIterations)) : 0;
    const next = { shown: [], revealed: [], tint: [], dim: [], squeeze: [] };

    chars.forEach((char, index) => {
      const blank = isSpace(char);
      const hot = blank ? 0 : heat[index] || 0;
      const hidden = !blank && !mask[index];
      const scrambled = hidden || (hot > 0 && Math.random() < hot * 2);

      const glyph = scrambled ? pool[Math.floor(Math.random() * pool.length)] : char;
      next.revealed.push(!scrambled);
      next.shown.push(glyph);
      next.squeeze.push(scrambled ? fit(index, glyph) : 1);

      if (!scrambled) {
        next.tint.push(0);
        next.dim.push(1);
      } else if (!hidden) {
        const level = Math.min(1, hot * 1.5);
        next.tint.push(level);
        next.dim.push(1 - (1 - FADE_FLOOR) * level);
      } else {
        const rank = ranks?.get(index);
        const lift = rank === undefined ? ramp : Math.max(0, 1 - rank / FADE_LEAD);
        next.tint.push(1);
        next.dim.push(FADE_FLOOR + (FADE_PEAK - FADE_FLOOR) * lift);
      }
    });

    setFrame(next);
  };

  const refit = () => {
    setFrame(previous => ({
      ...previous,
      squeeze: previous.shown.map((glyph, index) => (previous.revealed[index] ? 1 : fit(index, glyph)))
    }));
  };

  const stopTicker = () => {
    clearInterval(timerRef.current);
    timerRef.current = null;
  };

  const ensureTicker = () => {
    if (timerRef.current) return;
    timerRef.current = setInterval(() => engineRef.current?.tick(), step);
  };

  const stopRun = () => {
    runRef.current = null;
    clearTimeout(holdRef.current);
    holdRef.current = null;
    pendingRef.current = null;
  };

  const schedule = (task, delay) => {
    clearTimeout(holdRef.current);
    holdRef.current = setTimeout(() => {
      holdRef.current = null;
      if (visibleRef.current) task();
      else pendingRef.current = task;
    }, delay);
  };

  const startRun = (forward, { from, cycle = false, lead = 0 } = {}) => {
    stopRun();
    targetRef.current = forward;
    if (from) maskRef.current = from;

    if (prefersReducedMotion()) {
      maskRef.current = chars.map(char => forward || isSpace(char));
      paint();
      return;
    }

    const mask = maskRef.current;
    const order = forward ? revealOrder() : revealOrder().reverse();
    const queue = sequential ? order.filter(index => mask[index] !== forward) : [];

    runRef.current = {
      forward,
      cycle,
      iteration: 0,
      queue: [...new Array(lead).fill(null), ...queue]
    };
    paint();
    ensureTicker();
  };

  const finishRun = run => {
    if (!loop) return;
    if (run.forward) {
      schedule(() => engineRef.current?.startRun(false, { cycle: true }), Math.max(0, loopDelay));
    } else if (run.cycle) {
      startRun(true, { cycle: true, lead: sequential ? Math.max(2, Math.round(320 / step)) : 0 });
    }
  };

  const warm = () => {
    const heat = heatRef.current;
    const pointer = pointerRef.current;
    const box = pointer && containerRef.current ? containerRef.current.getBoundingClientRect() : null;
    const cool = 1 / Math.max(1, maxIterations);
    let active = false;

    chars.forEach((char, index) => {
      if (isSpace(char)) return;
      let target = 0;
      const center = centersRef.current[index];
      if (box && center) {
        const distance =
          Math.hypot(box.left + center.x - pointer.x, box.top + center.y - pointer.y) / Math.max(1, hoverRadius);
        if (distance < 1) target = 1 - distance * distance * (3 - 2 * distance);
      }
      const value = Math.max((heat[index] || 0) - cool, target);
      heat[index] = value > 0.001 ? value : 0;
      if (heat[index] > 0) active = true;
    });

    warmRef.current = active;
  };

  const tick = () => {
    const run = runRef.current;

    if (run) {
      run.iteration += 1;
      let mask = maskRef.current;
      let done;

      if (sequential) {
        const index = run.queue.shift();
        if (index !== undefined && index !== null) {
          mask = mask.slice();
          mask[index] = run.forward;
        }
        done = run.queue.length === 0;
      } else if (run.forward) {
        done = run.iteration >= maxIterations;
        if (done) mask = chars.map(() => true);
      } else {
        const visible = [];
        mask.forEach((shown, index) => {
          if (shown && !isSpace(chars[index])) visible.push(index);
        });
        const hideCount = Math.max(
          1,
          Math.ceil(chars.filter(char => !isSpace(char)).length / Math.max(1, maxIterations))
        );
        mask = mask.slice();
        for (let i = 0; i < hideCount && visible.length; i++) {
          mask[visible.splice(Math.floor(Math.random() * visible.length), 1)[0]] = false;
        }
        done = visible.length === 0 || run.iteration >= maxIterations;
        if (done) mask = chars.map(char => isSpace(char));
      }

      maskRef.current = mask;
      if (done) {
        runRef.current = null;
        finishRun(run);
      }
    }

    if (local) warm();
    else warmRef.current = false;

    paint();
    if (!runRef.current && !warmRef.current) stopTicker();
  };

  const measure = () => {
    const node = containerRef.current;
    if (!node) return;
    const box = node.getBoundingClientRect();
    centersRef.current = chars.map((char, index) => {
      const cell = cellsRef.current[index];
      if (isSpace(char) || !cell) return null;
      const rect = cell.getBoundingClientRect();
      return { x: rect.left - box.left + rect.width / 2, y: rect.top - box.top + rect.height / 2 };
    });
  };

  useIsomorphicLayoutEffect(() => {
    engineRef.current = { tick, paint, startRun, stopRun, ensureTicker, refit };
  });

  useIsomorphicLayoutEffect(() => {
    const engine = engineRef.current;
    engine.stopRun();
    playedRef.current = false;
    const hidden =
      animateOn === 'click' || ((animateOn === 'view' || animateOn === 'inViewHover') && !prefersReducedMotion());
    targetRef.current = !hidden;
    maskRef.current = chars.map(char => !hidden || isSpace(char));
    heatRef.current = chars.map(() => 0);
    pointerRef.current = null;
    engine.paint();
  }, [animateOn, chars]);

  useEffect(
    () => () => {
      clearInterval(timerRef.current);
      clearTimeout(holdRef.current);
    },
    []
  );

  useEffect(() => {
    const node = containerRef.current;
    if (!node || typeof ResizeObserver === 'undefined') return;
    const observer = new ResizeObserver(() => {
      const metrics = readFont(node);
      glyphWidths.delete(metrics.font);
      fontRef.current = metrics;
      engineRef.current?.refit();
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    if (!timerRef.current) return;
    clearInterval(timerRef.current);
    timerRef.current = null;
    engineRef.current?.ensureTicker();
  }, [step]);

  useEffect(() => {
    if (loop) return;
    clearTimeout(holdRef.current);
    holdRef.current = null;
    pendingRef.current = null;
  }, [loop]);

  useEffect(() => {
    const viewTrigger = animateOn === 'view' || animateOn === 'inViewHover';
    if (!viewTrigger && !loop) return;
    const node = containerRef.current;
    if (!node || typeof IntersectionObserver === 'undefined') return;
    const observer = new IntersectionObserver(
      entries => {
        const visible = entries.some(entry => entry.isIntersecting);
        visibleRef.current = visible;
        if (!visible) return;
        if (viewTrigger && !playedRef.current) {
          playedRef.current = true;
          engineRef.current?.startRun(true);
        }
        const resume = pendingRef.current;
        pendingRef.current = null;
        resume?.();
        if (!loop) observer.disconnect();
      },
      { threshold: 0.1 }
    );
    observer.observe(node);
    return () => {
      observer.disconnect();
      visibleRef.current = true;
    };
  }, [animateOn, loop, chars]);

  useIsomorphicLayoutEffect(() => {
    const previous = previousRef.current;
    previousRef.current = frame;
    if (effect !== 'flip' || previous.shown === frame.shown || prefersReducedMotion()) return;
    frame.revealed.forEach((revealed, index) => {
      const node = cellsRef.current[index];
      if (!node || typeof node.animate !== 'function' || isSpace(chars[index] ?? ' ')) return;
      if (!revealed) {
        node.animate(flipFrames(58 + Math.random() * 26, 0.3), {
          duration: Math.min(step * 0.92, 240),
          easing: 'cubic-bezier(0.3, 0.7, 0.4, 1)'
        });
      } else if (previous.revealed[index] === false) {
        node.animate(flipFrames(90, 0), {
          duration: Math.min(Math.max(step * 7, 360), 760),
          easing: 'cubic-bezier(0.34, 1.45, 0.64, 1)'
        });
      }
    });
  }, [frame, effect, chars, step]);

  const trackPointer = event => {
    if (prefersReducedMotion()) return;
    if (!pointerRef.current) measure();
    pointerRef.current = { x: event.clientX, y: event.clientY };
    ensureTicker();
  };

  const handleMouseEnter = event => {
    if (local) {
      trackPointer(event);
      return;
    }
    if (runRef.current) return;
    startRun(true, { from: chars.map(char => isSpace(char)) });
  };

  const handleMouseMove = event => {
    if (local) trackPointer(event);
  };

  const handleMouseLeave = () => {
    if (local) {
      pointerRef.current = null;
      return;
    }
    stopRun();
    targetRef.current = true;
    maskRef.current = chars.map(() => true);
    paint();
    if (loop && animateOn === 'inViewHover') {
      schedule(() => engineRef.current?.startRun(false, { cycle: true }), Math.max(0, loopDelay));
    }
  };

  const handleClick = () => {
    if (clickMode === 'toggle') {
      startRun(!targetRef.current);
      return;
    }
    if (!targetRef.current) startRun(true);
  };

  const groups = useMemo(() => {
    const result = [];
    let word = null;
    chars.forEach((char, index) => {
      if (isSpace(char)) {
        result.push(char);
        word = null;
        return;
      }
      if (!word) {
        word = { key: index, letters: [] };
        result.push(word);
      }
      word.letters.push(index);
    });
    return result;
  }, [chars]);

  const tinted = Boolean(scrambleColor);
  const fading = effect === 'fade';
  const flipping = effect === 'flip';
  const settles = tinted || fading;

  const glyphStyle = index => {
    const squeeze = frame.squeeze[index] ?? 1;
    return squeeze < 1 ? { transform: `scaleX(${squeeze})` } : undefined;
  };

  const cellClass = revealed =>
    [revealed && !flipping ? '' : 'relative inline-block', revealed ? className : encryptedClassName]
      .filter(Boolean)
      .join(' ');

  const cellStyle = (index, revealed) => {
    const style = {};
    if (settles) style.transition = revealed ? SETTLE : `color 0.12s linear, opacity ${step}ms linear`;
    if (tinted && !revealed) {
      const amount = Math.round((frame.tint[index] ?? 1) * 100);
      style.color = amount >= 100 ? scrambleColor : `color-mix(in srgb, ${scrambleColor} ${amount}%, currentColor)`;
    }
    if (fading) style.opacity = revealed ? 1 : (frame.dim[index] ?? FADE_FLOOR);
    return style;
  };

  const animateProps = hoverable
    ? { onMouseEnter: handleMouseEnter, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave }
    : animateOn === 'click'
      ? { onClick: handleClick }
      : {};

  return (
    <motion.span
      ref={containerRef}
      className={`inline-block whitespace-pre-wrap ${parentClassName}`}
      {...animateProps}
      {...props}
    >
      <span className="sr-only">{chars.join('')}</span>

      <span aria-hidden="true">
        {groups.map(group =>
          typeof group === 'string' ? (
            group
          ) : (
            <span key={group.key} className="whitespace-nowrap">
              {group.letters.map(index => {
                const char = chars[index];
                const revealed = frame.revealed[index] ?? true;
                return (
                  <span
                    key={index}
                    ref={node => {
                      cellsRef.current[index] = node;
                    }}
                    className={cellClass(revealed)}
                    style={cellStyle(index, revealed)}
                  >
                    {revealed ? (
                      char
                    ) : (
                      <>
                        <span className="invisible">{char}</span>
                        <span className="absolute top-0 left-1/2 -translate-x-1/2" style={glyphStyle(index)}>
                          {frame.shown[index] ?? char}
                        </span>
                      </>
                    )}
                  </span>
                );
              })}
            </span>
          )
        )}
      </span>
    </motion.span>
  );
}