#!/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

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() {
    for quirk in $QUIRKS; do
	[[ $quirk = $1 ]] && return 0
    done
    return 1
}

remove_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" || QUIRKS="$QUIRKS $1"
}

# It is important to distinguish between versions and items.
# There is an expectation of continuity between versions, whereas items are
# standalone values.

is_version() {
    case $1 in
	system.firmware.version) return 0 ;;
	system.hardware.primary_video.driver_version) return 0;;
	system.kernel.version) return 0 ;;
	*) return 1 ;;
    esac
}	

# Maybe these will be used someday.

nvidia_version() {
    RES="$(awk '/^NVRM version:/ {print $8}' </proc/driver/nvidia/version)"
}

kernel_version() {
    RES="$(uname -r)"
}

# Get some video related values when HAL has not gotten them for us.

videoget() {
    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) [[ -L ${dev}/driver ]] && {
		    RES="$(readlink "${dev}/driver")"
		    RES="${RES##*/}"
		} ;;
	esac
	break
    done
}

# Use DMI to get some important information about this system.

dmiget() {
    case $1 in 
	system.hardware.vendor) 
	    RES="$(dmidecode -s system-manufacturer)" ;;
	system.hardware.product) 
	    RES="$(dmidecode -s system-product-name)" ;;
	system.hardware.version)
	    RES="$(dmidecode -s system-version)" ;;
	system.firmware.version)
	    RES="$(dmidecode -s bios-version)" ;;
	system.hardware.primary_video.vendor) videoget vendor ;;
	system.hardware.primary_video.product) videoget device ;;
	system.hardware.primary_video.driver) videoget driver ;;
	*) echo "Don't know how to get $1" >&2
    esac
}

halget() {
    local hgp="hal-get-property --udi /org/freedesktop/Hal/devices/computer --key" 
    case $1 in
	system.hardware.vendor) RES="$($hgp "$1")" ;;
	system.hardware.product) RES="$($hgp "$1")" ;;
	system.hardware.version) RES="$($hgp "$1")" ;;
	system.firmware.version) 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 ;;
	*) echo "Don't know how to get $1" >&2
    esac
}

# Get a value using either HAL or DMI.

_getprop() {
    halget "$1"
    [[ $RES ]] || dmiget "$1"
    # Trim leading and trailing spaces.
    RES="${RES##+( )}"
    RES="${RES%%+( )}"
}

# We may end up calling this function several thousand 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.

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
}

# 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() {
    shopt -s nocasematch
    regex "$1" "$2"
    shopt -u nocasematch
}

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() {
	# $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
}

# when translating from .fdi to native, we need to massage things a little.
escape_match() {
    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
    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"
}

find_native() {
    local continue_match=0
    local found
    [[ -f $1 ]] || return 1
    while read action key matcher regex; do
	[[ $action ]] || continue
	case $action in
	    match) ((in_match++))
		((continue_match == 0)) || continue
		getprop "$key"
		[[ $matcher && $RES && $regex ]] || continue
		# if this matcher does not match, ignore everything 
		# nested below it.
		$matcher "$RES" "$regex" || continue_match=$in_match
		;;
	    endmatch) ((in_match <= continue_match)) && continue_match=0
		((in_match--))
		# If we found a complete match, stop now. No point in parsing
		# the rest of the file.
		[[ $found ]] && return 0;
		;;
	    addquirk) ((continue_match == 0)) || continue
		found=true
		add_quirk "$key"
		;;
	    delquirk) ((continue_match == 0)) || continue
	    	found=true
		remove_quirk "$key"
		;;
	esac
    done <$1
    return 1
}

# make the output look nicer

space_echo() {
    spaces=$1
    shift
    for ((; spaces; spaces--)); do
	printf " "
    done
    echo "$@"
}

# 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() {
    while read line; do
	# handle commented out and blank lines first
	# Maybe I should actually save the comments...
	[[ "$line" ]] || continue
	[[ "$line" =~ $begin_comment ]] && ((in_comment++))
	[[ "$line" =~ $end_comment ]] && { ((in_comment--)); continue; }
	(( in_comment > 0 )) && continue
	if [[ "$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
    x=0
    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() ( 
    exec >"$QUIRKDIR/last_known_working.quirkdb"
    for prop in system.hardware.vendor system.hardware.product \
	system.firmware.version system.hardware.primary_video.vendor \
	system.hardware.primary_video.product \
	system.hardware.primary_video.driver; do
	getprop "$prop"
	if is_version "$prop"; then
	    if isnum "$RES"; then
		matcher=numeric_compare_ge
	    else
		matcher=compare_ge
	    fi
	else
	    if isnum "$RES"; then
		matcher=numeric_compare_eq
	    else
		matcher=compare_eq
	    fi
	fi
	echo "match $prop $matcher ${RES}"
    done
    for quirk in $QUIRKS; do
	echo "addquirk $quirk"
    done
    for ((x=1; x<6; x++)); do
	echo endmatch
    done
)

maybe_update_native
if find_native "$QUIRKDIR/last_known_working.quirkdb"; then
    echo $QUIRKS
    exit
else
    rm "$QUIRKDIR/last_known_working.quirkdb" >/dev/null 2>&1
    nifs="
"
    oifs="$IFS"
    IFS="$nifs"
    for f in $(IFS="$oifs"; for g in $QUIRKDIR/*; do echo $g; done |\
		sort -r);
    do
	IFS="$oifs"
	find_native "$f" && break
	IFS="$nifs"
    done
fi
# 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"
[[ $QUIRKS ]] || exit
write_last_known_working
echo $QUIRKS
