#!/bin/bash
#
# License: Copyright 2015 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

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

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

# Timeout used to test for file/device paths to appear/disappear
PATHTIMEOUT=5

wait_for_paths_present() {
    local devs="$*"
    local d toutsecs

    # $SECONDS is special in bash
    toutsecs=$(( SECONDS + PATHTIMEOUT ))
    for d in $devs; do
	while ! [ -e $d ]; do
	    usleep 10000
	    if [ $SECONDS -gt $toutsecs ]; then
		echo "ERROR: timeout waiting for $devs to appear"
		return 1
	    fi
	done
    done

    return 0
}

wait_for_paths_gone() {
    local devs="$*"
    local d toutsecs

    # $SECONDS is special in bash
    toutsecs=$(( SECONDS + PATHTIMEOUT ))
    for d in $devs; do
	while [ -e $d ]; do
	    usleep 10000
	    if [ $SECONDS -gt $toutsecs ]; then
		echo "ERROR: timeout waiting for $devs to disappear"
		return 1
	    fi
	done
    done

    return 0
}

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/functions

    # Constants
    MONSYSFS=
    WINCTL=
    VPSSSYSFSDIR=
    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=""
    elif [ -e /sys/devices/platform/vpss/display0/timings ]; then
	# Ikebana without kernel vid_mode interface, but may have vid_modedb
	VPSSSYSFSDIR=/sys/devices/platform/vpss/display0
	MONSYSFS=$VPSSSYSFSDIR
    else
	MONSYSFS=/sys/class/graphics/fb0/device
	# This kernel version automatically recenters OSD0 on mode change
        # no need to access win_ctl
    fi
    FIXEDMODEDB=/usr/share/vidmode/vid.modes
    MAX_HSIZE=1920
    MAX_VSIZE=1080
    MAX_VSIZE_ED=600
    MAX_VSIZE_HD=800

}

set_power_mode() {
    local name out pm pmval
    echo -n "Setting video output power mode... "
    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"
		;;
	    *)
	        echo "Unrecognized output device '$name'"
		continue;
	esac
	case "$pm" in
	    auto)
	        pmval=2
		;;
	    on)
	        pmval=1
		;;
	    off)
	        pmval=0
		;;
	    "")
	        pmval=2
		;;
	    *)
	        echo "Unrecognized power mode '$pm' for '$name'"
		continue
	esac
	if [ -f "$out"/power_mode ]; then
	    echo "$pmval" > "$out"/power_mode
	    if [ $? -ne 0 ]; then
		echo "Failed setting power mode value '$pmval' for '$name'"
	    fi
	fi
    done
    echo "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
	echo "Could not create local video mode db"
	return 1
    fi
    if [ -f $MONSYSFS/vid_modedb -a "$USE_FIXED_MODEDB" != "yes" ]; then
	cat $MONSYSFS/vid_modedb > $MODEDB
	if [ $? -ne 0 ]; then
	    echo "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
    local ret

    if [ "$RUNMODE" == "set" ]; then
	echo -n "Setting video mode "
    else
	echo -n "Probing video mode "
    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
	    echo "Invalid RESOLUTION '$RESOLUTION' selected, fallback to VGA"
	    get_vresolutions VGA
	fi

	get_modedb
	if [ $? -ne 0 ]; then
	    echo "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
	    echo "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
		echo "ERROR: invalid custom mode string"
		return 1
	    fi
	    if [ -z "$MONSYSFS" -o ! -f "$MONSYSFS"/vid_mode ]; then
		if [ "$nf" != 11 ]; then
		    echo "ERROR: only full mode specification supported"
		    return 1
		fi
	    fi
	fi

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

    fi

    if [ "$RUNMODE" == "set" ]; then
	echo " found video mode $TIMING"
	if [ -n "$MONSYSFS" -a -f "$MONSYSFS"/vid_mode ]; then
	    echo "$TIMING" > "$MONSYSFS"/vid_mode
	    ret=$?
	elif [ -n "$VPSSSYSFSDIR" ]; then
	    # verify that we have at least a full mode spec
	    cvals=($(echo "${TIMING#*=}"))
	    if [ ${#cvals[@]} -lt 11 ]; then
		echo "ERROR: only full mode specification supported"
		return 1
	    fi
	    ret=0
	    # signal splashd to release the device and wait for it
	    splashd -n -c -s
	    echo 0 > "$VPSSSYSFSDIR"/enabled || ret=1
	    # this awk script converts between vid_mode format and vpss timing format
	    echo "$TIMING" | \
		awk '{ sub(/.*=[[:space:]]*/,""); width=$1; height=$2; vfreq=$3; hbp=$4; hfp=$5; vbp=$6; vfp=$7; hsw=$8; vsw=$9; sp=$10; scan=$11; htot = width + hbp + hfp + hsw; vtot = height + vbp + vfp + vsw; printf "%i,%i/%i/%i/%i,%i/%i/%i/%i,%i\n", htot*vtot*vfreq/1000, width, hfp, hbp, hsw, height, vfp, vbp, vsw, (scan!="i"?1:0)}' > \
		    "$VPSSSYSFSDIR"/timings || ret=1
	    # we desire for the V4L2 interface to pick up the display
	    # output resolution as its default mode, we can do that by
	    # unloading and reloading the ti81xxvo module, if present
	    local ti81xxvo_reload=
	    grep -q '^ti81xxvo[[:space:]]' /proc/modules && ti81xxvo_reload=1
	    if [ -n "$ti81xxvo_reload" ]; then
		modprobe -r ti81xxvo
	    fi
	    # the timings are actually applied on enable, so it must
	    # be done before re-loading the ti81xxvo module
	    echo 1 > "$VPSSSYSFSDIR"/enabled || ret=1
	    if [ -n "$ti81xxvo_reload" ]; then
		wait_for_paths_gone /dev/video1
		modprobe ti81xxvo
		wait_for_paths_present /dev/video1
	    fi
	else
	    echo "do not know how to set video mode"
	    ret=1
	fi
	if [ $ret -ne 0 ]; then
	    echo "ERROR: failed setting video mode"
	    return 1
	fi
	if [ -n "$WINCTL" ]; then
	    echo "osd0 center" > "$WINCTL"
	    if [ $? -ne 0 ]; then
		echo "ERROR: failed re-centering osd0"
		return 1
	    fi
	else
	    # signal splashd (if any) that it needs to reload for new video mode
	    splashd -n -r
	fi
    else
	if [ -n "$MONSYSFS" -a -f "$MONSYSFS"/vid_mode ]; then
	    echo "$TIMING" test > "$MONSYSFS"/vid_mode
	    if [ $? -ne 0 ]; then
		echo "ERROR: video mode not supported"
		return 1
	    fi
	else
	    echo -n " (WARNING: video mode test not supported)"
	fi
	echo " OK"
	echo "$TIMING"
	return 0
    fi
}

case "$1" in
    start|restart|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
	    echo "ERROR: missing settings file"
	    exit 1
	fi
	set_vmode
	exit $?
	;;
    power)
	init
	set_power_mode
	exit $?
	;;
    *)
	echo "Usage: $NAME {start|stop|restart|probe <settings file>|power|setmax}" >&2
	exit 1
	;;
esac
