import React, { useContext, useState } from "react";
import { useSpring, animated } from "react-spring";
import classNames from "classnames";
import { easeCubicInOut } from "d3-ease";

import { bemHOF } from "../../utilities/bem";
import { DrawerContext } from "../../contexts/drawer";
import { Box, BoxProps } from "../Box";
import { TopBar } from "../TopBar";
import { CloseButton } from "../CloseButton";
import { Flex } from "../Flex";

const cn = bemHOF("Drawer");

export interface WithDrawerProps {
  /**
   * The content of the drawer
   */
  drawerSlot: React.ReactNode;

  /**
   * Called when the close button is clicked
   */
  onClose: () => void;

  /**
   * Determines whether the `Drawer` is open
   */
  isOpen?: boolean;

  /**
   * Content that will shrink as the Drawer animates in
   */
  fixedSlotTop?: React.ReactNode;

  /**
   * Content that renders below the `scrollableSlot` and will shrink as the Drawer animates in
   */
  fixedSlotBottom?: React.ReactNode;

  /**
   * Scrollable content that will be pushed off screen as the Drawer animates in
   */
  scrollableSlot?: React.ReactNode;

  /**
   * Puts the drawer on the left or right of the content
   */
  drawerSide?: "left" | "right";

  /**
   * Removes the animation
   */
  noAnimation?: boolean;

  /**
   * Overrides the default drawer width
   */
  drawerWidth?: number;
}

const DEFAULT_DRAWER_WIDTH = 264;

const calculateTransformValue = (
  isOpen: boolean,
  drawerOnRight: boolean,
  drawerWidth: number,
) => {
  const translateZero = "translateX(0px)";
  const translateNegWidth = `translateX(${-drawerWidth}px)`;
  if (isOpen) {
    return drawerOnRight ? translateNegWidth : translateZero;
  }
  return drawerOnRight ? translateZero : translateNegWidth;
};

export const WithDrawer = ({
  drawerSlot,
  onClose,
  isOpen = false,
  fixedSlotTop,
  fixedSlotBottom,
  scrollableSlot,
  drawerSide = "left",
  noAnimation = false,
  drawerWidth = DEFAULT_DRAWER_WIDTH,
}: WithDrawerProps) => {
  const drawerOnRight = drawerSide === "right";
  const [isClosedAtRest, setIsClosedAtRest] = useState(false);
  const { transform, paddingLeft, paddingRight } = useSpring({
    transform: calculateTransformValue(isOpen, drawerOnRight, drawerWidth),
    paddingLeft: isOpen && !drawerOnRight ? drawerWidth : 0,
    paddingRight: isOpen && drawerOnRight ? drawerWidth : 0,
    onFrame: ({
      paddingLeft: pl,
      paddingRight: pr,
    }: {
      paddingLeft: number;
      paddingRight: number;
    }) => {
      const padding = drawerOnRight ? pr : pl;
      if (padding === 0 && !isClosedAtRest) {
        setIsClosedAtRest(true);
      } else if (padding !== 0 && isClosedAtRest) {
        setIsClosedAtRest(false);
      }
    },
    config: {
      duration: noAnimation ? 0 : 300,
      easing: easeCubicInOut,
    },
  });

  const { transform: transformReverse } = useSpring({
    transform:
      isOpen && !drawerOnRight
        ? `translateX(${drawerWidth}px)`
        : "translateX(0px)",
  });

  // Setting the width in js so we don't have to keep css/js in sync
  const wrapperCombinedStyle = {
    width: drawerWidth,
    transform,
  };

  const scrollableSlotPaddingToInterpolate = drawerOnRight
    ? paddingRight
    : paddingLeft;

  return (
    <DrawerContext.Provider
      value={{ isOpen, onClose, isClosedAtRest, drawerSide }}
    >
      <Box
        className={classNames(cn(), {
          [cn({ m: "isOpen" })]: isOpen,
        })}
      >
        <animated.div
          className={classNames(cn({ e: "wrapper" }), { drawerOnRight })}
          style={wrapperCombinedStyle}
        >
          {drawerSlot}
        </animated.div>
        <Flex
          className={classNames(cn({ e: "onScreenContent" }), {
            drawerOnRight,
          })}
        >
          {fixedSlotTop && (
            <animated.div
              style={{ paddingLeft, paddingRight }}
              className={cn({ e: "fixedSlot" })}
            >
              {fixedSlotTop}
            </animated.div>
          )}
          {scrollableSlot && (
            <animated.div
              style={{
                transform: transformReverse,
                width: scrollableSlotPaddingToInterpolate.interpolate(
                  (w) => `calc(100% - ${w}px)`,
                ),
              }}
              className={cn({ e: "scrollableSlot" })}
            >
              {scrollableSlot}
            </animated.div>
          )}
          {fixedSlotBottom && (
            <animated.div
              style={{ paddingLeft, paddingRight }}
              className={cn({ e: "fixedSlot" })}
            >
              {fixedSlotBottom}
            </animated.div>
          )}
        </Flex>
      </Box>
    </DrawerContext.Provider>
  );
};

export interface DrawerProps extends BoxProps {
  /**
   * The content on the left side of `Drawer`'s `TopBar`
   */
  headingSlot: React.ReactNode;

  /**
   * Optional footer content in the `Drawer`
   */
  footerSlot?: React.ReactNode;

  /**
   * Changes styles based on whether the `Drawer` is on the left or ride side
   */
  drawerSide?: "left" | "right";
}

export const Drawer = ({
  children,
  headingSlot,
  footerSlot,
  className,
  drawerSide,
  ...rest
}: DrawerProps) => {
  const { onClose } = useContext(DrawerContext);

  return (
    <Flex
      className={classNames(cn({ e: "innerWrapper" }), className)}
      {...rest}
    >
      <TopBar
        className={cn({ e: "topbar" })}
        leftSlot={headingSlot}
        rightSlot={
          <CloseButton className="text-icon -mr-8" onClick={onClose} />
        }
      />
      <Box className={cn({ e: "drawerContent" })}>{children}</Box>
      {!!footerSlot && <Box className={cn({ e: "footer" })}>{footerSlot}</Box>}
    </Flex>
  );
};
