#!/bin/bash
#
# License: Copyright 2007 SpinetiX S.A. This file is licensed
#          under the terms of the GNU General Public License version 2.
#          This program is licensed "as is" without any warranty of any
#          kind, whether express or implied.
#
# Copyright 2002, 2003, 2004 Sony Corporation
# Copyright 2002, 2003, 2004 Matsushita Electric Industrial Co., Ltd.
#
### BEGIN INIT INFO
# Required-Start:
# Required-Stop:
# Should-Start:
# Should-Stop:
# Default-Start: 2 3 5
# Default-Stop:
# Short-Description: Sets video modes
# Description: Sets video modes
### END INIT INFO
# chkconfig: 235 15 0

# Init script information
INIT_NAME=vidmode
DESC="Video mode changer"

SETTINGS=/etc/default/$INIT_NAME
RUNMODE=set

init() {

    # Default settings
    RESOLUTION=VGA
    FORCE_VFREQ=
    FORCE_AR=
    SCAN_TYPE=progressive
    USE_FIXED_MODEDB=
    FIXED_MODE_TYPES=
    DEFAULT_MON_AR=16:9
    HDMI_UNDERSCAN_IGNORE=
    HDMI_LINK_FORCE_TYPE=NONE
    VGA_DC_OFFSET=
    VFREQS="50 60"

    # Load init script configuration
    [ -f "$SETTINGS" ] && . "$SETTINGS"

    # Source the init script functions
    . /etc/init.d/init-functions

    # Constants
    if [ -e /sys/class/x-display/disp0/device ]; then
	MONSYSFS=/sys/class/x-display/disp0/device
	WINCTL=/sys/class/graphics/fb0/device/win_ctl
	[ -f "$WINCTL" ] || WINCTL=""
    else
	MONSYSFS=/sys/class/graphics/fb0/device
	# This kernel version automatically recenters OSD0 on mode change
        # no need to access win_ctl
    fi
    FIXEDMODEDB=/etc/vid.modes
    MAX_HSIZE=1920
    MAX_VSIZE=1080
    MAX_VSIZE_ED=600
    MAX_VSIZE_HD=800

}

set_power_mode() {
    local name out pm pmval
    log_status_msg "Setting video output power mode..." -n
    for out in "$MONSYSFS"/out[0-9]; do
	if [ ! -d "$out" ]; then
	    continue
	fi
	name="$(cat $out/name)"
	if [ -z "$name" ]; then
	    continue
	fi
	case "$name" in
	    HDMI)
		pm="$POWER_MODE_HDMI"
	    ;;
	    VGA)
		pm="$POWER_MODE_VGA"
	    ;;
	    *)
	        log_failure_msg "Unrecognized output device '$name'"
		continue;
	esac
	case "$pm" in
	    auto)
	        pmval=2
	    ;;
	    on)
	        pmval=1
	    ;;
	    off)
	        pmval=0
	    ;;
	    "")
	        pmval=2
	    ;;
	    *)
	        log_failure_msg "Unrecognized power mode '$pm' for '$name'"
		continue
	esac
	if [ -f "$out"/power_mode ]; then
	    echo "$pmval" > "$out"/power_mode
	    if [ $? -ne 0 ]; then
		log_failure_msg \
		    "Failed setting power mode value '$pmval' for '$name'"
	    fi
	fi
    done
    log_status_msg " done."
}

discover_mon_ar() {
	local name out out_ar
	MON_AR=
	if [ "$USE_FIXED_MODEDB" != "yes" ]; then
		for out in "$MONSYSFS"/out[0-9]; do
			if [ ! -d "$out" ]; then
				continue
			fi
			name=$(cat $out/name)
			if [ -f $out/mon_pict_ar ]; then
				out_ar=$(cat $out/mon_pict_ar)
				if [ -z "$out_ar" ]; then
					continue
				fi
				# Give preference to HDMI
				if [ -z "$MON_AR" -o "$name" = HDMI ]; then
					MON_AR=$out_ar
				fi
			fi
		done
	fi
	if [ -z "$MON_AR" ]; then
		MON_AR=$DEFAULT_MON_AR
	fi
	return 0
}

get_vresolutions() {
    MINH=0
    MAXH=$MAX_HSIZE # a large value
    MINV=0
    case "$1" in
	VGA)
		MINH=640
		MAXH=640
		MINV=480
		MAXV=480
	;;
	ED)
		MAXV=$MAX_VSIZE_ED
	;;
	HD)
		MAXV=$MAX_VSIZE_HD
	;;
	MAX)
		MAXV=$MAX_VSIZE
	;;
	[0-9]*)
		MINH=${1%x*}
		MAXH=${1%x*}
		MINV=${1#*x}
		MAXV=${1#*x}
	;;
	*)
		return 1
    esac
    return 0
}

match_vres_and_ar() {
	local ar scan type
	if [ -n "$1" ]; then
		ar="$1"
	else
		ar="[0-9x]+:[0-9x]+"
	fi
	case "$SCAN_TYPE" in
	    p*)
		scan='p'
		;;
	    i*)
		scan='i'
		;;
	    *)
		scan='[pi]'
	esac
	if [ -n "$FIXED_MODE_TYPES" -a "$USE_FIXED_MODEDB" = "yes" ]; then
	    type="\<(${FIXED_MODE_TYPES//,/|})\>"
	else
	    type=
	fi
	# Match all in range of H and V resolution and of the specified
	# vertical frequencies and sort by decreasing number of total pixels
	# and increasing vertical frequency, also prefer progressive to interlaced.
	# Then take the first match and remove added fields used for sorting.
	TIMING=$(awk -v minh=$MINH -v maxh=$MAXH -v minv=$MINV -v maxv=$MAXV -v vfs="$VFREQS" \
	    'BEGIN { split(vfs, vfreqs) }; (/^[0-9]+x[0-9]+@[0-9]+'"$scan"'-'"$ar"'.*'"$type"'/ && $3 >= minh && $3 <= maxh && $4 >= minv && $4 <= maxv) { for (i in vfreqs) if ($5 == vfreqs[i]) print $3*$4 " " $5 " " $13 "," $0}' $MODEDB | \
	    sort -u -k 1,1nr -k 2,2n -k 3,3r | \
	    head -n 1)
	[ -n "$TIMING" ] || return 1
	TIMING="${TIMING#*,}"
	return 0
}

# NOTE: could optimize the matching speed by turning the mode names
# (e.g., 640x480@60p-4:3) from the modedb into a list of strings and
# using shell functions instead of grep

get_modedb() {
	MODEDB=$(mktemp)
	if [ $? -ne 0 ]; then
		log_failure_msg "Could not create local video mode db (RET $?)"
		return 1
	fi
	if [ -f $MONSYSFS/vid_modedb -a "$USE_FIXED_MODEDB" != "yes" ]; then
		cat $MONSYSFS/vid_modedb > $MODEDB
		if [ $? -ne 0 ]; then
			log_failure_msg "Could not get video mode db, behaving as if no display was detected"
		fi
	elif [ "$USE_FIXED_MODEDB" = "yes" ]; then
		cat $FIXEDMODEDB > $MODEDB
	fi
	if [ ! -s $MODEDB ]; then
		grep -m 1 "^640x480@60p-4:3 " $FIXEDMODEDB > $MODEDB
	fi
	return 0
}

set_hdmi_underscan_ignore() {
    for out in "$MONSYSFS"/out[0-9]; do
	if [ ! -d "$out" ]; then
	    continue
	fi
	if [ -f "$out"/hdmi_underscan_ignore ]; then
	    if [ "$HDMI_UNDERSCAN_IGNORE" = "no" ]; then
		echo 0 > "$out"/hdmi_underscan_ignore
	    else
		echo 1 > "$out"/hdmi_underscan_ignore
	    fi
	fi
    done
}

set_hdmi_link_force_type() {
    for out in "$MONSYSFS"/out[0-9]; do
	[ -d "$out" ] || continue
	[ "$(< "$out"/name)" == "HDMI" ] || continue
	if [ -f "$out"/link_force_type ]; then
	    echo "$HDMI_LINK_FORCE_TYPE" > "$out"/link_force_type
	fi
    done
}

set_vga_dc_offset() {
    for out in "$MONSYSFS"/out[0-9]; do
	if [ ! -d "$out" ]; then
	    continue
	fi
	if [ -f "$out"/dc_offset ]; then
	    if [ "$VGA_DC_OFFSET" = "no" ]; then
		echo 0 > "$out"/dc_offset
	    else
		echo 1 > "$out"/dc_offset
	    fi
	fi
    done
}

set_vmode() {
	local cvals
	local nf

	if [ "$RUNMODE" == "set" ]; then
	    log_status_msg "Setting video mode" -n
	else
	    log_status_msg "Probing video mode" -n
	fi

	if [ "$RUNMODE" == "set" ]; then
	    set_hdmi_underscan_ignore
	    set_hdmi_link_force_type
	    set_vga_dc_offset
	fi

	if [ -n "$FORCE_AR" ]; then
		AR=$FORCE_AR
	else
		discover_mon_ar
		AR=$MON_AR
	fi

	if [ -z "$CUSTOMMODE" ]; then

	    get_vresolutions $RESOLUTION
	    if [ $? -ne 0 ]; then
		log_failure_msg "Invalid RESOLUTION '$RESOLUTION' selected, fallback to VGA"
		get_vresolutions VGA
	    fi

	    get_modedb
	    if [ $? -ne 0 ]; then
		log_failure_msg "ERROR: could not get mode db"
		return 1
	    fi

	    if [ -n "$FORCE_VFREQ" ]; then
		VFREQS="$FORCE_VFREQ"
	    fi

	    match_vres_and_ar $AR || match_vres_and_ar
	    RET=$?
	    rm -f $MODEDB
	    if [ $RET -ne 0 ]; then
		log_failure_msg "ERROR: no matching modes found in modedb, video mode unchanged"
		return 1
	    fi

	    # Always force aspect ratio
	    TIMING="$(echo $TIMING | cut -f1-13 -d' ') $AR"

	else

	    # When setting the mode we do not do validation, so as to keep backwards
	    # compatibility in case user entered a not strictly valid mode spec but
	    # that comes out to an acceptable spec by the kernel. Otherwise we validate
	    # to avoid problems with spurious fields at the end, like an aspect ratio or
	    # the "test" string.

	    if [ "$RUNMODE" != "set" ]; then
		# Parse $CUSTOMMODE spec fields into array, need to ignore anything
		# before the first '=' character, if any.
		cvals=($(echo "${CUSTOMMODE#*=}"))

		# If the fourth value is an integer it is a full mode specification,
		# otherwise it is a generic specification
		if [ -z "$(echo "${cvals[3]}" | tr -d -- '-+[:digit:]')" ]; then
		    nf=11 # full mode specification, requires 11 fields
		else
		    nf=5 # generic mode specification, requires 5 fields
		fi
		if [ ${#cvals[@]} -ne $nf ]; then
		    log_failure_msg "ERROR: invalid custom mode string"
		    return 1
		fi
	    fi

	    # Use the custom mode with the computed aspect ratio
	    TIMING="$CUSTOMMODE $AR"

	fi

	if [ "$RUNMODE" == "set" ]; then
	    log_status_msg " found video mode $TIMING"
	    echo "$TIMING" > $MONSYSFS/vid_mode
	    if [ $? -ne 0 ]; then
		log_failure_msg "ERROR: failed setting video mode"
		return 1
	    fi
	    if [ -n "$WINCTL" ]; then
		echo "osd0 center" > "$WINCTL"
		if [ $? -ne 0 ]; then
		    log_failure_msg "ERROR: failed re-centering osd0"
		    return 1
		fi
	    else
		# signal slashd (if any) that it needs to reload for new video mode
		splashd -n -r
	    fi
	else
	    echo "$TIMING" test > $MONSYSFS/vid_mode
	    if [ $? -ne 0 ]; then
		log_failure_msg "ERROR: video mode not supported"
		return 1
	    fi
	    log_status_msg " OK"
	    echo "$TIMING"
	    return 0
	fi
}

case "$1" in
	start|setmax)
		if [ "$1" = "setmax" ]; then
		    SETTINGS=/dev/null # prevent loading any settings
		fi
		init
		if [ "$1" = "setmax" ]; then
		    RESOLUTION=MAX
		    POWER_MODE_VGA=on # be sure VGA output is displayed
		    HDMI_UNDERSCAN_IGNORE=no
		fi
		set_vmode
		RET1=$?
		set_power_mode
		RET2=$?
		[ $RET1 -eq 0 -a $RET2 -eq 0 ]
		exit
		;;
	stop)
		exit 0
		;;
	probe)
	        RUNMODE="probe"
		SETTINGS="$2"
		init
		if [ ! -f "$SETTINGS" ]; then
		    log_failure_msg "ERROR: missing settings file"
		    exit 1
		fi
		set_vmode
		exit $?
		;;
	power)
		init
		set_power_mode
		exit $?
		;;
	*)
		echo "Usage: $INIT_NAME {start|stop|probe <settings file>|power|setmax}" >&2
		exit 1
		;;
esac
