import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { twMerge } from 'tailwind-merge';

/**
 * Bitmask for each significant unit
 *
 * - 4: days
 * - 3: hours
 * - 2: minutes
 * - 1: seconds
 * - 0: milliseconds
 */
export enum CountdownDisplay {
  DaysHoursMinutes = 0b11100,
  DaysHoursMinutesSeconds = 0b11110,
  HoursMinutesSeconds = 0b01110,
  HoursMinutes = 0b01100,
}

export enum CountdownPrecision {
  Millisecond = 1,
  Second = 1000,
  Minute = 60000,
  Hour = 3600000,
  Day = 86400000,
}

type Props = React.HTMLAttributes<HTMLDivElement> & {
  numberClassName?: string;
  digitsClassName?: string;
  labelClassName?: string;
  targetDate: number | Date;
  display?: CountdownDisplay;
  precision?: CountdownPrecision;
  onTimerEnd?: (instant?: boolean) => void;
};

const DEFAULT_NUMBER_CLASSNAME = 'flex-1 flex flex-col gap-1 max-w-[50px]';
const DEFAULT_DIGITS_CLASSNAME =
  'font-serif font-light text-[28px] leading-normal text-center';
const DEFAULT_LABEL_CLASSNAME =
  'font-sans text-[8px] leading-normal uppercase text-center';

function splitTimeInterval(
  timeRemaining: number
): [
  days: number,
  hours: number,
  minutes: number,
  seconds: number,
  milliseconds: number,
  sign: number,
] {
  return [
    Math.floor(timeRemaining / CountdownPrecision.Day),
    Math.floor((timeRemaining / CountdownPrecision.Hour) % 24),
    Math.floor((timeRemaining / CountdownPrecision.Minute) % 60),
    Math.floor((timeRemaining / CountdownPrecision.Second) % 60),
    timeRemaining % 1000,
    Math.sign(timeRemaining),
  ];
}

function getTimeInterval(targetTime: number, currentTime = Date.now()) {
  return targetTime - currentTime;
}

function getTimeRemaining(targetTime: number, currentTime = Date.now()) {
  return splitTimeInterval(
    Math.max(0, getTimeInterval(targetTime, currentTime))
  );
}

const PRECISION_BY_LSB = [
  CountdownPrecision.Millisecond,
  CountdownPrecision.Second,
  CountdownPrecision.Minute,
  CountdownPrecision.Hour,
  CountdownPrecision.Day,
];
function getPrecision(display: number) {
  return (
    PRECISION_BY_LSB.find((_precision, i) => {
      return (display >>> i) & 0b1;
    }) || CountdownPrecision.Millisecond
  );
}

const Countdown: React.FC<Props> = (props) => {
  const {
    numberClassName,
    labelClassName,
    targetDate,
    onTimerEnd,
    display = CountdownDisplay.HoursMinutesSeconds,
    precision = getPrecision(display),
    ...restProps
  } = props;

  const targetTime = useMemo(
    () => new Date(targetDate).getTime(),
    [targetDate]
  );

  const endTimerFired = useRef(false);

  const getCurrentTime = useCallback(
    () =>
      precision
        ? Math.floor(Date.now() / precision - 1) * precision + 1
        : Date.now(),
    [precision]
  );

  const [[days, hours, minutes, seconds], setTimeRemaining] = useState(() =>
    getTimeRemaining(targetTime, getCurrentTime())
  );

  useEffect(() => {
    const interval = setInterval(() => {
      const timeInterval = Math.max(
        0,
        getTimeInterval(targetTime, getCurrentTime())
      );
      setTimeRemaining(splitTimeInterval(timeInterval));
      if (!endTimerFired.current && timeInterval <= precision) {
        endTimerFired.current = true;
        onTimerEnd?.(false);
      }
    }, 1000);

    if (
      !endTimerFired.current &&
      getTimeInterval(targetTime, getCurrentTime()) <= precision
    ) {
      endTimerFired.current = true;
      onTimerEnd?.(true);
    }

    return () => {
      clearInterval(interval);
    };
  }, [targetTime, precision, getCurrentTime, onTimerEnd]);

  // Bitmasks
  const showDays = !!(display & 0b10000);
  const showSeconds = !!(display & 0b00010);

  return (
    <div
      {...restProps}
      className={twMerge(
        'align-center flex flex-row justify-center',
        'rounded-md',
        'mx-auto w-full max-w-full gap-2 px-6 py-4',
        '@sm:w-max @sm:gap-4 @sm:px-8',
        'bg-quaternary text-primary',
        props.className
      )}
    >
      {showDays && (
        <div className={twMerge(DEFAULT_NUMBER_CLASSNAME, numberClassName)}>
          <span className={twMerge(DEFAULT_DIGITS_CLASSNAME, numberClassName)}>
            {days}
          </span>
          <span className={twMerge(DEFAULT_LABEL_CLASSNAME, labelClassName)}>
            Days
          </span>
        </div>
      )}
      <div className={twMerge(DEFAULT_NUMBER_CLASSNAME, numberClassName)}>
        <span className={twMerge(DEFAULT_DIGITS_CLASSNAME, numberClassName)}>
          {showDays ? hours : days * 24 + hours}
        </span>
        <span className={twMerge(DEFAULT_LABEL_CLASSNAME, labelClassName)}>
          Hours
        </span>
      </div>
      <div className={twMerge(DEFAULT_NUMBER_CLASSNAME, numberClassName)}>
        <span className={twMerge(DEFAULT_DIGITS_CLASSNAME, numberClassName)}>
          {minutes}
        </span>
        <span className={twMerge(DEFAULT_LABEL_CLASSNAME, labelClassName)}>
          Minutes
        </span>
      </div>
      {showSeconds && (
        <div className={twMerge(DEFAULT_NUMBER_CLASSNAME, numberClassName)}>
          <span className={twMerge(DEFAULT_DIGITS_CLASSNAME, numberClassName)}>
            {seconds}
          </span>
          <span className={twMerge(DEFAULT_LABEL_CLASSNAME, labelClassName)}>
            Seconds
          </span>
        </div>
      )}
    </div>
  );
};

export default Countdown;
