/**
 * There are several UI aspects brought together in the tabbing implementation.
 *
 * 1) Rendering tab collections. A redux store in the window service provides the single source of truth for tab rendering. Each
 * WindowTitleBar subscribes to that store via remote-redux. Activity (such as clicking on a tab) is then published back to the
 * remote store via redux dispatch actions. There are no client side effects that use the FSBL library or FinsembleWindow.
 *
 * 2) Tab dragging - this includes re-ordering of tabs, moving tabs between windows, removing tabs from collections and using the tab
 * drag to initiate tiling. Tab dragging uses the `dragging` section of the remote-redux store to coordinate actions between windows.
 * The algorithm does not affect the permanent store until the dragging action is complete (when onDragEnd is triggered). The `Tabs` react
 * component temporarily removes the tab that is being dragged. `Tabs` react components in any window will then create "gaps" in the tab
 * collection to indicate where the tab will be relocated. Gaps are created with a CSS transform that shifts tabs to the right to make
 * the gap. Note that extra space is added to the end of the tab collection to balance the space lost when the tab was removed (transform
 * space is not accounted for by the browser).
 *
 * 3) scrolling - A tab collection can be scrolled either with the mousewheel or by clicking on scroll buttons (chevrons). The `useScroll()`
 * hook performs all the calculations necessary. The entire tab collection is then transformed to the left or right to emulate a scrolling
 * action.
 *
 * 4) Title editing - Double clicking on a title allows it to be edited by the user. When in editing mode, the tab is rendered with an
 * input field. Enter or Escape can be used to complete the edit.
 */
import React, { useContext, useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { actions } from "../../../platform/services/window/modules/actions";
import { WindowNameContext } from "./WindowTitleBarContext";
import { TabTitle } from "./TabTitle";
import { useDragRegion } from "../../hooks/useDragRegion";
import { useMaximize } from "./useMaximize";
import { TitleEdit } from "./TitleEdit";
import { FEAGlobals } from "../../../common/Globals";
const THROTTLE50MS = 50;
const TabClose = ({ tabWindowName }) => {
    const dispatch = useDispatch();
    const closeThisTab = (e) => {
        // Stop propagation to prevent the tab from being promoted while we're actually closing it
        e.stopPropagation();
        dispatch(actions.closeTab({
            targetWindowName: tabWindowName,
        }));
    };
    return (React.createElement("div", { className: "fsbl-tab-close", onClick: closeThisTab },
        React.createElement("i", { className: "ff-close" })));
};
const TabInnards = ({ editing, title, tabWindowName, cancelEditing, saveTitle, }) => {
    if (editing)
        return React.createElement(TitleEdit, { title: title, cancelEditing: cancelEditing, saveTitle: saveTitle });
    return (React.createElement(React.Fragment, null,
        React.createElement(TabTitle, { tabWindowName: tabWindowName }),
        React.createElement(TabClose, { tabWindowName: tabWindowName })));
};
/**
 * Sets a custom drag image that is a standard size and semi-opaque. This makes it easier
 * to see the tabs and gap below the cursor.
 */
const setDragImage = (transfer) => {
    const width = 60;
    const height = 20;
    const border = 4;
    const light = "rgba(255,255,255,.5)";
    const dark = "rgba(0,0,0,.5)";
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    for (let x = 0; x < width / 2; x++) {
        for (let y = 0; y < height / 2; y++) {
            ctx.fillStyle = (x + y) % 2 ? light : dark;
            ctx.fillRect(x * 2, y * 2, 2, 2);
        }
    }
    ctx.clearRect(border, border, width - border * 2, height - border * 2);
    // Add to browser body and then immediately remove in the timeout
    // Position it way off the edge of the screen
    Object.assign(canvas.style, {
        position: "absolute",
        left: "-4000px",
        top: "-4000px",
        width: `${width}px`,
        height: `${height}}px`,
    });
    document.body.appendChild(canvas);
    transfer.setDragImage(canvas, width / 2, height / 2);
    setTimeout(() => {
        canvas.remove();
    });
};
/**
 * Encapsulates logic relating to dragging an individual tab. This interacts with the remote redux store, turning dragging on
 * and off when dragstart and dragend events are triggered on the tab.
 */
const useDragTab = ({ draggable, tabWindowName, windowName, index, domRef, }) => {
    const dispatch = useDispatch();
    const state = useSelector((currentState) => currentState);
    const onDragStart = (e) => {
        var _a, _b, _c, _d;
        if (!draggable || !windowName)
            return;
        dispatch(actions.setDraggingStatus({
            windowNameBeingDragged: tabWindowName,
            inWindowName: windowName,
            temporaryPosition: index,
            tabWidth: (_c = (_b = (_a = domRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) === null || _b === void 0 ? void 0 : _b.width) !== null && _c !== void 0 ? _c : 60,
        }));
        // Not really used, but good housekeeping to have some data set
        e.dataTransfer.setData("application/windowName", tabWindowName);
        // Eliminates the default "+" that appears on the cursor
        e.dataTransfer.effectAllowed = "move";
        // Override the auto-generated image
        setDragImage(e.dataTransfer);
        /**
         * We can't use React's onDragEnd event because we're temporarily removing our tab during the dragging. When
         * we remove the tab, React loses its linkage to the orphaned DOM node. Instead, we manually set an HTML
         * "dragend" event to capture the end of the drag action (and we're careful to also remove that event handler)
         * after it triggers.
         */
        const onDragEnd = (dragEvent) => {
            var _a;
            (_a = dragEvent.currentTarget) === null || _a === void 0 ? void 0 : _a.removeEventListener("dragend", onDragEnd);
            dispatch(actions.stopDragging());
        };
        (_d = domRef.current) === null || _d === void 0 ? void 0 : _d.addEventListener("dragend", onDragEnd);
    };
    const onDragOver = (e) => {
        var _a;
        e.preventDefault();
        if (!draggable || !windowName || !domRef || !state.dragging)
            return;
        const rect = (_a = domRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
        if (!rect)
            return;
        const midpoint = rect.x + rect.width / 2;
        // If the mouse is over the left side of the tab then a gap should appear to the left of the tab. So if this tab is in position
        // zero then the gap should take over that position. But if the mouse is over the right side of the tab then the gap should be
        // to the right of this tab. So if this tab is in position zero then the gap should be in position 1.
        const gapPosition = e.clientX < midpoint ? index : index + 1;
        // Only dispatch if the position has changed. onDragOver fires continually as you're dragging (60 times per second) so we have
        // to be careful not to do too much work in each cycle and not to trigger renders unless necessary
        if (gapPosition !== state.dragging.temporaryPosition) {
            dispatch(actions.setDraggingStatus({
                ...state.dragging,
                inWindowName: windowName,
                temporaryPosition: gapPosition,
            }));
        }
    };
    return {
        onDragStart,
        onDragOver,
    };
};
/**
 * This hook encapsulates logic related to editing titles. Title editing is a local operation. It does
 * not interact with the remote redux store until the title has been fully edited. The actual editing
 * occurs in the TitleEdit component.
 */
const useEditTab = ({ allowEdit }) => {
    const [editing, setEditing] = useState(false);
    const dispatch = useDispatch();
    // Only allow editing if the tab is wide enough
    const maybeEditTitle = () => {
        if (!allowEdit)
            return;
        setEditing(true);
    };
    const cancelEditing = () => {
        setEditing(false);
    };
    const saveTitle = (title) => {
        dispatch(actions.setUserDefinedTitle({
            title,
        }));
        setEditing(false);
    };
    return {
        maybeEditTitle,
        cancelEditing,
        saveTitle,
        editing,
    };
};
/**
 * An individual tab. This handles dragging and clicking events.
 *
 * `index` is the tab's position in the tab list
 * `gap` indicates a gap section for a possible drop
 * `transform` the CSS transform that should be applied - which will create a gap when dragging tabs around
 */
const Tab = ({ tabWindowName, index, transform, tabMinWidth, }) => {
    var _a, _b, _c, _d;
    const dispatch = useDispatch();
    const domRef = useRef(null);
    const windowName = useContext(WindowNameContext);
    const state = useSelector((currentState) => currentState);
    const myState = state.windows[tabWindowName];
    const title = (_a = myState === null || myState === void 0 ? void 0 : myState.title) !== null && _a !== void 0 ? _a : null;
    const isThin = !!((_b = domRef === null || domRef === void 0 ? void 0 : domRef.current) === null || _b === void 0 ? void 0 : _b.clientWidth) && !state.dragging && ((_c = domRef === null || domRef === void 0 ? void 0 : domRef.current) === null || _c === void 0 ? void 0 : _c.clientWidth) < 80;
    const isPromoted = (myState === null || myState === void 0 ? void 0 : myState.tabCollectionId)
        ? ((_d = state.tabCollections[myState.tabCollectionId]) === null || _d === void 0 ? void 0 : _d.promotedWindowName) === tabWindowName
        : true;
    // Only show the close button on wide tabs. On thin tabs, only show the close button if it is the current promoted tab.
    const showCloseButton = !state.dragging && (!isThin || isPromoted);
    const { editing, maybeEditTitle, cancelEditing, saveTitle } = useEditTab({
        allowEdit: (myState === null || myState === void 0 ? void 0 : myState.editable) && isPromoted,
    });
    const draggable = !!windowName && !editing;
    const { onDragStart, onDragOver } = useDragTab({
        draggable,
        tabWindowName,
        windowName,
        index,
        domRef,
    });
    const promoteTab = () => {
        dispatch(actions.promoteTab({
            targetWindowName: tabWindowName,
        }));
    };
    const style = transform
        ? {
            transform,
            minWidth: tabMinWidth,
        }
        : {
            minWidth: tabMinWidth,
        };
    const className = isPromoted && !editing && !state.dragging ? "fsbl-tab fsbl-active-tab" : "fsbl-tab";
    return (React.createElement("div", { ref: domRef, draggable: !editing, onDragStart: onDragStart, onDragOver: onDragOver, onDoubleClick: maybeEditTitle, onClick: promoteTab, className: className, style: style, title: title !== null && title !== void 0 ? title : "", "data-show-close-button": showCloseButton },
        React.createElement(TabInnards, { title: title, tabWindowName: tabWindowName, editing: editing, cancelEditing: cancelEditing, saveTitle: saveTitle })));
};
const domNotReady = {
    scrollLeft: 0,
    scrollWidth: 0,
    offsetWidth: 0,
    clientWidth: 0,
};
/**
 * Scroll algorithm:
 *
 * Scrolling is accomplished using the "translateX" CSS transform. This transform shifts the entire fsbl-tabs-container to the right
 * or to the left, creating the illusion of scrolling. The "transition" CSS provides an animation. This is the primary reason for using
 * this technique (rather than adjusting scrollBy).
 *
 * `scrollAmount` is the distance that the dom element is shifted from its normal position. Note that a transform moves left in order to
 * give the illusion of scrolling to the right. Thus the transform is always the negative inverse of `scrollAmount`.
 *
 * The transition animation is applied by default, taking effect when the scroll buttons (chevrons) are pressed. The transition is
 * disabled when a mousewheel event is received. Mousewheel events provide a precise pixel amount, incorporating their own heuristics
 * for easing (such as touch pad gestures, which get translated into a stream of mousewheel events). Thus, the transition is disabled for
 * 500 milliseconds whenever a mousewheel event is received.
 *
 * scrollAmount is synchronized via remote-redux so that all of the windows show the same scrolled distance when switching back and forth
 * between tabs. When loading a tab collection from a workspace, the initial scrollAmount is estimated, but that estimated amount is only
 * applied if the window is small enough to require scrolling.
 */
const useScroll = (scrollAmount) => {
    var _a;
    const dispatch = useDispatch();
    const scrollAmountRef = useRef(scrollAmount);
    const [transition, setTransition] = useState(true);
    const [transitionTimeout, setTransitionTimeout] = useState(null);
    const tabContainerRef = useRef(null);
    scrollAmountRef.current = scrollAmount;
    const { offsetWidth, scrollWidth } = (_a = tabContainerRef.current) !== null && _a !== void 0 ? _a : domNotReady;
    const setScrollAmount = (newScrollAmount) => {
        scrollAmountRef.current = newScrollAmount;
        dispatch(actions.scroll({
            scroll: newScrollAmount,
        }));
    };
    const onWheel = (e) => {
        if (e.deltaY < 0) {
            setScrollAmount(Math.max(0, scrollAmount + e.deltaY));
        }
        else {
            setScrollAmount(Math.min(scrollWidth - offsetWidth, scrollAmount + e.deltaY));
        }
        setTransition(false);
        if (transitionTimeout)
            clearTimeout(transitionTimeout);
        setTransitionTimeout(setTimeout(() => {
            setTransition(true);
        }, 500));
    };
    const lessScroll = () => {
        setScrollAmount(Math.max(0, scrollAmount - 200));
    };
    const moreScroll = () => {
        setScrollAmount(Math.min(scrollWidth - offsetWidth, scrollAmount + 200));
    };
    // Only scroll the tabs if the window is small enough to require it
    const allowedScrollAmount = scrollWidth - offsetWidth > 0 ? scrollAmount : 0;
    const enableLeftScrollButton = allowedScrollAmount > 0;
    // It's possible for scrollLeft to be a half pixel. Adding "1" ensures that it's over the scrollWidth when all the way to the right.
    const enableRightScrollButton = allowedScrollAmount + offsetWidth + 1 < scrollWidth;
    useEffect(() => {
        // This code ensures that the scroll amount is never larged than the available space. Otherwise,
        // when resizing to a smaller size, the tabs could end up scrolled out of view.
        // Note, on MS Windows, hidden windows don't receive resize events until they are shown.
        window.addEventListener("resize", () => {
            var _a;
            const { offsetWidth: updatedOffsetWidth, scrollWidth: updatedScrollWidth } = (_a = tabContainerRef.current) !== null && _a !== void 0 ? _a : domNotReady;
            setScrollAmount(Math.min(updatedScrollWidth - updatedOffsetWidth, scrollAmountRef.current));
        });
    }, []);
    const transforms = {
        transform: `translateX(-${allowedScrollAmount}px)`,
        transition: transition ? `1s all ease` : `none`,
    };
    return {
        lessScroll,
        moreScroll,
        enableLeftScrollButton,
        enableRightScrollButton,
        transforms,
        onWheel,
        tabContainerRef,
    };
};
/**
 * Encapsulation of the scrollable section of the tab region, with chevrons.
 *
 * Pass the tabs in as children.
 */
const Scrollable = ({ children, noScroll, scroll, }) => {
    const { lessScroll, moreScroll, enableLeftScrollButton, enableRightScrollButton, onWheel, transforms, tabContainerRef, } = useScroll(scroll);
    const showScrollButtons = !noScroll && (enableLeftScrollButton || enableRightScrollButton);
    return (React.createElement(React.Fragment, null,
        showScrollButtons && (React.createElement("button", { className: "fsbl-tab-scroll-button", onClick: lessScroll, disabled: !enableLeftScrollButton }, "<")),
        React.createElement("div", { className: "fsbl-tabs-scroll-surround", onWheel: onWheel },
            React.createElement("div", { className: "fsbl-tabs-container", ref: tabContainerRef, style: transforms }, children)),
        showScrollButtons && (React.createElement("button", { className: "fsbl-tab-scroll-button", onClick: moreScroll, disabled: !enableRightScrollButton }, ">"))));
};
const createTab = ({ transform, tabWindowName, index, tabMinWidth, }) => (React.createElement(Tab, { tabWindowName: tabWindowName, index: index, transform: transform, tabMinWidth: tabMinWidth, key: tabWindowName }));
/**
 * The collection of tabs
 */
const Tabs = ({ useDOMBasedMovement, children }) => {
    var _a, _b, _c, _d, _e, _f;
    const dispatch = useDispatch();
    const [throttle, setThrottle] = useState(Date.now() - THROTTLE50MS);
    const domRef = useRef(null);
    const state = useSelector((currentState) => currentState);
    const windowName = useContext(WindowNameContext);
    const myState = windowName ? state.windows[windowName] : null;
    const { onMouseDown, onMouseMove } = useDragRegion((_a = myState === null || myState === void 0 ? void 0 : myState.maximized) !== null && _a !== void 0 ? _a : false);
    const { toggle: toggleMaximize } = useMaximize();
    const defaultMinWidth = 72;
    const [tabMinWidth, setTabMinWidth] = useState(defaultMinWidth);
    const tabCollectionId = (_b = myState === null || myState === void 0 ? void 0 : myState.tabCollectionId) !== null && _b !== void 0 ? _b : null;
    const defaultTabs = [windowName !== null && windowName !== void 0 ? windowName : ""];
    const tabCollection = (_c = (tabCollectionId ? state.tabCollections[tabCollectionId] : null)) !== null && _c !== void 0 ? _c : null;
    const tabs = (_d = tabCollection === null || tabCollection === void 0 ? void 0 : tabCollection.tabs) !== null && _d !== void 0 ? _d : defaultTabs;
    const { scroll } = tabCollection !== null && tabCollection !== void 0 ? tabCollection : { scroll: 0 };
    const gapPosition = (_f = (_e = state.dragging) === null || _e === void 0 ? void 0 : _e.temporaryPosition) !== null && _f !== void 0 ? _f : null;
    const gapWidth = 60; // state.dragging?.tabWidth contains the actual width of the tab but 60px provides a better UX
    // The default tab width is 72px, but you can change it by setting finsemble.servicesConfig.docking.tabbing.tabMinWidth in config.json.
    FEAGlobals.FSBL.Clients.ConfigClient.get(["finsemble", "servicesConfig", "docking", "tabbing", "tabMinWidth"], (err, response) => {
        setTabMinWidth(response !== undefined ? response : defaultMinWidth);
    });
    // If a window is being dragged then filter it out so that it isn't displayed, then convert from string to object and calculate the transform size to create a gap
    const tabsWithTransform = tabs
        .filter((tabWindowName) => { var _a; return tabWindowName !== ((_a = state.dragging) === null || _a === void 0 ? void 0 : _a.windowNameBeingDragged); })
        .map((tabWindowName, index) => {
        var _a;
        return {
            transform: ((_a = state.dragging) === null || _a === void 0 ? void 0 : _a.inWindowName) === windowName && gapPosition !== null && index >= gapPosition
                ? `translateX(${gapWidth}px)`
                : null,
            tabWindowName,
            index,
            tabMinWidth,
        };
    });
    /**
     * When we remove the tab that is being dragged it causes the tab collection space to shrink, since it thinks there
     * is one less element. So we add an equivalent amount of space to the end of the tab collection to prevent the
     * shrinkage, providing UX feedback to the end user when the tab is inserted.
     *
     * We "display" the gap space only when the tab is going to be dropped into that space (when the tab is going to be)
     * dropped into the final position.
     */
    const maybeAddGapSpace = () => {
        var _a, _b;
        const style = {
            width: `${(_a = state.dragging) === null || _a === void 0 ? void 0 : _a.tabWidth}px`,
        };
        if (((_b = state.dragging) === null || _b === void 0 ? void 0 : _b.inWindowName) !== windowName)
            return;
        // Only "display" the gap if the drag action is occurring in our window and the gap should be in the final position
        const dataDisplay = gapPosition === tabsWithTransform.length;
        if (gapPosition !== null)
            return React.createElement("div", { className: "fsbl-tab-gap", style: style, "data-display": dataDisplay });
        return null;
    };
    /**
     * This code is disabled due to a bug in Chromium on Windows. dragLeave events do not fire if
     * the draggable object is moved too quickly out of the window. To work around this problem the
     * docking system watches mouse movement during drag operations and detects when the mouse has
     * moved outside of the _calculated_ tab area. This code is in dockingMain.ts.
     *
     * Normal comment for when this function can be used again:
     * dragleave triggers when the dragged element leaves a valid drop target. This can therefore
     * trigger as the mouse moves from tab to tab. We only want to catch the situation where the
     * mouse leaves the tab area entirely, so we do some mouse location calculations to determine
     * this.
     *
     * If we have definitely left the tabbing region then we set tabPosition to null in order
     * that any gaps be closed.
     */
    const onDragLeave = (e) => {
        /*
        if (!state.dragging) return;
        const rect = domRef.current?.getBoundingClientRect();
        if (!rect) return;
        if (e.clientX > rect.left && e.clientX < rect.right && e.clientY > rect.top && e.clientY < rect.bottom) return;
        dispatch(
            actions.setDraggingStatus({
                ...state.dragging,
                temporaryPosition: null,
                inWindowName: null,
            })
        );*/
    };
    /**
     * If the user enters the tab collection space from the outside then the temporary position will probably be null.
     * We immediately set it to the end position of our current collection in order to indicate that it can now be
     * dropped into this tab collection.
     */
    const onDragOver = () => {
        var _a;
        if (!state.dragging)
            return;
        if (!windowName)
            return;
        // ondragover can trigger rapidly when the mouse is hovering. This ensures that we don't flood the system.
        if (throttle + THROTTLE50MS > Date.now())
            return;
        if (((_a = state.dragging) === null || _a === void 0 ? void 0 : _a.temporaryPosition) === null) {
            setThrottle(Date.now());
            dispatch(actions.setDraggingStatus({
                ...state.dragging,
                temporaryPosition: tabsWithTransform.length,
                inWindowName: windowName,
            }));
        }
    };
    /**
     * When dragging a tab over the right side of the WindowTitleBar, set the
     * drag position to the last position. Without this, sometimes the gap gets
     * stuck in the second to last position.
     */
    const dragOverDragRegion = () => {
        var _a;
        if (!state.dragging)
            return;
        if (!windowName)
            return;
        // ondragover can trigger rapidly when the mouse is hovering. This ensures that we don't flood the system.
        if (throttle + THROTTLE50MS > Date.now())
            return;
        if (((_a = state.dragging) === null || _a === void 0 ? void 0 : _a.temporaryPosition) !== tabsWithTransform.length) {
            setThrottle(Date.now());
            dispatch(actions.setDraggingStatus({
                ...state.dragging,
                temporaryPosition: tabsWithTransform.length,
                inWindowName: windowName,
            }));
        }
    };
    const className = useDOMBasedMovement
        ? "fsbl-guaranteed-drag-area fsbl-app-region"
        : "fsbl-guaranteed-drag-area fsbl-app-region fsbl-no-dom-movement";
    // don't show scroll while dragging or the window is not in a stack
    const noScroll = !!state.dragging || !(myState === null || myState === void 0 ? void 0 : myState.tabCollectionId);
    return (React.createElement("div", { className: "fsbl-tab-region-tabbing-enabled", onDragLeave: onDragLeave, onDragOver: onDragOver, ref: domRef },
        React.createElement(Scrollable, { noScroll: noScroll, scroll: scroll },
            tabsWithTransform.map((tab) => createTab(tab)),
            maybeAddGapSpace()),
        React.createElement("div", { className: "fsbl-tab-region-control" }, children),
        React.createElement("div", { className: className, onMouseDown: onMouseDown, onMouseMove: onMouseMove, onDoubleClick: toggleMaximize, onDragOver: dragOverDragRegion })));
};
/**
 * The "Tab Region", which is the area where tabs appear but also where just the title appears
 * if tabbing is disabled.
 */
export const TabRegion = ({ children }) => {
    var _a, _b;
    const windowName = useContext(WindowNameContext);
    const state = useSelector((currentState) => currentState);
    const myState = windowName ? state.windows[windowName] : null;
    const { useDOMBasedMovement = true } = state;
    const enableTabbing = (_a = myState === null || myState === void 0 ? void 0 : myState.enableTabbing) !== null && _a !== void 0 ? _a : false;
    const { onMouseDown, onMouseMove } = useDragRegion((_b = myState === null || myState === void 0 ? void 0 : myState.maximized) !== null && _b !== void 0 ? _b : false);
    const { toggle: toggleMaximize } = useMaximize();
    const className = useDOMBasedMovement
        ? "fsbl-tab-region-tabbing-disabled fsbl-app-region"
        : "fsbl-tab-region-tabbing-disabled fsbl-no-dom-movement fsbl-app-region";
    const singleTitleOrTabs = () => enableTabbing ? (React.createElement(Tabs, { useDOMBasedMovement: useDOMBasedMovement }, children)) : (React.createElement("div", { className: className, onDoubleClick: toggleMaximize, onMouseDown: onMouseDown, onMouseMove: onMouseMove },
        React.createElement(TabTitle, { tabWindowName: windowName })));
    return React.createElement("div", { className: "fsbl-tab-region" }, singleTitleOrTabs());
};
//# sourceMappingURL=TabRegion.js.map