export function isBody(elem) {
  return elem === document.body;
}
export function isTextNode(elem) {
  return elem && elem.nodeType === 3;
}

/**
 * Decides if given fitHeight fits below or above the target taking boundaries into account.
 */
export function getVerticalPlacement(target, boundariesElement, fitHeight, alignY, forcePlacement, preventOverflow) {
  if (forcePlacement && alignY) {
    return alignY;
  }
  if (!fitHeight) {
    return 'bottom';
  }
  if (isTextNode(target)) {
    // Ignored via go/ees005
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    target = target.parentElement;
  }
  const boundariesClientRect = boundariesElement.getBoundingClientRect();
  const {
    height: boundariesHeight
  } = boundariesClientRect;
  const boundariesTop = isBody(boundariesElement) ? 0 : boundariesClientRect.top;
  const {
    top: targetTop,
    height: targetHeight
  } = target.getBoundingClientRect();
  const spaceAbove = targetTop - (boundariesTop - boundariesElement.scrollTop);
  const spaceBelow = boundariesTop + boundariesHeight - (targetTop + targetHeight);

  // Force vertical placement to bottom if the space above doesn't accomodate the fitHeight
  if (preventOverflow) {
    if (spaceAbove <= fitHeight) {
      return 'bottom';
    }
  }
  if (spaceBelow >= fitHeight || spaceBelow >= spaceAbove) {
    return 'bottom';
  }
  return 'top';
}

/**
 * Decides if given fitWidth fits to the left or to the right of the target taking boundaries into account.
 */
export function getHorizontalPlacement(target, boundariesElement, fitWidth, alignX, forcePlacement, preventOverflow) {
  // force placement unless preventOverflow is enabled
  if (forcePlacement && alignX && !preventOverflow) {
    return alignX;
  }
  if (!fitWidth) {
    return alignX || 'left';
  }
  if (isTextNode(target)) {
    // Ignored via go/ees005
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    target = target.parentElement;
  }
  const {
    left: targetLeft,
    width: targetWidth
  } = target.getBoundingClientRect();
  const {
    left: boundariesLeft,
    width: boundariesWidth
  } = boundariesElement.getBoundingClientRect();
  const spaceLeft = targetLeft - boundariesLeft + targetWidth;
  const spaceRight = boundariesLeft + boundariesWidth - targetLeft;
  if (alignX && spaceLeft > fitWidth && spaceRight > fitWidth) {
    return alignX;
  } else if (spaceRight >= fitWidth || spaceRight >= spaceLeft && !alignX) {
    return 'left';
  }
  return 'right';
}
export function calculatePlacement(target, boundariesElement, fitWidth, fitHeight, alignX, alignY, forcePlacement, preventOverflow) {
  return [getVerticalPlacement(target, boundariesElement, fitHeight, alignY, forcePlacement, preventOverflow), getHorizontalPlacement(target, boundariesElement, fitWidth, alignX, forcePlacement, preventOverflow)];
}
const calculateHorizontalPlacement = ({
  placement,
  targetLeft,
  targetRight,
  targetWidth,
  isPopupParentBody,
  popupOffsetParentLeft,
  popupOffsetParentRight,
  popupOffsetParentScrollLeft,
  popupOffsetParentClientWidth,
  popupClientWidth,
  offset,
  allowOutOfBounds = false,
  minPopupMargin
}) => {
  const position = {};
  if (placement === 'left') {
    position.left = Math.ceil(targetLeft - popupOffsetParentLeft + (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0]);
  } else if (placement === 'center') {
    position.left = Math.ceil(targetLeft - popupOffsetParentLeft + (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0] + targetWidth / 2 - popupClientWidth / 2);
  } else if (placement === 'end') {
    const left = Math.ceil(targetRight - popupOffsetParentLeft + (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0]);
    position.left = left;
  } else {
    position.right = Math.ceil(popupOffsetParentRight - targetRight - (isPopupParentBody ? 0 : popupOffsetParentScrollLeft) + offset[0]);
  }
  if (!allowOutOfBounds) {
    if (position.left !== undefined) {
      position.left = getPopupXInsideParent(position.left, popupClientWidth, popupOffsetParentClientWidth, minPopupMargin);
    }
    if (position.right !== undefined) {
      position.right = getPopupXInsideParent(position.right, popupClientWidth, popupOffsetParentClientWidth, minPopupMargin);
    }
  }
  return position;
};
const getPopupXInsideParent = (x, popupClientWidth, popupOffsetParentClientWidth, minPopupMargin = 1) => {
  // prevent going too far right
  if (popupOffsetParentClientWidth < x + popupClientWidth) {
    x = popupOffsetParentClientWidth - popupClientWidth - minPopupMargin;
  }
  // prevent going too far left
  return Math.max(minPopupMargin, x);
};
const calculateVerticalStickBottom = ({
  target,
  targetTop,
  targetHeight,
  popup,
  offset,
  position,
  boundariesElement,
  scrollableElement
}) => {
  const scrollParent = scrollableElement || findOverflowScrollParent(target) || boundariesElement;
  const newPos = {
    ...position
  };
  if (scrollParent) {
    const topOffsetTop = targetTop - scrollParent.getBoundingClientRect().top;
    const targetEnd = targetHeight + topOffsetTop;
    if (scrollParent.clientHeight - targetEnd <= popup.clientHeight + offset[1] * 2 && topOffsetTop < scrollParent.clientHeight) {
      const scroll = targetEnd - scrollParent.clientHeight + offset[1] * 2;
      let top = newPos.top || 0;
      top = top - (scroll + popup.clientHeight);
      newPos.top = top;
    }
  }
  return newPos;
};
const calculateVerticalStickTop = ({
  target,
  targetTop,
  targetHeight,
  popupOffsetParentHeight,
  popupOffsetParent,
  offset,
  position,
  placement,
  boundariesElement,
  scrollableElement
}) => {
  const scrollParent = scrollableElement || findOverflowScrollParent(target) || boundariesElement;
  const newPos = {
    ...position
  };
  if (scrollParent) {
    const {
      top: scrollParentTop
    } = scrollParent.getBoundingClientRect();
    const topBoundary = targetTop - scrollParentTop;
    const scrollParentScrollTop = scrollParent.scrollTop;
    if (topBoundary < 0) {
      const isBelowNodeBoundary = targetTop + (scrollParentScrollTop - scrollParentTop) + targetHeight + offset[1] < scrollParentScrollTop;
      if (placement === 'top') {
        if (isBelowNodeBoundary) {
          newPos.bottom = popupOffsetParentHeight - (topBoundary + popupOffsetParent.scrollTop + targetHeight);
        } else {
          newPos.bottom = topBoundary + (newPos.bottom || 0);
        }
      }
      if (placement === 'start') {
        if (isBelowNodeBoundary) {
          newPos.top = topBoundary + popupOffsetParent.scrollTop + targetHeight;
        } else {
          newPos.top = Math.abs(topBoundary) + (newPos.top || 0) + offset[1];
        }
      }
    }
  }
  return newPos;
};
const calculateVerticalPlacement = ({
  placement,
  targetTop,
  targetHeight,
  isPopupParentBody,
  popupOffsetParentHeight,
  popupOffsetParentTop,
  popupOffsetParentScrollTop,
  borderBottomWidth,
  offset
}) => {
  const position = {};
  if (placement === 'top') {
    position.bottom = Math.ceil(popupOffsetParentHeight - (targetTop - popupOffsetParentTop) - (isPopupParentBody ? 0 : popupOffsetParentScrollTop) - borderBottomWidth + offset[1]);
  } else if (placement === 'start') {
    position.top = Math.ceil(targetTop - popupOffsetParentTop - offset[1] + (isPopupParentBody ? 0 : popupOffsetParentScrollTop));
  } else {
    const top = Math.ceil(targetTop - popupOffsetParentTop + targetHeight + (isPopupParentBody ? 0 : popupOffsetParentScrollTop) - borderBottomWidth + offset[1]);
    position.top = top;
  }
  return position;
};

/**
 * Calculates relative coordinates for placing popup along with the target.
 * Uses placement from calculatePlacement.
 */
export function calculatePosition({
  placement,
  target,
  popup,
  offset,
  stick,
  allowOutOfBounds = false,
  rect,
  boundariesElement,
  minPopupMargin,
  scrollableElement
}) {
  let position = {};
  if (!target || !popup || !popup.offsetParent || !isHTMLElementNode(popup.offsetParent)) {
    return position;
  }
  if (isTextNode(target)) {
    // Ignored via go/ees005
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    target = target.parentElement;
  }
  const popupOffsetParent = popup.offsetParent;
  const offsetParentStyle = popupOffsetParent.style;
  let borderBottomWidth = 0;
  if (offsetParentStyle && offsetParentStyle.borderBottomWidth) {
    borderBottomWidth = parseInt(offsetParentStyle.borderBottomWidth, 10);
  }
  const [verticalPlacement, horizontalPlacement] = placement;
  const {
    top: popupOffsetParentTop,
    left: popupOffsetParentLeft,
    right: popupOffsetParentRight,
    height: popupOffsetParentHeight
  } = rect ? rect : popupOffsetParent.getBoundingClientRect();

  // Calculate scrollbar dimensions to adjust positions
  // clientWidth/Height excludes scrollbars, offsetWidth/Height includes them
  const scrollbarWidth = popupOffsetParent.offsetWidth - popupOffsetParent.clientWidth;
  const scrollbarHeight = popupOffsetParent.offsetHeight - popupOffsetParent.clientHeight;
  const {
    top: targetTop,
    left: targetLeft,
    right: targetRight,
    height: targetHeight,
    width: targetWidth
  } = target.getBoundingClientRect();
  const isPopupParentBody = isBody(popupOffsetParent);
  const verticalPosition = calculateVerticalPlacement({
    placement: verticalPlacement,
    targetTop,
    isPopupParentBody,
    popupOffsetParentHeight: popupOffsetParentHeight - (isPopupParentBody ? 0 : scrollbarHeight),
    popupOffsetParentTop,
    popupOffsetParentScrollTop: popupOffsetParent.scrollTop || 0,
    targetHeight,
    borderBottomWidth,
    offset
  });
  position = {
    ...position,
    ...verticalPosition
  };
  if ((verticalPlacement === 'top' || verticalPlacement === 'start') && stick) {
    position = calculateVerticalStickTop({
      target,
      targetTop,
      targetHeight,
      popupOffsetParentHeight,
      popupOffsetParent,
      popup,
      offset,
      position,
      placement: verticalPlacement,
      boundariesElement,
      scrollableElement
    });
  }
  if (verticalPlacement !== 'top' && verticalPlacement !== 'start' && stick) {
    position = calculateVerticalStickBottom({
      target,
      targetTop,
      targetHeight,
      popup,
      offset,
      position,
      boundariesElement,
      scrollableElement
    });
  }
  const horizontalPosition = calculateHorizontalPlacement({
    placement: horizontalPlacement,
    targetLeft,
    targetRight,
    targetWidth,
    isPopupParentBody,
    popupOffsetParentLeft,
    popupOffsetParentRight: popupOffsetParentRight - (isPopupParentBody ? 0 : scrollbarWidth),
    popupOffsetParentScrollLeft: popupOffsetParent.scrollLeft || 0,
    popupOffsetParentClientWidth: popup.offsetParent.clientWidth,
    popupClientWidth: popup.clientWidth || 0,
    offset,
    allowOutOfBounds,
    minPopupMargin
  });
  position = {
    ...position,
    ...horizontalPosition
  };
  return position;
}
export function validatePosition(popup) {
  // popup.offsetParent does not exist if the popup element is not mounted
  if (!popup || !popup.offsetParent) {
    return false;
  }
  return true;
}

/**
 * Traverse DOM Tree upwards looking for popup parents with "overflow: scroll".
 */
export function findOverflowScrollParent(popup) {
  let parent = popup;
  if (!parent) {
    return false;
  }

  // Ignored via go/ees005
  // eslint-disable-next-line no-cond-assign
  while (parent = parent.parentElement) {
    // IE11 on Window 8 doesn't show styles from CSS when accessing through element.style property.
    const style = window.getComputedStyle(parent);
    if (style.overflow === 'scroll' || style.overflowX === 'scroll' || style.overflowY === 'scroll' || parent.classList.contains('fabric-editor-popup-scroll-parent')) {
      return parent;
    }
  }
  return false;
}

// Helper function to check if the passed node is of Element class
function isElementNode(node) {
  return node.nodeType === 1;
}

// Helper function to check if the passed node is of HTMLElement class
function isHTMLElementNode(node) {
  return isElementNode(node) && node.namespaceURI === 'http://www.w3.org/1999/xhtml';
}