import type { Anchor, ContextState, Position, Size } from "./types";

export function updateSizeFromElement(
  state: ContextState,
  element: HTMLElement,
  fallback: Size,
): void {
  const rect = element.getBoundingClientRect();
  state.size = {
    width: rect.width || fallback.width,
    height: rect.height || fallback.height,
  };
}

export function clampSize(
  size: Size,
  viewport: Size,
  edgeMargin: number,
  minWidth: number,
  minHeight: number,
): Size {
  const maxWidth = Math.max(minWidth, viewport.width - edgeMargin * 2);
  const maxHeight = Math.max(minHeight, viewport.height - edgeMargin * 2);

  return {
    width: clamp(size.width, minWidth, maxWidth),
    height: clamp(size.height, minHeight, maxHeight),
  };
}

export function constrainToViewport(
  state: ContextState,
  position: Position,
  viewport: Size,
  edgeMargin: number,
): Position {
  const maxX = Math.max(
    edgeMargin,
    viewport.width - state.size.width - edgeMargin,
  );
  const maxY = Math.max(
    edgeMargin,
    viewport.height - state.size.height - edgeMargin,
  );

  return {
    x: clamp(position.x, edgeMargin, maxX),
    y: clamp(position.y, edgeMargin, maxY),
  };
}

export function keepPositionWithinViewport(
  state: ContextState,
  viewport: Size,
  edgeMargin: number,
): void {
  state.position = constrainToViewport(
    state,
    state.position,
    viewport,
    edgeMargin,
  );
}

export function centerContext(
  state: ContextState,
  viewport: Size,
  edgeMargin: number,
): Position {
  const centered: Position = {
    x: Math.round((viewport.width - state.size.width) / 2),
    y: Math.round((viewport.height - state.size.height) / 2),
  };

  state.position = constrainToViewport(state, centered, viewport, edgeMargin);
  updateAnchorFromPosition(state, viewport, edgeMargin);
  return state.position;
}

export function updateAnchorFromPosition(
  state: ContextState,
  viewport: Size,
  edgeMargin: number,
): void {
  const centerX = state.position.x + state.size.width / 2;
  const centerY = state.position.y + state.size.height / 2;

  const horizontal: Anchor["horizontal"] =
    centerX < viewport.width / 2 ? "left" : "right";
  const vertical: Anchor["vertical"] =
    centerY < viewport.height / 2 ? "top" : "bottom";

  state.anchor = { horizontal, vertical };

  const maxHorizontalOffset = Math.max(
    edgeMargin,
    viewport.width - state.size.width - edgeMargin,
  );
  const maxVerticalOffset = Math.max(
    edgeMargin,
    viewport.height - state.size.height - edgeMargin,
  );

  state.anchorOffset = {
    x:
      horizontal === "left"
        ? clamp(state.position.x, edgeMargin, maxHorizontalOffset)
        : clamp(
            viewport.width - state.position.x - state.size.width,
            edgeMargin,
            maxHorizontalOffset,
          ),
    y:
      vertical === "top"
        ? clamp(state.position.y, edgeMargin, maxVerticalOffset)
        : clamp(
            viewport.height - state.position.y - state.size.height,
            edgeMargin,
            maxVerticalOffset,
          ),
  };
}

export function applyAnchorPosition(
  state: ContextState,
  viewport: Size,
  edgeMargin: number,
): Position {
  const maxHorizontalOffset = Math.max(
    edgeMargin,
    viewport.width - state.size.width - edgeMargin,
  );
  const maxVerticalOffset = Math.max(
    edgeMargin,
    viewport.height - state.size.height - edgeMargin,
  );

  const horizontalOffset = clamp(
    state.anchorOffset.x,
    edgeMargin,
    maxHorizontalOffset,
  );
  const verticalOffset = clamp(
    state.anchorOffset.y,
    edgeMargin,
    maxVerticalOffset,
  );

  const x =
    state.anchor.horizontal === "left"
      ? horizontalOffset
      : viewport.width - state.size.width - horizontalOffset;

  const y =
    state.anchor.vertical === "top"
      ? verticalOffset
      : viewport.height - state.size.height - verticalOffset;

  state.anchorOffset = { x: horizontalOffset, y: verticalOffset };
  state.position = constrainToViewport(state, { x, y }, viewport, edgeMargin);
  return state.position;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(min, value), max);
}
