import { Arr, Fun, Obj, Optional, Optionals } from '@ephox/katamari';
import { Class, Css, Height, SugarBody, SugarPosition, Width } from '@ephox/sugar';
import * as Boxes from '../../alien/Boxes';
import * as OffsetOrigin from '../../alien/OffsetOrigin';
import { NuPositionCss } from '../../positioning/view/PositionCss';
const appear = (component, contextualInfo) => {
    const elem = component.element;
    Class.add(elem, contextualInfo.transitionClass);
    Class.remove(elem, contextualInfo.fadeOutClass);
    Class.add(elem, contextualInfo.fadeInClass);
    contextualInfo.onShow(component);
};
const disappear = (component, contextualInfo) => {
    const elem = component.element;
    Class.add(elem, contextualInfo.transitionClass);
    Class.remove(elem, contextualInfo.fadeInClass);
    Class.add(elem, contextualInfo.fadeOutClass);
    contextualInfo.onHide(component);
};
const isPartiallyVisible = (box, bounds) => box.y < bounds.bottom && box.bottom > bounds.y;
const isTopCompletelyVisible = (box, bounds) => box.y >= bounds.y;
const isBottomCompletelyVisible = (box, bounds) => box.bottom <= bounds.bottom;
const forceTopPosition = (winBox, leftX, viewport) => ({
    location: 'top',
    leftX,
    topY: viewport.bounds.y - winBox.y
});
const forceBottomPosition = (winBox, leftX, viewport) => ({
    location: 'bottom',
    leftX,
    bottomY: winBox.bottom - viewport.bounds.bottom
});
const getDockedLeftPosition = (bounds) => {
    // Essentially, we are just getting the bounding client rect left here,
    // because winBox.x will be the scroll value.
    return bounds.box.x - bounds.win.x;
};
const tryDockingPosition = (modes, bounds, viewport) => {
    const winBox = bounds.win;
    const box = bounds.box;
    const leftX = getDockedLeftPosition(bounds);
    return Arr.findMap(modes, (mode) => {
        switch (mode) {
            case 'bottom':
                return !isBottomCompletelyVisible(box, viewport.bounds) ? Optional.some(forceBottomPosition(winBox, leftX, viewport)) : Optional.none();
            case 'top':
                return !isTopCompletelyVisible(box, viewport.bounds) ? Optional.some(forceTopPosition(winBox, leftX, viewport)) : Optional.none();
            default:
                return Optional.none();
        }
    }).getOr({
        location: 'no-dock'
    });
};
const isVisibleForModes = (modes, box, viewport) => Arr.forall(modes, (mode) => {
    switch (mode) {
        case 'bottom':
            return isBottomCompletelyVisible(box, viewport.bounds);
        case 'top':
            return isTopCompletelyVisible(box, viewport.bounds);
    }
});
const getXYForRestoring = (pos, viewport) => {
    const priorY = viewport.optScrollEnv.fold(Fun.constant(pos.bounds.y), (scrollEnv) => scrollEnv.scrollElmTop + (pos.bounds.y - scrollEnv.currentScrollTop));
    return SugarPosition(pos.bounds.x, priorY);
};
const getXYForSaving = (box, viewport) => {
    const priorY = viewport.optScrollEnv.fold(Fun.constant(box.y), (scrollEnv) => box.y + scrollEnv.currentScrollTop - scrollEnv.scrollElmTop);
    return SugarPosition(box.x, priorY);
};
const getPrior = (elem, viewport, state) => state.getInitialPos().map((pos) => {
    const xy = getXYForRestoring(pos, viewport);
    return {
        box: Boxes.bounds(xy.left, xy.top, Width.get(elem), Height.get(elem)),
        location: pos.location
    };
});
const storePrior = (elem, box, viewport, state, decision) => {
    const xy = getXYForSaving(box, viewport);
    const bounds = Boxes.bounds(xy.left, xy.top, box.width, box.height);
    state.setInitialPos({
        style: Css.getAllRaw(elem),
        position: Css.get(elem, 'position') || 'static',
        bounds,
        location: decision.location
    });
};
// When we are using APIs like forceDockToTop, then we only want to store the previous position
// if we weren't already docked. Otherwise, we still want to move the component, but keep its old
// restore values
const storePriorIfNone = (elem, box, viewport, state, decision) => {
    state.getInitialPos().fold(() => storePrior(elem, box, viewport, state, decision), () => Fun.noop);
};
const revertToOriginal = (elem, box, state) => state.getInitialPos().bind((position) => {
    var _a;
    state.clearInitialPos();
    switch (position.position) {
        case 'static':
            return Optional.some({
                morph: 'static'
            });
        case 'absolute':
            const offsetParent = OffsetOrigin.getOffsetParent(elem).getOr(SugarBody.body());
            const offsetBox = Boxes.box(offsetParent);
            // Adding the scrollDelta here may not be the right solution. The basic problem is that the
            // rest of the code isn't considering whether its absolute or not, and where the offset parent
            // is. In the situation where the offset parent is *inside* the scrolling environment, then
            // we don't need to consider the scroll, and that's what getXYForRestoring does ... it removes
            // the scroll. We don't need to consider the scroll because the sink is already affected by the
            // scroll. However, when the sink IS the scroller, its position is not moved by scrolling. But the
            // positions of everything inside it needs to consider the scroll. So we add the scroll value.
            //
            // This might also be a bit naive. It's possible that we need to check that the offsetParent
            // is THE scroller, not just that it has a scroll value. For example, if the offset parent
            // was the body, and the body had a scroll, this might give unexpected results. That's somewhat
            // countered by the fact that if the offset parent is outside the scroller, then you don't really
            // have a scrolling environment any more, because the offset parent isn't going to be impacted
            // at all by the scroller
            const scrollDelta = (_a = offsetParent.dom.scrollTop) !== null && _a !== void 0 ? _a : 0;
            return Optional.some({
                morph: 'absolute',
                positionCss: NuPositionCss('absolute', Obj.get(position.style, 'left').map((_left) => box.x - offsetBox.x), Obj.get(position.style, 'top').map((_top) => box.y - offsetBox.y + scrollDelta), Obj.get(position.style, 'right').map((_right) => offsetBox.right - box.right), Obj.get(position.style, 'bottom').map((_bottom) => offsetBox.bottom - box.bottom))
            });
        default:
            return Optional.none();
    }
});
const tryMorphToOriginal = (elem, viewport, state) => getPrior(elem, viewport, state)
    .filter(({ box }) => isVisibleForModes(state.getModes(), box, viewport))
    .bind(({ box }) => revertToOriginal(elem, box, state));
const tryDecisionToFixedMorph = (decision) => {
    switch (decision.location) {
        case 'top': {
            // We store our current position so we can revert to it once it's
            // visible again.
            return Optional.some({
                morph: 'fixed',
                positionCss: NuPositionCss('fixed', Optional.some(decision.leftX), Optional.some(decision.topY), Optional.none(), Optional.none())
            });
        }
        case 'bottom': {
            // We store our current position so we can revert to it once it's
            // visible again.
            return Optional.some({
                morph: 'fixed',
                positionCss: NuPositionCss('fixed', Optional.some(decision.leftX), Optional.none(), Optional.none(), Optional.some(decision.bottomY))
            });
        }
        default:
            return Optional.none();
    }
};
const tryMorphToFixed = (elem, viewport, state) => {
    const box = Boxes.box(elem);
    const winBox = Boxes.win();
    const decision = tryDockingPosition(state.getModes(), {
        win: winBox,
        box
    }, viewport);
    if (decision.location === 'top' || decision.location === 'bottom') {
        // We are moving from undocked to docked, so store the previous location
        // so that we can restore it when we switch out of docking (back to undocked)
        storePrior(elem, box, viewport, state, decision);
        return tryDecisionToFixedMorph(decision);
    }
    else {
        return Optional.none();
    }
};
const tryMorphToOriginalOrUpdateFixed = (elem, viewport, state) => {
    // When a "docked" element is docked to the top of a scroll container (due to optScrollEnv in
    // viewport), we need to reposition its fixed if the scroll container has itself moved its top position.
    // This isn't required when the docking is to the top of the window, because the entire window cannot
    // be scrolled up and down the page - it is the page.
    //
    // Imagine a situation where the toolbar has docked to the top of the scroll container, which is at
    // y = 200. Now, when the user scrolls the page another 50px down the page, the top of the scroll
    // container will now be 150px, but the "fixed" toolbar will still be at "200px". So this is a morph
    // from "fixed" to "fixed", but with new coordinates. So if we can't morph to original from "fixed",
    // we try to update our "fixed" position (if we have a scrolling environment in the viewport)
    return tryMorphToOriginal(elem, viewport, state)
        .orThunk(() => {
        // Importantly, we don't update our stored position for the element before "docking", because
        // this is a transition between "docked" and "docked", not "undocked" and "docked". We want to
        // keep our undocked position in our store, not a docked position.
        // So we don't change our stored position. We just improve our fixed.
        return viewport.optScrollEnv
            .bind((_) => getPrior(elem, viewport, state))
            .bind(({ box, location }) => {
            const winBox = Boxes.win();
            const leftX = getDockedLeftPosition({ win: winBox, box });
            // Keep the same docking location
            const decision = location === 'top'
                ? forceTopPosition(winBox, leftX, viewport)
                : forceBottomPosition(winBox, leftX, viewport);
            return tryDecisionToFixedMorph(decision);
        });
    });
};
const tryMorph = (component, viewport, state) => {
    const elem = component.element;
    const isDocked = Optionals.is(Css.getRaw(elem, 'position'), 'fixed');
    return isDocked
        ? tryMorphToOriginalOrUpdateFixed(elem, viewport, state)
        : tryMorphToFixed(elem, viewport, state);
};
// The difference between the "calculate" functions and the "try" functions is that the "try" functions
// will first consider whether there is a need to morph, whereas the "calculate" functions will just
// give you the morph details, bypassing the check to see if it's needed
const calculateMorphToOriginal = (component, viewport, state) => {
    const elem = component.element;
    return getPrior(elem, viewport, state)
        .bind(({ box }) => revertToOriginal(elem, box, state));
};
const forceDockWith = (elem, viewport, state, getDecision) => {
    const box = Boxes.box(elem);
    const winBox = Boxes.win();
    const leftX = getDockedLeftPosition({ win: winBox, box });
    const decision = getDecision(winBox, leftX, viewport);
    if (decision.location === 'bottom' || decision.location === 'top') {
        // We only want to store the values if we aren't already docking. If we are already docking, then
        // we just want to move the element, without updating where it started originally
        storePriorIfNone(elem, box, viewport, state, decision);
        return tryDecisionToFixedMorph(decision);
    }
    else {
        return Optional.none();
    }
};
export { appear, disappear, isPartiallyVisible, tryMorph, calculateMorphToOriginal, forceDockWith, forceTopPosition, forceBottomPosition };
//# sourceMappingURL=Dockables.js.map