#!/usr/bin/env bash
#
# install-pcan.sh — PEAK PCAN 驱动一键安装脚本（Ubuntu/Debian）
#
# 用法（学生只需拿到本脚本）：
#     sudo bash install-pcan.sh            # 默认：下载驱动 + DKMS 安装（chardev 模式）
#     sudo bash install-pcan.sh --netdev   # 用 SocketCAN(netdev) 模式
#     sudo bash install-pcan.sh --manual   # 不用 DKMS，手动 make install（切内核会失效）
#     sudo bash install-pcan.sh --src DIR  # 跳过下载，使用本地已解压的源码目录
#     sudo bash install-pcan.sh --uninstall
#     sudo bash install-pcan.sh --diagnose # 仅生成诊断日志
#
# 设计目标：
#   1. 默认走驱动自带的 DKMS 安装，根治“升级/切换内核后驱动失效”的问题。
#   2. 对常见报错做模式匹配，给出看得懂的中文修复建议。
#   3. 自包含：自动从固定地址下载驱动源码包并解压。

set -u

# ---------------------------------------------------------------------------
# 配置
# ---------------------------------------------------------------------------
DRIVER_URL="https://enpei-html.oss-cn-hangzhou.aliyuncs.com/scripts/peak-linux-driver-9.1.0.tar"
DRIVER_VERSION="9.1.0"
DKMS_NAME="peak-linux-driver"
WORKDIR="/usr/src/pcan-autoinstall"
LOGFILE="/tmp/pcan-install-$(date +%Y%m%d_%H%M%S).log"

# 运行选项（默认值）
MODE="chardev"        # chardev | netdev
USE_DKMS=1            # 1=优先 DKMS，0=手动
ACTION="install"      # install | uninstall | diagnose
SRC=""               # 非空表示使用本地源码，跳过下载

KREL="$(uname -r)"
CCVER=""             # 需要切换到的 gcc 主版本号（空=用默认 gcc）
CC_ARG=()            # 传给 make 的 CC 参数，如 (CC=gcc-12)

# ---------------------------------------------------------------------------
# 日志/输出
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
    C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'
    C_BLU=$'\033[36m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m'
else
    C_RED=""; C_GRN=""; C_YEL=""; C_BLU=""; C_BLD=""; C_RST=""
fi

info() { printf '%s[*]%s %s\n' "$C_BLU" "$C_RST" "$*"; }
ok()   { printf '%s[√]%s %s\n' "$C_GRN" "$C_RST" "$*"; }
warn() { printf '%s[!]%s %s\n' "$C_YEL" "$C_RST" "$*" >&2; }
err()  { printf '%s[×]%s %s\n' "$C_RED" "$C_RST" "$*" >&2; }
hr()   { printf '%s\n' "------------------------------------------------------------"; }

# run "描述" cmd args... —— 执行命令并把输出同时写到屏幕和日志文件
run() {
    local desc="$1"; shift
    info "$desc"
    printf '\n### %s\n### $ %s\n' "$desc" "$*" >>"$LOGFILE"
    "$@" 2>&1 | tee -a "$LOGFILE"
    return "${PIPESTATUS[0]}"
}

die() {
    err "$*"
    err "完整日志见：$LOGFILE"
    exit 1
}

# ---------------------------------------------------------------------------
# 错误模式匹配：扫描日志，命中即给出中文方案
# ---------------------------------------------------------------------------
match_error() {
    local log="$1"
    hr
    err "安装未成功。根据报错给出可能的解决方案："
    hr

    local matched=0
    _hit() { matched=1; warn "可能原因：$1"; printf '    解决：%s\n' "$2"; }

    if grep -qiE 'find the Kernel headers|No such file.*build|/lib/modules/.*/build' "$log"; then
        _hit "缺少当前内核 ($KREL) 的头文件" \
             "运行：sudo apt-get install -y linux-headers-$KREL ；若仍提示缺失，可能是刚升级过内核还没重启——请 reboot 后重试，或确认未使用自定义/HWE 内核。"
    fi
    if grep -qiE "make: .*command not found|gcc: not found|: gcc: |cc1: not found" "$log"; then
        _hit "缺少编译工具链" "运行：sudo apt-get install -y build-essential"
    fi
    if grep -qiE "cannot find -lpopt|popt\.h: No such file" "$log"; then
        _hit "缺少 libpopt（编译 pcan-settings 工具需要）" "运行：sudo apt-get install -y libpopt-dev"
    fi
    if grep -qiE "Required key not available|Key was rejected by service" "$log"; then
        _hit "Secure Boot 拒绝未签名的内核模块" \
             "在 BIOS/UEFI 中关闭 Secure Boot；或用 mokutil 为模块签名（DKMS 可配合 MOK 自动签名）。"
    fi
    if grep -qiE "Device or resource busy|could not insert 'pcan'|insmod.*pcan.*Busy" "$log"; then
        _hit "内核自带的 peak_* 驱动仍占用设备" \
             "运行：sudo rmmod peak_usb peak_pci peak_pciefd 2>/dev/null ；本脚本已安装黑名单，重启后不再自动加载。"
    fi
    if grep -qiE "dkms: (command )?not found|which dkms" "$log"; then
        _hit "未安装 DKMS" "运行：sudo apt-get install -y dkms （或加 --manual 走手动安装）"
    fi
    if grep -qiE "version magic .* should be|invalid module format|Invalid module format" "$log"; then
        _hit "模块是针对其它内核编译的，与当前内核不匹配" \
             "针对当前内核重新编译安装（本脚本默认的 DKMS 方式可避免此问题）。"
    fi
    if grep -qiE "unrecognized command-line option .*-ftrivial-auto-var-init|the compiler differs from the one used to build the kernel|unrecognized command-line option" "$log"; then
        _hit "编译模块用的 gcc 版本与内核编译时的 gcc 不一致（典型于 Ubuntu HWE 内核）" \
             "用与内核相同的 gcc 编译：先 apt install gcc-<版本>，再 make CC=gcc-<版本>。本脚本会自动检测并切换；若仍失败，确认对应 gcc 已安装。"
    fi
    if grep -qiE "implicit declaration of function|unknown type name|has no member named|too many arguments to function" "$log"; then
        _hit "内核版本可能新于本驱动 $DRIVER_VERSION 的适配范围（编译期 API 不匹配）" \
             "到 PEAK 官网下载更新版本的 peak-linux-driver 后重试。"
    fi
    if grep -qiE "Could not resolve host|Connection timed out|curl:|wget:|Failed to connect" "$log"; then
        _hit "下载驱动包失败（网络/代理问题）" \
             "检查网络后重试；或手动下载 $DRIVER_URL ，解压后用 --src <目录> 指向本地源码。"
    fi

    if [ "$matched" -eq 0 ]; then
        warn "未匹配到已知报错。请运行以下命令生成诊断日志，发给老师或 PEAK 支持："
        printf '    sudo bash %s --diagnose\n' "$0"
    fi
    hr
    err "完整日志见：$LOGFILE"
    exit 1
}

# ---------------------------------------------------------------------------
# 各步骤
# ---------------------------------------------------------------------------
require_root() {
    if [ "$(id -u)" -ne 0 ]; then
        die "需要 root 权限。请用：sudo bash $0 $*"
    fi
}

check_apt() {
    if ! command -v apt-get >/dev/null 2>&1; then
        die "本脚本仅支持 Ubuntu/Debian (apt) 系统。"
    fi
}

install_deps() {
    run "更新软件包索引 (apt-get update)" apt-get update || warn "apt-get update 失败，继续尝试安装（可能用缓存）"

    local pkgs=(build-essential "linux-headers-$KREL" libpopt-dev curl tar)
    [ "$USE_DKMS" -eq 1 ] && pkgs+=(dkms)

    run "安装依赖：${pkgs[*]}" apt-get install -y "${pkgs[@]}" \
        || match_error "$LOGFILE"

    # 关键校验：当前运行内核必须有可用的构建目录
    if [ ! -d "/lib/modules/$KREL/build" ]; then
        warn "未找到 /lib/modules/$KREL/build —— 当前运行内核 ($KREL) 缺少匹配的头文件。"
        warn "常见于：刚 apt upgrade 装了新内核但还没重启，或使用了自定义/HWE 内核。"
        die "请安装匹配的 linux-headers-$KREL，或 reboot 进入有头文件的内核后重试。"
    fi
    ok "依赖就绪，内核头文件存在：/lib/modules/$KREL/build"
    detect_set_gcc
}

# 检测内核是用哪个 gcc 编译的，与默认 gcc 不一致时装上并切换
# （Ubuntu 22.04 + HWE 6.8 内核常见：内核 gcc-12，系统默认 gcc-11，
#   会报 unrecognized command-line option '-ftrivial-auto-var-init=zero'）
detect_set_gcc() {
    local kgcc def
    # 1) /proc/version 里通常含 "...-gcc-12 (Ubuntu ...)"
    kgcc="$(grep -oE 'gcc-[0-9]+' /proc/version 2>/dev/null | head -1 | sed 's/gcc-//')"
    # 2) 退而求其次：从 "gcc ... ) 12.3.0" 里取主版本
    if [ -z "$kgcc" ]; then
        kgcc="$(grep -oE 'gcc[^)]*\) [0-9]+' /proc/version 2>/dev/null | grep -oE '[0-9]+$' | head -1)"
    fi
    # 3) 再退一步：内核 .config 里的 CONFIG_CC_VERSION_TEXT
    if [ -z "$kgcc" ] && [ -r "/lib/modules/$KREL/build/.config" ]; then
        kgcc="$(grep -oE 'CONFIG_CC_VERSION_TEXT="[^"]*gcc-[0-9]+' "/lib/modules/$KREL/build/.config" \
                 | grep -oE 'gcc-[0-9]+' | sed 's/gcc-//')"
    fi

    def="$(gcc -dumpversion 2>/dev/null | cut -d. -f1)"

    if [ -z "$kgcc" ]; then
        warn "无法判断内核所用的 gcc 版本，使用默认 gcc (主版本 ${def:-未知}) 编译。"
        return
    fi
    info "内核 ($KREL) 由 gcc-$kgcc 编译；当前默认 gcc 为 gcc-${def:-未知}"
    if [ "$kgcc" = "$def" ]; then
        ok "gcc 版本一致，无需切换。"
        return
    fi

    warn "gcc 版本不一致，将改用 gcc-$kgcc 编译，以匹配内核（否则会报 -ftrivial-auto-var-init 等错误）。"
    if ! command -v "gcc-$kgcc" >/dev/null 2>&1; then
        run "安装 gcc-$kgcc" apt-get install -y "gcc-$kgcc" || match_error "$LOGFILE"
    fi
    command -v "gcc-$kgcc" >/dev/null 2>&1 \
        || die "未能安装 gcc-$kgcc（apt 源里可能没有该版本）。请手动安装与内核匹配的 gcc 后重试。"
    CCVER="$kgcc"
    CC_ARG=("CC=gcc-$kgcc")
    ok "将使用 gcc-$kgcc 进行编译。"
}

download_src() {
    if [ -n "$SRC" ]; then
        [ -f "$SRC/Makefile" ] && [ -d "$SRC/driver" ] \
            || die "--src 指定的目录不是有效的驱动源码：$SRC"
        ok "使用本地源码：$SRC"
        return
    fi

    mkdir -p "$WORKDIR"
    local tarball="$WORKDIR/peak-linux-driver-$DRIVER_VERSION.tar"
    local extracted="$WORKDIR/peak-linux-driver-$DRIVER_VERSION"

    # tar 包已存在 -> 跳过下载；否则下载
    if [ -s "$tarball" ]; then
        ok "检测到已下载的驱动包，跳过下载：$tarball"
    else
        run "下载驱动源码包：$DRIVER_URL" curl -fSL "$DRIVER_URL" -o "$tarball" \
            || match_error "$LOGFILE"
    fi

    # 始终重新解压（清理旧目录），避免解压目录被改动过
    rm -rf "$extracted"
    run "解压驱动包" tar xf "$tarball" -C "$WORKDIR" || match_error "$LOGFILE"

    SRC="$extracted"
    [ -f "$SRC/Makefile" ] && [ -d "$SRC/driver" ] \
        || { err "解压结果不含预期的 Makefile/driver"; match_error "$LOGFILE"; }
    ok "源码已就绪：$SRC"
}

remove_conflicts() {
    info "卸载内核自带的冲突驱动（若已加载）"
    for m in peak_usb peak_pci peak_pciefd peak_pcmcia; do
        rmmod "$m" >/dev/null 2>&1 && info "  已卸载 $m" || true
    done
}

build_driver() {
    run "清理旧构建产物 (make clean)" make -C "$SRC" clean || true
    if [ "$MODE" = "netdev" ]; then
        run "编译驱动 (netdev / SocketCAN 模式)" make -C "$SRC" "${CC_ARG[@]}" netdev || match_error "$LOGFILE"
    else
        run "编译驱动 (chardev 默认模式)" make -C "$SRC" "${CC_ARG[@]}" || match_error "$LOGFILE"
    fi
    ok "编译完成"
}

# 把 CC=gcc-N 注入 dkms.conf 的 MAKE[0]，确保 DKMS 当前及未来内核升级
# 自动重建时都使用与内核匹配的 gcc。dkms.conf 在 build 阶段已由 make 生成；
# install 目标不依赖它，故 patch 不会被覆盖。
patch_dkms_cc() {
    [ -n "$CCVER" ] || return 0
    local conf="$SRC/driver/dkms.conf"
    [ -f "$conf" ] || make -C "$SRC/driver" "${CC_ARG[@]}" dkms.conf >/dev/null 2>&1
    [ -f "$conf" ] || { warn "未找到 dkms.conf，跳过 CC 注入。"; return 0; }
    if grep -q 'MAKE\[0\]="make CC=' "$conf"; then
        return 0   # 已注入
    fi
    sed -i "s|MAKE\[0\]=\"make |MAKE[0]=\"make CC=gcc-$CCVER |" "$conf"
    info "已将 CC=gcc-$CCVER 注入 dkms.conf（保证内核升级后自动重建也用正确的 gcc）。"
}

install_driver() {
    if [ "$USE_DKMS" -eq 1 ] && command -v dkms >/dev/null 2>&1; then
        # 先把正确的 gcc 注入 dkms.conf，再做 DKMS 安装（install 不会覆盖 dkms.conf）
        patch_dkms_cc
        # DKMS 装内核模块；用户态库/工具走普通 install
        run "通过 DKMS 安装内核模块" make -C "$SRC/driver" "${CC_ARG[@]}" install_with_dkms || match_error "$LOGFILE"
        for d in lib test libpcanbasic utils-xl; do
            [ -d "$SRC/$d" ] && run "安装 $d" make -C "$SRC/$d" install
        done
        ok "已用 DKMS 安装：升级/切换内核时会自动重建，无需重装。"
    else
        warn "未使用 DKMS —— 手动安装的模块在内核升级后将失效，到时需重跑本脚本。"
        run "安装驱动与库 (make install)" make -C "$SRC" "${CC_ARG[@]}" install || match_error "$LOGFILE"
    fi

    run "刷新动态库缓存 (ldconfig)" ldconfig || true
    run "刷新模块依赖 (depmod)" depmod -a "$KREL" || true
}

load_and_verify() {
    run "加载 pcan 模块 (modprobe)" modprobe pcan || match_error "$LOGFILE"

    hr
    if lsmod | grep -q '^pcan'; then
        ok "pcan 模块已加载"
    else
        err "pcan 模块未出现在 lsmod 中"
        match_error "$LOGFILE"
    fi

    # 用户的 Python 程序依赖 libpcanbasic.so，校验能否被找到
    if ldconfig -p | grep -qi 'libpcanbasic'; then
        ok "libpcanbasic.so 已注册到动态库缓存（Python 可加载）"
    else
        warn "未在 ldconfig 缓存中找到 libpcanbasic.so —— Python 可能报 'cannot open shared object file'。"
        warn "请确认 libpcanbasic 已安装，并运行 sudo ldconfig。"
    fi
    if command -v python3 >/dev/null 2>&1; then
        if python3 -c "import ctypes; ctypes.cdll.LoadLibrary('libpcanbasic.so')" 2>/dev/null; then
            ok "Python 成功加载 libpcanbasic.so"
        else
            warn "Python 加载 libpcanbasic.so 失败，请检查上面的库安装情况。"
        fi
    fi

    info "已识别的 PCAN 设备："
    if command -v lspcan >/dev/null 2>&1; then
        lspcan -a 2>/dev/null || lspcan 2>/dev/null || true
    fi
    hr
    ok "安装完成！若已插入 PCAN-USB 设备，上面应能看到设备；否则插入后即可使用。"
    info "完整日志：$LOGFILE"
}

# 安装前清理旧驱动：不论老版本是 DKMS 装的还是手动 make install 装的、
# 也不论是哪个版本号，统一清干净，之后一律用新方法（DKMS）重装。
# 全程 best-effort：任何一步失败都不中断后续安装。
pre_clean() {
    info "安装前清理旧的 PCAN 驱动（兼容任意旧版本/安装方式，best-effort）"

    # 1) 卸下当前加载的模块
    rmmod pcan >/dev/null 2>&1 && info "  已卸载当前加载的 pcan 模块" || true

    # 2) 移除 DKMS 里登记的「所有版本」（不只 9.1.0），覆盖旧 DKMS 安装
    if command -v dkms >/dev/null 2>&1; then
        local vers v
        vers="$(dkms status 2>/dev/null | grep -E "^$DKMS_NAME[ ,/]" \
                | sed -E "s#^$DKMS_NAME[/,] *##" | sed -E 's#[/,: ].*##' | sort -u)"
        for v in $vers; do
            info "  移除旧 DKMS 注册：$DKMS_NAME/$v"
            dkms remove "$DKMS_NAME/$v" --all >>"$LOGFILE" 2>&1 || true
        done
    fi

    # 3) 用新解压的源码 make uninstall，清掉标准路径下的旧文件
    #    （头文件、/usr/local/bin 工具、udev 规则、modprobe 配置等，路径历来稳定，
    #     因此也能清掉旧版本手动 make install 留下的同名文件）
    if [ -n "$SRC" ] && [ -f "$SRC/Makefile" ]; then
        info "  make uninstall 清理旧文件"
        make -C "$SRC" uninstall >>"$LOGFILE" 2>&1 || true
    fi

    # 4) 兜底：删掉「所有内核目录」下残留的 pcan.ko —— 覆盖任何手动安装、
    #    任意内核版本、任意安装路径，避免 modprobe 加载到旧模块
    find /lib/modules -name 'pcan.ko' -delete 2>/dev/null \
        && info "  已删除残留的 pcan.ko" || true

    # 5) 清掉旧的 modprobe 配置（老版本可能写在不同文件名里）
    rm -f /etc/modprobe.d/pcan.conf /etc/modprobe.d/blacklist-peak.conf \
          /etc/modprobe.d/blacklist-peak_pci.conf 2>/dev/null || true

    # 6) 删除完模块后刷新依赖缓存
    depmod -a "$KREL" >>"$LOGFILE" 2>&1 || true

    ok "旧驱动清理完成（已统一，准备用 DKMS 方式重装）"
}

do_uninstall() {
    info "开始卸载 PCAN 驱动"
    if command -v dkms >/dev/null 2>&1 && dkms status 2>/dev/null | grep -q "$DKMS_NAME"; then
        run "DKMS 移除" dkms remove "$DKMS_NAME/$DRIVER_VERSION" --all || true
    fi
    if [ -n "$SRC" ] && [ -f "$SRC/Makefile" ]; then
        run "make uninstall" make -C "$SRC" uninstall || true
    elif [ -d "$WORKDIR/peak-linux-driver-$DRIVER_VERSION" ]; then
        run "make uninstall" make -C "$WORKDIR/peak-linux-driver-$DRIVER_VERSION" uninstall || true
    else
        warn "未找到源码目录，跳过 make uninstall（仅做了 DKMS 移除）。"
    fi
    rmmod pcan >/dev/null 2>&1 || true
    ok "卸载完成。"
}

do_diagnose() {
    local diag=""
    for cand in "$SRC/driver/pcanosdiag.sh" \
                "$WORKDIR/peak-linux-driver-$DRIVER_VERSION/driver/pcanosdiag.sh" \
                "$(dirname "$0")/driver/pcanosdiag.sh"; do
        [ -f "$cand" ] && { diag="$cand"; break; }
    done
    if [ -n "$diag" ]; then
        run "运行 PEAK 诊断脚本" bash "$diag" || true
        info "诊断日志通常生成在 /tmp/pcanosdiag-*.log"
    else
        warn "未找到 pcanosdiag.sh，改为收集基本信息到日志。"
        { echo "uname -a:"; uname -a;
          echo; echo "lsmod | grep pcan:"; lsmod | grep -i pcan;
          echo; echo "ldconfig pcan:"; ldconfig -p | grep -i pcan;
          echo; echo "dmesg tail:"; dmesg | tail -n 50; } >>"$LOGFILE" 2>&1
        info "基本诊断信息已写入：$LOGFILE"
    fi
}

usage() {
    sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
    exit 0
}

# ---------------------------------------------------------------------------
# 参数解析
# ---------------------------------------------------------------------------
while [ $# -gt 0 ]; do
    case "$1" in
        --netdev)    MODE="netdev" ;;
        --manual)    USE_DKMS=0 ;;
        --src)       shift; SRC="${1:-}";;
        --uninstall) ACTION="uninstall" ;;
        --diagnose)  ACTION="diagnose" ;;
        -h|--help)   usage ;;
        *) err "未知参数：$1"; usage ;;
    esac
    shift
done

# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
printf '%s== PEAK PCAN 驱动自动安装脚本 (v%s) ==%s\n' "$C_BLD" "$DRIVER_VERSION" "$C_RST"
info "运行内核：$KREL ｜ 模式：$MODE ｜ 安装方式：$([ "$USE_DKMS" -eq 1 ] && echo DKMS || echo 手动)"
info "日志文件：$LOGFILE"
hr

require_root "$@"
check_apt

case "$ACTION" in
    uninstall) do_uninstall; exit 0 ;;
    diagnose)  do_diagnose;  exit 0 ;;
esac

install_deps
download_src
pre_clean
remove_conflicts
build_driver
install_driver
load_and_verify
