// (C) 2021-2026 GoodData Corporation
import { createSelector } from "@reduxjs/toolkit";
import { isEmpty, partition } from "lodash-es";
import { invariant } from "ts-invariant";
import { dashboardAttributeFilterItemLocalIdentifier, isDashboardAttributeFilterItem, isDashboardCommonDateFilter, isDashboardLayout, isInsightWidget, isKpiWidget, isVisualizationSwitcherWidget, objRefToString, } from "@gooddata/sdk-model";
import { newMapForObjectWithIdentity } from "../../../../_staging/metadata/objRefMap.js";
import { filterContextItemsToDashboardFiltersByWidget } from "../../../../converters/filterConverters.js";
import { isInsightPlaceholderWidget, isKpiPlaceholderWidget, isPlaceholderWidget, } from "../../../../widgets/placeholders/types.js";
import { isCustomWidget, isExtendedDashboardLayoutWidget, } from "../../../types/layoutTypes.js";
import { createMemoizedSelector } from "../../_infra/selectors.js";
import { createUndoableCommandsMapping } from "../../_infra/undoEnhancer.js";
import { selectEnableIgnoreCrossFiltering } from "../../config/configSelectors.js";
import { selectCrossFilteringFiltersLocalIdentifiers, selectCrossFilteringFiltersLocalIdentifiersByWidgetRef, } from "../../drill/drillSelectors.js";
import { getWidgetCoordinates, isItemWithBaseWidget } from "../../tabs/layout/layoutUtils.js";
import { selectFilterContextFilters } from "../filterContext/filterContextSelectors.js";
import { selectActiveTabLocalIdentifier, selectTabs } from "../tabsSelectors.js";
import { DEFAULT_TAB_ID } from "../tabsState.js";
import { layoutInitialState } from "./layoutState.js";
const selectSelf = createSelector(selectTabs, selectActiveTabLocalIdentifier, (tabs, activeTabId) => {
    if (!tabs || !activeTabId) {
        return layoutInitialState;
    }
    const activeTab = tabs.find((tab) => tab.localIdentifier === activeTabId);
    return activeTab?.layout ?? layoutInitialState;
});
const selectTabsArray = createSelector(selectTabs, (tabs) => [...(tabs ?? [])]);
/**
 * Returns layout keyed by tab identifier.
 *
 * @internal
 */
export const selectLayoutByTab = createSelector(selectTabsArray, (tabs) => {
    if (!tabs.length) {
        return {};
    }
    return tabs.reduce((acc, tab) => {
        const identifier = tab.localIdentifier ?? DEFAULT_TAB_ID;
        acc[identifier] = tab.layout?.layout ?? undefined;
        return acc;
    }, {});
});
/**
 * Returns basic layout keyed by tab identifier that does not contain any client-side extensions.
 *
 * This selector exists because analytical backend impls are not yet ready to handle persistence of custom
 * widgets (that may have arbitrary payloads). The selector is used in dirty checking to compare layouts
 * without any custom widgets. Note that this intentionally removes empty sections.
 *
 * @internal
 */
export const selectBasicLayoutByTab = createSelector(selectLayoutByTab, (layoutByTab) => {
    return Object.entries(layoutByTab).reduce((acc, [tabId, layout]) => {
        if (layout) {
            acc[tabId] = filterOutCustomWidgets(layout);
        }
        else {
            acc[tabId] = undefined;
        }
        return acc;
    }, {});
});
/**
 * This selector returns current layout's stash. This stash can contain items that were removed from the layout with the
 * intent of further using the item elsewhere on the layout. The stash is a mapping of stashIdentifier to an array
 * of stashed items. The stash identifiers and stash usage is fully under control of the user.
 *
 * @internal
 */
export const selectStash = createSelector(selectSelf, (layoutState) => {
    return layoutState.stash;
});
/**
 * This selector returns commands that impacted the layout and can now be undone.
 *
 * @internal
 */
export const selectUndoableLayoutCommands = createSelector(selectSelf, (layoutState) => {
    return createUndoableCommandsMapping(layoutState);
});
/**
 * This selector returns dashboard's layout. It is expected that the selector is called only after the layout state
 * is correctly initialized. Invocations before initialization lead to invariant errors.
 *
 * @public
 */
export const selectLayout = createSelector(selectSelf, (layoutState) => {
    invariant(layoutState.layout, "attempting to access uninitialized layout state");
    return layoutState.layout;
});
/**
 * This selector returns dashboard's current layout screen size. Valid screen size is provided after first render.
 *
 * @public
 */
export const selectScreen = createSelector(selectSelf, (layoutState) => {
    return layoutState.screen;
});
/**
 * Filters out custom widgets from a dashboard layout.
 *
 * This function removes all custom widgets (client-side extensions) from a layout,
 * keeping only analytical widgets (KPI, Insight, etc.). It also removes empty sections
 * that may result from filtering.
 *
 * This is used during save operations because analytical backend implementations are not yet
 * ready to handle persistence of custom widgets that may have arbitrary payloads.
 *
 * @param layout - The layout to filter
 * @returns A layout containing only analytical widgets
 *
 * @internal
 */
export const filterOutCustomWidgets = (layout) => {
    const dashboardLayout = {
        ...layout,
        sections: layout.sections
            .map((section) => {
            return {
                ...section,
                items: section.items.filter(isItemWithBaseWidget).map((item) => {
                    if (item.widget && isExtendedDashboardLayoutWidget(item.widget)) {
                        const filteredLayout = filterOutCustomWidgets(item.widget);
                        const widget = item.widget;
                        return {
                            ...item,
                            widget: {
                                ...widget,
                                sections: filteredLayout.sections,
                            },
                        };
                    }
                    return item;
                }),
            };
        })
            .filter((section) => !isEmpty(section.items)),
    };
    return dashboardLayout;
};
/**
 * This selector returns the basic dashboard layout that does not contain any client-side extensions.
 *
 *
 * @internal
 */
export const selectBasicLayout = createSelector(selectLayout, filterOutCustomWidgets);
/**
 * Selects dashboard widgets in an obj ref an array. This map will include both analytical and custom
 * widgets that are placed on the dashboard and also all widgets from visualization switchers
 *
 * @internal
 */
const getLayoutWidgets = (layout) => {
    const items = [];
    for (const section of layout.sections) {
        for (const item of section.items) {
            if (!item.widget) {
                continue;
            }
            if (isDashboardLayout(item.widget)) {
                items.push(item.widget);
                items.push(...getLayoutWidgets(item.widget));
            }
            else {
                items.push(item.widget);
                if (isVisualizationSwitcherWidget(item.widget)) {
                    items.push(...item.widget.visualizations);
                }
            }
        }
    }
    return items;
};
/**
 * Selects dashboard widgets in an obj ref an array. This map will include both analytical and custom
 * widgets that are placed on the dashboard and also all widgets from visualization switchers
 *
 * @internal
 */
export const selectWidgets = createSelector(selectLayout, getLayoutWidgets);
/**
 * Selects dashboard widgets in an obj ref to widget map. This map will include both analytical and custom
 * widgets that are placed on the dashboard.
 *
 * @internal
 */
export const selectWidgetsMap = createSelector(selectWidgets, (widgets) => {
    return newMapForObjectWithIdentity(widgets);
});
/**
 * Selects widget by its ref (including custom widgets).
 *
 * @remarks
 * To limit the scope only to analytical widgets, use {@link selectAnalyticalWidgetByRef}.
 *
 * @alpha
 */
export const selectWidgetByRef = createMemoizedSelector((ref) => createSelector(selectWidgetsMap, (widgetMap) => {
    if (!ref) {
        return undefined;
    }
    return widgetMap.get(ref);
}));
/**
 * Selects filterable widget by its ref (including custom widgets).
 * This selector will return undefined if the provided ref to non-filterable widget.
 *
 * @remarks
 * To limit the scope only to analytical widgets, use {@link selectAnalyticalWidgetByRef}.
 *
 * @alpha
 */
export const selectFilterableWidgetByRef = createMemoizedSelector((ref) => createSelector(selectWidgetsMap, (widgetMap) => {
    if (!ref) {
        return undefined;
    }
    const widget = widgetMap.get(ref);
    if (!widget || isExtendedDashboardLayoutWidget(widget)) {
        return undefined;
    }
    return widget;
}));
/**
 * Selects analytical widget by its ref. This selector will return undefined if the provided
 * widget ref is not analytical widget, eg. custom widget or nested layout.
 *
 * @remarks
 * To include custom widgets as well, use {@link selectWidgetByRef}.
 *
 * @alpha
 */
export const selectAnalyticalWidgetByRef = createMemoizedSelector((ref) => createSelector(selectWidgetsMap, (widgetMap) => {
    if (!ref) {
        return undefined;
    }
    const widget = widgetMap.get(ref);
    if (!widget || isCustomWidget(widget) || isExtendedDashboardLayoutWidget(widget)) {
        return undefined;
    }
    return widget;
}));
/**
 * @alpha
 */
export const selectIgnoredDrillDownHierarchiesByWidgetRef = createMemoizedSelector((ref) => createSelector(selectAnalyticalWidgetByRef(ref), (widget) => widget?.ignoredDrillDownHierarchies ?? []));
/**
 * @alpha
 */
export const selectIgnoredDrillToUrlAttributesByWidgetRef = createMemoizedSelector((ref) => createSelector(selectAnalyticalWidgetByRef(ref), (widget) => widget?.ignoredDrillToUrlAttributes ?? []));
/**
 * Selects widget drills by the widget ref.
 *
 * @alpha
 */
export const selectWidgetDrills = createMemoizedSelector((ref) => createSelector(selectAnalyticalWidgetByRef(ref), (widget) => widget?.drills ?? []));
/**
 * Selects all filters from filter context converted to filters specific for a widget specified by a ref.
 *
 * @remarks
 * This does NOT resolve things like ignored filters for a widget, etc.
 *
 * @internal
 */
export const selectAllFiltersForWidgetByRef = createMemoizedSelector((ref) => {
    return createSelector(selectWidgetByRef(ref), selectFilterContextFilters, selectCrossFilteringFiltersLocalIdentifiersByWidgetRef(ref), selectEnableIgnoreCrossFiltering, selectWidgetIgnoreCrossFiltering(ref), selectCrossFilteringFiltersLocalIdentifiers, (widget, dashboardFilters, crossFilteringFiltersLocalIdentifiers, isIgnoreCrossFilteringEnabled, shouldIgnoreCrossFiltering, allCrossFilteringLocalIdentifiers) => {
        invariant(widget, `widget with ref ${objRefToString(ref)} does not exist in the state`);
        if (isExtendedDashboardLayoutWidget(widget)) {
            return [[], []];
        }
        // Widget is the source of cross-filtering, so filtering out the cross-filtering filters
        const filtersWithoutCrossFilteringFilters = dashboardFilters.filter((f) => {
            if (isDashboardAttributeFilterItem(f)) {
                const localId = dashboardAttributeFilterItemLocalIdentifier(f);
                return !crossFilteringFiltersLocalIdentifiers?.includes(localId);
            }
            return true;
        });
        // Widget ignores cross-filtering, so we should remove all cross-filtering filters
        const filtersWithoutAllCrossFiltering = isIgnoreCrossFilteringEnabled && shouldIgnoreCrossFiltering
            ? filtersWithoutCrossFilteringFilters.filter((f) => {
                if (isDashboardAttributeFilterItem(f)) {
                    const localId = dashboardAttributeFilterItemLocalIdentifier(f);
                    return !allCrossFilteringLocalIdentifiers.includes(localId);
                }
                return true;
            })
            : [...filtersWithoutCrossFilteringFilters];
        const [commonDateFilters, otherFilters] = partition(filtersWithoutAllCrossFiltering, isDashboardCommonDateFilter);
        return [
            filterContextItemsToDashboardFiltersByWidget(commonDateFilters, widget),
            filterContextItemsToDashboardFiltersByWidget(otherFilters, widget),
        ];
    });
});
const selectAllWidgets = createSelector(selectWidgetsMap, (widgetMap) => {
    return Array.from(widgetMap.values());
});
/**
 * Selects a boolean indicating if the dashboard is empty.
 *
 * @alpha
 */
export const selectIsLayoutEmpty = createSelector(selectAllWidgets, (allWidgets) => {
    return allWidgets.length === 0;
});
/**
 * Selects all KPI widgets in the layout.
 *
 * @alpha
 */
export const selectAllKpiWidgets = createSelector(selectAllWidgets, (allWidgets) => {
    return allWidgets.filter(isKpiWidget);
});
/**
 * Selects all insight widgets in the layout.
 *
 * @alpha
 */
export const selectAllInsightWidgets = createSelector(selectAllWidgets, (allWidgets) => {
    return allWidgets.filter(isInsightWidget);
});
/**
 * Selects all custom widgets in the layout.
 *
 * @alpha
 */
export const selectAllCustomWidgets = createSelector(selectAllWidgets, (allWidgets) => {
    return allWidgets.filter(isCustomWidget);
});
/**
 * Selects all non-custom widgets in the layout.
 *
 * @alpha
 */
export const selectAllAnalyticalWidgets = createSelector(selectAllWidgets, (allWidgets) => {
    return allWidgets.filter((w) => !isCustomWidget(w));
});
/**
 * Selects a boolean indicating if the dashboard contains at least one non-custom widget.
 *
 * @alpha
 */
export const selectLayoutHasAnalyticalWidgets = createSelector(selectAllAnalyticalWidgets, (allAnalyticalWidgets) => {
    return allAnalyticalWidgets.length > 0;
});
/**
 * Selects layout path for a given widget.
 *
 * @alpha
 */
export const selectWidgetPathByRef = createMemoizedSelector((ref) => {
    return createSelector(selectLayout, (layout) => {
        const coords = getWidgetCoordinates(layout, ref);
        invariant(coords, `widget with ref ${objRefToString(ref)} does not exist in the state`);
        return coords;
    });
});
/**
 * Selects layout coordinates for a given widget.
 *
 * @alpha
 *
 * @deprecated The selector returns coordinates in its parent layout. The information is not useful on its
 *  own when dashboard uses nested layout. For this case use {@link selectWidgetPathByRef} instead.
 *
 *  TODO LX-648: remove this selector, exported only for backward compatible reasons.
 */
export const selectWidgetCoordinatesByRef = createMemoizedSelector((ref) => {
    return createSelector(selectLayout, (layout) => {
        const coords = getWidgetCoordinates(layout, ref);
        invariant(coords, `widget with ref ${objRefToString(ref)} does not exist in the state`);
        return coords[coords.length - 1];
    });
});
/**
 * @internal
 */
export const selectWidgetPlaceholder = createSelector(selectAllCustomWidgets, (customWidgets) => {
    return customWidgets.find(isPlaceholderWidget);
});
/**
 * @internal
 */
export const selectWidgetPlaceholderPath = createSelector(selectWidgetPlaceholder, selectLayout, (widgetPlaceholder, layout) => {
    return widgetPlaceholder ? getWidgetCoordinates(layout, widgetPlaceholder.ref) : undefined;
});
/**
 * @internal
 *
 * @deprecated The selector returns coordinates in its parent layout. The information is not useful on its
 *  own when dashboard uses nested layout. For this case use {@link selectWidgetPlaceholderPath} instead.
 *
 *  TODO LX-648: remove this selector, exported only for backward compatible reasons.
 */
export const selectWidgetPlaceholderCoordinates = createSelector(selectWidgetPlaceholder, selectLayout, (widgetPlaceholder, layout) => {
    const path = widgetPlaceholder ? getWidgetCoordinates(layout, widgetPlaceholder.ref) : undefined;
    return path === undefined ? undefined : path[path.length - 1];
});
/**
 * @internal
 */
export const selectInsightWidgetPlaceholder = createSelector(selectAllCustomWidgets, (customWidgets) => {
    return customWidgets.find(isInsightPlaceholderWidget);
});
/**
 * @internal
 */
export const selectInsightWidgetPlaceholderPath = createSelector(selectInsightWidgetPlaceholder, selectLayout, (widgetPlaceholder, layout) => {
    return widgetPlaceholder ? getWidgetCoordinates(layout, widgetPlaceholder.ref) : undefined;
});
/**
 * @internal
 *
 * @deprecated The selector returns coordinates in its parent layout. The information is not useful on its
 *  own when dashboard uses nested layout. For this case use {@link selectInsightWidgetPlaceholderPath} instead.
 *
 *  TODO LX-648: remove this selector, exported only for backward compatible reasons.
 */
export const selectInsightWidgetPlaceholderCoordinates = createSelector(selectInsightWidgetPlaceholder, selectLayout, (widgetPlaceholder, layout) => {
    const path = widgetPlaceholder ? getWidgetCoordinates(layout, widgetPlaceholder.ref) : undefined;
    return path === undefined ? undefined : path[path.length - 1];
});
/**
 * @internal
 */
export const selectKpiWidgetPlaceholder = createSelector(selectAllCustomWidgets, (customWidgets) => {
    return customWidgets.find(isKpiPlaceholderWidget);
});
/**
 * @internal
 */
export const selectKpiWidgetPlaceholderPath = createSelector(selectKpiWidgetPlaceholder, selectLayout, (widgetPlaceholder, layout) => {
    return widgetPlaceholder ? getWidgetCoordinates(layout, widgetPlaceholder.ref) : undefined;
});
/**
 * @internal
 *
 * @deprecated The selector returns coordinates in its parent layout. The information is not useful on its
 *  own when dashboard uses nested layout. For this case use {@link selectKpiWidgetPlaceholderPath} instead.
 *
 *  TODO LX-648: remove this selector, exported only for backward compatible reasons.
 */
export const selectKpiWidgetPlaceholderCoordinates = createSelector(selectKpiWidgetPlaceholder, selectLayout, (widgetPlaceholder, layout) => {
    const path = widgetPlaceholder ? getWidgetCoordinates(layout, widgetPlaceholder.ref) : undefined;
    return path === undefined ? undefined : path[path.length - 1];
});
/**
 * Selects whether widget should ignore cross-filtering filters
 *
 * @alpha
 */
export const selectWidgetIgnoreCrossFiltering = createMemoizedSelector((ref) => createSelector(selectAnalyticalWidgetByRef(ref), (widget) => {
    return widget?.ignoreCrossFiltering ?? false;
}));
/**
 * Selects the local identifier of the tab that contains the specified widget.
 * Returns undefined if the widget is not found in any tab.
 *
 * @param ref - widget reference to search for
 * @returns local identifier of the tab containing the widget, or undefined if not found
 *
 * @alpha
 */
export const selectTabLocalIdentifierByWidgetRef = createMemoizedSelector((ref) => createSelector(selectTabs, (tabs) => {
    if (!ref || !tabs) {
        return undefined;
    }
    // Search through all tabs to find which one contains the widget
    for (const tab of tabs) {
        if (tab.layout?.layout) {
            const widgetCoordinates = getWidgetCoordinates(tab.layout.layout, ref);
            if (widgetCoordinates) {
                return tab.localIdentifier;
            }
        }
    }
    return undefined;
}));
/**
 * Maps widget local identifiers to the tab that contains them.
 *
 * @alpha
 */
export const selectWidgetLocalIdToTabIdMap = createSelector(selectTabs, (tabs) => {
    if (!tabs) {
        return {};
    }
    return tabs.reduce((acc, tab) => {
        if (!tab.localIdentifier || !tab.layout?.layout) {
            return acc;
        }
        const widgets = getLayoutWidgets(tab.layout.layout);
        widgets.forEach((widget) => {
            if (widget.localIdentifier) {
                acc[widget.localIdentifier] = tab.localIdentifier;
            }
        });
        return acc;
    }, {});
});
//# sourceMappingURL=layoutSelectors.js.map