#!/usr/bin/env python2

# (C) 2005 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@ubuntu.com>
# License: GNU General Public License, version 2 or any later version
#
# Modified by: Thomas Hood, Daniel T Chen, Lukas Jirkovsky
#
# Get and set configuration parameters in ~/.asoundrc.asoundconf.

import sys, re, os.path


class SndCardInfo(object):
	def __init__(self):
		self.card_num = -1
		self.id_ = ''
		self.name = ''


class SndPcmInfo(object):
	def __init__(self):
		self.card_num = -1
		self.dev_num = -1
		self.id_ = ''
		self.name = ''


def parse_cards():
	'''Get card info from /proc/asound/cards'''

	cardspath = '/proc/asound/cards'
	if not os.path.exists(cardspath):
		raise IOError(cardspath + ' does not exist')
	procfile = open(cardspath, 'rb')

	cardline = re.compile('^\s*(\d+)\s*\[(\w+)\s*\].*-\s(.*)$') # capture card number, id and name
	card_lines = []

	lines = procfile.readlines()
	for l in lines:
		if cardline.match(l):
			groups = cardline.match(l).groups()
			c = SndCardInfo()
			c.card_num = int(groups[0])
			c.id_  = groups[1].strip()
			c.name = groups[2].strip()
			card_lines.append(c)

	return card_lines


def parse_devices():
	'''Get device numbers and names from /proc/asound/pcm'''

	devspath = '/proc/asound/pcm'
	if not os.path.exists(devspath):
		raise IOError(devspath + ' does not exist')
	procfile = open(devspath, 'rb')

	devnum = re.compile('(\d+)-(\d+)')
	dev_lines = []

	lines = procfile.readlines()
	for l in lines:
		fields = l.split(':')
		if len(fields) >= 3:
			if devnum.match(fields[0]):
				groups = devnum.match(fields[0]).groups()
				d = SndPcmInfo()
				d.card_num = int(groups[0])
				d.dev_num  = int(groups[1])
				d.id_  = fields[1].strip()
				d.name = fields[2].strip()
				dev_lines.append(d)

	return dev_lines

our_conf_file = os.path.expanduser('~/.asoundrc.asoundconf')
asoundrc_file = os.path.expanduser('~/.asoundrc')
predefs_file = '/usr/share/alsa/alsa.conf'

setting_re_template = '!?\s*%s\s*(?:=|\s)\s*([^;,]+)[;,]?$'

our_conf_header = '''# ALSA library configuration file managed by asoundconf(1).
#
# MANUAL CHANGES TO THIS FILE WILL BE OVERWRITTEN!
#
# Manual changes to the ALSA library configuration should be implemented
# by editing the ~/.asoundrc file, not by editing this file.
'''

asoundrc_header = '''# ALSA library configuration file
'''

inclusion_comment = '''# Include settings that are under the control of asoundconf(1).
# (To disable these settings, comment out this line.)'''

usage = '''Usage:
asoundconf is-active
asoundconf get|delete PARAMETER
asoundconf set PARAMETER VALUE
asoundconf list
asoundconf list-all

Convenience macro functions:
asoundconf set-default-card PARAMETER
asoundconf reset-default-card
asoundconf set-pulseaudio
asoundconf unset-pulseaudio
asoundconf set-oss PARAMETER
asoundconf unset-oss
'''


needs_default_card = '''You have omitted a necessary parameter.  Please see the output from `asoundconf list`, and use one of those sound card(s) as the parameter.
'''


needs_oss_dev = '''You have omitted a necessary parameter.  Please specify an OSS device (e.g., /dev/dsp).
'''


superuser_warn = '''Please note that you are attempting to run asoundconf as a privileged superuser, which may have unintended consequences.
'''


def get_default_predefs():
    try:
	if not os.path.exists(predefs_file):
	    return
	predefs_file_entire = open(predefs_file).readlines()
	r = re.compile('^defaults')
	## Between these hashes, add additional unique regexps that
	## must exist at the end of the user's custom asoundrc.
	s = re.compile('^defaults.namehint')
	##
	predefs_list = []
	must_append_predefs_list = []
	for i in predefs_file_entire:
	    if r.match(i) and not s.match(i):
		predefs_list.append(str(i).strip())
	    elif s.match(i):
		must_append_predefs_list.append(str(i).strip())
	for i in must_append_predefs_list:
	    predefs_list.append(str(i).strip())
	return predefs_list
    except IOError:
	pass


def ensure_our_conf_exists():
    '''If it does not exist then generate a default configuration
    file with no settings.

    Return: True on success, False if the file could not be created.
    '''

    if os.path.exists(our_conf_file):
        return True

    try:
        open(our_conf_file, 'w').write(our_conf_header)
        return True
    except IOError:
        print >> sys.stderr, 'Error: could not create', our_conf_file
        return False


def ensure_asound_rc_exists():
    '''Generate a default user configuration file with only the
    inclusion line.

    Return: True on success, False if the file could not be created.
    '''

    if os.path.exists(asoundrc_file):
        return True

    try:
        open(asoundrc_file, 'w').write('%s\n%s\n<%s>\n\n' % (asoundrc_header, inclusion_comment, our_conf_file))
        return True
    except IOError:
        print >> sys.stderr, 'Error: could not create', asoundrc_file
        return False


def sds_transition():
    '''Replace the magic comments added to the user configuration file
    by the obsolete set-default-soundcard program with the standard
    inclusion statement for our configuration file.
    '''

    if not os.path.exists(asoundrc_file):
        return

    lines = open(asoundrc_file).readlines()

    start_marker_re = re.compile('### BEGIN set-default-soundcard')
    end_marker_re = re.compile('### END set-default-soundcard')

    userconf_lines = []
    our_conf_lines = []

    # read up to start comment
    lineno = 0
    found = 0
    for l in lines:
        lineno = lineno+1
        if start_marker_re.match(l):
            found = 1
            break
        userconf_lines.append(l)

    if found:
        # replace magic comment section with include
        userconf_lines.append('%s\n<%s>\n\n' % (inclusion_comment, our_conf_file))
    else:
        # no magic comment
        return

    # read magic comment section until end marker and add it to asoundconf
    found = 0
    for l in lines[lineno:]:
        lineno = lineno+1
        if end_marker_re.match(l):
            found = 1
            break
        if not l.startswith('#'):
            our_conf_lines.append(l)

    if not found:
        # no complete magic comment
        return

    # add the rest to user conf
    userconf_lines = userconf_lines + lines[lineno:]

    # write our configuration file
    if not ensure_our_conf_exists():
        return
    try:
        open(our_conf_file, 'a').writelines(our_conf_lines)
    except IOError:
        return # panic out

    # write user configuration file
    try:
        open(asoundrc_file, 'w').writelines(userconf_lines)
    except IOError:
        pass


def is_active():
    '''Check that the user configuration file is either absent, or,
    if present, that it includifies the asoundconf configuration file;
    in those cases asoundconf can be used to change the user's ALSA
    library configuration.

    Also transition from the legacy set-default-soundcard program.

    Return True if the above condition is met, False if not.
    '''

    if not os.path.exists(asoundrc_file):
        return True

    sds_transition()

    # check whether or not the file has the inclusion line
    include_re = re.compile('\s*<\s*%s\s*>' % our_conf_file)
    for l in open(asoundrc_file):
        if include_re.match(l):
            return True

    return False

def get(prmtr):
    '''Print the value of the given parameter on stdout

    Also transition from the legacy set-default-soundcard program.

    Return True on success, and False if the value is not set.
    '''

    sds_transition()

    if not os.path.exists(our_conf_file):
        return False

    setting_re = re.compile(setting_re_template % prmtr)

    try:
        for line in open(our_conf_file):
            m = setting_re.match(line)
            if m:
                print m.group(1).strip()
                return True
        return False
    except IOError:
        return False


def list():
	'''Print list of card IDs'''

	card_lines = []
	try:
		card_lines = parse_cards()
	except IOError:
		return False

	print "Names of available sound cards:"
	for cardname in card_lines:
		print cardname.id_
	return True


def list_all():
	'''Print list of devices for all cards (including number, id and name)'''

	card_lines = []
	dev_lines = []
	try:
		card_lines = parse_cards()
		dev_lines = parse_devices()
	except IOError:
		return False

	print "Available devices for all sound cards:"
	for dev in dev_lines:
		card = next(c for c in card_lines if c.card_num == dev.card_num) # find card by number
		print "hw:%d,%d: %s : %s : %s : %s" % (
			dev.card_num, dev.dev_num,
			card.id_, card.name,
			dev.id_, dev.name)

	return True


def delete(prmtr):
    '''Delete the given parameter.

    Also transition from the legacy set-default-soundcard program.

    Return True on success, and False on an error.
    '''

    sds_transition()

    if not os.path.exists(our_conf_file):
        return False

    setting_re = re.compile(setting_re_template % prmtr)
    lines = []
    try:
        lines = open(our_conf_file).readlines()
    except IOError:
        return False

    found = 0
    for i in xrange(len(lines)):
        if setting_re.match(lines[i]):
            del lines[i]
            found = 1
            break

    if found:
        # write back file
        try:
            f = open(our_conf_file, 'w')
        except IOError:
            return False
        f.writelines(lines)

    return True


def set(prmtr, value):
    '''Set the given parameter to the given value

    Also transition from the legacy set-default-soundcard program.

    Return True on success, and False on an error.
    '''

    sds_transition()

    setting_re = re.compile(setting_re_template % prmtr)
    lines = []

    ensure_asound_rc_exists()
    # N.B. We continue even if asoundrc could not be created
    # and we do NOT ensure that our configuration is "active"

    if not ensure_our_conf_exists():
        return False

    try:
        lines = open(our_conf_file).readlines()
    except IOError:
        return False

    newsetting = '%s %s\n' % (prmtr, value)

    # if setting is already present, change it
    found = 0
    for i in xrange(len(lines)):
        if setting_re.match(lines[i]):
            lines[i] = newsetting
            found = 1
            break

    if not found:
        lines.append(newsetting)

    # write back file
    try:
        f = open(our_conf_file, 'w')
    except IOError:
        return False
    f.writelines(lines)
    return True

def set_default_card(card):
	clist = get_default_predefs()
	sep = re.compile(r'[ \t]')
	com = re.compile('[ \t]*#.*')
	r = re.compile('^defaults.pcm.card')
	s = re.compile('^defaults.ctl.card')
	## !defaults.pcm.card and defaults.ctl.card should lead
	## the user's custom asoundrc.
	if set('!defaults.pcm.card', card) and \
	   set('defaults.ctl.card', card):
	    for i in clist:
		# remove any comments
		i = com.sub("",i)
		(j, k) = sep.split(i)
		if not r.match(j) and not s.match(j):
		    if not set(j, k):
			return False
	    return True
	else:
	    return False

def reset_default_card():
	clist = get_default_predefs()
	sep = re.compile(r'[ \t]')
	com = re.compile('[ \t]*#.*')
	for i in clist:
	    i = com.sub("",i)
	    (j, k) = sep.split(i)
	    if not delete(j):
		return False
	return True

def delete_pcm_default():
	return delete('pcm.!default')

def delete_ctl_default():
	return delete('ctl.!default')

def set_pulseaudio():
    return set('pcm.!default', '{ type pulse }') and \
	set('ctl.!default', '{ type pulse }')

def unset_pulseaudio():
    return delete_pcm_default() and \
	delete_ctl_default()

def set_oss(device):
    endbrace = ' }'
    return set('pcm.!default { type oss  device', device + endbrace)

def unset_oss():
    return delete_pcm_default()

def exit_code(result):
    '''Exit program with code 0 if result is True, otherwise exit with code
    1.
    '''

    if result:
        sys.exit(0)
    else:
        sys.exit(1)


##
## main
##

if os.geteuid() == 0:
    print superuser_warn

if len(sys.argv) < 2 or sys.argv[1] == '--help' or sys.argv[1] == '-h':
    print usage
    sys.exit(0)

if sys.argv[1] == 'is-active':
    exit_code(is_active())

if sys.argv[1] == 'get':
    if len(sys.argv) != 3:
        print usage
        sys.exit(1)
    exit_code(get(sys.argv[2]))

if sys.argv[1] == 'delete':
    if len(sys.argv) != 3:
        print usage
        sys.exit(1)
    exit_code(delete(sys.argv[2]))

if sys.argv[1] == 'set':
    if len(sys.argv) != 4:
        print usage
        sys.exit(1)
    exit_code(set(sys.argv[2], sys.argv[3]))

if sys.argv[1] == 'list':
	exit_code(list())

if sys.argv[1] == 'list-all':
	exit_code(list_all())

if sys.argv[1] == 'set-default-card':
    if len(sys.argv) != 3:
	print needs_default_card
	sys.exit(1)
    exit_code(set_default_card(sys.argv[2]))

if sys.argv[1] == 'reset-default-card':
	exit_code(reset_default_card())

if sys.argv[1] == 'set-pulseaudio':
	exit_code(set_pulseaudio())

if sys.argv[1] == 'unset-pulseaudio':
	exit_code(unset_pulseaudio())

if sys.argv[1] == 'set-oss':
    if len(sys.argv) != 3:
	print needs_oss_dev
	sys.exit(1)
    exit_code(set_oss(sys.argv[2]))

if sys.argv[1] == 'unset-oss':
	exit_code(unset_oss())

print usage
sys.exit(1)

