import type { Position } from 'css-box-model';
import { invariant } from '../../invariant';
import type {
  Axis,
  DroppableDimension,
  DraggableDimension,
  DraggableDimensionMap,
  Scrollable,
  DroppableSubject,
  PlaceholderInSubject,
} from '../../types';
import getDraggablesInsideDroppable from '../get-draggables-inside-droppable';
import { add, patch } from '../position';
import getSubject from './util/get-subject';
import isHomeOf from './is-home-of';
import getDisplacedBy from '../get-displaced-by';

const getRequiredGrowthForPlaceholder = (
  droppable: DroppableDimension,
  placeholderSize: Position,
  draggables: DraggableDimensionMap,
): Position | null => {
  const axis: Axis = droppable.axis;

  // A virtual list will most likely not contain all of the Draggables
  // so counting them does not help.
  if (droppable.descriptor.mode === 'virtual') {
    return patch(axis.line, placeholderSize[axis.line]);
  }

  // TODO: consider margin collapsing?
  // Using contentBox as that is where the Draggables will sit
  const availableSpace: number = droppable.subject.page.contentBox[axis.size];
  const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable(
    droppable.descriptor.id,
    draggables,
  );
  const spaceUsed: number = insideDroppable.reduce(
    (sum: number, dimension: DraggableDimension): number =>
      sum + dimension.client.marginBox[axis.size],
    0,
  );
  const requiredSpace: number = spaceUsed + placeholderSize[axis.line];
  const needsToGrowBy: number = requiredSpace - availableSpace;

  // nothing to do here
  if (needsToGrowBy <= 0) {
    return null;
  }

  return patch(axis.line, needsToGrowBy);
};

const withMaxScroll = (frame: Scrollable, max: Position): Scrollable => ({
  ...frame,

  scroll: {
    ...frame.scroll,
    max,
  },
});

export const addPlaceholder = (
  droppable: DroppableDimension,
  draggable: DraggableDimension,
  draggables: DraggableDimensionMap,
): DroppableDimension => {
  const frame: Scrollable | null = droppable.frame;

  invariant(
    !isHomeOf(draggable, droppable),
    'Should not add placeholder space to home list',
  );

  invariant(
    !droppable.subject.withPlaceholder,
    'Cannot add placeholder size to a subject when it already has one',
  );

  const placeholderSize: Position = getDisplacedBy(
    droppable.axis,
    draggable.displaceBy,
  ).point;

  const requiredGrowth: Position | null = getRequiredGrowthForPlaceholder(
    droppable,
    placeholderSize,
    draggables,
  );

  const added: PlaceholderInSubject = {
    placeholderSize,
    increasedBy: requiredGrowth,
    oldFrameMaxScroll: droppable.frame ? droppable.frame.scroll.max : null,
  };

  if (!frame) {
    const subject: DroppableSubject = getSubject({
      page: droppable.subject.page,
      withPlaceholder: added,
      axis: droppable.axis,
      frame: droppable.frame,
    });
    return {
      ...droppable,
      subject,
    };
  }

  const maxScroll: Position = requiredGrowth
    ? add(frame.scroll.max, requiredGrowth)
    : frame.scroll.max;

  const newFrame: Scrollable = withMaxScroll(frame, maxScroll);

  const subject: DroppableSubject = getSubject({
    page: droppable.subject.page,
    withPlaceholder: added,
    axis: droppable.axis,
    frame: newFrame,
  });
  return {
    ...droppable,
    subject,
    frame: newFrame,
  };
};

export const removePlaceholder = (
  droppable: DroppableDimension,
): DroppableDimension => {
  const added: PlaceholderInSubject | null = droppable.subject.withPlaceholder;
  invariant(
    added,
    'Cannot remove placeholder form subject when there was none',
  );

  const frame: Scrollable | null = droppable.frame;

  if (!frame) {
    const subject: DroppableSubject = getSubject({
      page: droppable.subject.page,
      axis: droppable.axis,
      frame: null,
      // cleared
      withPlaceholder: null,
    });
    return {
      ...droppable,
      subject,
    };
  }

  const oldMaxScroll: Position | null = added.oldFrameMaxScroll;
  invariant(
    oldMaxScroll,
    'Expected droppable with frame to have old max frame scroll when removing placeholder',
  );

  const newFrame: Scrollable = withMaxScroll(frame, oldMaxScroll);

  const subject: DroppableSubject = getSubject({
    page: droppable.subject.page,
    axis: droppable.axis,
    frame: newFrame,
    // cleared
    withPlaceholder: null,
  });
  return {
    ...droppable,
    subject,
    frame: newFrame,
  };
};
