import { getDocument, getMousePosition } from 'choerodon-ui/pro/lib/_util/DocumentUtils';
import isString from 'lodash/isString';
import { pxToRem } from '../_util/UnitConvertor';
import { AlignPoint } from './Align';

type overflowType = { adjustX?: boolean; adjustY?: boolean };
type regionType = { left: number; top: number; width: number; height: number };
type positionType = { left: number; top: number };

function isFailX(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.left < visibleRect.left || elFuturePos.left + elRegion.width > visibleRect.right
  );
}

function isFailY(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.top < visibleRect.top || elFuturePos.top + elRegion.height > visibleRect.bottom
  );
}

function isCompleteFailX(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.left > visibleRect.right || elFuturePos.left + elRegion.width < visibleRect.left
  );
}

function isCompleteFailY(elFuturePos, elRegion, visibleRect) {
  return (
    elFuturePos.top > visibleRect.bottom || elFuturePos.top + elRegion.height < visibleRect.top
  );
}

function getParent(element: HTMLElement): HTMLElement | null {
  let parent: HTMLElement | null = element;
  do {
    parent = parent.parentElement;
  } while (parent && parent.nodeType !== 1 && parent.nodeType !== 9);
  return parent;
}

function getOffsetParentAndStyle(el: HTMLElement, defaultView: Window): { parent: HTMLElement, style: CSSStyleDeclaration | null } | null {
  const { position } = defaultView.getComputedStyle(el);
  if (position !== 'absolute' && position !== 'fixed') {
    if (!isString(el.nodeName) || el.nodeName.toLowerCase() !== 'html') {
      const parent = getParent(el);
      if (parent) {
        return {
          parent,
          style: null,
        };
      }
    }
  } else {
    const { body } = defaultView.document;
    for (
      let parent = getParent(el);
      parent && parent !== body && parent.nodeType !== 9;
      parent = getParent(parent)
    ) {
      const style = defaultView.getComputedStyle(parent);
      if (style.position !== 'static') {
        return { parent, style };
      }
    }
  }
  return null;
}

function getVisibleRectForElement(element: HTMLElement) {
  const { ownerDocument } = element;
  if (ownerDocument) {
    const { defaultView } = ownerDocument;
    if (defaultView) {
      const { body, documentElement } = ownerDocument;
      let offsetParentAndStyle = getOffsetParentAndStyle(element, defaultView);
      while (offsetParentAndStyle) {
        const { parent, style } = offsetParentAndStyle;
        if (!parent || parent === body || parent === documentElement) {
          break;
        }
        if ((style || defaultView.getComputedStyle(parent)).overflow !== 'visible') {
          const rect = parent.getBoundingClientRect();
          const { x, y } = getMousePosition(rect.left, rect.top, defaultView, true);
          return {
            top: y,
            right: rect.right + x - rect.left,
            bottom: rect.bottom + y - rect.top,
            left: x,
          };
        }
        offsetParentAndStyle = getOffsetParentAndStyle(parent, defaultView);
      }
    }
  }
  const { body } = getDocument(window);
  return {
    top: 0,
    right: body.clientWidth,
    bottom: body.clientHeight,
    left: 0,
  };
}

function getRegion(node: HTMLElement): regionType {
  const rect = node.getBoundingClientRect();
  const { ownerDocument } = node;
  const defaultView = ownerDocument ? ownerDocument.defaultView : null;
  const position = defaultView ? getMousePosition(rect.left, rect.top, defaultView, true) : { x: rect.left, y: rect.top };
  return {
    top: position.y,
    left: position.x,
    width: rect.width || node.offsetWidth,
    height: rect.height || node.offsetHeight,
  };
}

function isOutOfVisibleRect(target: HTMLElement) {
  const visibleRect = getVisibleRectForElement(target);
  const targetRegion = getRegion(target);

  return (
    !visibleRect ||
    targetRegion.left + targetRegion.width <= visibleRect.left ||
    targetRegion.top + targetRegion.height <= visibleRect.top ||
    targetRegion.left >= visibleRect.right ||
    targetRegion.top >= visibleRect.bottom
  );
}

function flip(points, reg, map) {
  return points.map(p => p.replace(reg, m => map[m]));
}

function flipOffset(offset, index) {
  offset[index] = -offset[index];
  return offset;
}

function getAlignOffset(region, align) {
  const V = align.charAt(0);
  const H = align.charAt(1);
  const w = region.width;
  const h = region.height;

  let x = region.left;
  let y = region.top;

  if (V === 'c') {
    y += h / 2;
  } else if (V === 'b') {
    y += h;
  }

  if (H === 'c') {
    x += w / 2;
  } else if (H === 'r') {
    x += w;
  }

  return {
    left: x,
    top: y,
  };
}

function getElFuturePos(elRegion, refNodeRegion, points, offset, targetOffset): positionType {
  const p1 = getAlignOffset(refNodeRegion, points[1]);
  const p2 = getAlignOffset(elRegion, points[0]);
  const diff = [p2.left - p1.left, p2.top - p1.top];

  return {
    left: elRegion.left - diff[0] + offset[0] - targetOffset[0],
    top: elRegion.top - diff[1] + offset[1] - targetOffset[1],
  };
}

function adjustForViewport(
  elFuturePos: positionType,
  elRegion: regionType,
  visibleRect,
  overflow,
): regionType {
  const pos = { ...elFuturePos };
  const size = {
    width: elRegion.width,
    height: elRegion.height,
  };

  if (overflow.adjustX && pos.left < visibleRect.left) {
    pos.left = visibleRect.left;
  }

  // Left edge inside and right edge outside viewport, try to resize it.
  if (
    overflow.resizeWidth &&
    pos.left >= visibleRect.left &&
    pos.left + size.width > visibleRect.right
  ) {
    size.width -= pos.left + size.width - visibleRect.right;
  }

  // Right edge outside viewport, try to move it.
  if (overflow.adjustX && pos.left + size.width > visibleRect.right) {
    // 保证左边界和可视区域左边界对齐
    pos.left = Math.max(visibleRect.right - size.width, visibleRect.left);
  }

  // Top edge outside viewport, try to move it.
  if (overflow.adjustY && pos.top < visibleRect.top) {
    pos.top = visibleRect.top;
  }

  // Top edge inside and bottom edge outside viewport, try to resize it.
  if (
    overflow.resizeHeight &&
    pos.top >= visibleRect.top &&
    pos.top + size.height > visibleRect.bottom
  ) {
    size.height -= pos.top + size.height - visibleRect.bottom;
  }

  // Bottom edge outside viewport, try to move it.
  if (overflow.adjustY && pos.top + size.height > visibleRect.bottom) {
    // 保证上边界和可视区域上边界对齐
    pos.top = Math.max(visibleRect.bottom - size.height, visibleRect.top);
  }

  return Object.assign(pos, size);
}

// function isFixedPosition(node: HTMLElement): boolean {
//   const { offsetParent, ownerDocument } = node;
//   if (
//     ownerDocument &&
//     offsetParent === ownerDocument.body &&
//     ownerDocument.defaultView &&
//     ownerDocument.defaultView.getComputedStyle(node).position !== 'fixed'
//   ) {
//     return false;
//   }
//   if (offsetParent) {
//     return isFixedPosition(offsetParent as HTMLElement);
//   }
//   return true;
// }

function doAlign(el: HTMLElement, refNodeRegion: regionType, align, isTargetNotOutOfVisible: boolean) {
  let points = align.points;
  let offset = (align.offset || [0, 0]).slice();
  let targetOffset = (align.targetOffset || [0, 0]).slice();
  const overflow: overflowType = align.overflow || {};
  const source: HTMLElement = align.source || el;
  const newOverflowCfg: overflowType = {};
  let fail = 0;
  const visibleRect = getVisibleRectForElement(el);
  const elRegion = getRegion(source);
  let elFuturePos = getElFuturePos(elRegion, refNodeRegion, points, offset, targetOffset);
  let newElRegion = Object.assign(elRegion, elFuturePos);

  if (visibleRect && (overflow.adjustX || overflow.adjustY) && isTargetNotOutOfVisible) {
    if (overflow.adjustX) {
      if (isFailX(elFuturePos, elRegion, visibleRect)) {
        const newPoints = flip(points, /[lr]/gi, {
          l: 'r',
          r: 'l',
        });
        const newOffset = flipOffset(offset, 0);
        const newTargetOffset = flipOffset(targetOffset, 0);
        const newElFuturePos = getElFuturePos(
          elRegion,
          refNodeRegion,
          newPoints,
          newOffset,
          newTargetOffset,
        );

        if (!isCompleteFailX(newElFuturePos, elRegion, visibleRect)) {
          fail = 1;
          points = newPoints;
          offset = newOffset;
          targetOffset = newTargetOffset;
        }
      }
    }

    if (overflow.adjustY) {
      if (isFailY(elFuturePos, elRegion, visibleRect)) {
        const _newPoints = flip(points, /[tb]/gi, {
          t: 'b',
          b: 't',
        });
        const _newOffset = flipOffset(offset, 1);
        const _newTargetOffset = flipOffset(targetOffset, 1);
        const _newElFuturePos = getElFuturePos(
          elRegion,
          refNodeRegion,
          _newPoints,
          _newOffset,
          _newTargetOffset,
        );

        if (!isCompleteFailY(_newElFuturePos, elRegion, visibleRect)) {
          fail = 1;
          points = _newPoints;
          offset = _newOffset;
          targetOffset = _newTargetOffset;
        }
      }
    }

    if (fail) {
      elFuturePos = getElFuturePos(elRegion, refNodeRegion, points, offset, targetOffset);
      Object.assign(newElRegion, elFuturePos);
    }

    newOverflowCfg.adjustX = overflow.adjustX && isFailX(elFuturePos, elRegion, visibleRect);

    newOverflowCfg.adjustY = overflow.adjustY && isFailY(elFuturePos, elRegion, visibleRect);

    if (newOverflowCfg.adjustX || newOverflowCfg.adjustY) {
      newElRegion = adjustForViewport(elFuturePos, elRegion, visibleRect, newOverflowCfg);
    }
  }
  const { width, height } = newElRegion;
  if (width !== elRegion.width) {
    source.style.width = width ? pxToRem(width, true)! : '0';
  }

  if (height !== elRegion.height) {
    source.style.height = height ? pxToRem(height, true)! : '0';
  }
  // const isTargetFixed = isFixedPosition(target);
  const { offsetParent, ownerDocument } = source;
  if (offsetParent) {
    const { left, top } = offsetParent.getBoundingClientRect();
    newElRegion.left -= left;
    newElRegion.top -= top;
  }
  if (ownerDocument) {
    const { x, y } = getMousePosition(0, 0, ownerDocument.defaultView || window, true);
    newElRegion.left -= x;
    newElRegion.top -= y;
  }
  Object.assign(source.style, {
    left: pxToRem(newElRegion.left, true),
    top: pxToRem(newElRegion.top, true),
  });

  // if (isTargetFixed) {
  //   source.style.position = 'fixed';
  // } else {
  //   source.style.position = '';
  // }

  return {
    points,
    offset,
    targetOffset,
    overflow: newOverflowCfg,
  };
}

export default function alignElement(el: HTMLElement, refNode: HTMLElement, align) {
  const target = align.target || refNode;
  const refNodeRegion = getRegion(target);
  const isTargetNotOutOfVisible = !isOutOfVisibleRect(target);
  return doAlign(el, refNodeRegion, align, isTargetNotOutOfVisible);
}

export function alignPoint(el: HTMLElement, tgtPoint: AlignPoint, align) {
  let left = 0;
  let top = 0;
  const { ownerDocument } = el;
  const defaultView = ownerDocument ? ownerDocument.defaultView : null;
  const documentElement = ownerDocument ? ownerDocument.documentElement : null;
  const scrollX = documentElement ? documentElement.scrollLeft : 0;
  const scrollY = documentElement ? documentElement.scrollTop : 0;

  if (tgtPoint.pageX !== undefined) {
    left = tgtPoint.pageX - scrollX;
  } else if (tgtPoint.clientX !== undefined) {
    left = scrollX + tgtPoint.clientX;
  }

  if (tgtPoint.pageY !== undefined) {
    top = tgtPoint.pageY - scrollY;
  } else if (tgtPoint.clientY !== undefined) {
    top = tgtPoint.clientY;
  }
  const position = defaultView ? getMousePosition(left, top, defaultView, true) : {
    x: left,
    y: top,
    vw: documentElement ? documentElement.clientWidth : 0,
    vh: documentElement ? documentElement.clientHeight : 0,
  };
  left = position.x;
  top = position.y;

  const tgtRegion: regionType = {
    left,
    top,
    width: 0,
    height: 0,
  };
  const pointInView = left >= 0 && left <= position.vw && top >= 0 && top <= position.vh; // Provide default target point

  const points = [align.points[0], 'cc'];
  return doAlign(el, tgtRegion, {
    ...align,
    points,
  }, pointInView);
}