// @flow
import invariant from 'tiny-invariant';
import {
  withScroll,
  createBox,
  type BoxModel,
  type Spacing,
  type Rect,
} from 'css-box-model';
import getDroppableDimension, {
  type Closest,
} from '../../droppable/get-droppable';
import type {
  Viewport,
  DroppableDimension,
  DroppableDimensionMap,
  Scrollable,
  Axis,
} from '../../../types';
import { isEqual } from '../../spacing';
import scrollDroppable from '../../droppable/scroll-droppable';
import { removePlaceholder } from '../../droppable/with-placeholder';
import getFrame from '../../get-frame';
import { toDroppableMap } from '../../dimension-structures';

const throwIfSpacingChange = (old: BoxModel, fresh: BoxModel) => {
  if (process.env.NODE_ENV !== 'production') {
    const getMessage = (spacingType: string) =>
      `Cannot change the ${spacingType} of a Droppable during a drag`;
    invariant(isEqual(old.margin, fresh.margin), getMessage('margin'));
    invariant(isEqual(old.border, fresh.border), getMessage('border'));
    invariant(isEqual(old.padding, fresh.padding), getMessage('padding'));
  }
};

const adjustBorderBoxSize = (axis: Axis, old: Rect, fresh: Rect): Spacing => ({
  // top and left positions cannot change
  top: old.top,
  left: old.left,
  // this is the main logic of this function - the size adjustment
  right: old.left + fresh.width,
  bottom: old.top + fresh.height,
});

type Args = {|
  existing: DroppableDimensionMap,
  modified: DroppableDimension[],
  viewport: Viewport,
|};

export default ({
  modified,
  existing,
  viewport,
}: Args): DroppableDimensionMap => {
  // dynamically adjusting the client subject and page subject
  // of a droppable in response to dynamic additions and removals

  // No existing droppables modified
  if (!modified.length) {
    return existing;
  }

  const adjusted: DroppableDimension[] = modified.map(
    (provided: DroppableDimension): DroppableDimension => {
      const raw: ?DroppableDimension = existing[provided.descriptor.id];
      invariant(raw, 'Could not locate droppable in existing droppables');

      const hasPlaceholder: boolean = Boolean(raw.subject.withPlaceholder);

      const dimension: DroppableDimension = hasPlaceholder
        ? removePlaceholder(raw)
        : raw;

      const oldClient: BoxModel = dimension.client;
      const newClient: BoxModel = provided.client;
      const oldScrollable: Scrollable = getFrame(dimension);
      const newScrollable: Scrollable = getFrame(provided);

      // Extra checks to help with development
      if (process.env.NODE_ENV !== 'production') {
        throwIfSpacingChange(dimension.client, provided.client);
        throwIfSpacingChange(
          oldScrollable.frameClient,
          newScrollable.frameClient,
        );

        const isFrameEqual: boolean =
          oldScrollable.frameClient.borderBox.height ===
            newScrollable.frameClient.borderBox.height &&
          oldScrollable.frameClient.borderBox.width ===
            newScrollable.frameClient.borderBox.width;

        invariant(
          isFrameEqual,
          'The width and height of your Droppable scroll container cannot change when adding or removing Draggables during a drag',
        );
      }

      const client: BoxModel = createBox({
        borderBox: adjustBorderBoxSize(
          dimension.axis,
          oldClient.borderBox,
          newClient.borderBox,
        ),
        margin: oldClient.margin,
        border: oldClient.border,
        padding: oldClient.padding,
      });

      const closest: Closest = {
        // not allowing a change to the scrollable frame size during a drag
        client: oldScrollable.frameClient,
        page: withScroll(oldScrollable.frameClient, viewport.scroll.initial),
        shouldClipSubject: oldScrollable.shouldClipSubject,
        // the scroll size can change during a drag
        scrollSize: newScrollable.scrollSize,
        // using the initial scroll point
        scroll: oldScrollable.scroll.initial,
      };

      const withSizeChanged: DroppableDimension = getDroppableDimension({
        descriptor: provided.descriptor,
        isEnabled: provided.isEnabled,
        isCombineEnabled: provided.isCombineEnabled,
        isFixedOnPage: provided.isFixedOnPage,
        direction: provided.axis.direction,
        client,
        page: withScroll(client, viewport.scroll.initial),
        closest,
      });

      const scrolled: DroppableDimension = scrollDroppable(
        withSizeChanged,
        // TODO: use .initial - i guess both work though..
        newScrollable.scroll.current,
      );

      return scrolled;
    },
  );

  const result: DroppableDimensionMap = {
    ...existing,
    // will override any conflicts with existing
    ...toDroppableMap(adjusted),
  };

  return result;
};
