import React from "react";
import gql from "graphql-tag";
import { useMutation, useQuery } from "@apollo/client";
import {
  Box,
  Button,
  IconButton,
  Select,
  Spinner,
  Switch,
  Wrap,
  WrapItem,
  useDisclosure,
  UnorderedList,
  ListItem,
} from "@chakra-ui/react";
import {
  ArrowBackIcon,
  ArrowForwardIcon,
  CheckCircleIcon,
  EditIcon,
} from "@chakra-ui/icons";

import HangerSpinner from "../../components/HangerSpinner";
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
import useSupport from "./useSupport";
import AppearanceLayerSupportModal from "./AppearanceLayerSupportModal";
import { petAppearanceForPosePickerFragment } from "../PosePicker";

function PosePickerSupport({
  speciesId,
  colorId,
  pose,
  appearanceId,
  initialFocusRef,
  dispatchToOutfit,
}) {
  const { loading, error, data } = useQuery(
    gql`
      query PosePickerSupport($speciesId: ID!, $colorId: ID!) {
        petAppearances(speciesId: $speciesId, colorId: $colorId) {
          id
          pose
          isGlitched
          layers {
            id
            zone {
              id
              label
            }

            # For AppearanceLayerSupportModal
            remoteId
            bodyId
            swfUrl
            svgUrl
            imageUrl: imageUrlV2(idealSize: SIZE_600)
            canvasMovieLibraryUrl
          }
          restrictedZones {
            id
            label
          }

          # For AppearanceLayerSupportModal to know the name
          species {
            id
            name
          }
          color {
            id
            name
          }

          # Also, anything the PosePicker wants that isn't here, so that we
          # don't have to refetch anything when we change the canonical poses.
          ...PetAppearanceForPosePicker
        }

        ...CanonicalPetAppearances
      }
      ${canonicalPetAppearancesFragment}
      ${petAppearanceForPosePickerFragment}
    `,
    { variables: { speciesId, colorId } },
  );

  // Resize the Popover when we toggle loading state, because it probably will
  // affect the content size. appearanceId might also affect content size, if
  // it occupies different zones.
  //
  // NOTE: This also triggers an additional necessary resize when the component
  //       first mounts, because PosePicker lazy-loads it, so it actually
  //       mounting affects size too.
  React.useLayoutEffect(() => {
    // HACK: To trigger a Popover resize, we simulate a window resize event,
    //       because Popover listens for window resizes to reposition itself.
    //       I've also filed an issue requesting an official API!
    //       https://github.com/chakra-ui/chakra-ui/issues/1853
    window.dispatchEvent(new Event("resize"));
  }, [loading, appearanceId]);

  const canonicalAppearanceIdsByPose = {
    HAPPY_MASC: data?.happyMasc?.id,
    SAD_MASC: data?.sadMasc?.id,
    SICK_MASC: data?.sickMasc?.id,
    HAPPY_FEM: data?.happyFem?.id,
    SAD_FEM: data?.sadFem?.id,
    SICK_FEM: data?.sickFem?.id,
    UNCONVERTED: data?.unconverted?.id,
    UNKNOWN: data?.unknown?.id,
  };
  const canonicalAppearanceIds = Object.values(
    canonicalAppearanceIdsByPose,
  ).filter((id) => id);

  const providedAppearanceId = appearanceId;
  if (!providedAppearanceId) {
    appearanceId = canonicalAppearanceIdsByPose[pose];
  }

  // If you don't already have `appearanceId` in the outfit state, opening up
  // PosePickerSupport adds it! That way, if you make changes that affect the
  // canonical poses, we'll still stay navigated to this one.
  React.useEffect(() => {
    if (!providedAppearanceId && appearanceId) {
      dispatchToOutfit({
        type: "setPose",
        pose,
        appearanceId,
      });
    }
  }, [providedAppearanceId, appearanceId, pose, dispatchToOutfit]);

  if (loading) {
    return (
      <Box display="flex" justifyContent="center">
        <HangerSpinner size="sm" />
      </Box>
    );
  }

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

  const currentPetAppearance = data.petAppearances.find(
    (pa) => pa.id === appearanceId,
  );
  if (!currentPetAppearance) {
    return (
      <Box color="red.400" marginTop="8">
        Pet appearance with ID {JSON.stringify(appearanceId)} not found
      </Box>
    );
  }

  return (
    <Box>
      <PosePickerSupportNavigator
        petAppearances={data.petAppearances}
        currentPetAppearance={currentPetAppearance}
        canonicalAppearanceIds={canonicalAppearanceIds}
        dropdownRef={initialFocusRef}
        dispatchToOutfit={dispatchToOutfit}
      />
      <Metadata
        fontSize="sm"
        // Build a new copy of this tree when the appearance changes, to reset
        // things like element focus and mutation state!
        key={currentPetAppearance.id}
      >
        <MetadataLabel>DTI ID:</MetadataLabel>
        <MetadataValue>{appearanceId}</MetadataValue>
        <MetadataLabel>Pose:</MetadataLabel>
        <MetadataValue>
          <PosePickerSupportPoseFields
            petAppearance={currentPetAppearance}
            speciesId={speciesId}
            colorId={colorId}
          />
        </MetadataValue>
        <MetadataLabel>Layers:</MetadataLabel>
        <MetadataValue>
          <Wrap spacing="1">
            {currentPetAppearance.layers
              .map((layer) => [`${layer.zone.label} (${layer.zone.id})`, layer])
              .sort((a, b) => a[0].localeCompare(b[0]))
              .map(([text, layer]) => (
                <WrapItem key={layer.id}>
                  <PetLayerSupportLink
                    outfitState={{ speciesId, colorId, pose }}
                    petAppearance={currentPetAppearance}
                    layer={layer}
                  >
                    {text}
                    <EditIcon marginLeft="1" />
                  </PetLayerSupportLink>
                </WrapItem>
              ))}
          </Wrap>
        </MetadataValue>
        <MetadataLabel>Restricts:</MetadataLabel>
        <MetadataValue maxHeight="64" overflowY="auto">
          {currentPetAppearance.restrictedZones.length > 0 ? (
            <UnorderedList>
              {currentPetAppearance.restrictedZones
                .map((zone) => `${zone.label} (${zone.id})`)
                .sort((a, b) => a[0].localeCompare(b[0]))
                .map((zoneText) => (
                  <ListItem key={zoneText}>{zoneText}</ListItem>
                ))}
            </UnorderedList>
          ) : (
            <Box fontStyle="italic" opacity="0.8">
              None
            </Box>
          )}
        </MetadataValue>
      </Metadata>
    </Box>
  );
}

function PetLayerSupportLink({ outfitState, petAppearance, layer, children }) {
  const { isOpen, onOpen, onClose } = useDisclosure();
  return (
    <>
      <Button size="xs" onClick={onOpen}>
        {children}
      </Button>
      <AppearanceLayerSupportModal
        outfitState={outfitState}
        petAppearance={petAppearance}
        layer={layer}
        isOpen={isOpen}
        onClose={onClose}
      />
    </>
  );
}

function PosePickerSupportNavigator({
  petAppearances,
  currentPetAppearance,
  canonicalAppearanceIds,
  dropdownRef,
  dispatchToOutfit,
}) {
  const currentIndex = petAppearances.indexOf(currentPetAppearance);
  const prevPetAppearance = petAppearances[currentIndex - 1];
  const nextPetAppearance = petAppearances[currentIndex + 1];

  return (
    <Box
      display="flex"
      justifyContent="flex-end"
      marginBottom="4"
      // Space for the position-absolute PosePicker mode switcher
      paddingLeft="12"
    >
      <IconButton
        aria-label="Go to previous appearance"
        icon={<ArrowBackIcon />}
        size="sm"
        marginRight="2"
        isDisabled={prevPetAppearance == null}
        onClick={() =>
          dispatchToOutfit({
            type: "setPose",
            pose: prevPetAppearance.pose,
            appearanceId: prevPetAppearance.id,
          })
        }
      />
      <Select
        size="sm"
        width="auto"
        value={currentPetAppearance.id}
        ref={dropdownRef}
        onChange={(e) => {
          const id = e.target.value;
          const petAppearance = petAppearances.find((pa) => pa.id === id);
          dispatchToOutfit({
            type: "setPose",
            pose: petAppearance.pose,
            appearanceId: petAppearance.id,
          });
        }}
      >
        {petAppearances.map((pa) => (
          <option key={pa.id} value={pa.id}>
            {POSE_NAMES[pa.pose]}{" "}
            {canonicalAppearanceIds.includes(pa.id) && "⭐️"}
            {pa.isGlitched && "👾"} ({pa.id})
          </option>
        ))}
      </Select>
      <IconButton
        aria-label="Go to next appearance"
        icon={<ArrowForwardIcon />}
        size="sm"
        marginLeft="2"
        isDisabled={nextPetAppearance == null}
        onClick={() =>
          dispatchToOutfit({
            type: "setPose",
            pose: nextPetAppearance.pose,
            appearanceId: nextPetAppearance.id,
          })
        }
      />
    </Box>
  );
}

function PosePickerSupportPoseFields({ petAppearance, speciesId, colorId }) {
  const { supportSecret } = useSupport();

  const [mutatePose, poseMutation] = useMutation(
    gql`
      mutation PosePickerSupportSetPetAppearancePose(
        $appearanceId: ID!
        $pose: Pose!
        $supportSecret: String!
      ) {
        setPetAppearancePose(
          appearanceId: $appearanceId
          pose: $pose
          supportSecret: $supportSecret
        ) {
          id
          pose
        }
      }
    `,
    {
      refetchQueries: [
        {
          query: gql`
            query PosePickerSupportRefetchCanonicalAppearances(
              $speciesId: ID!
              $colorId: ID!
            ) {
              ...CanonicalPetAppearances
            }
            ${canonicalPetAppearancesFragment}
          `,
          variables: { speciesId, colorId },
        },
      ],
    },
  );

  const [mutateIsGlitched, isGlitchedMutation] = useMutation(
    gql`
      mutation PosePickerSupportSetPetAppearanceIsGlitched(
        $appearanceId: ID!
        $isGlitched: Boolean!
        $supportSecret: String!
      ) {
        setPetAppearanceIsGlitched(
          appearanceId: $appearanceId
          isGlitched: $isGlitched
          supportSecret: $supportSecret
        ) {
          id
          isGlitched
        }
      }
    `,
    {
      refetchQueries: [
        {
          query: gql`
            query PosePickerSupportRefetchCanonicalAppearances(
              $speciesId: ID!
              $colorId: ID!
            ) {
              ...CanonicalPetAppearances
            }
            ${canonicalPetAppearancesFragment}
          `,
          variables: { speciesId, colorId },
        },
      ],
    },
  );

  return (
    <Box>
      <Box display="flex" flexDirection="row" alignItems="center">
        <Select
          size="sm"
          value={petAppearance.pose}
          flex="0 1 200px"
          icon={
            poseMutation.loading ? (
              <Spinner />
            ) : poseMutation.data ? (
              <CheckCircleIcon />
            ) : undefined
          }
          onChange={(e) => {
            const pose = e.target.value;
            mutatePose({
              variables: {
                appearanceId: petAppearance.id,
                pose,
                supportSecret,
              },
              optimisticResponse: {
                __typename: "Mutation",
                setPetAppearancePose: {
                  __typename: "PetAppearance",
                  id: petAppearance.id,
                  pose,
                },
              },
            }).catch((e) => {
              /* Discard errors here; we'll show them in the UI! */
            });
          }}
          isInvalid={poseMutation.error != null}
        >
          {Object.entries(POSE_NAMES).map(([pose, name]) => (
            <option key={pose} value={pose}>
              {name}
            </option>
          ))}
        </Select>
        <Select
          size="sm"
          marginLeft="2"
          flex="0 1 150px"
          value={petAppearance.isGlitched}
          icon={
            isGlitchedMutation.loading ? (
              <Spinner />
            ) : isGlitchedMutation.data ? (
              <CheckCircleIcon />
            ) : undefined
          }
          onChange={(e) => {
            const isGlitched = e.target.value === "true";
            mutateIsGlitched({
              variables: {
                appearanceId: petAppearance.id,
                isGlitched,
                supportSecret,
              },
              optimisticResponse: {
                __typename: "Mutation",
                setPetAppearanceIsGlitched: {
                  __typename: "PetAppearance",
                  id: petAppearance.id,
                  isGlitched,
                },
              },
            }).catch((e) => {
              /* Discard errors here; we'll show them in the UI! */
            });
          }}
          isInvalid={isGlitchedMutation.error != null}
        >
          <option value="false">Valid</option>
          <option value="true">Glitched</option>
        </Select>
      </Box>
      {poseMutation.error && (
        <Box color="red.400">{poseMutation.error.message}</Box>
      )}
      {isGlitchedMutation.error && (
        <Box color="red.400">{isGlitchedMutation.error.message}</Box>
      )}
    </Box>
  );
}

export function PosePickerSupportSwitch({ isChecked, onChange }) {
  return (
    <Box as="label" display="flex" flexDirection="row" alignItems="center">
      <Box fontSize="sm">
        <span role="img" aria-label="Support">
          💖
        </span>
      </Box>
      <Switch
        colorScheme="pink"
        marginLeft="1"
        size="sm"
        isChecked={isChecked}
        onChange={onChange}
      />
    </Box>
  );
}

const POSE_NAMES = {
  HAPPY_MASC: "Happy Masc",
  HAPPY_FEM: "Happy Fem",
  SAD_MASC: "Sad Masc",
  SAD_FEM: "Sad Fem",
  SICK_MASC: "Sick Masc",
  SICK_FEM: "Sick Fem",
  UNCONVERTED: "Unconverted",
  UNKNOWN: "Unknown",
};

const canonicalPetAppearancesFragment = gql`
  fragment CanonicalPetAppearances on Query {
    happyMasc: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: HAPPY_MASC
    ) {
      id
    }

    sadMasc: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: SAD_MASC
    ) {
      id
    }

    sickMasc: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: SICK_MASC
    ) {
      id
    }

    happyFem: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: HAPPY_FEM
    ) {
      id
    }

    sadFem: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: SAD_FEM
    ) {
      id
    }

    sickFem: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: SICK_FEM
    ) {
      id
    }

    unconverted: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: UNCONVERTED
    ) {
      id
    }

    unknown: petAppearance(
      speciesId: $speciesId
      colorId: $colorId
      pose: UNKNOWN
    ) {
      id
    }
  }
`;

export default PosePickerSupport;