import React from "react";
import { css } from "emotion";
import {
  Box,
  Editable,
  EditablePreview,
  EditableInput,
  Flex,
  IconButton,
  Skeleton,
  Tooltip,
  VisuallyHidden,
} from "@chakra-ui/core";
import { EditIcon, QuestionIcon } from "@chakra-ui/icons";
import { CSSTransition, TransitionGroup } from "react-transition-group";

import { Delay, Heading1, Heading2 } from "../util";
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";

/**
 * ItemsPanel shows the items in the current outfit, and lets the user toggle
 * between them! It also shows an editable outfit name, to help set context.
 *
 * Note that this component provides an effective 1 unit of padding around
 * itself, which is uncommon in this app: we usually prefer to let parents
 * control the spacing!
 *
 * This is because Item has padding, but it's generally not visible; so, to
 * *look* aligned with the other elements like the headings, the headings need
 * to have extra padding. Essentially: while the Items _do_ stretch out the
 * full width of the container, it doesn't look like it!
 */
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
  const { zonesAndItems, incompatibleItems } = outfitState;

  return (
    <Box>
      <Box px="1">
        <OutfitHeading
          outfitState={outfitState}
          dispatchToOutfit={dispatchToOutfit}
        />
      </Box>
      <Flex direction="column">
        {loading ? (
          <ItemZoneGroupsSkeleton itemCount={outfitState.allItemIds.length} />
        ) : (
          <TransitionGroup component={null}>
            {zonesAndItems.map(({ zoneLabel, items }) => (
              <CSSTransition key={zoneLabel} {...fadeOutAndRollUpTransition}>
                <ItemZoneGroup
                  zoneLabel={zoneLabel}
                  items={items}
                  outfitState={outfitState}
                  dispatchToOutfit={dispatchToOutfit}
                />
              </CSSTransition>
            ))}
            {incompatibleItems.length > 0 && (
              <ItemZoneGroup
                zoneLabel="Incompatible"
                afterHeader={
                  <Tooltip
                    label="These items don't fit this pet"
                    placement="top"
                    openDelay={100}
                  >
                    <QuestionIcon fontSize="sm" />
                  </Tooltip>
                }
                items={incompatibleItems}
                outfitState={outfitState}
                dispatchToOutfit={dispatchToOutfit}
                isDisabled
              />
            )}
          </TransitionGroup>
        )}
      </Flex>
    </Box>
  );
}

/**
 * ItemZoneGroup shows the items for a particular zone, and lets the user
 * toggle between them.
 *
 * For each item, it renders a <label> with a visually-hidden radio button and
 * the Item component (which will visually reflect the radio's state). This
 * makes the list screen-reader- and keyboard-accessible!
 */
function ItemZoneGroup({
  zoneLabel,
  items,
  outfitState,
  dispatchToOutfit,
  isDisabled = false,
  afterHeader = null,
}) {
  // onChange is fired when the radio button becomes checked, not unchecked!
  const onChange = (e) => {
    const itemId = e.target.value;
    dispatchToOutfit({ type: "wearItem", itemId });
  };

  // Clicking the radio button when already selected deselects it - this is how
  // you can select none!
  const onClick = (e) => {
    const itemId = e.target.value;
    if (outfitState.wornItemIds.includes(itemId)) {
      // We need the event handler to finish before this, so that simulated
      // events don't just come back around and undo it - but we can't just
      // solve that with `preventDefault`, because it breaks the radio's
      // intended visual updates when we unwear. So, we `setTimeout` to do it
      // after all event handlers resolve!
      setTimeout(() => dispatchToOutfit({ type: "unwearItem", itemId }), 0);
    }
  };

  return (
    <Box mb="10">
      <Heading2 display="flex" alignItems="center" mx="1">
        {zoneLabel}
        {afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
      </Heading2>
      <ItemListContainer>
        <TransitionGroup component={null}>
          {items.map((item) => {
            const itemNameId =
              zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
            const itemNode = (
              <Item
                item={item}
                itemNameId={itemNameId}
                isWorn={
                  !isDisabled && outfitState.wornItemIds.includes(item.id)
                }
                isInOutfit={outfitState.allItemIds.includes(item.id)}
                onRemove={() =>
                  dispatchToOutfit({ type: "removeItem", itemId: item.id })
                }
                isDisabled={isDisabled}
              />
            );

            return (
              <CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
                {isDisabled ? (
                  itemNode
                ) : (
                  <label>
                    <VisuallyHidden
                      as="input"
                      type="radio"
                      aria-labelledby={itemNameId}
                      name={zoneLabel}
                      value={item.id}
                      checked={outfitState.wornItemIds.includes(item.id)}
                      onChange={onChange}
                      onClick={onClick}
                      onKeyUp={(e) => {
                        if (e.key === " ") {
                          onClick(e);
                        }
                      }}
                    />
                    {itemNode}
                  </label>
                )}
              </CSSTransition>
            );
          })}
        </TransitionGroup>
      </ItemListContainer>
    </Box>
  );
}

/**
 * ItemZoneGroupSkeletons is a placeholder for when the items are loading.
 *
 * We try to match the approximate size of the incoming data! This is
 * especially nice for when you start with a fresh pet from the homepage, so
 * we don't show skeleton items that just clear away!
 */
function ItemZoneGroupsSkeleton({ itemCount }) {
  const groups = [];
  for (let i = 0; i < itemCount; i++) {
    // NOTE: I initially wrote this to return groups of 3, which looks good for
    //     outfit shares I think, but looks bad for pet loading... once shares
    //     become a more common use case, it might be useful to figure out how
    //     to differentiate these cases and show 1-per-group for pets, but
    //     maybe more for built outfits?
    groups.push(<ItemZoneGroupSkeleton key={i} itemCount={1} />);
  }
  return groups;
}

/**
 * ItemZoneGroupSkeleton is a placeholder for when an ItemZoneGroup is loading.
 */
function ItemZoneGroupSkeleton({ itemCount }) {
  return (
    <Box mb="10">
      <Delay>
        <Skeleton
          mx="1"
          // 2.25rem font size, 1.25rem line height
          height={`${2.25 * 1.25}rem`}
          width="12rem"
        />
        <ItemListSkeleton count={itemCount} />
      </Delay>
    </Box>
  );
}

/**
 * OutfitHeading is an editable outfit name, as a big pretty page heading!
 */
function OutfitHeading({ outfitState, dispatchToOutfit }) {
  return (
    <Box>
      <Box role="group" d="inline-block" position="relative" width="100%">
        <Heading1 mb="6">
          <Editable
            value={outfitState.name}
            placeholder="Untitled outfit"
            onChange={(value) =>
              dispatchToOutfit({ type: "rename", outfitName: value })
            }
          >
            {({ isEditing, onEdit }) => (
              <Flex align="flex-top">
                <EditablePreview />
                <EditableInput />
                {!isEditing && (
                  <Box
                    opacity="0"
                    transition="opacity 0.5s"
                    _groupHover={{ opacity: "1" }}
                    onClick={onEdit}
                  >
                    <IconButton
                      icon={<EditIcon />}
                      variant="link"
                      aria-label="Edit outfit name"
                      title="Edit outfit name"
                    />
                  </Box>
                )}
              </Flex>
            )}
          </Editable>
        </Heading1>
      </Box>
    </Box>
  );
}

/**
 * fadeOutAndRollUpTransition is the props for a CSSTransition, to manage the
 * fade-out and height decrease when an Item or ItemZoneGroup is removed.
 *
 * Note that this _cannot_ be implemented as a wrapper component that returns a
 * CSSTransition. This is because the CSSTransition must be the direct child of
 * the TransitionGroup, and a wrapper breaks the parent-child relationship.
 *
 * See react-transition-group docs for more info!
 */
const fadeOutAndRollUpTransition = {
  classNames: css`
    &-exit {
      opacity: 1;
      height: auto;
    }

    &-exit-active {
      opacity: 0;
      height: 0 !important;
      margin-top: 0 !important;
      margin-bottom: 0 !important;
      transition: all 0.5s;
    }
  `,
  timeout: 500,
  onExit: (e) => {
    e.style.height = e.offsetHeight + "px";
  },
};

export default ItemsPanel;