#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2021 Da Xue <da@libre.computer>
# PURPOSE: Manage the application and removal of device tree overlays in the Linux kernel.

set -e

if [ -z "$VENDOR" ]; then
	if [ ! -e /sys/class/dmi/id/board_vendor ]; then
		echo "No vendor found!" >&2
		exit 1
	fi
	VENDOR=$(tr -d '\0' < /sys/class/dmi/id/board_vendor)
fi


if [ -z "$BOARD" ]; then
	if [ ! -e /sys/class/dmi/id/board_name ]; then
		echo "No board name found!" >&2
		exit 1
	fi
	BOARD=$(tr -d '\0' < /sys/class/dmi/id/board_name)
fi

PATH_OF_CONFIG=/sys/kernel/config/device-tree/overlays
PATH_OF_DT=/sys/firmware/fdt
PATH_OF_DTB_OVERRIDE=
PATH_OF_EFI_DTB=

cd $(dirname $(readlink -f "${BASH_SOURCE[0]}"))

PATH_DTBO=$VENDOR/$BOARD/dt
PATH_DTCFG=$VENDOR/$BOARD/dt.config
MAP_FILE="$VENDOR/$BOARD/dt.map"
DEPS_FILE="$VENDOR/$BOARD/dt.deps"
TAB_CHAR='	'
declare -A MAP_DT_ALIAS
declare -A MAP_ALIAS_DT
declare -A MAP_DEPS

LDTO_loadMap()
{
	if [ -e "$MAP_FILE" ]; then
		while IFS= read -r mapping; do
			[ -z "$mapping" ] && continue
			[[ "$mapping" =~ ^# ]] && continue
			local dt_alias=${mapping%%$TAB_CHAR*}
			local dt_file=${mapping##*$TAB_CHAR}
			# skip non-TSV / empty keys (bash: bad array subscript)
			[ -z "$dt_alias" ] || [ -z "$dt_file" ] && continue
			[ "$dt_alias" = "$mapping" ] && continue
			MAP_DT_ALIAS[$dt_file]="$dt_alias"
			MAP_ALIAS_DT[$dt_alias]="$dt_file"
		done < <(grep -v '^#' "$MAP_FILE" || true)
	fi
}


LDTO_loadDeps()
{
	MAP_DEPS=()
	if [ -e "$DEPS_FILE" ]; then
		while IFS=$'\t' read -r consumer providers || [ -n "$consumer" ]; do
			[ -z "$consumer" ] && continue
			[[ "$consumer" =~ ^# ]] && continue
			MAP_DEPS[$consumer]="$providers"
		done < "$DEPS_FILE"
	fi
}

# Expand args with auto-providers from dt.deps (providers first, no dups).
LDTO_expandDeps()
{
	LDTO_loadDeps
	local -a ordered=()
	local -A seen=()

	_ldto_add() {
		local name="$1"
		local depth="${2:-0}"
		local dep realp
		if [ "$depth" -gt 6 ]; then
			return
		fi
		realp=$(LDTO_getAliasDT "$name")
		if [ -n "$realp" ]; then
			name="$realp"
		fi
		if [[ -v "seen[$name]" ]]; then
			return
		fi
		if [[ -v "MAP_DEPS[$name]" ]]; then
			for dep in ${MAP_DEPS[$name]}; do
				_ldto_add "$dep" $((depth + 1))
			done
		fi
		if [[ -v "seen[$name]" ]]; then
			return
		fi
		seen[$name]=1
		ordered+=("$name")
	}

	local arg
	for arg in "$@"; do
		_ldto_add "$arg" 0
	done
	printf '%s\n' "${ordered[@]}"
}

LDTO_getDTAlias()
{
	if [[ -v "MAP_DT_ALIAS[$1]" ]]; then
		echo "${MAP_DT_ALIAS[$1]}"
	fi
}

LDTO_getAliasDT()
{
	if [[ -v "MAP_ALIAS_DT[$1]" ]]; then
		echo "${MAP_ALIAS_DT[$1]}"
	fi
}

LDTO_getDTBO()
{
	local dtbo_file="$PATH_DTBO/$1.dtbo"
	if [ -f "$dtbo_file" ]; then
		echo "$dtbo_file"
	fi
}

LDTO_getSummary()
{
	# One-line Summary: from .dts header (empty if none)
	local dts="$1"
	[ -f "$dts" ] || return 0
	LDTO_extractHeaderComments "$dts" 2>/dev/null | awk '
		BEGIN { IGNORECASE = 1 }
		/^Summary:[[:space:]]*/ {
			sub(/^Summary:[[:space:]]*/, "")
			print
			exit
		}
	'
}

LDTO_list()
{
	local dtbos
	dtbos=$(ls "$PATH_DTBO"/*.dtbo 2>/dev/null || true)
	if [ -z "$dtbos" ]; then
		# Allow host-side list from .dts alone
		dtbos=$(ls "$PATH_DTBO"/*.dts 2>/dev/null || true)
		if [ -z "$dtbos" ]; then
			echo "No overlays detected. Did you run make?" >&2
			return 1
		fi
	fi
	LDTO_loadMap
	printf '#%-28s %-24s %s\n' "OVERLAY" "ALIAS" "SUMMARY" >&2
	local i dto alias summary dts
	for i in $dtbos; do
		dto=$(basename "$i")
		dto=${dto%.dtbo}
		dto=${dto%.dts}
		alias=$(LDTO_getDTAlias "$dto")
		dts=$(LDTO_getDTS "$dto")
		summary=""
		if [ -n "$dts" ]; then
			summary=$(LDTO_getSummary "$dts")
		fi
		printf '%-30s %-24s %s\n' "$dto" "${alias:--}" "${summary:--}"
	done
}

LDTO_checkOFConfig()
{
	if [ $# -eq 0 ]; then
		if ! test -r "$PATH_OF_CONFIG"; then
			echo "$FUNCNAME: OF path not readable! sudo?" >&2
			return 1
		fi
	else
		if ! test -w "$PATH_OF_CONFIG"; then
			echo "$FUNCNAME: OF path not writable! sudo?" >&2
			return 1
		fi
	fi
}

# Collect header pins claimed by an overlay (Header\tPin\tName\tSource)
# Source is "header" (Pins: table / pin notes) or "dts" (body pad → gpio.map).
LDTO_collectOverlayPins()
{
	local dto="$1"
	local dts
	dts=$(LDTO_getDTS "$dto")
	[ -n "$dts" ] || return 0
	LDTO_loadGpioMap
	[ -n "$GPIO_MAP_FILE" ] || return 0

	local comments def_hdr
	comments=$(LDTO_extractHeaderComments "$dts")
	def_hdr=$(grep -v '^#' "$GPIO_MAP_FILE" | head -n1 | cut -f1)

	# Structured Pins: 7J1.19  GPIOX_8 ...
	printf '%s\n' "$comments" | awk -v o="$dto" '
		{
			line = $0
			# strip leading "* " from comment extract if present
			sub(/^[[:space:]]+/, "", line)
			if (match(line, /[0-9]+[Jj][0-9]+\.[0-9]+[[:space:]]+[A-Za-z0-9_*]+/)) {
				tok = substr(line, RSTART, RLENGTH)
				split(tok, a, /[\.[:space:]]+/)
				# a[1]=7J1 a[2]=19 a[3]=GPIOX_8
				if (a[1] != "" && a[2] != "" && a[3] != "")
					print a[1] "\t" a[2] "\t" a[3] "\t" o "\theader"
			}
		}
	'

	# Body pad names → gpio.map
	local name row
	while IFS= read -r name; do
		[ -z "$name" ] && continue
		if row=$(LDTO_gpioLookupName "$name"); then
			while IFS= read -r r; do
				[ -z "$r" ] && continue
				local header pin chip line sysfs gname pad ref desc
				IFS=$'\t' read -r header pin chip line sysfs gname pad ref desc <<<"$r"
				case "$gname" in GND|5V|3.3V) continue ;; esac
				printf '%s\t%s\t%s\t%s\tdts\n' "$header" "$pin" "$gname" "$dto"
			done <<<"$row"
		fi
	done < <(LDTO_extractGpioNames "$dts")
}

LDTO_printPlan()
{
	# $@ = overlay names (already expanded or not)
	LDTO_loadMap
	LDTO_loadGpioMap
	local -a seq
	mapfile -t seq < <(LDTO_expandDeps "$@")
	echo "Plan (dependency order):"
	local dto n=0
	for dto in "${seq[@]}"; do
		local dto_real dtbo summary dts
		dto_real=$(LDTO_getAliasDT "$dto")
		[ -n "$dto_real" ] && dto=$dto_real
		n=$((n + 1))
		dtbo=$(LDTO_getDTBO "$dto")
		dts=$(LDTO_getDTS "$dto")
		summary=""
		[ -n "$dts" ] && summary=$(LDTO_getSummary "$dts")
		local state="apply"
		if [ -d "$PATH_OF_CONFIG" ] && [ -e "$PATH_OF_CONFIG/$dto" ]; then
			state="already-active"
		elif [ -z "$dtbo" ]; then
			state="MISSING-dtbo"
		fi
		printf '  %2d. %-28s [%s] %s\n' "$n" "$dto" "$state" "${summary:--}"
	done

	echo
	echo "Header pins that would be claimed:"
	printf '  %-6s %-4s %-14s %s\n' "Header" "Pin" "Name" "Overlay"
	local pins_all
	pins_all=$(
		for dto in "${seq[@]}"; do
			dto_real=$(LDTO_getAliasDT "$dto")
			[ -n "$dto_real" ] && dto=$dto_real
			LDTO_collectOverlayPins "$dto"
		done | sort -u
	)
	if [ -z "$pins_all" ]; then
		echo "  (none resolved — bus-only pinctrl or missing .dts/gpio.map)"
	else
		# collapse multi-source rows: Header Pin Name -> overlays
		printf '%s\n' "$pins_all" | awk -F '\t' '
			{
				key = $1 "\t" $2 "\t" $3
				if (!s[key, $4]++) {
					if (o[key] == "") o[key] = $4
					else o[key] = o[key] "," $4
				}
			}
			END {
				for (k in o) {
					split(k, a, "\t")
					printf "%s\t%02d\t%s\t%s\t%s\n", a[1], a[2]+0, a[2], a[3], o[k]
				}
			}
		' | sort -t$'\t' -k1,1 -k2,2n | awk -F '\t' '{
			printf "  %-6s %-4s %-14s %s\n", $1, $3, $4, $5
		}'
	fi
}

LDTO_enable()
{
	local dry_run=0
	local -a args=()
	while [ $# -gt 0 ]; do
		case "$1" in
			--dry-run|-n)
				dry_run=1
				shift
				;;
			--help|-h)
				echo "$0 enable [--dry-run|-n] OVERLAY [OVERLAY...]" >&2
				echo "  Auto-applies providers from dt.deps. --dry-run prints plan only." >&2
				return 0
				;;
			--)
				shift
				args+=("$@")
				break
				;;
			-*)
				echo "$FUNCNAME: unknown option $1" >&2
				return 1
				;;
			*)
				args+=("$1")
				shift
				;;
		esac
	done

	if [ ${#args[@]} -eq 0 ]; then
		LDTO_list
		return 1
	fi

	if [ "$dry_run" -eq 1 ]; then
		echo "DRY-RUN: no configfs changes" >&2
		LDTO_printPlan "${args[@]}"
		return 0
	fi

	LDTO_checkOFConfig RW
	LDTO_loadMap
	local -a seq
	mapfile -t seq < <(LDTO_expandDeps "${args[@]}")
	local dto
	for dto in "${seq[@]}"; do
		local dto_real
		dto_real=$(LDTO_getAliasDT "$dto")
		if [ -n "$dto_real" ]; then
			dto=$dto_real
		fi
		local dtbo_file
		dtbo_file=$(LDTO_getDTBO "$dto")
		if [ -z "$dtbo_file" ]; then
			echo "$FUNCNAME: $dto does not exist and cannot be added" >&2
			return 1
		fi
		if [ -e "$PATH_OF_CONFIG/$dto" ]; then
			echo "Overlay $dto: already active" >&2
			continue
		fi
		mkdir "$PATH_OF_CONFIG/$dto"
		cat "$dtbo_file" > "$PATH_OF_CONFIG/$dto/dtbo"
		echo "Overlay $dto: $(cat "$PATH_OF_CONFIG/$dto/status")" >&2
	done
}

LDTO_disable()
{
	if [ -z "$1" ]; then
		LDTO_active
		return 1
	fi
	LDTO_checkOFConfig RW
	LDTO_loadMap
	for dto in "$@"; do
		local dto_real=$(LDTO_getAliasDT "$dto")
		if [ ! -z "$dto_real" ]; then
			local dto=$dto_real
		fi
		if [ ! -e "$PATH_OF_CONFIG/$dto" ]; then
			echo "Overlay $dto: does not exist and cannot be removed" >&2
			return 1
		fi
		rmdir $PATH_OF_CONFIG/$dto
		echo "Overlay $dto: removed" >&2
	done
}

LDTO_status()
{
	echo "#Overlays active:" >&2
	for i in `ls $PATH_OF_CONFIG`;
	do
		basename $i
	done
}

LDTO_active()
{
	LDTO_checkOFConfig
	if [ -z "$1" ]; then
		LDTO_status
	else
		if [ -z "$1" ]; then
			return 1
		fi
		for i in `ls $PATH_OF_CONFIG`;
		do
			if [ "$1" = $(basename $i) ]; then
				return 0
			fi
		done
		return 1
	fi
}

LDTO_importDTConfig()
{
	if [ -f "$PATH_DTCFG" ]; then
		. "$PATH_DTCFG"
	else
		echo "$FUNCNAME: board device tree configuration cannot be found." >&2
		return 1
	fi
}

LDTO_checkFirmwareDT()
{
	if [ ! -e "$PATH_OF_DT" ]; then
		echo "$FUNCNAME: system device tree cannot be found." >&2
		return 1
	fi
	if ! test -r "$PATH_OF_DT"; then
		echo "$FUNCNAME: system device tree cannot be read! sudo?" >&2
		return 1
	fi
}

LDTO_findEFIDTBPath()
{
	local mnt_boot=$(cut -f 2 -d " " /proc/mounts  | grep ^/boot)
	local mnt_boot_count=$(echo "$mnt_boot" | wc -l)
	if [ $mnt_boot_count -lt 1 ]; then
		echo "$FUNCNAME: no mounts found under /boot directory." >&2
		return 1
	fi
	for mnt_dir in $mnt_boot; do
		if [ -d "$mnt_dir"/EFI/BOOT -o -e "$mnt_dir"/uboot.env ]; then
			PATH_OF_EFI_DTB="$mnt_dir"/dtb
			if [ $# -ne 0 ]; then
				if [ -d "$PATH_OF_EFI_DTB" ]; then
					if ! test -w "$PATH_OF_EFI_DTB"; then
						echo "$FUNCNAME: EFI path not writable! sudo?" >&2
						return 1
					fi
				else
					if ! test -w "$mnt_dir"; then
						echo "$FUNCNAME: EFI path not writable! sudo?" >&2
						return 1
					fi
					if [ ! -d "$PATH_OF_EFI_DTB" ]; then
						mkdir -p "$PATH_OF_EFI_DTB"
					fi
				fi
			fi
			return
		fi
	done
	if grep "root=/dev/nfs" /proc/cmdline > /dev/null; then
		PATH_OF_EFI_DTB=/boot/efi/dtb
		if [ ! -d "$PATH_OF_EFI_DTB" ]; then
			mkdir "$PATH_OF_EFI_DTB"
		fi
		return
	fi
	echo "$FUNCNAME: no EFI boot path found." >&2
	return 1
}

LDTO_apply()
{
	LDTO_findEFIDTBPath RW
	local base_dtb="$PATH_OF_DT"
	local target_dtb="$PATH_OF_EFI_DTB/$DT_OVERRIDE"
	local target_dtb_path="${target_dtb%/*}"
	if [ ! -d "$target_dtb_path" ]; then
		mkdir "$target_dtb_path"
	fi
	if [ -f "$target_dtb" ]; then
		local base_dtb="$target_dtb"
	fi
	fdtoverlay -i "$base_dtb" -o "$target_dtb" "$1"
}

LDTO_merge()
{
	LDTO_checkFirmwareDT
	LDTO_importDTConfig
	if [ -z "$1" ]; then
		LDTO_list
		return 1
	fi
	LDTO_loadMap
	local -a seq
	mapfile -t seq < <(LDTO_expandDeps "$@")
	local dto
	for dto in "${seq[@]}"; do
		local dto_real=$(LDTO_getAliasDT "$dto")
		if [ ! -z "$dto_real" ]; then
			dto=$dto_real
		fi
		local dtbo_file=$(LDTO_getDTBO "$dto")
		if [ -z "$dtbo_file" ]; then
			echo "$FUNCNAME: $dto does not exist and cannot be added." >&2
			return 1
		fi
		LDTO_apply "$dtbo_file"
		echo "Overlay $dto: merged for next boot" >&2
	done
}

LDTO_checkOverrideDT()
{
	LDTO_importDTConfig
	LDTO_findEFIDTBPath $1
	PATH_OF_DTB_OVERRIDE="$PATH_OF_EFI_DTB/$DT_OVERRIDE"
	if [ ! -f "$PATH_OF_DTB_OVERRIDE" ]; then
		echo "$FUNCNAME: no merged overlays detected on system." >&2
		return 1
	fi
}

LDTO_diff()
{
	LDTO_checkOverrideDT
	LDTO_checkFirmwareDT
	diff -u --color --suppress-common-lines --label=CURRENT <(dtc -I dtb -O dts "$PATH_OF_DT" 2> /dev/null) --label=NEXT <(dtc -I dtb -O dts "$PATH_OF_DTB_OVERRIDE" 2> /dev/null)
}

LDTO_show()
{
	LDTO_checkOverrideDT
	if [ -z "$1" ]; then
		dtc -I dtb -O dts "$PATH_OF_DTB_OVERRIDE" 2> /dev/null | less
	else
		local dtbo_file=$(LDTO_getDTBO "$1")
		if [ ! -z "$dtbo_file" ]; then
			dtc -I dtb -O dts "$dtbo_file" 2> /dev/null | less
		fi
	fi
}

LDTO_edit(){
	LDTO_checkOverrideDT
	dts=$(mktemp)
	dtc -I dtb -O dts "$PATH_OF_DTB_OVERRIDE" -o $dts 2> /dev/null
	vim $dts
	dtc -I dts -O dtb $dts -o "$PATH_OF_DTB_OVERRIDE"
	rm $dts
}

LDTO_current()
{
	LDTO_checkFirmwareDT
	dtc -I dtb -O dts "$PATH_OF_DT" 2> /dev/null | less
}

LDTO_reset()
{
	LDTO_checkOverrideDT RW
	rm -f "$PATH_OF_DTB_OVERRIDE"
	echo "Overlay: reset for next boot" >&2
}


GPIO_MAP_FILE=

LDTO_loadGpioMap()
{
	GPIO_MAP_FILE="$VENDOR/$BOARD/gpio.map"
	if [ ! -f "$GPIO_MAP_FILE" ]; then
		GPIO_MAP_FILE=""
	fi
}

# Lookup gpio.map row by SoC pad Name (column 6). Prints:
# Header Pin Chip Line sysfs Name Pad Ref Desc
LDTO_gpioLookupName()
{
	local name="$1"
	local bare="${name%\*}"
	[ -z "$GPIO_MAP_FILE" ] && return 1
	# Match Name column; allow trailing * in map (e.g. GPIOAO_8*)
	awk -F '\t' -v n="$bare" '
		/^#/ || NF < 6 { next }
		{
			mapn = $6
			sub(/\*$/, "", mapn)
			if (mapn == n) {
				print $0
				found = 1
			}
		}
		END { exit found ? 0 : 1 }
	' "$GPIO_MAP_FILE"
}

# Lookup by header + pin number
LDTO_gpioLookupPin()
{
	local header="$1" pin="$2"
	[ -z "$GPIO_MAP_FILE" ] && return 1
	awk -F '\t' -v h="$header" -v p="$pin" '
		BEGIN { hl = tolower(h) }
		/^#/ || NF < 2 { next }
		tolower($1) == hl && $2 == p { print; found = 1 }
		END { exit found ? 0 : 1 }
	' "$GPIO_MAP_FILE"
}

LDTO_getDTS()
{
	# Prefer source .dts (comments); fall back to nothing if only .dtbo
	local base="$PATH_DTBO/$1"
	if [ -f "${base}.dts" ]; then
		echo "${base}.dts"
	elif [ -L "${base}.dts" ]; then
		echo "${base}.dts"
	fi
}

# Print free-form header comments that precede /dts-v1/
LDTO_extractHeaderComments()
{
	local dts="$1"
	[ -f "$dts" ] || return 0
	awk '
		BEGIN { in_block = 0; spdx_done = 0 }
		/^\/dts-v1\// { exit }
		/^\/\// {
			# keep SPDX line out of Description (shown separately)
			if ($0 ~ /SPDX-License-Identifier/) next
			print
			next
		}
		/^\/\*/ {
			in_block = 1
			# strip leading /*
			line = $0
			sub(/^\/\*+[[:space:]]?/, "", line)
			if (line ~ /\*\//) {
				sub(/[[:space:]]*\*\/.*/, "", line)
				in_block = 0
				if (line != "" && line !~ /^[[:space:]]*$/) print line
				next
			}
			if (line != "" && line !~ /^[[:space:]]*$/) print line
			next
		}
		in_block {
			line = $0
			if (line ~ /\*\//) {
				sub(/[[:space:]]*\*\/.*/, "", line)
				sub(/^[[:space:]]*\*[[:space:]]?/, "", line)
				if (line != "" && line !~ /^[[:space:]]*$/) print line
				in_block = 0
				next
			}
			sub(/^[[:space:]]*\*[[:space:]]?/, "", line)
			print line
			next
		}
	' "$dts"
}

# Collect unique SoC pad names referenced in DTS body (not GPIO_ACTIVE_*)
LDTO_extractGpioNames()
{
	local dts="$1"
	[ -f "$dts" ] || return 0
	# Body only (after /dts-v1/) so header prose is not double-counted as "from DTS"
	awk '
		BEGIN { body = 0 }
		/^\/dts-v1\// { body = 1 }
		body { print }
	' "$dts" | grep -oE '\b(GPIO[A-Z][A-Z0-9_]*|GPIODV_[0-9]+|TEST_N|RK_P[A-Z0-9_]+|PIN_[A-Z0-9_]+)\b' \
		| grep -vE '^(GPIO_ACTIVE_|GPIO_OPEN_|GPIO_PULL_|GPIO_PERSISTENT)' \
		| sort -u
}

# Reverse-deps: consumers that list this overlay as a provider
LDTO_requiredBy()
{
	local name="$1"
	[ -e "$DEPS_FILE" ] || return 0
	awk -F '\t' -v n="$name" '
		/^#/ || NF < 2 { next }
		{
			for (i = 2; i <= NF; i++) {
				# providers may be space-separated in field 2+
				split($i, a, /[[:space:]]+/)
				for (j in a) if (a[j] == n) print $1
			}
		}
	' "$DEPS_FILE" | sort -u
}

LDTO_infoHelp()
{
	echo "$0 info [OVERLAY|ALIAS]..." >&2
	echo "  Show overlay description, deps, map aliases, and gpio.map pinout." >&2
	echo "  Set VENDOR/BOARD when not running on the target board." >&2
}

LDTO_info()
{
	if [ -z "$1" ]; then
		LDTO_infoHelp
		return 1
	fi
	LDTO_loadMap
	LDTO_loadDeps
	LDTO_loadGpioMap

	local dto
	for dto in "$@"; do
		local dto_real
		dto_real=$(LDTO_getAliasDT "$dto")
		if [ -n "$dto_real" ]; then
			echo "Alias: $dto -> $dto_real"
			dto=$dto_real
		fi

		local dts dtbo
		dts=$(LDTO_getDTS "$dto")
		dtbo=$(LDTO_getDTBO "$dto")
		if [ -z "$dts" ] && [ -z "$dtbo" ]; then
			echo "Overlay $dto: not found under $PATH_DTBO" >&2
			return 1
		fi

		echo "Overlay: $dto"
		if [ -n "$dts" ]; then
			if [ -L "$dts" ]; then
				echo "Source:  $dts -> $(readlink -f "$dts" 2>/dev/null || readlink "$dts")"
			else
				echo "Source:  $dts"
			fi
		fi
		if [ -n "$dtbo" ]; then
			echo "Binary:  $dtbo"
		else
			echo "Binary:  (missing — run make BOARD_NAME=$BOARD)"
		fi

		# H40P / product aliases that point at this overlay
		local aliases=""
		local a
		for a in "${!MAP_ALIAS_DT[@]}"; do
			if [ "${MAP_ALIAS_DT[$a]}" = "$dto" ]; then
				aliases="$aliases $a"
			fi
		done
		if [ -n "$aliases" ]; then
			echo "Map aliases:$aliases"
		fi

		# Requires (providers)
		local prov=""
		if [[ -v "MAP_DEPS[$dto]" ]]; then
			prov="${MAP_DEPS[$dto]}"
		fi
		# also check symlink basename deps if dto is alias consumer
		if [ -z "$prov" ] && [ -n "$1" ]; then
			:
		fi
		if [ -n "$prov" ]; then
			echo "Requires: $prov"
		else
			echo "Requires: (none)"
		fi

		local rev
		rev=$(LDTO_requiredBy "$dto" | tr '\n' ' ')
		if [ -n "${rev// }" ]; then
			echo "Required by: $rev"
		fi

		# Active?
		if [ -d "$PATH_OF_CONFIG" ] && [ -e "$PATH_OF_CONFIG/$dto" ]; then
			echo "Active: yes ($(cat "$PATH_OF_CONFIG/$dto/status" 2>/dev/null || echo applied))"
		elif [ -d "$PATH_OF_CONFIG" ]; then
			echo "Active: no"
		fi

		if [ -n "$dts" ]; then
			echo
			local desc
			desc=$(LDTO_extractHeaderComments "$dts")
			if [ -n "$desc" ]; then
				# Prefer structured Summary/Pins/Requires/Notes if present
				if printf '%s\n' "$desc" | grep -q '^Summary:'; then
					printf '%s\n' "$desc" | awk '
						BEGIN { skip_copy = 1 }
						/^[Cc]opyright/ { next }
						/^[Aa]uthor:/ { next }
						{ print "  " $0 }
					'
				else
					echo "Description:"
					printf '%s\n' "$desc" | sed 's/^/  /'
				fi
			else
				echo "Description:"
				echo "  (no header comment)"
			fi
		fi

		# GPIO cross-ref table
		echo
		if [ -z "$GPIO_MAP_FILE" ]; then
			echo "GPIO map: (not available for $VENDOR/$BOARD)"
		else
			echo "GPIO connections (DTS pad refs x gpio.map):"
			printf '  %-6s %-4s %-14s %-8s %-10s %s\n' "Header" "Pin" "Name" "Pad" "Chip:Line" "Ref / Desc"
			local name row header pin chip line sysfs gname pad ref desc
			local any=0
			while IFS= read -r name; do
				[ -z "$name" ] && continue
				if row=$(LDTO_gpioLookupName "$name"); then
					# may be multiple rows (unlikely); print each
					while IFS= read -r r; do
						[ -z "$r" ] && continue
						IFS=$'\t' read -r header pin chip line sysfs gname pad ref desc <<<"$r"
						printf '  %-6s %-4s %-14s %-8s %-10s %s\n' \
							"$header" "$pin" "$gname" "$pad" "${chip}:${line}" "${ref}${desc:+ / $desc}"
						any=1
					done <<<"$row"
				else
					printf '  %-6s %-4s %-14s %-8s %-10s %s\n' \
						"-" "-" "$name" "-" "-" "(not in gpio.map)"
					any=1
				fi
			done < <(LDTO_extractGpioNames "$dts")
			if [ "$any" -eq 0 ]; then
				echo "  (no SoC pad names in DTS body — bus/pinctrl may use phandles;"
				echo "   see Description for header pin notes, or enable provider bus overlay)"
			fi
		fi

		# Header pin notes from comment prose × gpio.map
		if [ -n "$dts" ] && [ -n "$GPIO_MAP_FILE" ]; then
			local pin_notes def_hdr hdr_from_cmt pins_csv
			pin_notes=$(LDTO_extractHeaderComments "$dts")
			def_hdr=$(grep -v '^#' "$GPIO_MAP_FILE" | head -n1 | cut -f1)
			hdr_from_cmt=$(printf '%s\n' "$pin_notes" | grep -oE '\b[0-9]+[Jj][0-9]+\b' | head -n1 || true)
			[ -n "$hdr_from_cmt" ] && def_hdr=$hdr_from_cmt

			pins_csv=$(
				printf '%s\n' "$pin_notes" | awk -v defh="$def_hdr" '
					BEGIN { IGNORECASE = 1 }
					function emit(h, p) {
						p = p + 0
						if (p < 1 || p > 99) return
						key = h SUBSEP p
						if (key in seen) return
						seen[key] = 1
						print h "\t" p
					}
					{
						line = $0
						# 7J1.24
						while (match(line, /[0-9]+[Jj][0-9]+\.[0-9]+/)) {
							tok = substr(line, RSTART, RLENGTH)
							split(tok, a, /\./)
							emit(a[1], a[2])
							line = substr(line, RSTART + RLENGTH)
						}
						line = $0
						# 7J1 pin 22
						while (match(line, /[0-9]+[Jj][0-9]+[[:space:]]+pin[[:space:]]*[0-9]+/)) {
							tok = substr(line, RSTART, RLENGTH)
							match(tok, /[0-9]+[Jj][0-9]+/)
							h = substr(tok, RSTART, RLENGTH)
							match(tok, /[0-9]+$/)
							emit(h, substr(tok, RSTART, RLENGTH))
							line = substr(line, RSTART + RLENGTH)
						}
						line = $0
						# bare "pin 22" (word pin, not pins / pinmux / pinctrl)
						while (match(line, /\<pin[[:space:]]+[0-9]+/)) {
							tok = substr(line, RSTART, RLENGTH)
							match(tok, /[0-9]+/)
							emit(defh, substr(tok, RSTART, RLENGTH))
							line = substr(line, RSTART + RLENGTH)
						}
						# "Pins 19 (MOSI), 21 ..." or "pins (7J1 19/21/23/24)" — plural only
						if (match($0, /\<pins[[:space:]]+/)) {
							rest = substr($0, RSTART + RLENGTH)
							if (match(rest, /^\([^)]*\)/)) {
								inner = substr(rest, 2, RLENGTH - 2)
								gsub(/[0-9]+[Jj][0-9]+/, " ", inner)
								n = split(inner, parts, /[^0-9]+/)
								for (i = 1; i <= n; i++)
									if (parts[i] != "") emit(defh, parts[i])
							} else {
								# strip (role) labels so SS0/CS0 digits are not pins
								gsub(/\([^)]*\)/, " ", rest)
								# stop before trailing prose
								sub(/[[:space:]]+(come|from|with|using|on)[[:space:]].*/i, "", rest)
								n = split(rest, parts, /[^0-9]+/)
								for (i = 1; i <= n; i++)
									if (parts[i] != "") emit(defh, parts[i])
							}
						}
					}
				' | sort -t$'\t' -k1,1 -k2,2n
			)
			if [ -n "$pins_csv" ]; then
				echo
				echo "Header pin notes (comment text x gpio.map, header $def_hdr):"
				printf '  %-6s %-4s %-14s %-8s %s\n' "Header" "Pin" "Name" "Pad" "Ref"
				local seen_hp=""
				while IFS=$'\t' read -r h p; do
					[ -z "$p" ] && continue
					r=""
					if r=$(LDTO_gpioLookupPin "$h" "$p"); then
						true
					else
						map_def=$(grep -v '^#' "$GPIO_MAP_FILE" | head -n1 | cut -f1)
						if [ "$h" != "$map_def" ]; then
							r=$(LDTO_gpioLookupPin "$map_def" "$p" || true)
						fi
					fi
					if [ -n "$r" ]; then
						IFS=$'\t' read -r header pin chip line sysfs gname pad ref desc <<<"$r"
						# skip pure power/GND rows (noise from stray digit matches)
						case "$gname" in
							GND|5V|3.3V) continue ;;
						esac
						case " $seen_hp " in
							*" $header:$pin "*) continue ;;
						esac
						seen_hp="$seen_hp $header:$pin"
						printf '  %-6s %-4s %-14s %-8s %s\n' "$header" "$pin" "$gname" "$pad" "$ref"
					fi
					# omit unresolved silk-header misses when map fallback also failed
				done <<<"$pins_csv"
			fi
		fi

		echo
		echo "----"
	done
}


LDTO_conflictsHelp()
{
	echo "$0 conflicts [OVERLAY|ALIAS]..." >&2
	echo "  Report header pin collisions among the named overlays (with deps)" >&2
	echo "  and any currently active overlays. No args = active only." >&2
}

LDTO_conflicts()
{
	if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
		LDTO_conflictsHelp
		return 0
	fi
	LDTO_loadMap
	LDTO_loadGpioMap
	LDTO_loadDeps

	local -a names=()
	local a
	for a in "$@"; do
		names+=("$a")
	done

	# Include active overlays when configfs is present
	if [ -d "$PATH_OF_CONFIG" ]; then
		local act
		for act in "$PATH_OF_CONFIG"/*; do
			[ -e "$act" ] || continue
			names+=("$(basename "$act")")
		done
	fi

	if [ ${#names[@]} -eq 0 ]; then
		echo "$FUNCNAME: no overlays given and none active." >&2
		LDTO_conflictsHelp
		return 1
	fi

	local -a seq
	mapfile -t seq < <(LDTO_expandDeps "${names[@]}")

	echo "Overlays considered (deps expanded): ${seq[*]}"
	echo

	# pin_key -> list of overlays
	local pins_blob
	pins_blob=$(
		for dto in "${seq[@]}"; do
			dto_real=$(LDTO_getAliasDT "$dto")
			[ -n "$dto_real" ] && dto=$dto_real
			LDTO_collectOverlayPins "$dto"
		done
	)

	if [ -z "$pins_blob" ]; then
		echo "No header pins resolved from overlays (need .dts + gpio.map)."
		return 0
	fi

	# Occupancy table
	echo "Pin occupancy:"
	printf '  %-6s %-4s %-14s %s\n' "Header" "Pin" "Name" "Overlays"
	printf '%s\n' "$pins_blob" | awk -F '\t' '
		{
			key = $1 SUBSEP $2
			nm[key] = $3
			h[key] = $1
			p[key] = $2
			if (!seen[key, $4]++) {
				if (own[key] == "")
					own[key] = $4
				else
					own[key] = own[key] " " $4
			}
		}
		END {
			for (k in own)
				printf "%s\t%02d\t%s\t%s\t%s\n", h[k], p[k]+0, p[k], nm[k], own[k]
		}
	' | sort -t$'\t' -k1,1 -k2,2n | awk -F '\t' '{
		printf "  %-6s %-4s %-14s %s\n", $1, $3, $4, $5
	}'


	echo
	# True conflicts: same Header.Pin claimed by overlays that are NOT
	# in a provider→consumer relationship (bus+device sharing SPI pins is OK).
	LDTO_loadDeps
	local pin_groups
	pin_groups=$(
		printf '%s\n' "$pins_blob" | awk -F '\t' '
			{
				key = $1 "\t" $2 "\t" $3
				if (!seen[key, $4]++) {
					if (list[key] == "") list[key] = $4
					else list[key] = list[key] " " $4
					cnt[key]++
				}
			}
			END {
				for (k in cnt)
					if (cnt[k] > 1)
						print k "\t" list[k]
			}
		'
	)

	_ldto_is_related() {
		local a="$1" b="$2" x
		local -a exp
		mapfile -t exp < <(LDTO_expandDeps "$a")
		for x in "${exp[@]}"; do
			[ "$x" = "$b" ] && return 0
		done
		mapfile -t exp < <(LDTO_expandDeps "$b")
		for x in "${exp[@]}"; do
			[ "$x" = "$a" ] && return 0
		done
		return 1
	}

	local conf_tmp
	conf_tmp=$(mktemp)
	if [ -n "$pin_groups" ]; then
		while IFS=$'\t' read -r h p name owners; do
			[ -z "$owners" ] && continue
			local -a oarr=($owners)
			local i j related_all=1
			for ((i=0; i<${#oarr[@]}; i++)); do
				for ((j=i+1; j<${#oarr[@]}; j++)); do
					if ! _ldto_is_related "${oarr[i]}" "${oarr[j]}"; then
						related_all=0
						break 2
					fi
				done
			done
			if [ "$related_all" -eq 0 ]; then
				printf '%s\t%s\t%s\t%s\n' "$h" "$p" "$name" "$owners" >> "$conf_tmp"
			fi
		done <<<"$pin_groups"
	fi

	if [ ! -s "$conf_tmp" ]; then
		rm -f "$conf_tmp"
		echo "Conflicts: none (shared bus→device pins are OK)"
		return 0
	fi
	echo "Conflicts (unrelated overlays share a pin):"
	printf '  %-6s %-4s %-14s %s\n' "Header" "Pin" "Name" "Overlays"
	sort -t$'\t' -k1,1 -k2,2n "$conf_tmp" | awk -F '\t' '{
		printf "  %-6s %-4s %-14s %s\n", $1, $2, $3, $4
	}'
	rm -f "$conf_tmp"
	return 2
}


LDTO_help(){
	echo "$0 list"
	echo "$0 status"
	echo "$0 active [DTBO]"
	echo "$0 enable [--dry-run|-n] [DTBO]   # auto-applies providers from dt.deps"
	echo "$0 disable [DTBO]"
	echo "$0 info [DTBO]     # description, deps, gpio.map pinout"
	echo "$0 conflicts [DTBO]...  # pin occupancy / collisions (deps + active)"
	echo "$0 current"
	echo "$0 merge [DTBO]    # auto-merges providers from dt.deps"
	echo "$0 show"
	echo "$0 diff"
	echo "$0 reset"
}

LDTO_requireConfigFS()
{
	if [ ! -d "$PATH_OF_CONFIG" ]; then
		echo "The running kernel does not support this tool (configfs overlays)." >&2
		return 1
	fi
}

cmd=help
if [ ! -z "$1" ]; then
	cmd=$1
	shift
fi
cmd_lc=${cmd,,}
case "$cmd_lc" in
	help|list|info|conflicts)
		;;
	enable)
		# enable --dry-run does not need configfs
		dry=0
		for a in "$@"; do
			case "$a" in --dry-run|-n) dry=1 ;; esac
		done
		if [ "$dry" -eq 0 ]; then
			LDTO_requireConfigFS || exit 1
		fi
		;;
	*)
		LDTO_requireConfigFS || exit 1
		;;
esac
LDTO_${cmd_lc} "$@"
