import React from "react";
import { ClassNames } from "@emotion/react";
import {
  Box,
  Button,
  DarkMode,
  Flex,
  FormControl,
  FormHelperText,
  FormLabel,
  HStack,
  IconButton,
  ListItem,
  Popover,
  PopoverArrow,
  PopoverBody,
  PopoverContent,
  PopoverTrigger,
  Portal,
  Stack,
  Switch,
  Tooltip,
  UnorderedList,
  useClipboard,
  useToast,
} from "@chakra-ui/react";
import {
  ArrowBackIcon,
  CheckIcon,
  ChevronDownIcon,
  DownloadIcon,
  LinkIcon,
  SettingsIcon,
} from "@chakra-ui/icons";
import { MdPause, MdPlayArrow } from "react-icons/md";

import { getBestImageUrlForLayer } from "../components/OutfitPreview";
import HTML5Badge, { layerUsesHTML5 } from "../components/HTML5Badge";
import PosePicker from "./PosePicker";
import SpeciesColorPicker from "../components/SpeciesColorPicker";
import { loadImage, useLocalStorage } from "../util";
import useCurrentUser from "../components/useCurrentUser";
import useOutfitAppearance from "../components/useOutfitAppearance";
import OutfitKnownGlitchesBadge from "./OutfitKnownGlitchesBadge";
import usePreferArchive from "../components/usePreferArchive";

/**
 * OutfitControls is the set of controls layered over the outfit preview, to
 * control things like species/color and sharing links!
 */
function OutfitControls({
  outfitState,
  dispatchToOutfit,
  showAnimationControls,
  appearance,
}) {
  const [focusIsLocked, setFocusIsLocked] = React.useState(false);
  const onLockFocus = React.useCallback(
    () => setFocusIsLocked(true),
    [setFocusIsLocked]
  );
  const onUnlockFocus = React.useCallback(
    () => setFocusIsLocked(false),
    [setFocusIsLocked]
  );

  // HACK: As of 1.0.0-rc.0, Chakra's `toast` function rebuilds unnecessarily,
  //       which triggers unnecessary rebuilds of the `onSpeciesColorChange`
  //       callback, which causes the `React.memo` on `SpeciesColorPicker` to
  //       fail, which harms performance. But it seems to work just fine if we
  //       hold onto the first copy of the function we get! :/
  const _toast = useToast();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const toast = React.useMemo(() => _toast, []);

  const onSpeciesColorChange = React.useCallback(
    (species, color, isValid, closestPose) => {
      if (isValid) {
        dispatchToOutfit({
          type: "setSpeciesAndColor",
          speciesId: species.id,
          colorId: color.id,
          pose: closestPose,
        });
      } else {
        // NOTE: This shouldn't be possible to trigger, because the
        //       `stateMustAlwaysBeValid` prop should prevent it. But we have
        //       it as a fallback, just in case!
        toast({
          title: `We haven't seen a ${color.name} ${species.name} before! 😓`,
          status: "warning",
        });
      }
    },
    [dispatchToOutfit, toast]
  );

  const maybeUnlockFocus = (e) => {
    // We lock focus when a touch-device user taps the area. When they tap
    // empty space, we treat that as a toggle and release the focus lock.
    if (e.target === e.currentTarget) {
      onUnlockFocus();
    }
  };

  return (
    <ClassNames>
      {({ css, cx }) => (
        <Box
          role="group"
          pos="absolute"
          left="0"
          right="0"
          top="0"
          bottom="0"
          height="100%" // Required for Safari to size the grid correctly
          padding={{ base: 2, lg: 6 }}
          display="grid"
          overflow="auto"
          gridTemplateAreas={`"back play-pause sharing"
                          "space space space"
                          "picker picker picker"`}
          gridTemplateRows="auto minmax(1rem, 1fr) auto"
          className={cx(
            css`
              opacity: 0;
              transition: opacity 0.2s;

              &:focus-within,
              &.focus-is-locked {
                opacity: 1;
              }

              /* Ignore simulated hovers, only reveal for _real_ hovers. This helps
           * us avoid state conflicts with the focus-lock from clicks. */
              @media (hover: hover) {
                &:hover {
                  opacity: 1;
                }
              }
            `,
            focusIsLocked && "focus-is-locked"
          )}
          onClickCapture={(e) => {
            const opacity = parseFloat(
              getComputedStyle(e.currentTarget).opacity
            );
            if (opacity < 0.5) {
              // If the controls aren't visible right now, then clicks on them are
              // probably accidental. Ignore them! (We prevent default to block
              // built-in behaviors like link nav, and we stop propagation to block
              // our own custom click handlers. I don't know if I can prevent the
              // select clicks though?)
              e.preventDefault();
              e.stopPropagation();

              // We also show the controls, by locking focus. We'll undo this when
              // the user taps elsewhere (because it will trigger a blur event from
              // our child components), in `maybeUnlockFocus`.
              setFocusIsLocked(true);
            }
          }}
          onContextMenuCapture={() => {
            if (!toast.isActive("outfit-controls-context-menu-hint")) {
              toast({
                id: "outfit-controls-context-menu-hint",
                title:
                  "By the way, to save this image, use the Download button!",
                description: "It's in the top right of the preview area.",
                duration: 10000,
                isClosable: true,
              });
            }
          }}
          data-test-id="wardrobe-outfit-controls"
        >
          <Box gridArea="back" onClick={maybeUnlockFocus}>
            <BackButton outfitState={outfitState} />
          </Box>

          <Flex
            gridArea="play-pause"
            // HACK: Better visual centering with other controls
            paddingTop="0.3rem"
            direction="column"
            align="center"
          >
            {showAnimationControls && <PlayPauseButton />}
            <Box height="2" />
            <HStack spacing="2" align="center" justify="center">
              <OutfitHTML5Badge appearance={appearance} />
              <OutfitKnownGlitchesBadge appearance={appearance} />
              <SettingsButton
                onLockFocus={onLockFocus}
                onUnlockFocus={onUnlockFocus}
              />
            </HStack>
          </Flex>
          <Stack
            gridArea="sharing"
            alignSelf="flex-end"
            spacing={{ base: "2", lg: "4" }}
            align="flex-end"
            onClick={maybeUnlockFocus}
          >
            <Box>
              <DownloadButton outfitState={outfitState} />
            </Box>
            <Box>
              <CopyLinkButton outfitState={outfitState} />
            </Box>
          </Stack>
          <Box gridArea="space" onClick={maybeUnlockFocus} />
          {outfitState.speciesId && outfitState.colorId && (
            <Flex gridArea="picker" justify="center" onClick={maybeUnlockFocus}>
              {/**
               * We try to center the species/color picker, but the left spacer will
               * shrink more than the pose picker container if we run out of space!
               */}
              <Flex
                flex="1 1 0"
                paddingRight="3"
                align="center"
                justify="flex-end"
              />
              <Box flex="0 0 auto">
                <DarkMode>
                  <SpeciesColorPicker
                    speciesId={outfitState.speciesId}
                    colorId={outfitState.colorId}
                    idealPose={outfitState.pose}
                    onChange={onSpeciesColorChange}
                    stateMustAlwaysBeValid
                    speciesTestId="wardrobe-species-picker"
                    colorTestId="wardrobe-color-picker"
                  />
                </DarkMode>
              </Box>
              <Flex flex="1 1 0" align="center" pl="2">
                <PosePicker
                  speciesId={outfitState.speciesId}
                  colorId={outfitState.colorId}
                  pose={outfitState.pose}
                  appearanceId={outfitState.appearanceId}
                  dispatchToOutfit={dispatchToOutfit}
                  onLockFocus={onLockFocus}
                  onUnlockFocus={onUnlockFocus}
                  data-test-id="wardrobe-pose-picker"
                />
              </Flex>
            </Flex>
          )}
        </Box>
      )}
    </ClassNames>
  );
}

function OutfitHTML5Badge({ appearance }) {
  const petIsUsingHTML5 =
    appearance.petAppearance?.layers.every(layerUsesHTML5);

  const itemsNotUsingHTML5 = appearance.items.filter((item) =>
    item.appearance.layers.some((l) => !layerUsesHTML5(l))
  );
  itemsNotUsingHTML5.sort((a, b) => a.name.localeCompare(b.name));

  const usesHTML5 = petIsUsingHTML5 && itemsNotUsingHTML5.length === 0;

  let tooltipLabel;
  if (usesHTML5) {
    tooltipLabel = (
      <>This outfit is converted to HTML5, and ready to use on Neopets.com!</>
    );
  } else {
    tooltipLabel = (
      <Box>
        <Box as="p">
          This outfit isn't converted to HTML5 yet, so it might not appear in
          Neopets.com customization yet. Once it's ready, it could look a bit
          different than our temporary preview here. It might even be animated!
        </Box>
        {!petIsUsingHTML5 && (
          <Box as="p" marginTop="1em" fontWeight="bold">
            This pet is not yet converted.
          </Box>
        )}
        {itemsNotUsingHTML5.length > 0 && (
          <>
            <Box as="header" marginTop="1em" fontWeight="bold">
              The following items aren't yet converted:
            </Box>
            <UnorderedList>
              {itemsNotUsingHTML5.map((item) => (
                <ListItem key={item.id}>{item.name}</ListItem>
              ))}
            </UnorderedList>
          </>
        )}
      </Box>
    );
  }

  return (
    <HTML5Badge
      usesHTML5={usesHTML5}
      isLoading={appearance.loading}
      tooltipLabel={tooltipLabel}
    />
  );
}

/**
 * BackButton takes you back home, or to Your Outfits if this outfit is yours.
 */
function BackButton({ outfitState }) {
  const currentUser = useCurrentUser();
  const outfitBelongsToCurrentUser =
    outfitState.creator && outfitState.creator.id === currentUser.id;

  return (
    <ControlButton
      as="a"
      href={outfitBelongsToCurrentUser ? "/your-outfits" : "/"}
      icon={<ArrowBackIcon />}
      aria-label="Leave this outfit"
      d="inline-flex" // Not sure why <a> requires this to style right! ^^`
      data-test-id="wardrobe-nav-back-button"
    />
  );
}

/**
 * DownloadButton downloads the outfit as an image!
 */
function DownloadButton({ outfitState }) {
  const { visibleLayers } = useOutfitAppearance(outfitState);

  const [downloadImageUrl, prepareDownload] =
    useDownloadableImage(visibleLayers);

  return (
    <Tooltip label="Download" placement="left">
      <Box>
        <ControlButton
          icon={<DownloadIcon />}
          aria-label="Download"
          as="a"
          // eslint-disable-next-line no-script-url
          href={downloadImageUrl || "#"}
          onClick={(e) => {
            if (!downloadImageUrl) {
              e.preventDefault();
            }
          }}
          download={(outfitState.name || "Outfit") + ".png"}
          onMouseEnter={prepareDownload}
          onFocus={prepareDownload}
          cursor={!downloadImageUrl && "wait"}
        />
      </Box>
    </Tooltip>
  );
}

/**
 * CopyLinkButton copies the outfit URL to the clipboard!
 */
function CopyLinkButton({ outfitState }) {
  const { onCopy, hasCopied } = useClipboard(outfitState.url);

  return (
    <Tooltip label={hasCopied ? "Copied!" : "Copy link"} placement="left">
      <Box>
        <ControlButton
          icon={hasCopied ? <CheckIcon /> : <LinkIcon />}
          aria-label="Copy link"
          onClick={onCopy}
        />
      </Box>
    </Tooltip>
  );
}

function PlayPauseButton() {
  const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true);

  // We show an intro animation if this mounts while paused. Whereas if we're
  // not paused, we initialize as if we had already finished.
  const [blinkInState, setBlinkInState] = React.useState(
    isPaused ? { type: "ready" } : { type: "done" }
  );
  const buttonRef = React.useRef(null);

  React.useLayoutEffect(() => {
    if (blinkInState.type === "ready" && buttonRef.current) {
      setBlinkInState({
        type: "started",
        position: {
          left: buttonRef.current.offsetLeft,
          top: buttonRef.current.offsetTop,
        },
      });
    }
  }, [blinkInState, setBlinkInState]);

  return (
    <ClassNames>
      {({ css }) => (
        <>
          <PlayPauseButtonContent
            isPaused={isPaused}
            setIsPaused={setIsPaused}
            ref={buttonRef}
          />
          {blinkInState.type === "started" && (
            <Portal>
              <PlayPauseButtonContent
                isPaused={isPaused}
                setIsPaused={setIsPaused}
                position="absolute"
                left={blinkInState.position.left}
                top={blinkInState.position.top}
                backgroundColor="gray.600"
                borderColor="gray.50"
                color="gray.50"
                onAnimationEnd={() => setBlinkInState({ type: "done" })}
                // Don't disrupt the hover state of the controls! (And the button
                // doesn't seem to click correctly, not sure why, but instead of
                // debugging I'm adding this :p)
                pointerEvents="none"
                className={css`
                  @keyframes fade-in-out {
                    0% {
                      opacity: 0;
                    }

                    10% {
                      opacity: 1;
                    }

                    90% {
                      opacity: 1;
                    }

                    100% {
                      opacity: 0;
                    }
                  }

                  opacity: 0;
                  animation: fade-in-out 2s;
                `}
              />
            </Portal>
          )}
        </>
      )}
    </ClassNames>
  );
}

const PlayPauseButtonContent = React.forwardRef(
  ({ isPaused, setIsPaused, ...props }, ref) => {
    return (
      <TranslucentButton
        ref={ref}
        leftIcon={isPaused ? <MdPause /> : <MdPlayArrow />}
        onClick={() => setIsPaused(!isPaused)}
        {...props}
      >
        {isPaused ? <>Paused</> : <>Playing</>}
      </TranslucentButton>
    );
  }
);

function SettingsButton({ onLockFocus, onUnlockFocus }) {
  return (
    <Popover onOpen={onLockFocus} onClose={onUnlockFocus}>
      <PopoverTrigger>
        <TranslucentButton size="xs" aria-label="Settings">
          <SettingsIcon />
          <Box width="1" />
          <ChevronDownIcon />
        </TranslucentButton>
      </PopoverTrigger>
      <Portal>
        <PopoverContent width="25ch">
          <PopoverArrow />
          <PopoverBody>
            <HiResModeSetting />
          </PopoverBody>
        </PopoverContent>
      </Portal>
    </Popover>
  );
}

function HiResModeSetting() {
  const [hiResMode, setHiResMode] = useLocalStorage("DTIHiResMode", false);
  const [preferArchive, setPreferArchive] = usePreferArchive();

  return (
    <Box>
      <FormControl>
        <Flex>
          <Box>
            <FormLabel htmlFor="hi-res-mode-setting" fontSize="sm" margin="0">
              Hi-res mode (SVG)
            </FormLabel>
            <FormHelperText marginTop="0" fontSize="xs">
              Crisper at higher resolutions, but not always accurate
            </FormHelperText>
          </Box>
          <Box width="2" />
          <Switch
            id="hi-res-mode-setting"
            size="sm"
            marginTop="0.1rem"
            isChecked={hiResMode}
            onChange={(e) => setHiResMode(e.target.checked)}
          />
        </Flex>
      </FormControl>
      <Box height="2" />
      <FormControl>
        <Flex>
          <Box>
            <FormLabel
              htmlFor="prefer-archive-setting"
              fontSize="sm"
              margin="0"
            >
              Use DTI's image archive
            </FormLabel>
            <FormHelperText marginTop="0" fontSize="xs">
              Turn this on when images.neopets.com is slow!
            </FormHelperText>
          </Box>
          <Box width="2" />
          <Switch
            id="prefer-archive-setting"
            size="sm"
            marginTop="0.1rem"
            isChecked={preferArchive ?? false}
            onChange={(e) => setPreferArchive(e.target.checked)}
          />
        </Flex>
      </FormControl>
    </Box>
  );
}

const TranslucentButton = React.forwardRef(({ children, ...props }, ref) => {
  return (
    <Button
      ref={ref}
      size="sm"
      color="gray.100"
      variant="outline"
      borderColor="gray.200"
      borderRadius="full"
      backgroundColor="blackAlpha.600"
      boxShadow="md"
      _hover={{
        backgroundColor: "gray.600",
        borderColor: "gray.50",
        color: "gray.50",
      }}
      _focus={{
        backgroundColor: "gray.600",
        borderColor: "gray.50",
        color: "gray.50",
      }}
      {...props}
    >
      {children}
    </Button>
  );
});

/**
 * ControlButton is a UI helper to render the cute round buttons we use in
 * OutfitControls!
 */
function ControlButton({ icon, "aria-label": ariaLabel, ...props }) {
  return (
    <IconButton
      icon={icon}
      aria-label={ariaLabel}
      isRound
      variant="unstyled"
      backgroundColor="gray.600"
      color="gray.50"
      boxShadow="md"
      d="flex"
      alignItems="center"
      justifyContent="center"
      transition="backgroundColor 0.2s"
      _focus={{ backgroundColor: "gray.500" }}
      _hover={{ backgroundColor: "gray.500" }}
      outline="initial"
      {...props}
    />
  );
}

/**
 * useDownloadableImage loads the image data and generates the downloadable
 * image URL.
 */
function useDownloadableImage(visibleLayers) {
  const [hiResMode] = useLocalStorage("DTIHiResMode", false);
  const [preferArchive] = usePreferArchive();

  const [downloadImageUrl, setDownloadImageUrl] = React.useState(null);
  const [preparedForLayerIds, setPreparedForLayerIds] = React.useState([]);
  const toast = useToast();

  const prepareDownload = React.useCallback(async () => {
    // Skip if the current image URL is already correct for these layers.
    const layerIds = visibleLayers.map((l) => l.id);
    if (layerIds.join(",") === preparedForLayerIds.join(",")) {
      return;
    }

    // Skip if there are no layers. (This probably means we're still loading!)
    if (layerIds.length === 0) {
      return;
    }

    setDownloadImageUrl(null);

    // NOTE: You could argue that we may as well just always use PNGs here,
    //       regardless of hi-res mode… but using the same src will help both
    //       performance (can use cached SVG), and predictability (image will
    //       look like what you see here).
    const imagePromises = visibleLayers.map((layer) =>
      loadImage(getBestImageUrlForLayer(layer, { hiResMode }), {
        crossOrigin: "anonymous",
        preferArchive,
      })
    );

    let images;
    try {
      images = await Promise.all(imagePromises);
    } catch (e) {
      console.error("Error building downloadable image", e);
      toast({
        status: "error",
        title: "Oops, sorry, we couldn't download the image!",
        description:
          "Check your connection, then reload the page and try again.",
      });
      return;
    }

    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");
    canvas.width = 600;
    canvas.height = 600;

    for (const image of images) {
      context.drawImage(image, 0, 0);
    }

    console.debug(
      "Generated image for download",
      layerIds,
      canvas.toDataURL("image/png")
    );
    setDownloadImageUrl(canvas.toDataURL("image/png"));
    setPreparedForLayerIds(layerIds);
  }, [preparedForLayerIds, visibleLayers, toast, hiResMode, preferArchive]);

  return [downloadImageUrl, prepareDownload];
}

export default OutfitControls;