#!/bin/sh -e
#
# Functions for qos class definitions
#

ARGS_REQ='argument(s) required'
CLASSES=

#
# Print error message and exit with exit code 1
#
_error() {
	echo "ERROR: $*" >&2
	exit 1
}

#
# Get given option from a string
#
_get_option() {
	[ $# -ge 3 ] && [ "$1" ] && [ "$2" ] && [ "$3" ] ||
		_error "_get_option: At least three $ARGS_REQ"

	local _var _opt _arg

	# Variable name
	_var=$1
	# Option name
	_opt=$2

	shift 2

	# Search...
	for _arg in "$@"; do
		case $_arg in
			$_opt=?*)
				# Has value.
				eval $_var='"${_arg#*=}"'
				return 0
				;
			$_opt)
				# Treat option-name only (without following
				# '=') as if "yes" was specified as value.
				eval $_var=yes
				return 0
				;
		esac
	done

	# Not found
	return 1
}

#
# Return 0 if class is available in class list
#
_class_available() {
	[ $# -eq 1 ] && [ "$1" ] ||
		_error "_class_available: Exactly one $ARGS_REQ"

	local _class=$1

	# Class available
	case "$CLASSES" in
		*$_class*)
			return 0
			;
	esac

	return 1
}

#
# Return 0 if protocol assigned to a class
#
_protocol_available() {
	[ $# -eq 1 ] && [ "$1" ] ||
		_error "_protocol_available: Exactly one $ARGS_REQ"

	local _class _members _protocol

	_protocol=$1

	# Search class members for protocol
	for _class in $CLASSES; do
		eval _members=\"\$_${_class}_members\"
		case "$_members" in
			*$_protocol*)
				return 0
				;
		esac
	done

	return 1
}

#
# Print number of classes in list
#
_count_classes() {
	set -- $CLASSES
	printf %d $#
}

#
# Add a class to the list
#
_qos_add() {
	[ $# -eq 2 ] && [ "$1" ] && [ "$2" ] ||
		_error "_qos_add: Exactly two $ARGS_REQ: (classname) (desc)"

	local _class=${1#class=}
	shift

	# Check that class name is unique
	! _class_available $_class ||
		_error "_qos_add: Class '$_class' already defined"

	_get_option _desc desc "$@" || :

	CLASSES="$CLASSES $_class"

	# Initialize variables
	eval _${_class}_desc=\$_desc
	eval unset _${_class}_members
}

#
# Attach info to the class
#
_qos_add_info() {
	[ $# -ge 2 ] && [ "$1" ] && [ "$2" ] ||
		_error "_qos_add_info: At least two $ARGS_REQ:" \
			'(classname) (info) ...'

	local _class=${1#class=}
	shift

	# Check that class is available
	_class_available $_class ||
		_error "_qos_add_info: Class '$_class' not defined"

	# Loop through info
	while [ $# -gt 0 ]; do
		_info=${1%=*}
		_value=${1#*=}
		eval _${_class}_$_info=\"$_value\"
		shift
	done
}

#
# Add a protocol to a class
#
_qos_prot() {
	[ $# -ge 2 ] && [ "$1" ] && [ "$2" ] ||
		_error "_qos_prot: At least two $ARGS_REQ:" \
			'(classname) (protocol) ...'

	local _class _protocol

	_class=${1#class=}
	shift

	# Check if class defined
	_class_available $_class ||
		_error "_qos_prot: Class '$_class' not defined"

	# Append protocols
	for _protocol in $@ do
		# Check that protocol not already available elsewhere
		! _protocol_available $_protocol ||
			_error "_qos_prot: Protocol '$_protocol' already" \
				'assigned to a class'
		eval _${_class}_members=\"\$_${_class}_members \$_protocol\"
	done
}

#
# Get QoS info for given protocol
#
_get_protocol_info() {
	local _ret_val _found _info _protocol _class _protocols

	_ret_val=
	_found=0
	_info=$1
	_protocol=$2
	shift 2

	# Search in classes
	for _class in $CLASSES; do
		eval _protocols=\"\$_${_class}_members\"
		case "$_protocols" in
			*$_protocol*)
				# Found it
				eval _ret_val=\"\$_${_class}_$_info\"
				_found=1
				;
		esac
	done

	[ $_found -ne 0 ] ||
		_error "_get_protocol_info: Protocol '$_protocol' not found"

	printf %s "$_ret_val"
}

#
# Exec command for each member in given class
#
_exec_member_command() {
	local _f=_exec_member_command

	[ $# -ge 2 ] && [ "$1" ] && [ "$2" ] ||
		_error "$_f: Exactly two $ARGS_REQ: (info) (classname)"

	local _info _class _info_val _members _member _cmd _skip _output

	_info=$1
	_class=$2

	# Check that class is available
	_class_available $_class ||
		_error "$_f: class '$_class' not defined"

	# Iterate through all members of this protocol
	# and see if they have any attached parameters
	eval _info_val=\$_${_class}_$_info
	eval _members=\$_${_class}_members
	for _member in $_members; do
		eval _cmd=\$${_member}_COMMAND
		eval _skip=\$${_member}_SKIP
		eval _skip=\"$_skip\"
		[ $_skip ] || [ -z "$_cmd" ] || {
			_cmd=$(echo $_cmd | sed -e 's/<info>/$_info_val/')
			eval _cmd=\"$_cmd\"
			_output=$($_cmd 2>&1) ||
				echo "$_f: Command '$_cmd' failed with" \
					"'$_output'" >&2
		}
	done
}

#
# Dump class/protocol structure
#
_dump() {
	local _class _members _member _cmd

	for _class in $CLASSES; do
		echo "Class: $_class"
		eval echo "Desc: "\$_${_class}_desc
		eval echo "DSCP: "\$_${_class}_dscp
		eval _members=\$_${_class}_members
		echo "Proto: $_members"
		for _member in $_members; do
			eval _cmd=\$${_member}_COMMAND
			echo "Command: $_member -> $_cmd"
		done
		echo
	done
}

#
# Print usage text
#
_usage() {
	printf %s "\
Usage: $0 <command>

commands:
 classes                Print number of registered classes.
 dump                   Print dumped QoS table.
 exec <info> <class>    Execute class member commands.
 get <info> <protocol>  Print info for protocol.
 help                   Print this usage string.

"
}

#
# Parse command-line input
#
_input_parser() {
	local _f _command _info _proto
	f=_input_parser

	while [ $# -gt 0 ]; do
		_command=$1
		shift

		case $_command in
			get)
				[ $# -eq 2 ] ||
					_error "$_f: Exactly two $ARGS_REQ" \
						"by command '$_command':" \
						'(info) (protocol)'
				_info=$1
				_proto=$2
				shift 2
				_get_protocol_info $_info $_proto
				;
			exec)
				[ $# -eq 2 ] ||
					_error "$_f: Exactly two $ARGS_REQ" \
						"by command '$_command':" \
						'(info) (class)'
				_info=$1
				_class=$2
				shift 2
				_exec_member_command $_info $_class
				;
			classes)
				_count_classes
				;
			dump)
				_dump
				;
			help)
				_usage
				exit 0
				;
			*)
				_usage
				_error "$_f: Unknown command '$_command'"
				;
		esac

		# Add seperator if more output expected
		[ $# -lt 1 ] || printf ' '
	done
}

#
# Configuration interface
#
class() {
	local _command

	_command=$1
	shift

	case $_command in
		create)
			_qos_add "$@"
			;
		info)
			_qos_add_info "$@"
			;
		member)
			_qos_prot "$@"
			;
		*)
			_error "class: Invalid command '$_command'"
			;
	esac
}

# Include configuration
CONF_NAME=qos.conf
CONF_PATH=/etc/qos/$CONF_NAME
[ -f $CONF_PATH ] || CONF_PATH=./$CONF_NAME
[ -f $CONF_PATH ] || CONF_PATH=${0%/*}/$CONF_NAME
[ -f $CONF_PATH ] || _error "Failed to locate '$CONF_NAME'"
. $CONF_PATH || _error "Failed to source '$CONF_PATH'"

# Parse input command, if any
[ $# -lt 1 ] || _input_parser "$@"