#!/usr/bin/python3
#
# Simple mixer controller with meters, using hpi python bindings



import sys
import audioscience.hpi as hpi
from sys import exit
try:
    from tkinter import *
except ImportError:
    from tkinter import *

from time import *
from audioscience.scrolled import Scrolledframe
from collections import defaultdict
from audioscience.hpimeters import HpiMeter

def parseArgs():
    """Parse the commandline arguments"""
    from optparse import OptionParser, OptionGroup

    parser = OptionParser()
    parser.add_option('-a','--adapter',type='int',dest='adapter',
                      help='Adapter index. Default=%default', default=0)

    parser.add_option('-I','--interface-ip-address',type='string',dest='ipaddr',
                          help='Interface IP address.', default=None)

    return parser.parse_args()

opts,args = parseArgs()
hpi.setup(opts.adapter, opts.ipaddr)

def hpi_nodename(id):
    names = {100:'none', 101:'Stream', 102:'Line', 103:'AES3',
    104:'Tuner', 105:'Rf', 106:'Clock', 107:'Bitstream', 108:'Microphone',
    109:'Cobranet', 110:'Analog', 111:'Adapter', 112:'RTP', 113:'Internal',
    114:'AVB', 115:'BluLink',
    200:'none',201:'Stream',202:'Line',203:'AES3',204:'Rf',
    205:'Speaker',206:'Cobranet', 207:'Analog', 208:'RTP',
    209:'AVB', 210:'Internal', 211:'BluLink'}

    return names.get(id, 'HpiNode%d' % id)

def AdapterInfoString(adapter):
  e,nos,nis,ver,serial,atype = hpi.Adapter_GetInfo(adapter)
  return "%d: ASI%x IS=%d OS=%d" % (adapter,atype, nis, nos)

class HpiVolume (Frame):

  def __init__(self, master, hmixer, node1, index1, node2=0, index2=0):
    self.hx = hmixer

    if (node2) :
        e,self.hv=hpi.Mixer_GetControl(self.hx,node1,index1,node2,index2,hpi.CONTROL_VOLUME)
    elif (node1 < 200):
        e,self.hv=hpi.Mixer_GetControl(self.hx,node1,index1,0,0,hpi.CONTROL_VOLUME)
    else:
        e,self.hv=hpi.Mixer_GetControl(self.hx,0,0,node1,index1,hpi.CONTROL_VOLUME)

    if e:
        raise LookupError('Volume on %s %d %s %d not found' % (hpi.SourceDict[node1],index1,hpi.DestDict[node2],index2))

    Frame.__init__(self, master, borderwidth=2, relief='groove')

    e,self.chans = hpi.Volume_QueryChannels(self.hv)
    if e or self.chans > 2:
        self.chans = 2

    r = hpi.Volume_QueryRange(self.hv)
    if r[0]:
        self.min = -100
        self.max = 0
    else:
        self.min = r[1] / hpi.UNITS_PER_dB
        self.max = r[2] / hpi.UNITS_PER_dB

    e,r = hpi.Volume_GetGain(self.hv)
    self.current = r

    self.left = IntVar()
    self.left.set(r[0] / hpi.UNITS_PER_dB)
    self.left.trace('w', self.onLeft)

    self.right = IntVar()
    self.right.set(r[1] / hpi.UNITS_PER_dB)
    self.right.trace('w', self.onRight)

    self.createWidgets()

  def createWidgets(self):
    self.ml = Scale(self)
    self.ml.pack(side='left')
    self.ml['from'] = self.max
    self.ml['to'] = self.min
    self.ml["variable"]=self.left

    if self.chans > 1:
        self.mr = Scale(self)
        self.mr['from'] = self.max
        self.mr['to'] = self.min
        self.mr["variable"]=self.right
        self.mr.pack(side='left')
    # registering callback
    self.listenID = self.after(1000, self.updateVol)

  def updateVol(self):
    self.listenID = self.after(250, self.updateVol)
    e,r = hpi.Volume_GetGain(self.hv)
    if r != self.current:
        self.current = r
        self.left.set(r[0] / hpi.UNITS_PER_dB)
        self.right.set(r[1] / hpi.UNITS_PER_dB)

  def onLeft(self,name,index,mode):
    v = self.left.get() * hpi.UNITS_PER_dB
    if v != self.current[0]:
        self.current = (int(v), self.current[1])
        hpi.Volume_SetGain(self.hv, self.current)

  def onRight(self,name,index,mode):
    v = self.right.get() * hpi.UNITS_PER_dB
    self.current = (self.current[0], int(v))
    hpi.Volume_SetGain(self.hv, self.current)


class HpiMux(Frame):
  '''GUI for an HPI multiplexer control'''
  def __init__(self, master, hmixer, node, index):
    if (node < 200):
        e,self.hm = hpi.Mixer_GetControl(hmixer, node, index, 0, 0, hpi.CONTROL_MULTIPLEXER)
    else:
        e,self.hm = hpi.Mixer_GetControl(hmixer, 0, 0, node, index, hpi.CONTROL_MULTIPLEXER)

    if e:
        raise LookupError('Mux on %d %d not found' % (node, index))

    Frame.__init__(self, master, borderwidth=2, relief='groove')

    self.byname = {}
    self.byid = {}
    for i in range(500):
        e,node,index = hpi.Multiplexer_QuerySource(self.hm, i)
        if e:
            break;
        item_name = '%s-%d' % (hpi_nodename(node), index)
        self.byname[item_name] = (node, index)
        self.byid[(node, index)] = item_name

    self.current = None
    self.var = StringVar(self)
    self.var.trace('w', self.changed)
    self.update()

    self.m = OptionMenu(self, self.var, *iter(self.byname.keys()))
    self.m.pack(side='left')

  def changed(self, *args):
    '''When gui is changed, update the hardware'''
    item = self.var.get()
    node, index = self.byname[item]
    e = hpi.Multiplexer_SetSource(self.hm, node, index)
    if not e:
      self.current = (node, index)
    else:
      self.update()

  def update(self):
    '''Periodically read the mux and update the gui'''
    e,cnode,cindex = hpi.Multiplexer_GetSource(self.hm)
    if e:
      return
    setting = (cnode, cindex)
    if setting != self.current:
        self.current = setting
        self.var.set(self.byid[setting])

    self.listenID = self.after(1000, self.update)


class HpiLevel(Frame):
    '''GUI for an HPI level control'''
    def __init__(self, master, hmixer, node, index):
        if (node < 200):
            e,self.h = hpi.Mixer_GetControl(hmixer, node, index, 0, 0, hpi.CONTROL_LEVEL)
        else:
            e,self.h = hpi.Mixer_GetControl(hmixer, 0, 0, node, index, hpi.CONTROL_LEVEL)

        if e:
            raise LookupError('Level on %d %d not found' % (node, index))

        Frame.__init__(self, master, borderwidth=2, relief='groove')
        self.var = IntVar(self)
        self.var.set(-999) # must be different from current for update
        self.update()
        self.var.trace('w', self.changed)

        e,lmin,lmax,step = hpi.Level_QueryRange(self.h)
        if not e:
            lmin = lmin / hpi.UNITS_PER_dB
            lmax = lmax / hpi.UNITS_PER_dB
        else:
            lmin = -10
            lmax = 26

        Label(self, text='Level ').pack(side='left')
        Spinbox(self, from_=lmin, to=lmax, width=3, textvariable=self.var).pack(side='left')
        Label(self, text='dBu').pack(side='left')

    def changed(self, *args):
        '''When gui is changed, update the hardware'''
        level = self.var.get() * int(hpi.UNITS_PER_dB)
        e = hpi.Level_SetGain(self.h, [level, level])
        if e:
            self.update()

    def update(self):
        e, level = hpi.Level_GetGain(self.h);
        if e:
            return

        level = int(level[0] / hpi.UNITS_PER_dB)
        if level != self.var.get():
            self.var.set(level)

        self.listenID = self.after(1000, self.update)


class HpiProfile(Frame):
    def __init__(self, master, adapter):
        Frame.__init__(self, master)
        self.adapter = adapter
        e = hpi.Adapter_Open(adapter)
        e, self.hp, self.np = hpi.Profile_OpenAll(adapter, 0)
        if e: return
        e = hpi.Profile_StartAll(self.hp)
        if e: return
        Label(self, text="DSP Utilization").pack(side=TOP)
        self.var = StringVar(self)
        Label(self, textvariable=self.var).pack(side=TOP, anchor=N)
        self.update()

    def update(self):
        e, util = hpi.Profile_GetUtilization(self.hp)
        self.var.set(str(util / 100.0) + '%\n')
        self.listenID = self.after(1000, self.update)


def control_matrix(master, adapter):

    e, hmixer = hpi.Mixer_Open(adapter)
    sources, dests = count_nodes(hmixer, hpi.CONTROL_METER)
    sources.sort()
    dests.sort(reverse=True)

    print(sources, dests)

    # Top corner labels
    Label(master, text='> Destinations >').grid(row=0, column=2)
    Label(master, text='v Sources v').grid(row=2, column=0)
    Label(master, text='Master').grid(row=2, column=2)
    HpiProfile(master, adapter).grid(row=1, column=0, sticky=N)

    row = 0
    col = 3
    # Top row labels
    for d in dests:
        for di in range(d[1]):
            f = Frame(master)

            Label(f, text='%s#%d' % (hpi_nodename(d[0]), di)).pack(side=TOP, anchor=W)
            try:
                HpiMux(f, hmixer, d[0], di).pack(side=TOP, anchor=W)
            except LookupError:
                pass

            try:
                HpiLevel(f, hmixer, d[0], di).pack(side=TOP, anchor=W)
            except LookupError:
                pass

            f.grid(row=row, column=col)

            HpiMeter(master, hmixer, d[0], di).grid(row=row+1, column=col, sticky=W)

            try:
                HpiVolume(master, hmixer, d[0], di).grid(row=row+2, column=col)
            except LookupError:
                pass
            col += 1

    row = 2
    for s in sources:
        for si in range(s[1]):
            row += 1
            col = 0

            f = Frame(master)

            Label(f, text='%s#%d' % (hpi_nodename(s[0]), si)).pack(side=TOP, anchor=W)
            try:  #
                HpiMux(f, hmixer, s[0], si).pack(side=TOP, anchor=W)
            except LookupError:
                pass

            try:  #
                HpiLevel(f, hmixer, s[0], si).pack(side=TOP, anchor=W)
            except LookupError:
                pass

            f.grid(row=row, column=col, sticky=W)
            col += 1

            try:
                HpiMeter(master, hmixer, s[0], si).grid(row=row, column=col)
            except LookupError:
                pass
            col += 1

            try:
                HpiVolume(master, hmixer, s[0], si).grid(row=row, column=col)
            except LookupError:
                pass
            col += 1

            for d in dests:
                for di in range(d[1]):
                    try:
                        HpiVolume(master, hmixer, s[0], si, d[0], di).grid(row=row, column=col)
                        col += 1
                    except LookupError:
                        col += 1
                        continue


def count_nodes(hmixer, req_type=None):
    sd = defaultdict(int)
    dd = defaultdict(int)
    for ci in range(10000):
        e,srctype,srcindex,dsttype,dstindex,ctrltype,ch = hpi.Mixer_GetControlByIndex(hmixer, ci)
        if e:
            break
        if req_type and req_type != ctrltype:
            continue

        if srctype != hpi.SOURCENODE_NONE:
            if sd[srctype] < srcindex + 1:
                sd[srctype] = srcindex + 1
        if dsttype != hpi.DESTNODE_NONE:
            if dd[dsttype] < dstindex + 1:
                dd[dsttype] = dstindex + 1

    sd = list(sd.items())
    sd.sort()
    dd = list(dd.items())
    dd.sort()
    return sd, dd


if __name__ == "__main__":
    adapter = opts.adapter
    hpi.setup(opts.adapter)

    e = hpi.Adapter_Open(adapter)
    if e:
        sys.exit('HPI error %d' % e)

    e,nos,nis,ver,serial,atype = hpi.Adapter_GetInfo(adapter)
    e,amode = hpi.Adapter_GetMode(adapter)
    if not e:
        mode = hpi.AdapterModeDict[amode]
        mode = '\nMode:' + mode.lstrip('ADAPTER_MODE').lower()
    else:
        mode = ''

    app = Tk()
    app.title("HPI Mixer")
    scrollframe = Scrolledframe(app,stretch=True)

    Label(scrollframe(), text='ASI%04X\n#%d%s' % (atype, serial, mode)).grid()

    control_matrix(scrollframe(), adapter)

    scrollx = Scrollbar(app, orient="horizontal", command=scrollframe.xview)
    scrollx.grid(row=1, column=0, sticky="nwse")

    scrolly = Scrollbar(app, orient="vertical", command=scrollframe.yview)
    scrolly.grid(row=0, column=1, sticky="nwse")

    scrollframe["xscrollcommand"] = scrollx.set
    scrollframe["yscrollcommand"] = scrolly.set

    scrollframe.grid(row=0,column=0,sticky="nwse")
    #scrollframe.pack()

    app.columnconfigure(0,weight=1)
    app.rowconfigure(0,weight=1)
    app.geometry("1280x1024")
    app.mainloop()
############################################################################
