import React, {
  useState,
  useContext,
  ReactNode,
  Context,
  createContext,
  useEffect,
} from "react";
import styled from "styled-components";
import { useDrop, XYCoord, DropTargetMonitor } from "react-dnd";
import { WidgetProps } from "widgets/BaseWidget";
import { WidgetConfigProps } from "reducers/entityReducers/widgetConfigReducer";
import WidgetFactory from "utils/WidgetFactory";
import { widgetOperationParams, noCollision } from "utils/WidgetPropsUtils";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import { FocusContext, DragResizeContext } from "pages/Editor/CanvasContexts";
import {
  MAIN_CONTAINER_WIDGET_ID,
  WIDGET_PADDING,
} from "constants/WidgetConstants";
import { calculateDropTargetRows } from "./DropTargetUtils";
import DragLayerComponent from "./DragLayerComponent";
import { AppState } from "reducers";
import { useSelector } from "react-redux";
import { theme } from "constants/DefaultTheme";

type DropTargetComponentProps = WidgetProps & {
  children?: ReactNode;
  snapColumnSpace: number;
  snapRowSpace: number;
};

type DropTargetBounds = {
  x: number;
  y: number;
  width: number;
  height: number;
};

const StyledDropTarget = styled.div`
  transition: height 100ms ease-in;
`;

/* 
  This context will provide the function which will help the draglayer and resizablecomponents trigger
  an update of the main container's rows
*/
export const DropTargetContext: Context<{
  updateDropTargetRows?: (row: number) => boolean;
  persistDropTargetRows?: (widgetId: string, rows: number) => void;
}> = createContext({});

export const DropTargetComponent = (props: DropTargetComponentProps) => {
  // Hook to keep the offset of the drop target container in state
  const [dropTargetOffset, setDropTargetOffset] = useState({ x: 0, y: 0 });
  const [rows, setRows] = useState(props.snapRows);
  useEffect(() => {
    setRows(props.snapRows);
  }, [props.snapRows]);
  const { updateWidget, occupiedSpaces, updateWidgetProperty } = useContext(
    EditorContext,
  );
  const { selectWidget, showPropertyPane, selectedWidget } = useContext(
    FocusContext,
  );
  const { isResizing } = useContext(DragResizeContext);

  const spacesOccupiedBySiblingWidgets =
    occupiedSpaces && occupiedSpaces[props.widgetId]
      ? occupiedSpaces[props.widgetId]
      : undefined;

  const childWidgets = useSelector(
    (state: AppState) => state.entities.canvasWidgets[props.widgetId].children,
  );

  const persistDropTargetRows = (widgetId: string, widgetBottomRow: number) => {
    if (props.widgetId === MAIN_CONTAINER_WIDGET_ID) {
      const occupiedSpacesByChildren =
        occupiedSpaces && occupiedSpaces[MAIN_CONTAINER_WIDGET_ID];

      const rowsToPersist = calculateDropTargetRows(
        widgetId,
        widgetBottomRow,
        rows,
        occupiedSpacesByChildren,
      );
      setRows(rowsToPersist);

      /* Update the main container's rows, ONLY if it has changed since the last render */
      if (props.snapRows !== rowsToPersist) {
        updateWidgetProperty &&
          updateWidgetProperty(props.widgetId, "snapRows", rowsToPersist);
        updateWidgetProperty &&
          updateWidgetProperty(
            props.widgetId,
            "bottomRow",
            Math.round(
              (rowsToPersist * props.snapRowSpace) / props.parentRowSpace,
            ),
          );
      }
    }
  };

  /* Update the rows of the main container based on the current widget's (dragging/resizing) bottom row */
  const updateDropTargetRows = (widgetBottomRow: number) => {
    if (props.widgetId === MAIN_CONTAINER_WIDGET_ID) {
      /* If the widget has reached the penultimate row of the main container */
      if (widgetBottomRow > rows - 1) {
        setRows(rows + 2);
        return true;
        // If the current widget's (dragging/resizing) bottom row has moved back up
      } else if (widgetBottomRow < rows - 2 && rows - props.snapRows >= 2) {
        setRows(rows - 2);
        return true;
      }
      return false;
    }

    return false;
  };

  const isChildFocused =
    !!childWidgets &&
    !!selectedWidget &&
    childWidgets.length > 0 &&
    childWidgets.indexOf(selectedWidget) > -1;

  const isChildResizing = !!isResizing && isChildFocused;
  // Make this component a drop target
  const [{ isExactlyOver }, drop] = useDrop({
    accept: Object.values(WidgetFactory.getWidgetTypes()),
    drop(widget: WidgetProps & Partial<WidgetConfigProps>, monitor) {
      // Make sure we're dropping in this container.
      if (isExactlyOver) {
        const updateWidgetParams = widgetOperationParams(
          widget,
          monitor.getSourceClientOffset() as XYCoord,
          dropTargetOffset,
          props.snapColumnSpace,
          props.snapRowSpace,
          props.widgetId,
        );

        // Only show propertypane if this is a new widget.
        // If it is not a new widget, then let the DraggableComponent handle it.
        showPropertyPane &&
          updateWidgetParams.payload.newWidgetId &&
          showPropertyPane(updateWidgetParams.payload.newWidgetId);

        // Select the widget if it is a new widget
        selectWidget &&
          updateWidgetParams.payload.newWidgetId &&
          selectWidget(updateWidgetParams.payload.newWidgetId);

        /* currently dropped widget's bottom row */
        const droppedWidgetBottomRow = updateWidgetParams.payload.rows
          ? updateWidgetParams.payload.topRow + updateWidgetParams.payload.rows
          : updateWidgetParams.payload.topRow +
            (widget.bottomRow - widget.topRow);

        persistDropTargetRows(
          widget.widgetId || updateWidgetParams.payload.newWidgetId,
          droppedWidgetBottomRow,
        );

        /* Finally update the widget */
        updateWidget &&
          updateWidget(
            updateWidgetParams.operation,
            updateWidgetParams.widgetId,
            updateWidgetParams.payload,
          );
      }
      return undefined;
    },
    // Collect isOver for ui transforms when hovering over this component
    collect: (monitor: DropTargetMonitor) => ({
      isOver:
        (monitor.isOver({ shallow: true }) &&
          props.widgetId !== monitor.getItem().widgetId) ||
        (monitor.isOver() && props.widgetId !== monitor.getItem().widgetId),
      isExactlyOver: monitor.isOver({ shallow: true }),
    }),
    // Only allow drop if the drag object is directly over this component
    // As opposed to the drag object being over a child component, or outside the component bounds
    // Also only if the dropzone does not overlap any existing children
    canDrop: (widget, monitor) => {
      // Check if the draggable is the same as the dropTarget
      if (isExactlyOver) {
        const hasCollision = !noCollision(
          monitor.getSourceClientOffset() as XYCoord,
          props.snapColumnSpace,
          props.snapRowSpace,
          widget,
          dropTargetOffset,
          spacesOccupiedBySiblingWidgets,
          rows,
          props.snapColumns,
        );
        return !hasCollision;
      }
      return false;
    },
  });

  const handleBoundsUpdate = (rect: DOMRect) => {
    if (rect.x !== dropTargetOffset.x || rect.y !== dropTargetOffset.y) {
      setDropTargetOffset({
        x: rect.x,
        y: rect.y,
      });
    }
  };

  const handleFocus = () => {
    if (!props.parentId && !isResizing) {
      selectWidget && selectWidget(props.widgetId);
      showPropertyPane && showPropertyPane();
    }
  };

  const width =
    props.widgetId === MAIN_CONTAINER_WIDGET_ID
      ? `calc(100% - ${WIDGET_PADDING * 2}px)`
      : "100%";

  const height =
    props.widgetId === MAIN_CONTAINER_WIDGET_ID
      ? `${rows * props.snapRowSpace}px`
      : "100%";

  const marginTop =
    props.widgetId === MAIN_CONTAINER_WIDGET_ID ? `${theme.spaces[9]}px` : 0;
  const marginBottom =
    props.widgetId === MAIN_CONTAINER_WIDGET_ID ? "500px" : 0;

  return (
    <DropTargetContext.Provider
      value={{ updateDropTargetRows, persistDropTargetRows }}
    >
      <StyledDropTarget
        onClick={handleFocus}
        ref={drop}
        style={{
          position: "relative",
          width,
          height,
          marginTop,
          marginBottom,
          userSelect: "none",
          opacity: 0.99,
        }}
      >
        {props.children}
        <DragLayerComponent
          parentOffset={dropTargetOffset}
          parentWidgetId={props.widgetId}
          parentRowHeight={props.snapRowSpace}
          parentColumnWidth={props.snapColumnSpace}
          visible={isExactlyOver || isChildResizing}
          isOver={isExactlyOver}
          dropTargetOffset={dropTargetOffset}
          occupiedSpaces={spacesOccupiedBySiblingWidgets}
          onBoundsUpdate={handleBoundsUpdate}
          parentRows={rows}
          parentCols={props.snapColumns}
          isResizing={isChildResizing}
        />
      </StyledDropTarget>
    </DropTargetContext.Provider>
  );
};

export default DropTargetComponent;
