/**
 * TODO(rcebulko): Migrate the actual ViewportInterface into core or an extern.
 * @typedef {{
 *   getHeight: function(this:ViewportInterfaceDef):number,
 *   getWidth: function(this:ViewportInterfaceDef):number,
 * }}
 */
let ViewportInterfaceDef;

/**
 * The structure that combines position and size for an element. The exact
 * interpretation of position and size depends on the use case.
 *
 * @typedef {{
 *   top: number,
 *   bottom: number,
 *   left: number,
 *   right: number,
 *   width: number,
 *   height: number,
 *   x: number,
 *   y: number
 * }} LayoutRectDef;
 */

/**
 * The structure that contains the size for an element. The exact
 * interpretation of the size depends on the use case.
 *
 * @typedef {{
 *   width: number,
 *   height: number,
 * }} LayoutSizeDef;
 */

/**
 * The structure that represents the margins of an Element.
 *
 * @typedef {{
 *   top: number,
 *   right: number,
 *   bottom: number,
 *   left: number
 * }} LayoutMarginsDef;
 */

/**
 * The structure that represents a requested change to the margins of an
 * Element. Any new values specified will replace existing ones (rather than
 * being additive).
 *
 * @typedef {{
 *   top: (number|undefined),
 *   right: (number|undefined),
 *   bottom: (number|undefined),
 *   left: (number|undefined)
 * }} LayoutMarginsChangeDef;
 */

/**
 * RelativePositions_Enum
 *
 * Describes the relative position of an element to another (whether the
 * first is inside the second, on top of the second or on the bottom
 * @enum {string}
 */
export const RelativePositions_Enum = {
    INSIDE: 'inside',
    TOP: 'top',
    BOTTOM: 'bottom',
};

/**
 * Creates a layout rect based on the left, top, width and height parameters
 * in that order.
 * @param {number} left
 * @param {number} top
 * @param {number} width
 * @param {number} height
 * @return {LayoutRectDef}
 */
export function layoutRectLtwh(left, top, width, height) {
    return {
        left,
        top,
        width,
        height,
        bottom: top + height,
        right: left + width,
        x: left,
        y: top,
    };
}

/**
 * Creates a layout rect based on the DOMRect, e.g. obtained from calling
 * getBoundingClientRect.
 * @param {ClientRect} rect
 * @return {LayoutRectDef}
 */
export function layoutRectFromDomRect(rect) {
    return layoutRectLtwh(
        Number(rect.left),
        Number(rect.top),
        Number(rect.width),
        Number(rect.height)
    );
}

/**
 * Returns true if the specified two rects overlap by a single pixel.
 * @param {LayoutRectDef|ClientRect} r1
 * @param {LayoutRectDef|ClientRect} r2
 * @return {boolean}
 */
export function rectsOverlap(r1, r2) {
    return (
        r1.top <= r2.bottom &&
        r2.top <= r1.bottom &&
        r1.left <= r2.right &&
        r2.left <= r1.right
    );
}

/**
 * Returns the intersection between a, b or null if there is none.
 * @param {...?LayoutRectDef|undefined} var_args
 * @return {?LayoutRectDef}
 */
export function rectIntersection(var_args) {
    let x0 = -Infinity;
    let x1 = Infinity;
    let y0 = -Infinity;
    let y1 = Infinity;
    for (let i = 0; i < arguments.length; i++) {
        const current = arguments[i];
        if (!current) {
            continue;
        }
        x0 = Math.max(x0, current.left);
        x1 = Math.min(x1, current.left + current.width);
        y0 = Math.max(y0, current.top);
        y1 = Math.min(y1, current.top + current.height);
        if (x1 < x0 || y1 < y0) {
            return null;
        }
    }
    if (x1 == Infinity) {
        return null;
    }
    return layoutRectLtwh(x0, y0, x1 - x0, y1 - y0);
}

/**
 * Returns the position of r2 relative to r1
 * @param {LayoutRectDef} r1
 * @param {LayoutRectDef} r2
 * @return {RelativePositions_Enum}
 */
export function layoutRectsRelativePos(r1, r2) {
    if (r1.top < r2.top) {
        return RelativePositions_Enum.TOP;
    } else if (r1.bottom > r2.bottom) {
        return RelativePositions_Enum.BOTTOM;
    } else {
        return RelativePositions_Enum.INSIDE;
    }
}

/**
 * Determines if any portion of a layoutBox would be onscreen in the given
 * viewport, when scrolled to the specified position.
 * @param {LayoutRectDef} layoutBox
 * @param {ViewportInterfaceDef} viewport
 * @param {number} scrollPos
 * @return {RelativePositions_Enum}
 */
export function layoutPositionRelativeToScrolledViewport(
    layoutBox,
    viewport,
    scrollPos
) {
    const scrollLayoutBox = layoutRectFromDomRect(
        /** @type {ClientRect} */
        ({
            top: scrollPos,
            bottom: scrollPos + viewport.getHeight(),
            left: 0,
            right: viewport.getWidth(),
        })
    );
    if (rectsOverlap(layoutBox, scrollLayoutBox)) {
        return RelativePositions_Enum.INSIDE;
    } else {
        return layoutRectsRelativePos(layoutBox, scrollLayoutBox);
    }
}

/**
 * Expand the layout rect using multiples of width and height.
 * @param {LayoutRectDef} rect Original rect.
 * @param {number} dw Expansion in width, specified as a multiple of width.
 * @param {number} dh Expansion in height, specified as a multiple of height.
 * @return {LayoutRectDef}
 */
export function expandLayoutRect(rect, dw, dh) {
    return layoutRectLtwh(
        rect.left - rect.width * dw,
        rect.top - rect.height * dh,
        rect.width * (1 + dw * 2),
        rect.height * (1 + dh * 2)
    );
}

/**
 * Moves the layout rect using dx and dy.
 * @param {LayoutRectDef} rect Original rect.
 * @param {number} dx Move horizontally with this value.
 * @param {number} dy Move vertically with this value.
 * @return {LayoutRectDef}
 */
export function moveLayoutRect(rect, dx, dy) {
    if ((dx == 0 && dy == 0) || (rect.width == 0 && rect.height == 0)) {
        return rect;
    }
    return layoutRectLtwh(rect.left + dx, rect.top + dy, rect.width, rect.height);
}

/**
 * @param {LayoutMarginsDef} margins
 * @param {LayoutMarginsChangeDef} change
 * @return {boolean}
 */
export function areMarginsChanged(margins, change) {
    return (
        (change.top !== undefined && change.top != margins.top) ||
        (change.right !== undefined && change.right != margins.right) ||
        (change.bottom !== undefined && change.bottom != margins.bottom) ||
        (change.left !== undefined && change.left != margins.left)
    );
}

/**
 * @param {LayoutRectDef} from
 * @param {LayoutRectDef} to
 * @return {boolean}
 */
export function layoutRectSizeEquals(from, to) {
    return from.width == to.width && from.height === to.height;
}

/**
 * @param {?LayoutRectDef} r1
 * @param {?LayoutRectDef} r2
 * @return {boolean}
 */
export function layoutRectEquals(r1, r2) {
    if (!r1 || !r2) {
        return false;
    }
    return (
        r1.left == r2.left &&
        r1.top == r2.top &&
        r1.width == r2.width &&
        r1.height == r2.height
    );
}

/**
 * @param {LayoutMarginsChangeDef|undefined} marginsChange
 * @return {LayoutMarginsChangeDef|undefined}
 */
export function cloneLayoutMarginsChangeDef(marginsChange) {
    if (!marginsChange) {
        return marginsChange;
    }
    return {
        top: marginsChange.top,
        bottom: marginsChange.bottom,
        left: marginsChange.left,
        right: marginsChange.right,
    };
}

/**
 * @param {LayoutRectDef|ClientRect|DOMRect} rect
 * @return {LayoutSizeDef}
 */
export function layoutSizeFromRect(rect) {
    const {
        height,
        width
    } = rect;
    return {
        width,
        height
    };
}