import type { TeactNode } from '../../lib/teact/teact';
import {
  memo, useEffect, useLayoutEffect, useMemo, useRef, useState, useUnmountCleanup,
} from '../../lib/teact/teact';

import type { ApiFormattedText } from '../../api/types';

import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import { REM } from './helpers/mediaDimensions';

import useLastCallback from '../../hooks/useLastCallback';

import AnimatedIconWithPreview from './AnimatedIconWithPreview';

import styles from './TypingWrapper.module.scss';

type OwnProps = {
  formattedText: ApiFormattedText;
  shouldAnimateMask?: boolean;
  renderText: (text: ApiFormattedText) => TeactNode;
};

const CHUNK_SIZE = 67;
const CHUNK_SPREAD_DURATION = 500;
const HEADWAY_DURATION = 750;
const PLACEHOLDER_SIZE = 1.25 * REM;
const SPREAD_CHARS = 20;
const PROGRESS_CSS_PROPERTY = '--typing-draft-progress';
const SPREAD_CSS_PROPERTY = '--typing-draft-spread';

try {
  window.CSS.registerProperty({
    name: PROGRESS_CSS_PROPERTY,
    syntax: '<percentage>',
    inherits: false,
    initialValue: '0%',
  });
} catch (_) {
  // Ignore duplicate registrations
}

function getRunningProgress(animation: Animation | undefined, baseProgress: number) {
  const timing = animation?.effect?.getComputedTiming().progress;
  if (typeof timing !== 'number') return baseProgress;
  return baseProgress + (100 - baseProgress) * timing;
}

const TypingWrapper = ({ formattedText, shouldAnimateMask, renderText }: OwnProps) => {
  const ref = useRef<HTMLSpanElement>();
  const animationRef = useRef<Animation>();
  const progressRef = useRef(0);
  const prevRevealedRef = useRef(0);

  const [revealedLength, setRevealedLength] = useState(0);
  const revealedLengthRef = useRef(0);
  const chunkTimerRef = useRef<number>();
  const prevFullTextRef = useRef('');

  const fullText = formattedText.text;

  const stopAnimation = useLastCallback(() => {
    animationRef.current?.cancel();
    animationRef.current = undefined;
  });

  const scheduleChunks = useLastCallback((from: number, to: number) => {
    window.clearTimeout(chunkTimerRef.current);

    const delta = to - from;
    if (delta <= 0) return;

    const numChunks = Math.ceil(delta / CHUNK_SIZE);
    const chunkInterval = numChunks > 1 ? CHUNK_SPREAD_DURATION / (numChunks - 1) : 0;

    let position = from;

    const addChunk = () => {
      position = Math.min(position + CHUNK_SIZE, to);
      revealedLengthRef.current = position;
      setRevealedLength(position);

      if (position < to) {
        chunkTimerRef.current = window.setTimeout(addChunk, chunkInterval);
      } else {
        chunkTimerRef.current = undefined;
      }
    };

    addChunk();
  });

  const resetChunking = useLastCallback(() => {
    window.clearTimeout(chunkTimerRef.current);
    chunkTimerRef.current = undefined;
    revealedLengthRef.current = 0;
    prevRevealedRef.current = 0;
    progressRef.current = 0;
    stopAnimation();
    setRevealedLength(0);
  });

  // --- Chunking: spread incoming text over time ---
  useEffect(() => {
    if (fullText === prevFullTextRef.current) return;
    prevFullTextRef.current = fullText;

    const fullLen = fullText.length;
    const revealed = revealedLengthRef.current;

    if (fullLen < revealed) {
      resetChunking();
      scheduleChunks(0, fullLen);
      return;
    }

    scheduleChunks(revealed, fullLen);
  });

  // --- Mask animation: smooth reveal of rendered content (layout effect to prevent flash) ---
  useLayoutEffect(() => {
    const element = ref.current;
    if (!element) return;

    const revealed = revealedLength;
    const prevRevealed = prevRevealedRef.current;
    if (revealed === prevRevealed) return;

    prevRevealedRef.current = revealed;

    if (!shouldAnimateMask) {
      stopAnimation();
      progressRef.current = 100;
      element.style.setProperty(PROGRESS_CSS_PROPERTY, '100%');
      return;
    }

    let progress = animationRef.current
      ? getRunningProgress(animationRef.current, progressRef.current)
      : progressRef.current;

    stopAnimation();

    if (revealed < prevRevealed) {
      progress = 0;
    } else if (prevRevealed && revealed) {
      progress = Math.min((prevRevealed * progress) / revealed, 100);
    } else if (!prevRevealed) {
      progress = 0;
    }

    if (!revealed) {
      progress = 100;
    }

    progressRef.current = progress;
    const remaining = 100 - progress;
    const spread = revealed ? (SPREAD_CHARS / revealed) * 100 : 0;

    if (!revealed || remaining <= 0) {
      progressRef.current = 100;
      element.style.setProperty(PROGRESS_CSS_PROPERTY, '100%');
      return;
    }

    element.style.setProperty(SPREAD_CSS_PROPERTY, `${spread}%`);
    element.style.setProperty(PROGRESS_CSS_PROPERTY, `${progress}%`);

    const animation = element.animate([
      { [PROGRESS_CSS_PROPERTY]: `${progress}%` },
      { [PROGRESS_CSS_PROPERTY]: '100%' },
    ] as Keyframe[], {
      duration: HEADWAY_DURATION,
      easing: 'linear',
      fill: 'forwards',
    });

    animationRef.current = animation;

    animation.onfinish = () => {
      if (animationRef.current !== animation) return;

      progressRef.current = 100;
      animationRef.current = undefined;

      requestMutation(() => {
        element.style.setProperty(PROGRESS_CSS_PROPERTY, '100%');
      });
    };

    animation.oncancel = () => {
      if (animationRef.current !== animation) return;
      animationRef.current = undefined;
    };
  });

  useUnmountCleanup(() => {
    window.clearTimeout(chunkTimerRef.current);
    stopAnimation();
  });

  const truncatedText = useMemo(() => ({
    text: fullText.slice(0, revealedLength),
    entities: formattedText.entities,
  }), [fullText, formattedText.entities, revealedLength]);

  return (
    <span ref={ref} className={styles.root}>
      {renderText(truncatedText)}
      <span key="typing-placeholder" className={styles.placeholder}>
        <AnimatedIconWithPreview
          tgsUrl={LOCAL_TGS_URLS.Typing}
          size={PLACEHOLDER_SIZE}
          play
          noLoop={false}
          shouldUseTextColor
        />
      </span>
    </span>
  );
};

export default memo(TypingWrapper);
