<script lang="ts">
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { type Layout, NODE_W, NODE_H } from '$lib/person/genealogy/layout/buildLayout';
import { DIMMED_OPACITY } from '$lib/person/genealogy/layout/highlightLineage';
import type { components } from '$lib/generated/api';

type RelationshipDTO = components['schemas']['RelationshipDTO'];

interface Props {
	edges: RelationshipDTO[];
	positions: Layout['positions'];
	/**
	 * Displaced parent→child edges (cross-level intra-family marriages): the
	 * child lives in a spouse's run elsewhere, so the connector is drawn with a
	 * distinct dash so it never reads as a normal parent drop or an ended
	 * marriage. Geometry still lands on the child, so meaning is redundant.
	 */
	crossLinks?: Layout['crossLinks'];
	/**
	 * Whether the connector joining two people is active (full strength). A
	 * connector is active only when both endpoints are active; otherwise it is
	 * dimmed. Defaults to always-active, so no lineage highlight means no dimming.
	 */
	isConnectorActive?: (aId: string, bId: string) => boolean;
}

let { edges, positions, crossLinks = [], isConnectorActive = () => true }: Props = $props();

// Dash cadence + opacity for a cross-link, deliberately distinct from the
// ended-marriage spouse dash (`4 4`) so the two never read alike (WCAG 1.4.1:
// geometry carries the meaning too, not stroke alone). Opacity stays at 0.7 so
// the dotted line clears the WCAG 1.4.11 3:1 non-text contrast floor for
// senior / low-vision readers (a lighter 0.55 fell just under).
const CROSS_LINK_DASH = '2 6';
const CROSS_LINK_OPACITY = 0.7;

const crossLinkSet = $derived(new SvelteSet(crossLinks.map((c) => `${c.parentId}->${c.childId}`)));
function isCrossLink(parentId: string, childId: string): boolean {
	return crossLinkSet.has(`${parentId}->${childId}`);
}

/** SVG group opacity for a connector: full when active, dimmed otherwise. */
function connectorOpacity(active: boolean): number | undefined {
	return active ? undefined : DIMMED_OPACITY;
}

function nodeCenter(id: string): { x: number; y: number } | null {
	const p = positions.get(id);
	if (!p) return null;
	return { x: p.x + NODE_W / 2, y: p.y + NODE_H / 2 };
}

const parentEdges = $derived(edges.filter((e) => e.relationType === 'PARENT_OF'));
const spouseEdges = $derived(edges.filter((e) => e.relationType === 'SPOUSE_OF'));

function pairKey(a: string, b: string): string {
	return a < b ? `${a}|${b}` : `${b}|${a}`;
}

type ParentLinks = {
	// One entry per spouse-pair-with-children: drives the drop + sibling-bar
	// + per-child vertical pattern in the SVG.
	shared: { key: string; parentA: string; parentB: string; childIds: string[] }[];
	// One entry per remaining parent → child edge (single parents, or the
	// "second" parent edge when only one parent is in the spouse pair).
	single: { key: string; parentId: string; childId: string }[];
};

const parentLinks = $derived.by<ParentLinks>(() => {
	const spousePairs = new SvelteSet<string>();
	for (const e of spouseEdges) {
		spousePairs.add(pairKey(e.personId, e.relatedPersonId));
	}

	const childToParents = new SvelteMap<string, string[]>();
	for (const e of parentEdges) {
		const list = childToParents.get(e.relatedPersonId) ?? [];
		list.push(e.personId);
		childToParents.set(e.relatedPersonId, list);
	}

	const sharedMap = new SvelteMap<
		string,
		{ parentA: string; parentB: string; childIds: string[] }
	>();
	const single: ParentLinks['single'] = [];
	for (const [childId, parents] of childToParents) {
		const consumed = new SvelteSet<string>();
		for (let i = 0; i < parents.length; i++) {
			if (consumed.has(parents[i])) continue;
			for (let j = i + 1; j < parents.length; j++) {
				if (consumed.has(parents[j])) continue;
				if (spousePairs.has(pairKey(parents[i], parents[j]))) {
					const groupKey = pairKey(parents[i], parents[j]);
					const existing = sharedMap.get(groupKey);
					if (existing) {
						existing.childIds.push(childId);
					} else {
						sharedMap.set(groupKey, {
							parentA: parents[i],
							parentB: parents[j],
							childIds: [childId]
						});
					}
					consumed.add(parents[i]);
					consumed.add(parents[j]);
					break;
				}
			}
		}
		for (const parentId of parents) {
			if (consumed.has(parentId)) continue;
			single.push({ key: `${parentId}->${childId}`, parentId, childId });
		}
	}

	const shared: ParentLinks['shared'] = [];
	for (const [key, group] of sharedMap) shared.push({ key, ...group });
	return { shared, single };
});
</script>

<!-- Shared parent-pair → children: drop from spouse midpoint to a sibling
     bar, then short verticals from the bar to each child top. -->
{#each parentLinks.shared as group (group.key)}
	{@const aCenter = nodeCenter(group.parentA)}
	{@const bCenter = nodeCenter(group.parentB)}
	{@const childCenters = group.childIds
		.map((id) => {
			const c = nodeCenter(id);
			return c ? { id, x: c.x, y: c.y } : null;
		})
		.filter((c): c is { id: string; x: number; y: number } => c !== null)}
	{#if aCenter && bCenter && childCenters.length > 0}
		{@const midX = (aCenter.x + bCenter.x) / 2}
		{@const parentBottomY = aCenter.y + NODE_H / 2}
		{@const childTopY = childCenters[0].y - NODE_H / 2}
		{@const barY = (parentBottomY + childTopY) / 2}
		{@const xs = childCenters.map((c) => c.x)}
		{@const minX = Math.min(midX, ...xs)}
		{@const maxX = Math.max(midX, ...xs)}
		{@const pairActive = isConnectorActive(group.parentA, group.parentB)}
		<!-- Drop from the spouse midpoint to the sibling bar: joins the parent pair. -->
		<g class="lineage-fade" opacity={connectorOpacity(pairActive)}>
			<line
				x1={midX}
				y1={parentBottomY}
				x2={midX}
				y2={barY}
				stroke="var(--c-primary)"
				stroke-width="1.5"
			/>
			{#if minX !== maxX}
				<line
					x1={minX}
					y1={barY}
					x2={maxX}
					y2={barY}
					stroke="var(--c-primary)"
					stroke-width="1.5"
				/>
			{/if}
		</g>
		{#each childCenters as cc (cc.id)}
			<!-- Each vertical joins the parent pair to one child: active only when
			     both parents and that child are active, so the same bar can stay lit
			     to the lineage child while dimming to a collateral sibling. The child
			     id rides on the centre object, so it never desyncs from the filtered
			     centres (a child without a position drops out of both together). -->
			{@const childActive =
				isConnectorActive(group.parentA, cc.id) && isConnectorActive(group.parentB, cc.id)}
			{@const childCross =
				isCrossLink(group.parentA, cc.id) || isCrossLink(group.parentB, cc.id)}
			<g class="lineage-fade" opacity={connectorOpacity(childActive)}>
				<line
					x1={cc.x}
					y1={barY}
					x2={cc.x}
					y2={childTopY}
					stroke="var(--c-primary)"
					stroke-width="1.5"
					stroke-dasharray={childCross ? CROSS_LINK_DASH : undefined}
					stroke-opacity={childCross ? CROSS_LINK_OPACITY : undefined}
				/>
			</g>
		{/each}
	{/if}
{/each}

<!-- Single-parent → child connectors: parent bottom → bar → child top. -->
{#each parentLinks.single as link (link.key)}
	{@const parentCenter = nodeCenter(link.parentId)}
	{@const childCenter = nodeCenter(link.childId)}
	{#if parentCenter && childCenter}
		{@const parentBottomY = parentCenter.y + NODE_H / 2}
		{@const childTopY = childCenter.y - NODE_H / 2}
		{@const barY = (parentBottomY + childTopY) / 2}
		{@const active = isConnectorActive(link.parentId, link.childId)}
		{@const cross = isCrossLink(link.parentId, link.childId)}
		{@const dash = cross ? CROSS_LINK_DASH : undefined}
		{@const strokeOpacity = cross ? CROSS_LINK_OPACITY : undefined}
		<g class="lineage-fade" opacity={connectorOpacity(active)}>
			<line
				x1={parentCenter.x}
				y1={parentBottomY}
				x2={parentCenter.x}
				y2={barY}
				stroke="var(--c-primary)"
				stroke-width="1.5"
				stroke-dasharray={dash}
				stroke-opacity={strokeOpacity}
			/>
			{#if parentCenter.x !== childCenter.x}
				<line
					x1={parentCenter.x}
					y1={barY}
					x2={childCenter.x}
					y2={barY}
					stroke="var(--c-primary)"
					stroke-width="1.5"
					stroke-dasharray={dash}
					stroke-opacity={strokeOpacity}
				/>
			{/if}
			<line
				x1={childCenter.x}
				y1={barY}
				x2={childCenter.x}
				y2={childTopY}
				stroke="var(--c-primary)"
				stroke-width="1.5"
				stroke-dasharray={dash}
				stroke-opacity={strokeOpacity}
			/>
		</g>
	{/if}
{/each}

<!-- Spouse connectors -->
{#each spouseEdges as e (e.id)}
	{@const aCenter = nodeCenter(e.personId)}
	{@const bCenter = nodeCenter(e.relatedPersonId)}
	{#if aCenter && bCenter}
		{@const active = isConnectorActive(e.personId, e.relatedPersonId)}
		<g class="lineage-fade" opacity={connectorOpacity(active)}>
			<line
				x1={aCenter.x}
				y1={aCenter.y}
				x2={bCenter.x}
				y2={bCenter.y}
				stroke="var(--c-primary)"
				stroke-width="1.5"
				stroke-dasharray={e.toDate ? '4 4' : undefined}
			/>
			<circle
				cx={(aCenter.x + bCenter.x) / 2}
				cy={(aCenter.y + bCenter.y) / 2}
				r="6"
				fill="var(--c-primary)"
			/>
		</g>
	{/if}
{/each}

<style>
/* Ease the lineage focus+dim transition; instant for reduced-motion users. */
.lineage-fade {
	transition: opacity 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
	.lineage-fade {
		transition: none;
	}
}
</style>
