#!/usr/bin/python3

# (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
import asoundconf_common

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('Error: could not create', our_conf_file, file=sys.stderr)
        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('Error: could not create', asoundrc_file, file=sys.stderr)
        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 = asoundconf_common.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 = asoundconf_common.parse_cards()
                dev_lines = asoundconf_common.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:{},{}: {} : {} : {} : {}'.format(
                        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 range(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 range(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)

