#!/bin/bash
# Prototype video quirk database handler that does not rely on HAL.
# It knows just enough about the video quirk .fdi files to translate them
# into a format that bash can more easily parse.
# While we are at it, replace the .fdi ad-hoc pattern matching language with
# extended regular expressions where we can.

shopt -s extglob

[[ $PM_DEBUG ]] && { 
    exec 2>/var/log/vtrace.log
    export PS4='${BASH_SOURCE}@${LINENO}(${FUNCNAME[0]}): ';
    set -x
}

begin_match='<match key="([a-z._]+)" ([a-z_]+)="([^"]+)">'
end_match='</match>'
merge='<merge key="([^"]+)" type="bool">true</merge>'
remove='<remove key="([^"]+)"></remove>'
begin_comment='<!--'
end_comment='-->'
in_comment=0
in_match=0
QUIRKS=""
QUIRKDIR="/usr/share/video-quirks"

# Some quirklist manipulation functions

has_quirk() {
    local quirk
    for quirk in $QUIRKS; do
	[[ $quirk = $1 ]] && return 0
    done
    return 1
}

remove_quirk() {
    local new_quirks quirk
    local new_quirks=""
    for quirk in $QUIRKS; do
	[[ $quirk = $1 ]] || new_quirks="$quirk $new_quirks"
    done
    QUIRKS="$new_quirks"
}

add_quirk() {
    has_quirk "$1" && return
    QUIRKS="$QUIRKS $1"
}

# video specific helper functions.

# Are we using the nVidia binary driver?
using_nvidia() {  [[ -d /sys/module/nvidia ]]; }

# How about the ATI one?
using_fglrx() { [[ -d /sys/module/fglrx ]]; }

# OK, what about a driver that is using kernel modesetting?
using_kms() { grep -q -E '(noveau|drm)fb' /proc/fb; }

# Get some video related values when HAL has not gotten them for us, or 
# HAL is not available.
videoget() {
    local dev pci
    pci="/sys/bus/pci/devices"
    for dev in "$pci"/*; do
	[[ -f "${dev}/class" ]] || continue
	[[ "$(cat "${dev}/class")" = "0x030000" ]] || continue
	case $1 in
	    vendor) RES="$(cat "${dev}/vendor")" ;;
	    device) RES="$(cat "${dev}/device")" ;;
	    driver) 
		if [[ -L ${dev}/driver ]]; then
		    RES="$(readlink "${dev}/driver")"
		    RES="${RES##*/}"
		elif using_nvidia; then
		    RES=nvidia
		elif using_fglrx; then
		    RES=fglrx
		fi
		;;
	    using_kms)
		if using_kms; then
		    RES=true
		else
		    RES=false
		fi
		;;
	esac
	break
    done
}

# Get some important information about this system.

# If we have /sys/class/dmi/id, life is easy, and we do not need to 
# depend on HAL or dmidecode.
_dmisysget() {
    [[ -r /sys/class/dmi/id/$1 ]] || RES=""
    read RES < "/sys/class/dmi/id/$1"
}

dmisysget() {
    case $1 in 
	system.firmware.vendor) _dmisysget bios_vendor ;;
	system.firmware.version) _dmisysget bios_version ;;
	system.firmware.release_date) _dmisysget bios_date ;;
	system.hardware.vendor) _dmisysget sys_vendor ;;
	system.hardware.product) _dmisysget product_name ;;
	system.hardware.version) _dmisysget product_version ;;
	system.board.product) _dmisysget board_name ;;
	system.board.version) _dmisysget board_version ;;
	system.board.vendor) _dmisysget board_vendor ;;
	system.hardware.primary_video.vendor) videoget vendor ;;
	system.hardware.primary_video.product) videoget device ;;
	system.hardware.primary_video.driver) videoget driver ;;
	system.hardware.primary_video.using_kms) videoget using_kms ;;
	system.kernel.version) RES=$(uname -r) ;;
	*) return 1
    esac
}

# Get system information using dmidecode.  Slow and ugly, but
# should be supported just about everywhere.
_dmidecodeget() {
    RES=$(dmidecode -s $1)
}

dmidecodeget() {
    case $1 in 
	system.firmware.vendor) _dmidecodeget bios-vendor ;;
	system.firmware.version) _dmidecodeget bios-version ;;
	system.firmware.release_date) _dmidecodeget bios-release-date;;
	system.hardware.vendor) _dmidecodeget system-manufacturer;;
	system.hardware.product) _dmidecodeget system-product-name;;
	system.hardware.version) _dmidecodeget system-version;;
	system.board.product) _dmidecodeget baseboard-product-name;;
	system.board.version) _dmidecodeget baseboard-version;;
	system.board.vendor) _dmidecodeget baseboard-manufacturer;;
	system.hardware.primary_video.vendor) videoget vendor ;;
	system.hardware.primary_video.product) videoget device ;;
	system.hardware.primary_video.driver) videoget driver ;;
	system.hardware.primary_video.using_kms) videoget using_kms ;;
	system.kernel.version) RES=$(uname -r);;
	*) return 1
    esac
}

# If we have HAL, it has already done most of the work for us.
halget() {
    local hgp="hal-get-property --udi /org/freedesktop/Hal/devices/computer --key" 
    case $1 in
	system.firmware.version) RES=$($hgp "$1") ;;
	system.firmware.vendor) RES=$($hgp "$1") ;;
	system.firmware.release_date) RES=$($hgp "$1") ;;
	system.hardware.vendor) RES=$($hgp "$1") ;;
	system.hardware.product) RES=$($hgp "$1") ;;
	system.hardware.version) RES=$($hgp "$1") ;;
	system.board.product) RES=$($hgp "$1") ;;
	system.board.version) RES=$($hgp "$1") ;;
	system.board.vendor) RES=$($hgp "$1") ;;
	system.hardware.primary_video.vendor) RES=$($hgp "$1") ;;
	system.hardware.primary_video.product) RES=$($hgp "$1") ;;
	system.hardware.primary_video.driver) videoget driver ;;
	system.hardware.primary_video.using_kms) videoget using_kms ;;
	system.kernel.version) RES=$($hgp "$1") ;;
	*) return 1
    esac
}

# Get a value using either sysfs, HAL, or dmidecode in that order.

_getprop() {
    local f
    for f in dmisysget halget dmidecodeget; do
	"$f" "$1" || continue
	[[ $RES ]] || continue
	RES="${RES##+( )}"
	RES="${RES%%+( )}"
	return
    done
    echo "Don't know how to get $1!" >&2
}

# We may end up calling this function lots of times, and the results
# for any given parameter will never change on any given run.  
# Use some stupid shell memoization tricks to speed things up.

# This should be scrapped and rewritten to be nicer -- we precache everything
# anyways, so getprop does not have to be so ornate.  A task for later.

getprop() {
    RES=""
    if [[ $1 =~ ^[a-z.]+$ ]]; then
        # memoization for great performance win
	p="${1//./_}"
	if [[ ${!p} ]]; then
	    # we already know the answer.
	    RES="${!p}"
	    return
	else
	    # Remember the answer for future use
	    _getprop "$1"
	    read "$p" <<<$RES
	fi
    else
	# We cannot memoize our answer.  Expect things to slow down.
	_getprop "$1"
    fi
}

# cache all the properties we will need.
for prop in system.firmware.version system.firmware.vendor \
    system.firmware.release_date system.hardware.vendor \
    system.hardware.product system.hardware.version \
    system.board.product system.board.version system.board.vendor \
    system.hardware.primary_video.vendor \
    system.hardware.primary_video.product \
    system.hardware.primary_video.driver \
    system.hardware.primary_video.using_kms \
    system.kernel.version; do
    getprop $prop
done



# test to see if the parameter passed is a decimal or hexidecimal number
# Note the complete lack of floating point support.
isnum() {
    [[ $1 =~ ^[0-9]+\$  || $1 =~ ^0[xX][0-9a-fA-F]+\$ ]]
}

# for all the matching functions, 
# $2 = the given constant (or regular expression),
# $1 = the raw data grabbed from HAL or dmidecode or wherever

regex() { [[ $1 =~ $2 ]]; }

regex_ncase() {
    local r
    shopt -s nocasematch
    regex "$1" "$2"
    r=$?
    shopt -u nocasematch
    return $r
}

regex_inverse() { ! regex "$1" "$2"; }
compare_eq() { [[ $1 = $2 ]]; }
compare_ne() { [[ $1 != $2 ]]; }
compare_gt() { [[ $1 > $2 ]]; }
compare_ge() { compare_eq "$@" || compare_gt "$@"; }
compare_lt() { [[ $1 < $2 ]]; }
compare_le() { compare_eq "$@" || compare_lt "$@"; }
numeric_compare_eq() { (( $1 == $2 )); }
numeric_compare_ne() { (( $1 != $2 )); }
numeric_compare_gt() { (( $1 > $2 )); }
numeric_compare_ge() { (( $1 >= $2 )); }
numeric_compare_lt() { (( $1 < $2 )); }
numeric_compare_le() { (( $1 <= $2 )); }
numeric_compare_eq_list() {
    local key val
	# $1 = key val to compare
	# $2 = list to compare to
    key=$1
    val="${2//;/ }"
    for x in $val; do
	(( $key == $x )) && return 0
    done
    return 1
}

# Helper function for nVidia g80 gpus.  They require extra special handling 
# when not using the nVidia binary driver.
have_nvidia_g80() {
    getprop system.hardware.primary_video.vendor
    numeric_compare_eq $RES 0x10de || return
    getprop system.hardware.primary_video.product
    numeric_compare_eq_list $RES '0x190;0x191;0x192;0x193;0x194;0x195;0x196;0x197;0x198;0x199;0x19a;0x19b;0x19c;0x19d;0x19e;0x19f;0x400;0x401;0x402;0x403;0x404;0x405;0x406;0x407;0x408;0x409;0x40a;0x40b;0x40c;0x40d;0x40e;0x40f;0x420;0x421;0x422;0x423;0x424;0x425;0x426;0x427;0x428;0x429;0x42a;0x42b;0x42c;0x42d;0x42e;0x42f' || return
}

# Helper function for recent Intel framebuffer drivers.
have_smart_intel() {
    getprop system.kernel.version
    local kernel_rev=$RES
    getprop system.hardware.primary_video.driver
    local driver=$RES
    # currently, intel kernel modesetting is not quite smart enough
    # we still need acpi s3 kernel modesetting hooks, so don't remove those
    # options if they were passed.
    [[ $driver = i915 && ( $kernel_rev > 2.6.26 || $kernel_rev = 2.6.26 ) ]]
}

# find the appropriate quirks for this system using the native-format
# quirks database. Since the database is tree-ish, we use some stupid
# recursion tricks.

# $1 = whether to ignore what we are reading
_find_native() {
    local action key matcher regex
    while read action key matcher regex; do
	[[ $action && ${action:0:1} != '#' ]] || continue
	case $action in
	    match) 
		if [[ $1 = ignore ]]; then
		    _find_native $1
		else
		    getprop "$key"
		    [[ $matcher && $regex ]] || find_native ignore
		# if this matcher matches, look at nodes farther out. 
		    if $matcher "$RES" "$regex"; then
			_find_native work
		    else
			_find_native ignore
		    fi
		fi
		;;
	    endmatch)
		[[ $found ]] && {
		    echo $QUIRKS; return 0
		} 
		return 1
		;;
	    addquirk) [[ $1 = ignore ]] && continue
		found=true
		add_quirk "$key"
		;;
	    delquirk) [[ $1 = ignore ]]&& continue
		found=true
		remove_quirk "$key"
		;;
	esac
    done
}

find_native() (
    exec <"$1"
    _find_native work
)

space_echo() {
    for ((i=$1; i>0; i--)); do
	printf ' '
	done
    shift
    echo "$1"
}

# Helper function for when translating from .fdi to native format, 
# we need to massage things a little.
escape_match() {
    local fun lit
    fun="regex"
    # first, escape special ere characters
    lit="$2"
    lit="${lit//\\/\\\\}"
    lit="${lit//./\\.}"
    lit="${lit//^/\\^}"
    lit="${lit//[/\\\[}"
    lit="${lit//]/\\\]}"
    lit="${lit//(/\\\(}"
    lit="${lit//)/\\\)}"
    lit="${lit//\*/\\\*}"
    lit="${lit//\+/\\\+}"
    lit="${lit//\?/\\\?}"
    lit="${lit//\{/\\\{}"
    lit="${lit//\}/\\\}}"
    lit="${lit//\|/\\\|}"
    lit="${lit//\$/\\\$}"
    # nuke leading and trailing spaces, they just confuse things.
    lit="${lit%%+( )}"
    lit="${lit##+( )}"
    # second, handle the various comparison cases, munging them into
    # extended regular expressions that bash understands.
    case $1 in
	string) lit="^${lit}\$" ;;
	int|uint64) fun=numeric_compare_eq ;;
	string_outof) lit="${lit%;}"; lit="^(${lit//@(*( );*( ))/|})\$" ;;
	int_outof) fun=numeric_compare_eq_list ;;
	contains) ;;
	contains_outof) lit="${lit%;}"; lit="${lit//@(*( );*( ))/|}" ;;
	prefix) lit="^${lit}" ;;
	prefix_outof) lit="${lit%;}"; lit="^(${lit//@(*( );*( ))/|})" ;;
	suffix) lit="${lit}\$" ;;
	contains_ncase) fun=regex_ncase ;;
	contains_not) fun=regex_inverse ;;
	prefix_ncase) fun=regex_ncase;  lit="^${lit}" ;;
	suffix_ncase) fun=regex_ncase; lit="${lit}\$" ;;
	compare_lt|compare_le|compare_gt|compare_ge|compare_ne)
	    # I know, this may not always be the right thing to do.
	    # It is the right thing most of the time, though.
	    if isnum "$lit"; then
		fun="numeric_$1"
	    else
		fun="$1" 
	    fi
	    ;;
	*) echo "$1 not implemented." >&2 ; exit 1 ;;
    esac
    echo "$fun $lit"
}

# This is so not the right way to parse XML, but it happens to work with
# the current hal-info video quirk .fdi files. Maybe someday I will rewrite
# this in a language that actually understands XML, even though XML is the devil. 
translate_xml() {
    local line key val matcher in_comment in_matcher
    while read line; do
	[[ "$line" ]] || continue
	if [[ $line =~ $begin_comment && $line =~ $end_comment ]]; then
	    space_echo $in_match "# $line"
	elif [[ $line =~ $begin_comment ]]; then
	    space_echo $in_match "# $line"
	    ((in_comment++))
	elif [[ $line =~ $end_comment ]]; then
	    space_echo $in_match "# $line"
	    ((in_comment--))
	elif ((in_comment > 0)); then
	    space_echo $in_match "# $line"
	elif [[ $line =~ $begin_match ]]; then
	    ((in_match++))
	    key="${BASH_REMATCH[1]}"
	    matcher="${BASH_REMATCH[2]}"
	    val="${BASH_REMATCH[3]}"
	    space_echo $in_match "match $key $(escape_match "$matcher" "$val")"
	elif [[ $line =~ $end_match ]]; then
	    space_echo $in_match "endmatch"
	    ((in_match--))
	elif [[ $line =~ $merge ]]; then
	    key="${BASH_REMATCH[1]##*.}"
	    space_echo $((in_match + 1)) "addquirk --quirk-${key//_/-}"
	elif [[ $line =~ $remove ]]; then
	    key="${BASH_REMATCH[1]##*.}"
	    space_echo $((in_match + 1)) "delquirk --quirk-${key//_/-}"
	fi
    done

}

maybe_update_native() {
    # if any of the video quirk .fdi files are more recent,
    # recreate the corresponding native ones and delete the cached
    # quirks.
    local x=0 f lf 
    mkdir -p "$QUIRKDIR"
    for f in /etc/hal/fdi/information/99local-pm-utils-quirks.fdi \
	/usr/share/hal/fdi/information/10freedesktop/2[10]-video-quirk*.fdi;
    do
	lf="$QUIRKDIR/${f##*/}"
	lf="${lf%.*}.quirkdb"
	[[ -f $f ]] || continue
	[[ $f -nt $lf ]] || continue 
	translate_xml < "$f" > "$lf"
	rm "$QUIRKDIR/last_known_working.quirkdb" >/dev/null 2>&1
    done
}

# This assumes we will actually suspend/resume correctly with whatever
# set of quirks we found.  We should actually check that was the case 
# but right now we don't.
write_last_known_working() ( 
    local matcher quirk
    exec >"$QUIRKDIR/last_known_working.quirkdb"
    for prop in system.firmware.version system.firmware.vendor \
	system.firmware.release_date system.hardware.vendor \
	system.hardware.product system.hardware.version \
	system.board.product system.board.version system.board.vendor \
	system.hardware.primary_video.vendor \
	system.hardware.primary_video.product \
	system.hardware.primary_video.driver \
	system.hardware.primary_video.using_kms \
	system.kernel.version; do
	getprop "$prop"
	if isnum "$RES"; then
	    matcher=numeric_compare_eq
	else
	    matcher=compare_eq
	fi
	echo "match $prop $matcher ${RES}"
    done
    if [[ $QUIRKS ]]; then 
	for quirk in $QUIRKS; do
	    echo "addquirk $quirk"
	done
    else
	echo "addquirk --quirk-none"
    fi
    for ((x=1; x<14; x++)); do
	echo endmatch
    done
)

# Aaand.... GO

# Using kernel modesetting?  No quirks, and do not change vts.
if using_kms; then
    QUIRKS="--quirk-no-chvt"
# How about an nvidia binary driver?
elif using_nvidia; then
    QUIRKS="--quirk-no-chvt"
# Or maybe the fglrx drivers?  They probably don't have to change vts in 
# userspace, but I don't know of anyone who has tested it.
elif using_fglrx; then
    QUIRKS="--quirk-none"
# nVidia G80 GPUs require special handling when not using noveau or nvidia
elif have_nvidia_g80; then
    QUIRKS="--quirk-vbe-post"
else
# Go ahead and get our quirks -- we will handle non-KMS Intel drivers
# later.
    maybe_update_native
    if ! find_native "$QUIRKDIR/last_known_working.quirkdb" >/dev/null; then
	rm "$QUIRKDIR/last_known_working.quirkdb" >/dev/null 2>&1
	for f in "$QUIRKDIR"/*
	do
	    QUIRKS=$(find_native "$f") && break
	done
    fi
    if have_smart_intel; then
	remove_quirk --quirk-dpms-on
	remove_quirk --quirk-dpms-suspend 
	remove_quirk --quirk-vbe-post
	remove_quirk --quirk-vbe-post
	remove_quirk --quirk-vga-mode3
	remove_quirk --quirk-vbemode-restore
	remove_quirk --quirk-vbestate-restore
	remove_quirk --quirk-reset-brightness
	remove_quirk --quirk-radeon-off
	remove_quirk --quirk-no-fb
	remove_quirk --quirk-pci-save
    else
    # fallback quirks if nothing else was defined.  
	[[ -z $QUIRKS ]] && QUIRKS="--quirk-vbe-post --quirk-dpms-on 
			    --quirk-dpms-suspend --quirk-vbestate-restore
			    --quirk-vbemode-restore --quirk-vga-mode-3"
    fi
fi
[[ $QUIRKS ]] || $QUIRKS="--quirk-none"
write_last_known_working
echo $QUIRKS
