import { getBox, withScroll, createBox, expand } from 'css-box-model';
import type { BoxModel, Position, Spacing } from 'css-box-model';
import getDroppableDimension from '../../state/droppable/get-droppable';
import type { Closest } from '../../state/droppable/get-droppable';
import type { Env } from './get-env';
import type {
  DroppableDimension,
  DroppableDescriptor,
  Direction,
  ScrollSize,
} from '../../types';
import getScroll from './get-scroll';

const getClient = (
  targetRef: HTMLElement,
  closestScrollable?: Element | null,
): BoxModel => {
  const base: BoxModel = getBox(targetRef);

  // Droppable has no scroll parent
  if (!closestScrollable) {
    return base;
  }

  // Droppable is not the same as the closest scrollable
  if (targetRef !== closestScrollable) {
    return base;
  }

  // Droppable is scrollable

  // Element.getBoundingClient() returns a clipped padding box:
  // When not scrollable: the full size of the element
  // When scrollable: the visible size of the element
  // (which is not the full width of its scrollable content)
  // So we recalculate the borderBox of a scrollable droppable to give
  // it its full dimensions. This will be cut to the correct size by the frame

  // Creating the paddingBox based on scrollWidth / scrollTop
  // scrollWidth / scrollHeight are based on the paddingBox of an element
  // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
  const top: number = base.paddingBox.top - closestScrollable.scrollTop;
  const left: number = base.paddingBox.left - closestScrollable.scrollLeft;
  const bottom: number = top + closestScrollable.scrollHeight;
  const right: number = left + closestScrollable.scrollWidth;

  // unclipped padding box
  const paddingBox: Spacing = {
    top,
    right,
    bottom,
    left,
  };

  // Creating the borderBox by adding the borders to the paddingBox
  const borderBox: Spacing = expand(paddingBox, base.border);

  // We are not accounting for scrollbars
  // Adjusting for scrollbars is hard because:
  // - they are different between browsers
  // - scrollbars can be activated and removed during a drag
  // We instead account for this slightly in our auto scroller

  const client: BoxModel = createBox({
    borderBox,
    margin: base.margin,
    border: base.border,
    padding: base.padding,
  });
  return client;
};

interface Args {
  ref: HTMLElement;
  descriptor: DroppableDescriptor;
  env: Env;
  windowScroll: Position;
  direction: Direction;
  isDropDisabled: boolean;
  isCombineEnabled: boolean;
  shouldClipSubject: boolean;
}

export default ({
  ref,
  descriptor,
  env,
  windowScroll,
  direction,
  isDropDisabled,
  isCombineEnabled,
  shouldClipSubject,
}: Args): DroppableDimension => {
  const closestScrollable: Element | null = env.closestScrollable;
  const client: BoxModel = getClient(ref, closestScrollable);
  const page: BoxModel = withScroll(client, windowScroll);

  const closest: Closest | null = (() => {
    if (!closestScrollable) {
      return null;
    }

    const frameClient: BoxModel = getBox(closestScrollable);
    const scrollSize: ScrollSize = {
      scrollHeight: closestScrollable.scrollHeight,
      scrollWidth: closestScrollable.scrollWidth,
    };

    return {
      client: frameClient,
      page: withScroll(frameClient, windowScroll),
      scroll: getScroll(closestScrollable),
      scrollSize,
      shouldClipSubject,
    };
  })();

  const dimension: DroppableDimension = getDroppableDimension({
    descriptor,
    isEnabled: !isDropDisabled,
    isCombineEnabled,
    isFixedOnPage: env.isFixedOnPage,
    direction,
    client,
    page,
    closest,
  });

  return dimension;
};
