import { getVisibleInsertOptions } from "../../insertMenuItems";
import { measureBridgeComponentRect, measureBridgePlaceholderRect, rectEquals, } from "./geometry";
import { normalizeMarkerId } from "../../utils";
/**
 * Convert a raw zone index (over all DOM children) into the layout-excluded
 * index that `MoveComponentOperation` / `addComponent` expect. Shared by the
 * drop-zone dots, the insertion line, and keyboard reordering so all three
 * agree around hidden layout components.
 */
export function layoutAwareIndex(components, rawIndex) {
    return components.slice(0, rawIndex).filter((c) => !c.layoutId).length;
}
/**
 * Compute the set of insertion gaps for the current drag/insert context, at
 * their TRUE positions (no overlap-avoidance shuffling). This is the single
 * source of truth consumed by both the drop-zone dots (which then apply
 * presentation-only overlap avoidance) and the live insertion zone.
 */
export function computePlaceholderZones(input) {
    const { placeholders, geometry, metrics, dragObject, selectedForInsertion, insertMode, showLayoutComponents, hoveredComponentId, scopeInsertModeToHoveredComponent = true, } = input;
    if (!geometry)
        return [];
    if (!dragObject && !selectedForInsertion && !insertMode)
        return [];
    const selectedForInsertionScope = getInsertModeScopeForComponent(placeholders, selectedForInsertion);
    const selectedForInsertionIsComponent = !!selectedForInsertion && hasInsertModeScope(selectedForInsertionScope);
    const scopedInsertModeScope = !dragObject && selectedForInsertionIsComponent
        ? directPlaceholderScope(selectedForInsertionScope)
        : scopeInsertModeToHoveredComponent &&
            !dragObject &&
            (insertMode || selectedForInsertion)
            ? getInsertModeScopeForComponent(placeholders, hoveredComponentId)
            : undefined;
    const matchingPlaceholders = placeholders
        .filter((x) => showLayoutComponents === true ||
        !isInsideLayoutComponent(x.parentComponent))
        .filter((x) => {
        if (!scopedInsertModeScope)
            return true;
        return (scopedInsertModeScope.directPlaceholderKeys.has(x.key) ||
            scopedInsertModeScope.siblingGaps.has(x.key));
    })
        .filter((x) => {
        return placeholderAcceptsInsertContext(x, {
            dragObject,
            selectedForInsertion,
            insertMode,
            selectedForInsertionIsComponent,
        });
    });
    const measuredZones = [];
    matchingPlaceholders.forEach((placeholder) => {
        measuredZones.push(...measureBridgePlaceholderZones(placeholder, geometry, metrics));
    });
    const scopedMeasuredZones = scopedInsertModeScope
        ? measuredZones.filter((z) => scopedInsertModeScope.directPlaceholderKeys.has(z.placeholder.key) ||
            scopedInsertModeScope.siblingGaps
                .get(z.placeholder.key)
                ?.has(z.index))
        : measuredZones;
    const noOpRangeByPlaceholderKey = new Map();
    const zonesWithoutNoOpMoveGaps = scopedMeasuredZones.filter((zone) => {
        const key = zone.placeholder.key;
        if (!noOpRangeByPlaceholderKey.has(key)) {
            noOpRangeByPlaceholderKey.set(key, getNoOpMoveGapRange(dragObject, zone.placeholder));
        }
        const range = noOpRangeByPlaceholderKey.get(key);
        return !range || zone.index < range.first || zone.index > range.last;
    });
    const zonesWithoutBeforeFirstLayout = zonesWithoutNoOpMoveGaps.filter((z) => {
        const components = z.placeholder.components;
        const firstLayoutIndex = components.findIndex((c) => !!c.layoutId);
        if (firstLayoutIndex >= 0 && z.index <= firstLayoutIndex) {
            return false;
        }
        return true;
    });
    return showLayoutComponents === false
        ? zonesWithoutBeforeFirstLayout.filter((z) => {
            const components = z.placeholder.components;
            if (z.index >= 1 && z.index < components.length) {
                const prev = components[z.index - 1];
                const next = components[z.index];
                if (prev?.layoutId && next?.layoutId)
                    return false;
            }
            return true;
        })
        : zonesWithoutBeforeFirstLayout;
}
export function placeholderAcceptsInsertContext(placeholder, { dragObject, selectedForInsertion, insertMode, selectedForInsertionIsComponent, }) {
    const visibleInsertOptions = getVisibleInsertOptions(placeholder.insertOptions);
    if (!placeholder.editable || visibleInsertOptions.length === 0) {
        return false;
    }
    if (!!insertMode && !dragObject && !selectedForInsertion)
        return true;
    if (!dragObject && selectedForInsertionIsComponent)
        return true;
    return !!visibleInsertOptions.find((y) => y.typeId === selectedForInsertion ||
        y.compatibleTypeIds?.find((z) => z == selectedForInsertion) !==
            undefined ||
        y.typeId === dragObject?.typeId ||
        y.typeId === dragObject?.templateId ||
        y.compatibleTypeIds?.find((z) => z == dragObject?.typeId) !== undefined ||
        y.typeId === dragObject?.templateId ||
        y.compatibleTypeIds?.find((z) => z == dragObject?.templateId) !==
            undefined);
}
// Pull the cross-axis distance toward the cursor harder than the main axis so
// the cursor prefers a gap in the column/row it is actually hovering before
// snapping to the nearest gap within it.
const CROSS_AXIS_WEIGHT = 2.5;
/**
 * Pick the insertion gap nearest the cursor (in overlay/document space),
 * weighting the placeholder's cross axis so the cursor stays within the
 * hovered column/row.
 */
export function resolveNearestGap(point, zones) {
    let best = null;
    let bestScore = Infinity;
    for (const zone of zones) {
        const dx = zone.position.x - point.x;
        const dy = zone.position.y - point.y;
        const mainDelta = zone.orientation === "horizontal" ? dx : dy;
        const crossDelta = zone.orientation === "horizontal" ? dy : dx;
        const score = mainDelta * mainDelta + crossDelta * crossDelta * CROSS_AXIS_WEIGHT;
        if (score < bestScore) {
            bestScore = score;
            best = zone;
        }
    }
    return best;
}
/**
 * True when the gap between two measured siblings sits at a layout wrap:
 * `next` starts a new row (horizontal placeholder) or a new column (vertical
 * placeholder), as in a wrapping/grid layout the single-axis orientation model
 * cannot see. Detected as no overlap on the cross axis, or `next` starting
 * before `previous` on the main axis (a wrapped item restarts at the leading
 * edge). At such a gap there is no shared gutter between the two rects, so
 * "between"-style visuals spanning min→max across both rows must not be used.
 */
export function isWrapInsertionGap(orientation, previousRect, nextRect) {
    if (orientation === "horizontal") {
        const crossOverlap = Math.min(previousRect.y + previousRect.height, nextRect.y + nextRect.height) - Math.max(previousRect.y, nextRect.y);
        return crossOverlap <= 0 || nextRect.x < previousRect.x;
    }
    const crossOverlap = Math.min(previousRect.x + previousRect.width, nextRect.x + nextRect.width) -
        Math.max(previousRect.x, nextRect.x);
    return crossOverlap <= 0 || nextRect.y < previousRect.y;
}
export function placeholderZonesEqual(previous, next) {
    if (previous.length !== next.length)
        return false;
    for (let i = 0; i < previous.length; i++) {
        const previousZone = previous[i];
        const nextZone = next[i];
        if (previousZone.placeholder.key !== nextZone.placeholder.key ||
            previousZone.index !== nextZone.index ||
            (previousZone.element ?? null) !== (nextZone.element ?? null) ||
            previousZone.anchor !== nextZone.anchor ||
            previousZone.description !== nextZone.description ||
            !rectEquals(previousZone.targetRect, nextZone.targetRect) ||
            !rectEquals({ ...previousZone.position, width: 0, height: 0 }, { ...nextZone.position, width: 0, height: 0 })) {
            return false;
        }
    }
    return true;
}
function getPositionFromRect(rect, orientation) {
    return orientation === "horizontal"
        ? {
            x: rect.x + rect.width,
            y: rect.y + rect.height / 2,
        }
        : {
            x: rect.x + rect.width / 2,
            y: rect.y + rect.height,
        };
}
export function measureBridgePlaceholderZones(placeholder, geometry, metrics) {
    const zones = [];
    const placeholderMeasurement = measureBridgePlaceholderRect(geometry, placeholder.key, metrics);
    const orientation = getBridgePlaceholderOrientation(placeholder, geometry, metrics, placeholderMeasurement?.target.orientation);
    const firstComponent = placeholder.components[0];
    const firstComponentMeasurement = firstComponent
        ? measureBridgeComponentRect(geometry, firstComponent, metrics)
        : null;
    const parentMeasurement = placeholder.parentComponent
        ? measureBridgeComponentRect(geometry, placeholder.parentComponent, metrics)
        : null;
    const referenceRect = placeholderMeasurement?.rect ??
        firstComponentMeasurement?.rect ??
        parentMeasurement?.rect;
    if (!referenceRect)
        return zones;
    const isUsingParentBounds = !placeholderMeasurement?.rect &&
        !firstComponentMeasurement?.rect &&
        !!parentMeasurement?.rect;
    const position = isUsingParentBounds || placeholder.components.length === 0
        ? {
            x: referenceRect.x + referenceRect.width / 2,
            y: referenceRect.y + referenceRect.height / 2,
        }
        : // Anchor "before first" to the FIRST COMPONENT when it measures — that is
            // also the zone's targetRect, so the dot and its activation area agree. The
            // placeholder rect spans ALL rows of a wrapping grid, which would park the
            // dot rows away from the gap it inserts into.
            getBeforePositionFromRect(firstComponentMeasurement?.rect ?? referenceRect, orientation);
    zones.push({
        placeholder,
        position,
        index: 0,
        targetRect: isUsingParentBounds || placeholder.components.length === 0
            ? referenceRect
            : (firstComponentMeasurement?.rect ?? referenceRect),
        element: null,
        anchor: isUsingParentBounds || orientation !== "horizontal" ? "top" : "left",
        orientation,
        description: placeholderMeasurement?.target.description,
    });
    placeholder.components.forEach((component, index) => {
        const componentMeasurement = measureBridgeComponentRect(geometry, component, metrics);
        if (!componentMeasurement?.rect)
            return;
        zones.push({
            placeholder,
            position: getPositionFromRect(componentMeasurement.rect, orientation),
            index: index + 1,
            targetRect: componentMeasurement.rect,
            element: null,
            anchor: orientation === "horizontal" ? "right" : "bottom",
            orientation,
            description: placeholderMeasurement?.target.description,
        });
    });
    return zones;
}
/**
 * Gap indices that would re-insert the dragged components exactly where they
 * already sit. For a component drag whose dragged set lives entirely in
 * `placeholder` as one contiguous DOM run [first..last], the gaps hugging and
 * inside that run (first..last+1) reproduce the current order, so offering
 * them as drop targets is noise — they are hidden. Releasing over the original
 * spot then resolves no nearby zone and the drag cancels in place instead of
 * snapping to the next real gap. Non-contiguous selections stay offered:
 * moving them compacts the selection at the target, which IS a change.
 */
function getNoOpMoveGapRange(dragObject, placeholder) {
    if (dragObject?.type !== "component")
        return null;
    const dragged = dragObject.components;
    if (!dragged?.length)
        return null;
    const indices = [];
    for (const descriptor of dragged) {
        const normalizedId = normalizeMarkerId(descriptor.id);
        if (!normalizedId)
            return null;
        const index = placeholder.components.findIndex((candidate) => normalizeMarkerId(candidate.id) === normalizedId);
        if (index < 0)
            return null; // part of the drag lives outside this placeholder
        indices.push(index);
    }
    indices.sort((a, b) => a - b);
    const first = indices[0];
    const last = indices[indices.length - 1];
    if (last - first + 1 !== indices.length)
        return null; // non-contiguous run
    return { first, last: last + 1 };
}
function componentMatchesId(component, id) {
    const normalizedId = normalizeMarkerId(id);
    if (!component || !normalizedId)
        return false;
    return (normalizeMarkerId(component.id) === normalizedId ||
        normalizeMarkerId(component.datasourceItem?.id) === normalizedId);
}
function hasInsertModeScope(scope) {
    return scope.directPlaceholderKeys.size > 0 || scope.siblingGaps.size > 0;
}
function directPlaceholderScope(scope) {
    return {
        directPlaceholderKeys: new Set(scope.directPlaceholderKeys),
        siblingGaps: new Map(),
    };
}
function getInsertModeScopeForComponent(placeholders, componentId) {
    const scope = {
        directPlaceholderKeys: new Set(),
        siblingGaps: new Map(),
    };
    if (!componentId)
        return scope;
    for (const placeholder of placeholders) {
        if (componentMatchesId(placeholder.parentComponent, componentId)) {
            scope.directPlaceholderKeys.add(placeholder.key);
        }
        const componentIndex = placeholder.components.findIndex((component) => componentMatchesId(component, componentId));
        if (componentIndex >= 0) {
            scope.siblingGaps.set(placeholder.key, new Set([componentIndex, componentIndex + 1]));
        }
    }
    return scope;
}
function getBeforePositionFromRect(rect, orientation) {
    return orientation === "horizontal"
        ? {
            x: rect.x,
            y: rect.y + rect.height / 2,
        }
        : {
            x: rect.x + rect.width / 2,
            y: rect.y,
        };
}
function getBridgePlaceholderOrientation(placeholder, geometry, metrics, explicitOrientation) {
    const measuredOrientation = inferPlaceholderOrientationFromComponents(placeholder, geometry, metrics);
    if (measuredOrientation) {
        return measuredOrientation;
    }
    if (explicitOrientation === "horizontal" ||
        explicitOrientation === "vertical") {
        return explicitOrientation;
    }
    return "vertical";
}
function inferPlaceholderOrientationFromComponents(placeholder, geometry, metrics) {
    if (placeholder.components.length > 1) {
        const measuredComponents = placeholder.components
            .map((component) => measureBridgeComponentRect(geometry, component, metrics))
            .filter((measurement) => !!measurement?.rect);
        const first = measuredComponents[0];
        const second = measuredComponents[1];
        if (first?.rect && second?.rect) {
            const horizontalDiff = Math.abs(second.rect.x - first.rect.x);
            const verticalDiff = Math.abs(second.rect.y - first.rect.y);
            return horizontalDiff > verticalDiff ? "horizontal" : "vertical";
        }
    }
    return null;
}
export function isInsideLayoutComponent(component) {
    let current = component;
    while (current) {
        if (current.layoutId)
            return true;
        current = current.parentPlaceholder?.parentComponent;
    }
    return false;
}
//# sourceMappingURL=placeholderZones.js.map