import styled from '@emotion/styled';
import {
  Dispatch,
  SetStateAction,
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

/*

The Idea:
- there's a layout that contains elements
- the elements can ask the layout to resize to make them bigger or smaller

*/

const ListWrapper = styled.div`
  display: flex;
  flex-direction: column;
  gap: 2px;
`;

const TRANSITION_TIME_MS = 150;
const MODAL_PADDING = 20;

const SlotWrapper = styled.div`
  transition:
    height ${TRANSITION_TIME_MS}ms ease-in-out,
    width ${TRANSITION_TIME_MS}ms ease-in-out;
`;

const SlotContent = styled.div`
  height: 100%;
  width: 100%;
  transition:
    height ${TRANSITION_TIME_MS}ms ease-in-out,
    width ${TRANSITION_TIME_MS}ms ease-in-out,
    top ${TRANSITION_TIME_MS}ms ease-in-out,
    left ${TRANSITION_TIME_MS}ms ease-in-out;
`;

export type SlottableRole = 'slot' | 'modal' | 'page';

export const useNewLayoutSlotContext = ({
  setWidth: inputSetWidth,
  setHeight: inputSetHeight,
}: {
  setWidth?: Dispatch<SetStateAction<number>>;
  setHeight?: Dispatch<SetStateAction<number>>;
}) => {
  const enclosingContext = useContext(LayoutSlotContext);
  const setWidth = inputSetWidth || enclosingContext?._setWidth;
  const setHeight = inputSetHeight || enclosingContext?._setHeight;

  useEffect(() => {
    const onPopState = () => {
      const state = history.state;
      if (state?.putBack) {
        setMode(state.putBack);
      }
    };
    window.addEventListener('popstate', onPopState);
    return () => {
      window.removeEventListener('popstate', onPopState);
    };
  }, []);

  const requestSize = useCallback(
    ({
      width,
      height,
    }: {
      width?: number | ((prev: number) => number);
      height?: number | ((prev: number) => number);
    }) => {
      if (width && setWidth) setWidth(width);
      if (height && setHeight) setHeight(height);
    },
    [setWidth, setHeight]
  );

  const [mode, setMode] = useState<SlottableRole>('slot');

  const requestModal = useCallback(() => {
    setMode('modal');
  }, []);

  const requestPage = useCallback(
    (url: string) => {
      setMode('page');
      history.replaceState(
        {
          putBack: mode,
        },
        '',
        window.location.pathname
      );
      history.pushState({}, '', url);
    },
    [mode]
  );

  const putBack = useCallback(() => {
    setMode('slot');
  }, [mode]);

  return useMemo(
    () => ({
      requestSize,
      requestModal,
      requestPage,
      putBack,
      mode,
      _setWidth: setWidth,
      _setHeight: setHeight,
    }),
    [requestSize, requestModal, requestPage, putBack, mode, setWidth, setHeight]
  );
};

export const useLayoutSlotContext = ({
  minHeight,
  maxHeight,
  minWidth,
  maxWidth,
}: {
  minHeight?: number;
  maxHeight?: number;
  minWidth?: number;
  maxWidth?: number;
}) => {
  const layoutSlotContext = useContext(LayoutSlotContext);

  const requestSize = useCallback(
    ({
      height,
      width,
    }: {
      height?: number | ((prev: number) => number);
      width?: number | ((prev: number) => number);
    }) => {
      if (typeof height === 'number') {
        if (minHeight && height < minHeight) {
          height = minHeight;
        }
        if (maxHeight && height > maxHeight) {
          height = maxHeight;
        }
      } else if (typeof height === 'function') {
        const oldHeightFn = height;
        height = (prev: number) => {
          const result = oldHeightFn(prev);
          if (minHeight && result < minHeight) {
            return minHeight;
          }
          if (maxHeight && result > maxHeight) {
            return maxHeight;
          }
          return result;
        };
      }
      if (typeof width === 'number') {
        if (minWidth && width < minWidth) {
          width = minWidth;
        }
        if (maxWidth && width > maxWidth) {
          width = maxWidth;
        }
      } else if (typeof width === 'function') {
        const oldWidthFn = width;
        width = (prev: number) => {
          const result = oldWidthFn(prev);
          if (minWidth && result < minWidth) {
            return minWidth;
          }
          if (maxWidth && result > maxWidth) {
            return maxWidth;
          }
          return result;
        };
      }
      layoutSlotContext.requestSize({ height, width });
    },
    [layoutSlotContext]
  );

  return useMemo(
    () => ({
      ...layoutSlotContext,
      requestSize,
    }),
    [requestSize, layoutSlotContext]
  );
};

export const LayoutSlotContext = createContext<{
  requestSize: (size: {
    height?: number | ((prev: number) => number);
    width?: number | ((prev: number) => number);
  }) => void;
  requestModal: () => void;
  requestPage: (url: string) => void;
  putBack: () => void;
  mode: SlottableRole;
  _setWidth: Dispatch<SetStateAction<number>>;
  _setHeight: Dispatch<SetStateAction<number>>;
}>(undefined as never);

const animateToModal = (content: HTMLDivElement) => {
  const contentRect = content.getBoundingClientRect();

  const contentHeight = contentRect.height;
  const contentWidth = contentRect.width;
  const contentTop = contentRect.top;
  const contentLeft = contentRect.left;

  content.style.transition = 'none';
  content.style.height = `${contentHeight}px`;
  content.style.width = `${contentWidth}px`;
  content.style.top = `${contentTop}px`;
  content.style.left = `${contentLeft}px`;
  content.style.zIndex = '1000';
  content.style.position = 'fixed';
  setTimeout(() => {
    content.style.transition = '';
    content.style.height = window.innerHeight - MODAL_PADDING * 2 + 'px';
    content.style.width = window.innerWidth - MODAL_PADDING * 2 + 'px';
    content.style.top = `${MODAL_PADDING}px`;
    content.style.left = `${MODAL_PADDING}px`;
    setTimeout(() => {
      content.style.height = `calc(100vh - ${MODAL_PADDING * 2}px)`;
      content.style.width = `calc(100vw - ${MODAL_PADDING * 2}px)`;
      content.style.top = `${MODAL_PADDING}px`;
      content.style.left = `${MODAL_PADDING}px`;
    }, TRANSITION_TIME_MS);
  }, 0);
};

const animateToPage = (content: HTMLDivElement) => {
  const contentRect = content.getBoundingClientRect();

  const contentHeight = contentRect.height;
  const contentWidth = contentRect.width;
  const contentTop = contentRect.top;
  const contentLeft = contentRect.left;

  content.style.transition = 'none';
  content.style.height = `${contentHeight}px`;
  content.style.width = `${contentWidth}px`;
  content.style.top = `${contentTop}px`;
  content.style.left = `${contentLeft}px`;
  content.style.zIndex = '1000';
  content.style.position = 'fixed';
  setTimeout(() => {
    content.style.transition = '';
    content.style.height = `${window.innerHeight}px`;
    content.style.width = `${window.innerWidth}px`;
    content.style.top = '0';
    content.style.left = '0';
    setTimeout(() => {
      content.style.height = `100vh`;
      content.style.width = `100vw`;
      content.style.top = '0';
      content.style.left = '0';
    }, TRANSITION_TIME_MS);
  }, 0);
};

const animateToSlot = (wrapper: HTMLDivElement, content: HTMLDivElement) => {
  const wrapperRect = wrapper.getBoundingClientRect();

  const wrapperHeight = wrapperRect.height;
  const wrapperWidth = wrapperRect.width;
  const wrapperTop = wrapperRect.top;
  const wrapperLeft = wrapperRect.left;

  content.style.transition = '';
  content.style.height = `${wrapperHeight}px`;
  content.style.width = `${wrapperWidth}px`;
  content.style.top = `${wrapperTop}px`;
  content.style.left = `${wrapperLeft}px`;
  setTimeout(() => {
    content.style.position = '';
    content.style.transition = '';
    content.style.top = '';
    content.style.left = '';
    content.style.width = '';
    content.style.height = '';
    content.style.zIndex = '';
  }, TRANSITION_TIME_MS);
};

const SlotAnimator = ({
  children,
  width,
  height,
}: {
  children: React.ReactNode;
  width?: string | number;
  height?: string | number;
}) => {
  const layoutSlotContext = useContext(LayoutSlotContext);
  const wrapperRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const previousRole = useRef(layoutSlotContext.mode);

  useEffect(() => {
    const wrapper = wrapperRef.current;
    const content = contentRef.current;
    if (!wrapper || !content) return;
    if (layoutSlotContext.mode === 'page' && previousRole.current !== 'page') {
      previousRole.current = 'page';
      animateToPage(content);
    } else if (
      layoutSlotContext.mode === 'modal' &&
      previousRole.current !== 'modal'
    ) {
      previousRole.current = 'modal';
      animateToModal(content);
    } else if (
      layoutSlotContext.mode === 'slot' &&
      previousRole.current !== 'slot'
    ) {
      previousRole.current = 'slot';
      animateToSlot(wrapper, content);
    }
  }, [layoutSlotContext.mode, animateToModal, animateToSlot]);

  return (
    <SlotWrapper ref={wrapperRef} style={{ width, height }}>
      <SlotContent ref={contentRef}>{children}</SlotContent>
    </SlotWrapper>
  );
};

export const RowSlot = ({
  height,
  setHeight,
  children,
}: {
  height: number;
  setHeight: Dispatch<SetStateAction<number>>;
  children: React.ReactNode;
}) => {
  const layoutSlotContext = useNewLayoutSlotContext({ setHeight });

  return (
    <LayoutSlotContext.Provider value={layoutSlotContext}>
      <SlotAnimator height={height}>{children}</SlotAnimator>
    </LayoutSlotContext.Provider>
  );
};

export const ColumnSlot = ({
  width,
  setWidth,
  children,
}: {
  width: number;
  setWidth: Dispatch<SetStateAction<number>>;
  children: React.ReactNode;
}) => {
  const layoutSlotContext = useNewLayoutSlotContext({ setWidth });

  return (
    <LayoutSlotContext.Provider value={layoutSlotContext}>
      <SlotAnimator width={width}>{children}</SlotAnimator>
    </LayoutSlotContext.Provider>
  );
};

export const BoxSlot = ({
  width,
  height,
  setWidth,
  setHeight,
  children,
}: {
  width: number;
  height: number;
  setWidth: Dispatch<SetStateAction<number>>;
  setHeight: Dispatch<SetStateAction<number>>;
  children: React.ReactNode;
}) => {
  const layoutSlotContext = useNewLayoutSlotContext({ setWidth, setHeight });

  return (
    <LayoutSlotContext.Provider value={layoutSlotContext}>
      <SlotAnimator width={width} height={height}>
        {children}
      </SlotAnimator>
    </LayoutSlotContext.Provider>
  );
};

export const Slot = ({ children }: { children: React.ReactNode }) => {
  const layoutSlotContext = useNewLayoutSlotContext({});

  return (
    <LayoutSlotContext.Provider value={layoutSlotContext}>
      <SlotAnimator width={'100%'} height={'100%'}>
        {children}
      </SlotAnimator>
    </LayoutSlotContext.Provider>
  );
};

export const SlottableList = ({
  children,
  defaultRowHeight,
}: {
  children: React.ReactNode[];
  defaultRowHeight: number;
}) => {
  const [rowHeights, setRowHeights] = useState<number[]>(() =>
    children.map(() => defaultRowHeight)
  );
  useEffect(() => {
    setRowHeights(children.map(() => defaultRowHeight));
  }, [children, defaultRowHeight]);
  const setRowHeight = useCallback(
    (index: number, height: number | ((prev: number) => number)) => {
      if (typeof height === 'number') {
        setRowHeights((prev) => [
          ...prev.slice(0, index),
          height,
          ...prev.slice(index + 1),
        ]);
      } else {
        setRowHeights((prev) => {
          const newHeights = [...prev];
          newHeights[index] = height(prev[index]);
          return newHeights;
        });
      }
    },
    []
  );
  return (
    <ListWrapper>
      {rowHeights.map((h, i) => (
        <RowSlot
          key={i}
          height={h}
          setHeight={(height) => setRowHeight(i, height)}
        >
          {children[i] || null}
        </RowSlot>
      ))}
    </ListWrapper>
  );
};
