import React from "react";
import { css } from "emotion";
import {
  AspectRatio,
  Badge,
  Button,
  Box,
  IconButton,
  Skeleton,
  SkeletonText,
  Tooltip,
  VisuallyHidden,
  VStack,
  useBreakpointValue,
  useColorModeValue,
  useTheme,
  useToast,
} from "@chakra-ui/core";
import {
  CheckIcon,
  ExternalLinkIcon,
  ChevronRightIcon,
  StarIcon,
  WarningIcon,
} from "@chakra-ui/icons";
import { MdPause, MdPlayArrow } from "react-icons/md";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { useParams } from "react-router-dom";

import {
  ItemBadgeList,
  ItemThumbnail,
  NcBadge,
  NpBadge,
} from "./components/ItemCard";
import { Delay, Heading1, usePageTitle } from "./util";
import {
  itemAppearanceFragment,
  petAppearanceFragment,
} from "./components/useOutfitAppearance";
import OutfitPreview from "./components/OutfitPreview";
import SpeciesColorPicker from "./components/SpeciesColorPicker";
import { useLocalStorage } from "./util";

function ItemPage() {
  const { itemId } = useParams();
  return <ItemPageContent itemId={itemId} />;
}

/**
 * ItemPageContent is the content of ItemPage, but we also use it as the
 * entry point for ItemPageDrawer! When embedded in ItemPageDrawer, the
 * `isEmbedded` prop is true, so we know not to e.g. set the page title.
 */
export function ItemPageContent({ itemId, isEmbedded }) {
  return (
    <VStack spacing="8">
      <ItemPageHeader itemId={itemId} isEmbedded={isEmbedded} />
      <ItemPageOwnWantButtons itemId={itemId} />
      <ItemPageOutfitPreview itemId={itemId} />
    </VStack>
  );
}

function ItemPageHeader({ itemId, isEmbedded }) {
  const { error, data } = useQuery(
    gql`
      query ItemPage($itemId: ID!) {
        item(id: $itemId) {
          id
          name
          isNc
          thumbnailUrl
          description
          createdAt
        }
      }
    `,
    { variables: { itemId }, returnPartialData: true }
  );

  usePageTitle(data?.item?.name, { skip: isEmbedded });

  // Show 2 lines of description text placeholder on small screens, or when
  // embedded in the wardrobe page's narrow drawer. In larger contexts, show
  // just 1 line.
  const viewportNumDescriptionLines = useBreakpointValue({ base: 2, md: 1 });
  const numDescriptionLines = isEmbedded ? 2 : viewportNumDescriptionLines;

  if (error) {
    return <Box color="red.400">{error.message}</Box>;
  }

  const item = data?.item;

  return (
    <>
      <Box
        display="flex"
        alignItems="center"
        justifyContent="flex-start"
        width="100%"
      >
        <SubtleSkeleton isLoaded={item?.thumbnailUrl} marginRight="4">
          <ItemThumbnail item={item} size="lg" isActive flex="0 0 auto" />
        </SubtleSkeleton>
        <Box>
          <SubtleSkeleton isLoaded={item?.name}>
            <Heading1
              lineHeight="1.1"
              // Nudge down the size a bit in the embed case, to better fit the
              // tighter layout!
              size={isEmbedded ? "xl" : "2xl"}
            >
              {item?.name || "Item name here"}
            </Heading1>
          </SubtleSkeleton>
          <ItemPageBadges item={item} isEmbedded={isEmbedded} />
        </Box>
      </Box>
      <Box width="100%" alignSelf="flex-start">
        {item?.description || (
          <Box
            maxWidth="40em"
            minHeight={numDescriptionLines * 1.5 + "em"}
            display="flex"
            flexDirection="column"
            alignItems="stretch"
            justifyContent="center"
          >
            <Delay ms={500}>
              <SkeletonText noOfLines={numDescriptionLines} spacing="4" />
            </Delay>
          </Box>
        )}
      </Box>
    </>
  );
}

function ItemPageBadges({ item, isEmbedded }) {
  const searchBadgesAreLoaded = item?.name != null && item?.isNc != null;

  return (
    <ItemBadgeList>
      <SubtleSkeleton isLoaded={item?.isNc != null}>
        {item?.isNc ? <NcBadge /> : <NpBadge />}
      </SubtleSkeleton>
      {
        // If the createdAt date is null (loaded and empty), hide the badge.
        item.createdAt !== null && (
          <SubtleSkeleton
            // Distinguish between undefined (still loading) and null (loaded and
            // empty).
            isLoaded={item.createdAt !== undefined}
          >
            <Badge
              display="block"
              minWidth="5.25em"
              boxSizing="content-box"
              textAlign="center"
            >
              {item.createdAt && <ShortTimestamp when={item.createdAt} />}
            </Badge>
          </SubtleSkeleton>
        )
      }
      <SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
        <LinkBadge
          href={`https://impress.openneo.net/items/${item.id}`}
          isEmbedded={isEmbedded}
        >
          Old DTI
        </LinkBadge>
      </SubtleSkeleton>
      <SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
        <LinkBadge
          href={
            "https://items.jellyneo.net/search/?name=" +
            encodeURIComponent(item.name) +
            "&name_type=3"
          }
          isEmbedded={isEmbedded}
        >
          Jellyneo
        </LinkBadge>
      </SubtleSkeleton>
      <SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
        {!item?.isNc && (
          <LinkBadge
            href={
              "http://www.neopets.com/market.phtml?type=wizard&string=" +
              encodeURIComponent(item.name)
            }
            isEmbedded={isEmbedded}
          >
            Shop Wiz
          </LinkBadge>
        )}
      </SubtleSkeleton>
      <SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
        {!item?.isNc && (
          <LinkBadge
            href={
              "http://www.neopets.com/portal/supershopwiz.phtml?string=" +
              encodeURIComponent(item.name)
            }
            isEmbedded={isEmbedded}
          >
            Super Wiz
          </LinkBadge>
        )}
      </SubtleSkeleton>
      <SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
        {!item?.isNc && (
          <LinkBadge
            href={
              "http://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact&search_string=" +
              encodeURIComponent(item.name)
            }
            isEmbedded={isEmbedded}
          >
            Trade Post
          </LinkBadge>
        )}
      </SubtleSkeleton>
      <SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
        {!item?.isNc && (
          <LinkBadge
            href={
              "http://www.neopets.com/genie.phtml?type=process_genie&criteria=exact&auctiongenie=" +
              encodeURIComponent(item.name)
            }
            isEmbedded={isEmbedded}
          >
            Auctions
          </LinkBadge>
        )}
      </SubtleSkeleton>
    </ItemBadgeList>
  );
}

function LinkBadge({ children, href, isEmbedded }) {
  return (
    <Badge
      as="a"
      href={href}
      display="flex"
      alignItems="center"
      // Normally we want to act like a normal webpage, and treat links as
      // normal. But when we're on the wardrobe page, we want to avoid
      // disrupting the outfit, and open in a new window instead.
      target={isEmbedded ? "_blank" : undefined}
    >
      {children}
      {
        // We also change the icon to signal whether this will launch in a new
        // window or not!
        isEmbedded ? <ExternalLinkIcon marginLeft="1" /> : <ChevronRightIcon />
      }
    </Badge>
  );
}

const fullDateFormatter = new Intl.DateTimeFormat("en-US", {
  dateStyle: "long",
});
const monthYearFormatter = new Intl.DateTimeFormat("en-US", {
  month: "short",
  year: "numeric",
});
const monthDayYearFormatter = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  year: "numeric",
});
function ShortTimestamp({ when }) {
  const date = new Date(when);

  // To find the start of last month, take today, then set its date to the 1st
  // and its time to midnight (the start of this month), and subtract one
  // month. (JS handles negative months and rolls them over correctly.)
  const startOfLastMonth = new Date();
  startOfLastMonth.setDate(1);
  startOfLastMonth.setHours(0);
  startOfLastMonth.setMinutes(0);
  startOfLastMonth.setSeconds(0);
  startOfLastMonth.setMilliseconds(0);
  startOfLastMonth.setMonth(startOfLastMonth.getMonth() - 1);

  const dateIsOlderThanLastMonth = date < startOfLastMonth;

  return (
    <Tooltip
      label={`First seen on ${fullDateFormatter.format(date)}`}
      placement="top"
      openDelay={400}
    >
      {dateIsOlderThanLastMonth
        ? monthYearFormatter.format(date)
        : monthDayYearFormatter.format(date)}
    </Tooltip>
  );
}

function ItemPageOwnWantButtons({ itemId }) {
  const theme = useTheme();
  const toast = useToast();

  const [currentUserOwnsThis, setCurrentUserOwnsThis] = React.useState(false);
  const [currentUserWantsThis, setCurrentUserWantsThis] = React.useState(false);

  const { loading, error } = useQuery(
    gql`
      query ItemPageOwnWantButtons($itemId: ID!) {
        item(id: $itemId) {
          id
          currentUserOwnsThis
          currentUserWantsThis
        }
      }
    `,
    {
      variables: { itemId },
      onCompleted: (data) => {
        setCurrentUserOwnsThis(data?.item?.currentUserOwnsThis || false);
        setCurrentUserWantsThis(data?.item?.currentUserWantsThis || false);
      },
    }
  );

  if (error) {
    return <Box color="red.400">{error.message}</Box>;
  }

  return (
    <Box display="flex">
      <SubtleSkeleton isLoaded={!loading} marginRight="4">
        <Box as="label">
          <VisuallyHidden
            as="input"
            type="checkbox"
            checked={currentUserOwnsThis}
            onChange={(e) => {
              setCurrentUserOwnsThis(e.target.checked);
              toast({
                title: "Todo: This doesn't actually work yet!",
                status: "info",
                duration: 1500,
              });
            }}
          />
          <Button
            as="div"
            colorScheme={currentUserOwnsThis ? "green" : "gray"}
            size="lg"
            cursor="pointer"
            transitionDuration="0.4s"
            className={css`
              input:focus + & {
                box-shadow: ${theme.shadows.outline};
              }
            `}
          >
            <IconCheckbox
              icon={<CheckIcon />}
              isChecked={currentUserOwnsThis}
              marginRight="0.5em"
            />
            I own this
          </Button>
        </Box>
      </SubtleSkeleton>

      <SubtleSkeleton isLoaded={!loading}>
        <Box as="label">
          <VisuallyHidden
            as="input"
            type="checkbox"
            isChecked={currentUserWantsThis}
            onChange={(e) => {
              setCurrentUserWantsThis(e.target.checked);
              toast({
                title: "Todo: This doesn't actually work yet!",
                status: "info",
                duration: 1500,
              });
            }}
          />
          <Button
            as="div"
            colorScheme={currentUserWantsThis ? "blue" : "gray"}
            size="lg"
            cursor="pointer"
            transitionDuration="0.4s"
            className={css`
              input:focus + & {
                box-shadow: ${theme.shadows.outline};
              }
            `}
          >
            <IconCheckbox
              icon={<StarIcon />}
              isChecked={currentUserWantsThis}
              marginRight="0.5em"
            />
            I want this
          </Button>
        </Box>
      </SubtleSkeleton>
    </Box>
  );
}

function IconCheckbox({ icon, isChecked, ...props }) {
  return (
    <Box display="grid" gridTemplateAreas="the-same-area" {...props}>
      <Box
        gridArea="the-same-area"
        width="1em"
        height="1em"
        border="2px solid currentColor"
        borderRadius="md"
        opacity={isChecked ? "0" : "0.75"}
        transform={isChecked ? "scale(0.75)" : "none"}
        transition="all 0.4s"
      />
      <Box
        gridArea="the-same-area"
        display="flex"
        opacity={isChecked ? "1" : "0"}
        transform={isChecked ? "none" : "scale(0.1)"}
        transition="all 0.4s"
      >
        {icon}
      </Box>
    </Box>
  );
}

function ItemPageOutfitPreview({ itemId }) {
  const idealPose = React.useMemo(
    () => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
    []
  );
  const [petState, setPetState] = React.useState({
    // We'll fill these in once the canonical appearance data arrives.
    speciesId: null,
    colorId: null,
    pose: null,

    // We use appearance ID, in addition to the above, to give the Apollo cache
    // a really clear hint that the canonical pet appearance we preloaded is
    // the exact right one to show! But switching species/color will null this
    // out again, and that's okay. (We'll do an unnecessary reload if you
    // switch back to it though... we could maybe do something clever there!)
    appearanceId: null,
  });

  // Start by loading the "canonical" pet and item appearance for the outfit
  // preview. We'll use this to initialize both the preview and the picker.
  //
  // TODO: If this is a non-standard pet color, like Mutant, we'll do an extra
  //       query after this loads, because our Apollo cache can't detect the
  //       shared item appearance. (For standard colors though, our logic to
  //       cover standard-color switches works for this preloading too.)
  const { loading, error } = useQuery(
    gql`
      query ItemPageOutfitPreview($itemId: ID!) {
        item(id: $itemId) {
          id
          canonicalAppearance {
            id
            ...ItemAppearanceForOutfitPreview
            body {
              id
              canonicalAppearance {
                id
                species {
                  id
                }
                color {
                  id
                }
                pose

                ...PetAppearanceForOutfitPreview
              }
            }
          }
        }
      }

      ${itemAppearanceFragment}
      ${petAppearanceFragment}
    `,
    {
      variables: { itemId },
      onCompleted: (data) => {
        const canonicalBody = data?.item?.canonicalAppearance?.body;
        const canonicalPetAppearance = canonicalBody?.canonicalAppearance;

        setPetState({
          speciesId: canonicalPetAppearance?.species?.id,
          colorId: canonicalPetAppearance?.color?.id,
          pose: canonicalPetAppearance?.pose,
          appearanceId: canonicalPetAppearance?.id,
        });
      },
    }
  );

  // To check whether the item is compatible with this pet, query for the
  // appearance, but only against the cache. That way, we don't send a
  // redundant network request just for this (the OutfitPreview component will
  // handle it!), but we'll get an update once it arrives in the cache.
  const { data: cachedData } = useQuery(
    gql`
      query ItemPageOutfitPreview_CacheOnly(
        $itemId: ID!
        $speciesId: ID!
        $colorId: ID!
      ) {
        item(id: $itemId) {
          appearanceOn(speciesId: $speciesId, colorId: $colorId) {
            layers {
              id
            }
          }
        }
      }
    `,
    {
      variables: {
        itemId,
        speciesId: petState.speciesId,
        colorId: petState.colorId,
      },
      fetchPolicy: "cache-only",
    }
  );

  const [hasAnimations, setHasAnimations] = React.useState(false);
  const [isPaused, setIsPaused] = useLocalStorage("DTIOutfitIsPaused", true);

  const borderColor = useColorModeValue("green.700", "green.400");
  const errorColor = useColorModeValue("red.600", "red.400");

  if (error) {
    return <Box color="red.400">{error.message}</Box>;
  }

  // If the layers are null-y, then we're still loading. Otherwise, if the
  // layers are an empty array, then we're incomaptible. Or, if they're a
  // non-empty array, then we're compatible!
  const layers = cachedData?.item?.appearanceOn?.layers;
  const isIncompatible = Array.isArray(layers) && layers.length === 0;

  return (
    <VStack spacing="3" width="100%">
      <AspectRatio
        width="300px"
        maxWidth="100%"
        ratio="1"
        border="1px"
        borderColor={borderColor}
        transition="border-color 0.2s"
        borderRadius="lg"
        boxShadow="lg"
        overflow="hidden"
      >
        <Box>
          <OutfitPreview
            speciesId={petState.speciesId}
            colorId={petState.colorId}
            pose={petState.pose}
            appearanceId={petState.appearanceId}
            wornItemIds={[itemId]}
            isLoading={loading}
            spinnerVariant="corner"
            loadingDelayMs={2000}
            engine="canvas"
            onChangeHasAnimations={setHasAnimations}
          />
          {hasAnimations && (
            <IconButton
              icon={isPaused ? <MdPlayArrow /> : <MdPause />}
              aria-label={isPaused ? "Play" : "Pause"}
              onClick={() => setIsPaused(!isPaused)}
              borderRadius="full"
              boxShadow="md"
              color="gray.50"
              backgroundColor="blackAlpha.700"
              position="absolute"
              bottom="2"
              left="2"
              _hover={{ backgroundColor: "blackAlpha.900" }}
              _focus={{ backgroundColor: "blackAlpha.900" }}
            />
          )}
        </Box>
      </AspectRatio>
      <Box display="flex" width="100%" alignItems="center">
        <Box
          // This empty box grows at the same rate as the box on the right, so
          // the middle box will be centered, if there's space!
          flex="1 0 0"
        />
        <SpeciesColorPicker
          speciesId={petState.speciesId}
          colorId={petState.colorId}
          pose={petState.pose}
          idealPose={idealPose}
          onChange={(species, color, _, closestPose) => {
            setPetState({
              speciesId: species.id,
              colorId: color.id,
              pose: closestPose,
              appearanceId: null,
            });
          }}
          size="sm"
          showPlaceholders
          // This is just a UX affordance: while we could handle invalid states
          // from a UI perspective, we figure that, if a pet preview is already
          // visible and responsive to changes, it feels better to treat the
          // changes as atomic and always-valid.
          stateMustAlwaysBeValid
        />
        <Box flex="1 0 0" lineHeight="1">
          {isIncompatible && (
            <Tooltip label="No data yet" placement="top">
              <WarningIcon
                color={errorColor}
                transition="color 0.2"
                marginLeft="2"
              />
            </Tooltip>
          )}
        </Box>
      </Box>
    </VStack>
  );
}

/**
 * SubtleSkeleton hides the skeleton animation until a second has passed, and
 * doesn't fade in the content if it loads near-instantly. This helps avoid
 * flash-of-content stuff!
 *
 * For plain Skeletons, we often use <Delay><Skeleton /></Delay> instead. But
 * that pattern doesn't work as well for wrapper skeletons where we're using
 * placeholder content for layout: we don't want the delay if the content
 * really _is_ present!
 */
function SubtleSkeleton({ isLoaded, ...props }) {
  const [shouldFadeIn, setShouldFadeIn] = React.useState(false);
  const [shouldShowSkeleton, setShouldShowSkeleton] = React.useState(false);

  React.useEffect(() => {
    const t = setTimeout(() => {
      if (!isLoaded) {
        setShouldFadeIn(true);
      }
    }, 150);
    return () => clearTimeout(t);
  });

  React.useEffect(() => {
    const t = setTimeout(() => setShouldShowSkeleton(true), 500);
    return () => clearTimeout(t);
  });

  return (
    <Skeleton
      fadeDuration={shouldFadeIn ? undefined : 0}
      startColor={shouldShowSkeleton ? undefined : "transparent"}
      endColor={shouldShowSkeleton ? undefined : "transparent"}
      isLoaded={isLoaded}
      {...props}
    />
  );
}

export default ItemPage;