import { useRef, MutableRefObject } from 'react';
import memoizeOne from 'memoize-one';
import { useMemo, useCallback } from 'use-memo-one';
import { invariant } from '../../invariant';
import type { StyleMarshal } from './style-marshal-types';
import type { ContextId, DropReason } from '../../types';
import getStyles from './get-styles';
import type { Styles } from './get-styles';
import { prefix } from '../data-attributes';
import useLayoutEffect from '../use-isomorphic-layout-effect';

const getHead = (): HTMLHeadElement => {
  const head: HTMLHeadElement | null = document.querySelector('head');
  invariant(head, 'Cannot find the head to append a style to');
  return head;
};

const createStyleEl = (nonce?: string): HTMLStyleElement => {
  const el: HTMLStyleElement = document.createElement('style');
  if (nonce) {
    el.setAttribute('nonce', nonce);
  }
  el.type = 'text/css';
  return el;
};

export default function useStyleMarshal(contextId: ContextId, nonce?: string) {
  const styles: Styles = useMemo(() => getStyles(contextId), [contextId]);
  const alwaysRef = useRef<HTMLStyleElement | null>(null);
  const dynamicRef = useRef<HTMLStyleElement | null>(null);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const setDynamicStyle = useCallback(
    // Using memoizeOne to prevent frequent updates to textContext
    memoizeOne((proposed: string) => {
      const el: HTMLStyleElement | null = dynamicRef.current;
      invariant(el, 'Cannot set dynamic style element if it is not set');
      el.textContent = proposed;
    }),
    [],
  );

  const setAlwaysStyle = useCallback((proposed: string) => {
    const el: HTMLStyleElement | null = alwaysRef.current;
    invariant(el, 'Cannot set dynamic style element if it is not set');
    el.textContent = proposed;
  }, []);

  // using layout effect as programatic dragging might start straight away (such as for cypress)
  useLayoutEffect(() => {
    invariant(
      !alwaysRef.current && !dynamicRef.current,
      'style elements already mounted',
    );

    const always: HTMLStyleElement = createStyleEl(nonce);
    const dynamic: HTMLStyleElement = createStyleEl(nonce);

    // store their refs
    alwaysRef.current = always;
    dynamicRef.current = dynamic;

    // for easy identification
    always.setAttribute(`${prefix}-always`, contextId);
    dynamic.setAttribute(`${prefix}-dynamic`, contextId);

    // add style tags to head
    getHead().appendChild(always);
    getHead().appendChild(dynamic);

    // set initial style
    setAlwaysStyle(styles.always);
    setDynamicStyle(styles.resting);

    return () => {
      const remove = (ref: MutableRefObject<HTMLStyleElement | null>) => {
        const current: HTMLStyleElement | null = ref.current;
        invariant(current, 'Cannot unmount ref as it is not set');
        getHead().removeChild(current);
        ref.current = null;
      };

      remove(alwaysRef);
      remove(dynamicRef);
    };
  }, [
    nonce,
    setAlwaysStyle,
    setDynamicStyle,
    styles.always,
    styles.resting,
    contextId,
  ]);

  const dragging = useCallback(
    () => setDynamicStyle(styles.dragging),
    [setDynamicStyle, styles.dragging],
  );
  const dropping = useCallback(
    (reason: DropReason) => {
      if (reason === 'DROP') {
        setDynamicStyle(styles.dropAnimating);
        return;
      }
      setDynamicStyle(styles.userCancel);
    },
    [setDynamicStyle, styles.dropAnimating, styles.userCancel],
  );
  const resting = useCallback(() => {
    // Can be called defensively
    if (!dynamicRef.current) {
      return;
    }
    setDynamicStyle(styles.resting);
  }, [setDynamicStyle, styles.resting]);

  const marshal: StyleMarshal = useMemo(
    () => ({
      dragging,
      dropping,
      resting,
    }),
    [dragging, dropping, resting],
  );

  return marshal;
}
