import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root',
})
export class BodyScrollService {
    body!: HTMLElement;
    viewportHeight!: number;

    constructor() {
        this.body = document.querySelector('body');
        this.viewportHeight = window.innerHeight;
    }

    scrollToElement(element: HTMLElement) {
        const top = this.getTopOffset(element);

        if (!this.shouldScroll(top)) {
            return;
        }

        this.body.scrollTo({
            top,
            left: 0,
            behavior: 'smooth',
        });
    }

    /**
     * @param coordinateToScroll - offset from the top border of the body
     *
     * This function checks if coordinate to which we try to scroll
     * is in viewport already. If so we don't need to perform scrolling.
     * */
    private shouldScroll(coordinateToScroll: number) {
        const scrollPosition = this.body.scrollTop;
        const buffer = this.viewportHeight / 2;

        return Math.abs(coordinateToScroll - scrollPosition) > buffer;
    }

    /**
     * @param element - element for which offset from top border of
     * the viewport is calculated.
     *
     * This function returns top coordinate of the element inside
     * viewport. To do this it calculates difference between viewport
     * height and element height.
     *
     * If element height is larger than viewport height then we should place
     * the element at the top of the screen to show a major part of the
     * element. In this case function returns FIXED_DATE_TABS_HEIGHT
     * which contains height of date tabs which flows above all elements
     * inside body.
     *
     * Otherwise, we should calculate top position so to place center of
     * the element at the center of the viewport. To do this we need to
     * divide the difference by 2.
     *
     * @returns number - position of top border of the element relative to
     * the viewport.
     * */
    private getTopOffsetRelativeViewport(element: HTMLElement) {
        const FIXED_DATE_TABS_HEIGHT = 80;
        const elementHeight = element.clientHeight;
        const diff = this.viewportHeight - elementHeight;

        return diff > 0 ? diff / 2 : FIXED_DATE_TABS_HEIGHT;
    }

    /**
     * @param element - element for which offset is calculated
     *
     * This function returns top coordinate of element relative to
     * screen. It calculates the coordinate so that
     * the center of the element will be placed in the middle of
     * the screen (if the element's height is smaller than viewport
     * height) or at the top of the screen (otherwise).
     *
     * @returns {number} - coordinate of top border of the element.
     * */
    private getTopOffset(element: HTMLElement) {
        const topOffset = this.getAccumulatedTopOffset(element);
        const viewportOffset = this.getTopOffsetRelativeViewport(element);

        return topOffset - viewportOffset;
    }

    /**
     * @param element - element for which offset from the top of the body
     * is calculated.
     *
     * We cannot just use element.offsetTop because it returns
     * the distance between top border of the element and non-static
     * positioned parent:
     * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop
     *
     * So we need to loop over offsetParent property of elements hierarchy.
     * If offsetParent is body we should break the loop because
     * this service scrolls to elements inside body.
     *
     * Also, we should break the loop if offsetParent is null. It can
     * be null in cases when element has position: fixed (in firefox
     * offsetParent will be body in this case).
     * https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
     *
     * @returns number - calculated offset.
     * */
    private getAccumulatedTopOffset(element: HTMLElement) {
        let offsetParent: HTMLElement = element.offsetParent as any;
        let topOffset = element.offsetTop;

        while (offsetParent && offsetParent !== this.body) {
            topOffset += offsetParent.offsetTop;
            offsetParent = offsetParent.offsetParent as any;
        }

        return topOffset;
    }
}
