import * as React from "react";
import gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client";
import {
  Badge,
  Box,
  Button,
  Drawer,
  DrawerBody,
  DrawerCloseButton,
  DrawerContent,
  DrawerHeader,
  DrawerOverlay,
  Flex,
  FormControl,
  FormErrorMessage,
  FormHelperText,
  FormLabel,
  HStack,
  Link,
  Select,
  Spinner,
  Stack,
  Text,
  useBreakpointValue,
  useColorModeValue,
  useDisclosure,
} from "@chakra-ui/react";
import {
  CheckCircleIcon,
  ChevronRightIcon,
  ExternalLinkIcon,
} from "@chakra-ui/icons";

import AllItemLayersSupportModal from "./AllItemLayersSupportModal";
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
import useOutfitAppearance from "../../components/useOutfitAppearance";
import { OutfitStateContext } from "../useOutfitState";
import useSupport from "./useSupport";
import ItemSupportAppearanceLayer from "./ItemSupportAppearanceLayer";

/**
 * ItemSupportDrawer shows Support UI for the item when open.
 *
 * This component controls the drawer element. The actual content is imported
 * from another lazy-loaded component!
 */
function ItemSupportDrawer({ item, isOpen, onClose }) {
  const placement = useBreakpointValue({ base: "bottom", lg: "right" });

  return (
    <Drawer
      placement={placement}
      size="md"
      isOpen={isOpen}
      onClose={onClose}
      // blockScrollOnMount doesn't matter on our fullscreen UI, but the
      // default implementation breaks out layout somehow 🤔 idk, let's not!
      blockScrollOnMount={false}
    >
      <DrawerOverlay>
        <DrawerContent
          maxHeight={placement === "bottom" ? "90vh" : undefined}
          overflow="auto"
        >
          <DrawerCloseButton />
          <DrawerHeader>
            {item.name}
            <Badge colorScheme="pink" marginLeft="3">
              Support <span aria-hidden="true">đź’–</span>
            </Badge>
          </DrawerHeader>
          <DrawerBody paddingBottom="5">
            <Metadata>
              <MetadataLabel>Item ID:</MetadataLabel>
              <MetadataValue>{item.id}</MetadataValue>
              <MetadataLabel>Restricted zones:</MetadataLabel>
              <MetadataValue>
                <ItemSupportRestrictedZones item={item} />
              </MetadataValue>
            </Metadata>
            <Stack spacing="8" marginTop="6">
              <ItemSupportFields item={item} />
              <ItemSupportAppearanceLayers item={item} />
            </Stack>
          </DrawerBody>
        </DrawerContent>
      </DrawerOverlay>
    </Drawer>
  );
}

function ItemSupportRestrictedZones({ item }) {
  const { speciesId, colorId } = React.useContext(OutfitStateContext);

  // NOTE: It would be a better reflection of the data to just query restricted
  //       zones right off the item... but we already have them in cache from
  //       the appearance, so query them that way to be instant in practice!
  const { loading, error, data } = useQuery(
    gql`
      query ItemSupportRestrictedZones(
        $itemId: ID!
        $speciesId: ID!
        $colorId: ID!
      ) {
        item(id: $itemId) {
          id
          appearanceOn(speciesId: $speciesId, colorId: $colorId) {
            restrictedZones {
              id
              label
            }
          }
        }
      }
    `,
    { variables: { itemId: item.id, speciesId, colorId } },
  );

  if (loading) {
    return <Spinner size="xs" />;
  }

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

  const restrictedZones = data?.item?.appearanceOn?.restrictedZones || [];
  if (restrictedZones.length === 0) {
    return "None";
  }

  return restrictedZones
    .map((z) => `${z.label} (${z.id})`)
    .sort()
    .join(", ");
}

function ItemSupportFields({ item }) {
  const { loading, error, data } = useQuery(
    gql`
      query ItemSupportFields($itemId: ID!) {
        item(id: $itemId) {
          id
          manualSpecialColor {
            id
          }
          explicitlyBodySpecific
        }
      }
    `,
    {
      variables: { itemId: item.id },

      // HACK: I think it's a bug in @apollo/client 3.1.1 that, if the
      //     optimistic response sets `manualSpecialColor` to null, the query
      //     doesn't update, even though its cache has updated :/
      //
      //     This cheap trick of changing the display name every re-render
      //     persuades Apollo that this is a different query, so it re-checks
      //     its cache and finds the empty `manualSpecialColor`. Weird!
      displayName: `ItemSupportFields-${new Date()}`,
    },
  );

  const errorColor = useColorModeValue("red.500", "red.300");

  return (
    <>
      {error && <Box color={errorColor}>{error.message}</Box>}
      <ItemSupportSpecialColorFields
        loading={loading}
        error={error}
        item={item}
        manualSpecialColor={data?.item?.manualSpecialColor?.id}
      />
      <ItemSupportPetCompatibilityRuleFields
        loading={loading}
        error={error}
        item={item}
        explicitlyBodySpecific={data?.item?.explicitlyBodySpecific}
      />
    </>
  );
}

function ItemSupportSpecialColorFields({
  loading,
  error,
  item,
  manualSpecialColor,
}) {
  const { supportSecret } = useSupport();

  const {
    loading: colorsLoading,
    error: colorsError,
    data: colorsData,
  } = useQuery(gql`
    query ItemSupportDrawerAllColors {
      allColors {
        id
        name
        isStandard
      }
    }
  `);

  const [
    mutate,
    { loading: mutationLoading, error: mutationError, data: mutationData },
  ] = useMutation(gql`
    mutation ItemSupportDrawerSetManualSpecialColor(
      $itemId: ID!
      $colorId: ID
      $supportSecret: String!
    ) {
      setManualSpecialColor(
        itemId: $itemId
        colorId: $colorId
        supportSecret: $supportSecret
      ) {
        id
        manualSpecialColor {
          id
        }
      }
    }
  `);

  const onChange = React.useCallback(
    (e) => {
      const colorId = e.target.value || null;
      const color =
        colorId != null ? { __typename: "Color", id: colorId } : null;
      mutate({
        variables: {
          itemId: item.id,
          colorId,
          supportSecret,
        },
        optimisticResponse: {
          __typename: "Mutation",
          setManualSpecialColor: {
            __typename: "Item",
            id: item.id,
            manualSpecialColor: color,
          },
        },
      }).catch((e) => {
        // Ignore errors from the promise, because we'll handle them on render!
      });
    },
    [item.id, mutate, supportSecret],
  );

  const nonStandardColors =
    colorsData?.allColors?.filter((c) => !c.isStandard) || [];
  nonStandardColors.sort((a, b) => a.name.localeCompare(b.name));

  const linkColor = useColorModeValue("green.500", "green.300");

  return (
    <FormControl isInvalid={Boolean(error || colorsError || mutationError)}>
      <FormLabel>Special color</FormLabel>
      <Select
        placeholder={
          loading || colorsLoading
            ? "Loading…"
            : "Default: Auto-detect from item description"
        }
        value={manualSpecialColor?.id}
        isDisabled={mutationLoading}
        icon={
          loading || colorsLoading || mutationLoading ? (
            <Spinner />
          ) : mutationData ? (
            <CheckCircleIcon />
          ) : undefined
        }
        onChange={onChange}
      >
        {nonStandardColors.map((color) => (
          <option key={color.id} value={color.id}>
            {color.name}
          </option>
        ))}
      </Select>
      {colorsError && (
        <FormErrorMessage>{colorsError.message}</FormErrorMessage>
      )}
      {mutationError && (
        <FormErrorMessage>{mutationError.message}</FormErrorMessage>
      )}
      {!colorsError && !mutationError && (
        <FormHelperText>
          This controls which previews we show on the{" "}
          <Link
            href={`https://impress.openneo.net/items/${
              item.id
            }-${item.name.replace(/ /g, "-")}`}
            color={linkColor}
            isExternal
          >
            classic item page <ExternalLinkIcon />
          </Link>
          .
        </FormHelperText>
      )}
    </FormControl>
  );
}

function ItemSupportPetCompatibilityRuleFields({
  loading,
  error,
  item,
  explicitlyBodySpecific,
}) {
  const { supportSecret } = useSupport();

  const [
    mutate,
    { loading: mutationLoading, error: mutationError, data: mutationData },
  ] = useMutation(gql`
    mutation ItemSupportDrawerSetItemExplicitlyBodySpecific(
      $itemId: ID!
      $explicitlyBodySpecific: Boolean!
      $supportSecret: String!
    ) {
      setItemExplicitlyBodySpecific(
        itemId: $itemId
        explicitlyBodySpecific: $explicitlyBodySpecific
        supportSecret: $supportSecret
      ) {
        id
        explicitlyBodySpecific
      }
    }
  `);

  const onChange = React.useCallback(
    (e) => {
      const explicitlyBodySpecific = e.target.value === "true";
      mutate({
        variables: {
          itemId: item.id,
          explicitlyBodySpecific,
          supportSecret,
        },
        optimisticResponse: {
          __typename: "Mutation",
          setItemExplicitlyBodySpecific: {
            __typename: "Item",
            id: item.id,
            explicitlyBodySpecific,
          },
        },
      }).catch((e) => {
        // Ignore errors from the promise, because we'll handle them on render!
      });
    },
    [item.id, mutate, supportSecret],
  );

  return (
    <FormControl isInvalid={Boolean(error || mutationError)}>
      <FormLabel>Pet compatibility rule</FormLabel>
      <Select
        value={explicitlyBodySpecific ? "true" : "false"}
        isDisabled={mutationLoading}
        icon={
          loading || mutationLoading ? (
            <Spinner />
          ) : mutationData ? (
            <CheckCircleIcon />
          ) : undefined
        }
        onChange={onChange}
      >
        {loading ? (
          <option>Loading…</option>
        ) : (
          <>
            <option value="false">
              Default: Auto-detect whether this fits all pets
            </option>
            <option value="true">
              Body specific: Always different for each pet body
            </option>
          </>
        )}
      </Select>
      {mutationError && (
        <FormErrorMessage>{mutationError.message}</FormErrorMessage>
      )}
      {!mutationError && (
        <FormHelperText>
          By default, we assume Background-y zones fit all pets the same. When
          items don't follow that rule, we can override it.
        </FormHelperText>
      )}
    </FormControl>
  );
}

/**
 * NOTE: This component takes `outfitState` from context, rather than as a prop
 *       from its parent, for performance reasons. We want `Item` to memoize
 *       and generally skip re-rendering on `outfitState` changes, and to make
 *       sure the context isn't accessed when the drawer is closed. So we use
 *       it here, only when the drawer is open!
 */
function ItemSupportAppearanceLayers({ item }) {
  const outfitState = React.useContext(OutfitStateContext);
  const { speciesId, colorId, pose, altStyleId, appearanceId } = outfitState;
  const { error, visibleLayers } = useOutfitAppearance({
    speciesId,
    colorId,
    pose,
    altStyleId,
    appearanceId,
    wornItemIds: [item.id],
  });

  const biologyLayers = visibleLayers.filter((l) => l.source === "pet");
  const itemLayers = visibleLayers.filter((l) => l.source === "item");
  itemLayers.sort((a, b) => a.zone.depth - b.zone.depth);

  const modalState = useDisclosure();

  return (
    <FormControl>
      <Flex align="center">
        <FormLabel>Appearance layers</FormLabel>
        <Box width="4" flex="1 0 auto" />
        <Button size="xs" onClick={modalState.onOpen}>
          View on all pets <ChevronRightIcon />
        </Button>
        <AllItemLayersSupportModal
          item={item}
          isOpen={modalState.isOpen}
          onClose={modalState.onClose}
        />
      </Flex>
      <HStack spacing="4" overflow="auto" paddingX="1">
        {itemLayers.map((itemLayer) => (
          <ItemSupportAppearanceLayer
            key={itemLayer.id}
            item={item}
            itemLayer={itemLayer}
            biologyLayers={biologyLayers}
            outfitState={outfitState}
          />
        ))}
      </HStack>
      {error && <FormErrorMessage>{error.message}</FormErrorMessage>}
    </FormControl>
  );
}

export default ItemSupportDrawer;