#!/bin/sh

# dts2config -- config discovery script for device-tree supported devices
# Almost certainly the worst code I've ever written. Apologies.
# Copyright (C) 2023 Alper Nebi Yasak <alpernebiyasak@gmail.com>
# Licensed under GPL v2 or any later version

# Completely rewritten, but based on what is originally:
#   https://salsa.debian.org/kernel-team/linux/-/merge_requests/717#note_401554
#   Copyright: Diederik de Haas <didi.debian@cknow.org>
#   "gpl-2.0+ or WTFPL (or whatever) is fine with me" on #debian-kernel IRC

INDENT=""
BREAK="--BREAK--"

: "${VERBOSE:=true}"
: "${RECURSE:=false}"
: "${DEBIAN:=true}"
: "${WORKDIR:=$(pwd)}"

if [ "$VERBOSE" = false ]; then
    RECURSE=false
    DEBIAN=false
fi

if [ -d "debian" ] && [ ! -f "Kconfig" ]; then
    echo "No upstream files, try: debian/rules orig"
    exit 1
elif [ ! -f "Kconfig" ]; then
    echo "Needs to be run from a Linux tree (upstream or Debian)"
    exit 1
fi

msg() {
    if [ "$VERBOSE" = true ]; then
        echo "$INDENT" "$@"
    fi
}

result() {
    if [ "$VERBOSE" = true ]; then
        echo "$INDENT" "$@"
    else
        echo "$@"
    fi
}

heading() {
    if [ "$VERBOSE" = true ]; then
        echo ""
        echo "$@"
        echo "$@" | tr -c "\n" "="
    fi
}

indent() {
    if [ "$#" -gt 0 ]; then
        msg "$@"
    fi
    case "$INDENT" in
        "") INDENT=" -";;
        *)  INDENT="  $INDENT";;
    esac
}

dedent() {
    case "$INDENT" in
        " -") INDENT="";;
        *)    INDENT="${INDENT#  }";;
    esac
}

deduplicate() {
    awk '!seen[$0]++'
}

dts_compatibles() {
    cat "$@" \
        | tr "\n" " " \
        | tr ";" "\n" \
        | sed -n -e 's/^.*[^[:alnum:]_-]compatible\s*=\s*\([^;]*\)$/\1/p' \
        | sed -e "s/\s\+/ /g" -e 's/"\s*,\s*"/"\n"/g' \
        | sed -e 's/^"\([^"]*\)"$/\1/' \
        | deduplicate
}

dts_includes() {
    local dts include_pat
    dts="$1"
    shift

    dtsdir="$(dirname "$dts")"

    sed <"$dts" \
        -e "/^\(\#include\|\/include\/\)\s*/! d" \
        -e "s/^\(\#include\|\/include\/\)\s*//" \
        -e "s/^[<\"]\(.*\)[>\"]/\1/" \
        -e "\@^\(arm\|arm64\|riscv\)/@! s@^.*@$dtsdir/&@" \
        -e "s@^\(arm\|arm64\|riscv\)/@arch/\1/boot/dts/@" \
        -e "/.*\.dtsi\?/! d" \
        -e "s/\(.*\.dtsi\?\).*/\1/g" \
        -e '/\.\./ { t f; :f; s@\/[^/]*\+/\.\./@/@g; t f }'
}

compatible_sources_data() {
    grep -r \
        --exclude-dir=".*" --exclude-dir="debian" \
        --exclude-dir="Documentation" --exclude-dir="dts" \
        --include="*.c" --include="*.h" \
        -e ".compatible\s*=\s*\"[^\"]\+\"" \
        -e "CLK_OF_DECLARE([^,]\+,\s*\"[^\"]\+\",.*" \
        -e "CLK_OF_DECLARE_DRIVER([^,]\+,\s*\"[^\"]\+\",.*" \
        -e "TIMER_OF_DECLARE([^,]\+,\s*\"[^\"]\+\",.*" \
        -e "RESERVEDMEM_OF_DECLARE([^,]\+,\s*\"[^\"]\+\",.*" \
        -e "IRQCHIP_DECLARE([^,]\+,\s*\"[^\"]\+\",.*" \
        -e "OF_MATCH(\s*\"[^\"]\+\",.*" \
        -e "IRQCHIP_MATCH(\s*\"[^\"]\+\",.*" \
        "$WORKDIR" \
        | sed -e "s|^$WORKDIR/||"
}

if [ -f "$COMPATIBLE_SOURCES_FILE" ]; then
    COMPATIBLE_SOURCES="$(cat "$COMPATIBLE_SOURCES_FILE")"
fi
if [ -z "$COMPATIBLE_SOURCES" ]; then
    COMPATIBLE_SOURCES="$(compatible_sources_data)"
fi

compatible_sources() {
    local compatible
    compatible="$1"
    shift

    # Avoid irrelevant false positives
    case "$compatible" in
        simple-bus) echo "drivers/of/platform.c"; return ;;
        fixed-clock) echo "drivers/clk/clk-fixed-rate.c"; return ;;
        fixed-factor-clock) echo "drivers/clk/clk-fixed-factor.c"; return ;;
    esac

    printf "%s\n" "$COMPATIBLE_SOURCES" \
        | grep "\"$compatible\"" \
        | cut -d: -f1 \
        | deduplicate
}

driver_configs_data() {
    local makefile dir

    find "$WORKDIR" \
        \( -type d \( -iname "..*" -o -iname "debian" \) -prune \) \
        -o \( -type f \( -name "Makefile*" -o -name "Kbuild*" \) -print \) \
        | while read -r makefile; do
            dir="$(dirname "$makefile")/"
            dir="${dir#$WORKDIR/}"

            { cat "$makefile"; echo; } \
                | sed \
                    -e 's/\s*\#.*$//g' \
                    -e ':a; /\\$/ { N; s/\\\n//; t a }' \
                    -e 's/\s\+/ /g' \
                    -e 's/^\s*//g' \
                    -e 's/[+:]=/=/g' \
                    -e '/\(FLAGS\|flags\|MAKE\|make\)/ d' \
                    -e '/^.*=.*=.*$/ { t b; :b; s/^\(ifeq\|ifneq\|ifdef\|ifndef\|endif\)/\1/; t; d }' \
                    -e '/^[^=]*$/ { t c; :c; s/^\(ifeq\|ifneq\|ifdef\|ifndef\|endif\)/\1/; t; d }' \
                    -e '/^.*=.*/ { :d; h; s/^\([^=[:space:]]*\)\s*=\s*\([^=[:space:]]\+\).*/\1 = \2/; t e; d }' \
                    -e '// { :e; p; x; s/^\([^=[:space:]]*\)\s*=\s*[^=[:space:]]\+\s*\(.*\)/\1 = \2/; t d; d }' \
                | sed \
                    -e 's@\(.*\)/$@\1/obj@g' \
                    -e 's@\(.*\)/\s*=\s*@\1/obj =@g' \
                    -e 's@\(.\+\)-$(CONFIG_\(.\+\))\s*=\s*\(.*\)@'"$dir"'\3::CONFIG_\2\n'"$dir"'\3::'"$dir"'\1@' \
                    -e 's@\(.\+\)-y\s*=\s*\(.*\)\(\.o\|/\)\?@'"$dir"'\2::'"$dir"'\1.o@' \
                    -e 's@\$(\([^\$()]*\))@\1@g' \
                    -e 's@\(.*\) = \(.*\)@'"$dir"'\2::'"$dir"'\1@g' \
                    -e '/\.\./ { t f; :f; s@\/[^/]*\+/\.\./@/@g; t f }' \
                    -e 's/-objs/.o/g' \
                    -e 's/obj.o/obj/g'
        done \
        | grep \
            -e "^\(ifeq\|ifneq\|ifdef\|ifndef\|endif\)" \
            -e "^[[:alnum:]/\._-]\+::[[:alnum:]/\._-]\+$" \
        | python3 -c \
        '
import sys, re
ifs = []
pats = [
    re.compile("ifeq \(\"?(CONFIG_\w*)\"?,\s*\"?[ym]*\)\"?"),
    re.compile("ifneq \(\"?(CONFIG_\w*)\"?,\s*[0n]*\)"),
    re.compile("ifdef (CONFIG_\w*)"),
    re.compile("ifneq \(\$?\(?filter y,((CONFIG_\w*\s*)+)\)?,\s*[0n]*\)"),
    re.compile("ifeq \(\$?\(?filter y,((CONFIG_\w*\s*)+)\)?,\s*[ym]*\)"),
]
for l in sys.stdin.readlines():
    l = l.strip()
    if l.startswith("if"):
        for p in pats:
            m = p.match(l)
            if m:
                ifs.append(str.join(",", m.group(1).split()))
                break

    elif l.startswith("endif"):
        if ifs:
            ifs.pop()

    else:
        print(l)
        lhs, rhs = l.split("::")
        for ifstr in ifs:
            for oneif in ifstr.split(","):
                print("{}::{}".format(lhs, oneif.strip()))
        ' \
        | sort -u
}

if [ -f "$DRIVER_CONFIGS_FILE" ]; then
    DRIVER_CONFIGS="$(cat "$DRIVER_CONFIGS_FILE")"
fi
if [ -z "$DRIVER_CONFIGS" ]; then
    DRIVER_CONFIGS="$(driver_configs_data)"
fi

driver_configs() {
    local srcfile pair rhs
    srcfile="$1"
    shift

    # Avoid irrelevant false positives
    case "$srcfile" in
        obj) return ;;
        drivers/usb/host/obj)
            printf "%s\n" "USB_XHCI_HCD" "USB_EHCI_HCD" "USB_OHCI_HCD" "USB_UHCI_HCD"
            driver_configs "drivers/usb/obj"
            return ;;
    esac

    printf "%s\n" "$DRIVER_CONFIGS" \
        | grep "^$srcfile::" \
        | while read -r pair; do
            rhs="${pair#*::}"
            case "$rhs" in
                CONFIG_*) echo "${rhs#CONFIG_}" ;;
                *) driver_configs "$rhs" ;;
            esac
        done \
        | deduplicate
}

kconfig_files_data() {
    grep -r \
        --exclude-dir=".*" --exclude-dir="debian" \
        --include="Kconfig" --include="Kconfig.*" \
        -e "config [[:alnum:]_]*\+$" \
        "$WORKDIR" \
        | sed -e "s|^$WORKDIR/||"
}

if [ -f "$KCONFIG_FILES_FILE" ]; then
    KCONFIG_FILES="$(cat "$KCONFIG_FILES_FILE")"
fi
if [ -z "$KCONFIG_FILES" ]; then
    KCONFIG_FILES="$(kconfig_files_data)"
fi

kconfig_files() {
    local config line
    config="$1"
    shift

    printf "%s\n" "$KCONFIG_FILES" \
        | grep "config $config$" \
        | cut -d: -f1 \
        | deduplicate
}

kconfig_type() {
    local config line ctype cdefault cprompt lhs rhs
    config="$1"
    shift

    cat "$@" \
        | sed -n -e "/^\(menuconfig\|config\) $config\$/,/^\$/ s/^\s*// p" \
        | {
            while read -r line; do
                lhs="${line%% *}"
                rhs="${line#* }"

                case "$lhs" in
                    bool|tristate|int)
                        ctype="$lhs";
                        cprompt="${cprompt:-$rhs}"
                        ;;
                    def_*)
                        ctype="$lhs";
                        cdefault="$rhs"
                        ;;
                    prompt)
                        cprompt="$rhs"
                        ;;
                    default)
                        cdefault="${cdefault:-$rhs}"
                        ;;
                    help|*-help-*)
                        break
                        ;;
                esac
            done

            if [ -n "$cprompt" ] && [ -n "$cdefault" ]; then
                echo "$ctype, default $cdefault"
            elif [ -n "$cprompt" ] && [ -z "$cdefault" ]; then
                echo "$ctype"
            else
                echo "non-visible"
            fi
        }
}

debian_config_data() {
    if [ -d "$WORKDIR/debian/config" ]; then
        grep -r \
            --exclude-dir=".*" \
            --include="config" --include="config.*" \
            -e "CONFIG_[[:alnum:]_]\+\(=\| is not set\)" \
            "$WORKDIR/debian/config/config" \
            "$WORKDIR/debian/config/config".* \
            "$WORKDIR/debian/config/"kernelarch-*/ \
            "$WORKDIR/debian/config/" \
            | sed -e "s|^$WORKDIR/||" \
            | deduplicate
    fi
}

if [ -f "$DEBIAN_CONFIGS_FILE" ]; then
    DEBIAN_CONFIGS="$(cat "$DEBIAN_CONFIGS_FILE")"
fi
if [ -z "$DEBIAN_CONFIGS" ]; then
    DEBIAN_CONFIGS="$(debian_config_data)"
fi

debian_config() {
    local config arch
    config="${1#CONFIG_}"
    arch="$2"
    shift 2

    case "$arch" in
        arm)        arch="kernelarch-arm.*\|armel.*\|armhf.*" ;;
        arm64)      arch="kernelarch-arm.*\|arm64.*" ;;
        powerpc)    arch="kernelarch-powerpc.*\|ppc.*" ;;
        mips)       arch="kernelarch-mips.*\|mips.*" ;;
        riscv)      arch="kernelarch-riscv.*\|riscv.*" ;;
        sh)         arch="kernelarch-sh.*\|sh.*" ;;
        x86)        arch="kernelarch-x86.*\|i386.*\|amd64.*" ;;
        "")         arch=".*" ;;
    esac

    printf "%s\n" "$DEBIAN_CONFIGS" \
        | grep \
            -e "^debian/config/[^/]\+:.*\bCONFIG_$config\b.*" \
            -e "^debian/config/\($arch\)/.*:.*\bCONFIG_$config\b.*"
}

dts_to_config() {
    set -- "$@" "$BREAK"
    while [ "$#" -ge 1 ]; do
        dts="$1"
        shift

        arch="${dts#arch/}"
        arch="${arch%%/*}"

        if [ "$dts" = "$BREAK" ]; then
            break
        elif [ -z "$dts" ] || [ ! -f "$dts" ]; then
            continue
        fi

        heading "dts: $dts"

        includes=false
        for include in $(dts_includes "$dts"); do
            includes=true

            if [ "$RECURSE" = true ]; then
                indent "includes:" "$include"
                VERBOSE=false dts_to_config "$include" \
                    | sed -e "s/\s\s\s*/\t/g" \
                    | sort -u \
                    | {
                        while read -r line; do
                            result "$line"
                        done
                    } | {
                        if [ "$VERBOSE" = true ]; then
                            column -t -s "$(printf '\t')"
                        else
                            cat
                        fi
                    }
                dedent
                msg ""
            elif [ "$VERBOSE" = true ]; then
                msg "includes:" "$include"
            fi

            case "$@" in
                "$include")       continue ;;
                "${include} "*)   continue ;;
                *" ${include} "*) continue ;;
                *" ${include}")   continue ;;
            esac

            set -- "$include" "$@"
        done

        if [ "$RECURSE" = false ] && [ "$includes" = true ]; then
            msg ""
        fi

        if [ "${dts%.dts}" != "$dts" ]; then
            indent "dts:" "$dts"
            for config in $(driver_configs "${dts%.dts}.dtb"); do
                indent "$config"

                for file in $(kconfig_files "$config"); do
                    indent "$file"

                    ktype="$(kconfig_type "$config" "$file")"

                    if [ "$VERBOSE" = true ]; then
                        result "$config" "(${ktype})"
                    else
                        result "$(basename "$dts")\t${config}\t(${ktype})"
                    fi

                    dedent
                done

                if [ "$DEBIAN" = true ]; then
                    debian_config "$config" "$arch" | {
                        while read -r line; do
                            deb_file="${line%%:*}"
                            deb_config="${line#*:}"
                            indent "$deb_file"
                            msg "$deb_config"
                            dedent
                        done
                    }
                fi

                dedent
            done

            dedent
            msg ""
        fi

        for compatible in $(dts_compatibles "$dts"); do
            indent "compatible: \"${compatible}\""

            found=false
            for srcfile in $(compatible_sources "$compatible"); do
                indent "source: $srcfile"
                found=true

                for config in $(driver_configs "${srcfile%.*}.o"); do
                    indent "$config"

                    for file in $(kconfig_files "$config"); do
                        indent "$file"

                        ktype="$(kconfig_type "$config" "$file")"

                        if [ "$VERBOSE" = true ]; then
                            result "$config" "(${ktype})"
                        else
                            result "\"${compatible}\"\t${config}\t(${ktype})"
                        fi

                        dedent
                    done

                    if [ "$DEBIAN" = true ]; then
                        debian_config "$config" "$arch" | {
                            while read -r line; do
                                deb_file="${line%%:*}"
                                deb_config="${line#*:}"
                                indent "$deb_file"
                                msg "$deb_config"
                                dedent
                            done
                        }
                    fi

                    dedent
                done

                dedent
            done

            if [ "$VERBOSE" = false ] && [ "$found" = false ]; then
                result "\"${compatible}\"\t\t"
            fi

            dedent
            msg ""
        done

        set -- "$@" "$dts"
    done
}

if [ "$#" -eq 0 ] ; then
    echo "Need an input dts/dtsi file."
    exit 1
fi

if [ "$1" = "--eval" ]; then
    TMPDIR="$(mktemp -d -t dts2config.XXXXXX)"
    printf "%s\n" "$COMPATIBLE_SOURCES" >"$TMPDIR/compatible-sources"
    printf "%s\n" "$DRIVER_CONFIGS" >"$TMPDIR/driver-confgs"
    printf "%s\n" "$KCONFIG_FILES" >"$TMPDIR/kconfig-files"
    printf "%s\n" "$DEBIAN_CONFIGS" >"$TMPDIR/debian-configs"

    FORMAT="%s=%s\n"
    printf "$FORMAT" "COMPATIBLE_SOURCES_FILE" "${TMPDIR}/compatible-sources"
    printf "$FORMAT" "DRIVER_CONFIGS_FILE" "${TMPDIR}/driver-confgs"
    printf "$FORMAT" "KCONFIG_FILES_FILE" "${TMPDIR}/kconfig-files"
    printf "$FORMAT" "DEBIAN_CONFIGS_FILE" "${TMPDIR}/debian-configs"

    exit
fi

if [ "$VERBOSE" = true ]; then
    dts_to_config "$@"
else
    dts_to_config "$@" \
        | sort -u \
        | column -t -s "$(printf '\t')"
fi
