# Written by David Henningsson <david.henningsson@canonical.com>
# Copyright Canonical Ltd 2011. 
# Licensed under GPLv3.

import re
import os
import subprocess
from apport.hookutils import *

class SoundCard:
    def init_alsa(self, index, name, longname):
        self.alsa_index = index
        self.alsa_shortname = name
        self.alsa_longname = longname
        self.jacks = parse_jacks(index)

    def init_pa(self, pactl_card):
        self.pa_card = pactl_card
        self.pa_properties = pactl_card['Properties']
        self.pa_longname = self.pa_properties['device.description']
        if not 'jacks' in self.__dict__:
            self.jacks = []

    def pretty_name(self):
        try:
            return '%s - %s' % (self.pa_longname, self.pa_properties['alsa.card_name'])
        except:
            if not 'pa_longname' in self.__dict__:
                return self.alsa_longname
            return self.pa_longname

    def has_sink_or_source(self, ssname):
        # TODO: Check that this works for bluetooth as well,
        # and/or implement something more sophisticated
        try:
            a, b, ssname = ssname.partition(".")
            return self.pa_card['Name'].find(ssname) != -1
        except:
            return False

    def get_controlnames(self, isOutput):
        if isOutput:
            return set(["PCM", "Hardware Master", "Master", "Master Front", "Front"])
        else:
            return set(["Capture"])

    def get_codecinfo(self):
        try:
            s = ""
            for codecpath in glob.glob('/proc/asound/card%s/codec*' % self.alsa_index):
                if os.path.isfile(codecpath):    
                    s = s + read_file(codecpath)
            return s
        except:
            return ""


class Jack:
    def isOutput(self):
        if self.jack_type.find("Out") != -1: 
            return True
        if self.jack_type == "Speaker":
            return True
        return False

    def needed_channelcount(self):
        if self.jack_type == "Line Out": 
            try:
                colormap = {'Orange': 3, 'Black': 5, 'Grey': 7}
                return colormap[self.color]
            except:
                pass
        return 1

            
    def get_controlnames(self):
        if self.isOutput():
            # Do not localize
            c = set(["PCM", "Hardware Master", "Master", "Master Front", "Front"])
            if self.jack_type == "Speaker":
                c |= set(["Speaker", "Desktop Speaker"])
            nc = self.needed_channelcount()
            if nc >= 3:
                c |= set(["Center", "LFE", "Surround"])
            if nc >= 7:
                c.add("Side")

        else:
            c = set(["Capture", self.jack_type])
            if self.jack_type.find("Mic"):
                c.add(self.jack_type+" Boost")
        return c

    def pretty_name(self):
        # Hmm, this is not going to be easy to localize.
        jack_type = self.jack_type
        if jack_type == 'HP Out':
            jack_type = 'Headphone Out'
        color = self.color
        if (color != 'Unknown'):
            jack_type = '%s %s' % (color, jack_type)
    
        if self.jack_conns == 'Fixed':
            return '%s, Internal' % jack_type
        if self.connectivity == 'Sep':
            return '%s, %s, Docking station' % (jack_type, self.location)
        return '%s, %s' % (jack_type, self.location)

def parse_jacks(alsa_card_index):
    ''' Returns list of jacks on a specific card. '''
    result = []
    dirname = '/proc/asound/card%d' % int(alsa_card_index)
    for fname in os.listdir(dirname):
        if not fname.startswith('codec#'):
            continue
        codecname = ''
        for line in open(os.path.join(dirname,fname)):

            m = re.search('Codec: (.*)', line)
            if m:
                codecname = m.groups(1)[0]

            m = re.search(r'Pin Default 0x(.*?): \[(.*?)\] (.*?) at (.*?) (.*)', line)
            if m:
                item = Jack()
                item.codecname = codecname
                item.hex_value = m.groups(1)[0] 
                item.jack_conns = m.groups(1)[1] 
                item.jack_type = m.groups(1)[2] 
                item.connectivity = m.groups(1)[3]
                item.location = m.groups(1)[4]
                if not item.jack_conns == 'N/A':
                    result.append(item)
                continue

            m = re.search('Conn = (.*?), Color = (.*)', line)
            if m:
                item.connection = m.groups(1)[0]
                item.color = m.groups(1)[1]
    return result

def parse_alsa_cards():
    ''' Returns list of SoundCard as seen by alsa '''
    # Parse /proc/asound/cards
    alsacards = []
    try:
        for card in open('/proc/asound/cards'):
            m = re.search(r' (\d+) \[(\w+)\s*\]: (.+)', card)
            if not m is None:
                s = SoundCard()
                s.init_alsa(*tuple(m.groups(1)))
                alsacards.append(s)
    except:
        pass
    return alsacards

def parse_pactl_list(pactlvalues):
    ''' Returns a structured version of pactl list '''
    # Not perfect, but good enough for its purpose
    result = dict()
    for line in pactlvalues.splitlines():
        m = re.match(r'^(\w+) #(\d+)', line)
        if m:
            category = m.groups(1)[0]
            index = int(m.groups(1)[1])
            if not category in result:
                result[category] = dict()
            curitem = dict()
            result[category][index] = curitem
            continue
        m = re.match(r'^\t(\w+.*?): (.*)', line)
        if m:
            curname = m.groups(1)[0]
            curvalue = m.groups(1)[1]
            curitem[curname] = curvalue
            continue
        m = re.match(r'^\t(\w+.*?):', line)
        if m:
            curname = m.groups(1)[0]
            curitem[curname] = dict()
            continue
        m = re.match(r'^\t\t(\w+.*?) = "(.*)"', line)
        if m:
            curpropname = m.groups(1)[0]
            curpropvalue = m.groups(1)[1]
            curitem[curname][curpropname] = curpropvalue
    return result

def add_pa_cards(cards, pactlvalues):
    if not 'Card' in pactlvalues:
        return cards

    for pa_card in pactlvalues['Card'].values():
        s = None
        try:
            index = pa_card['Properties']['alsa.card']
            for c in cards:
                if index == c.alsa_index: 
                    s = c
        except:
            pass

        if not s:
            s = SoundCard()
            cards.append(s)
        s.init_pa(pa_card)

    return cards

def get_pa_default_profile(isOutput):
    ''' Returns sinkname,sinkprofile,channelcount '''
    pactlstat = command_output(['pactl', 'info'])
    ss = "Default %s: (.*)" % ("Sink" if isOutput else "Source")

    for line in pactlstat.splitlines():
        m = re.match(ss, line)
        if m:
            sink = m.groups(1)[0]
            sinkname, dummy, sinkprofile = sink.rpartition(".")
            if dummy == '': 
                sinkname = sinkprofile
                sinkprofile = "analog-stereo" # easy processing later
            continue
    
    # TODO: handle missing sinkname/sinkprofile match?
    
    # calculate channel count
    a = sinkprofile.split('-')
    cc = 2
    if 'mono' in a:
        cc = 1
    elif 'surround' in a:
        try:
            # e g surround-51 => 51 => 5+1 => 6
            cc = int(a[len(a)-1])
            cc = cc/10 + cc % 10
        except:
            cc = 2

    return sinkname, sinkprofile, cc


def get_hw_title(card, isOutput, jack, text):
    a = []
    if jack is not None:
        # Get motherboard/system name
        try:
            f = open("/sys/class/dmi/id/product_name")
            a.append(''.join(f.readlines()).strip())
        except:
            pass
        a.append(jack.codecname)
        a.append(jack.pretty_name())

    else:
        try:
            a.append(card.alsa_longname)
        except:
            a.append(card.pretty_name())
        a.append("playback" if isOutput else "recording")

    return "[%s] %s" % (', '.join(a), text)


def get_users_in_group(groupname):
    match = "%s:(.*?):(.*?):(.*)" % groupname
    for line in open("/etc/group"):
        m = re.match(match, line)
        if not m:
            continue
        s = m.groups(1)[2]
        return s.split(',')
    # group does not exist
    return []

def pa_start_logging():
    command_output(['pacmd','set-log-level','4'])
    command_output(['pacmd','set-log-time','1'])

def pa_finish_logging(report):
    command_output(['pacmd','set-log-level','1'])
    command_output(['pacmd','set-log-time','0'])
    s = recent_syslog(re.compile("pulseaudio"))
    report['Symptom_PulseAudioLog'] = s
    return s

def run_subprocess(report, reportkey, args):
    ''' Helper function to run a subprocess.  
        Returns stdout and writes stderr, if any, to the report. '''

    # avoid localized strings
    env = os.environ
    env['LC_MESSAGES'] = 'C'
    sub_mon = subprocess.Popen(args,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env,
        universal_newlines=True)
    sub_out, sub_err = sub_mon.communicate()
    if sub_err is not None and (len(str(sub_err).strip()) > 0):
        report[reportkey+'Stderr'] = ' '.join(sub_err)
    return sub_out


def parse_cards():
    cards = parse_alsa_cards()
    pactlvalues = run_subprocess(dict(), 'PactlList',  ['pactl', 'list'])
    pactl_parsed = parse_pactl_list(pactlvalues)    
    cards = add_pa_cards(cards, pactl_parsed)
    return cards

'''test Main'''
if __name__ == '__main__':
    cards = parse_cards()
    for c in cards:
        print(c.pretty_name())
        for j in c.jacks:
            print(j.pretty_name())
