#!/usr/bin/python

import cwiid, sys
import pypm
from PyQt4 import QtGui, QtCore
import math

pypm.Initialize()


def printDevices(InOrOut):
    for loop in range(pypm.CountDevices()):
        interf,name,inp,outp,opened = pypm.GetDeviceInfo(loop)
        if ((InOrOut == 0) & (inp == 1) |
            (InOrOut == 1) & (outp ==1)):
            print loop, name," ",
            if (inp == 1): print "(input) ",
            else: print "(output) ",
            if (opened == 1): print "(opened)"
            else: print "(unopened)"
    print


printDevices(1)
dev = int(raw_input("Type output number: "))
latency = 0
midi = pypm.Output(dev, latency)
print "Midi Output opened with ",latency," latency"


print "Press 1&2 to connect 'mote.."
wii = cwiid.Wiimote()
#wii = None


app = QtGui.QApplication(sys.argv)

class Window(QtGui.QMainWindow):
  def __init__(self, wii, midi):
    self.regions = [
      {'area': [0, 0, 1024, 260], 'voice': 19, 'center': 60, 'range': 16},
      {'area': [0, 261, 1024, 520], 'voice': 60, 'center': 60, 'range': 16},
      {'area': [0, 521, 511, 768], 'voice': 47, 'center': 60, 'range': 16},
      {'area': [512, 521, 1024, 768], 'voice': 53, 'center': 60, 'range': 16}
    ]
    self.notes = []
    self.channels = range(0,8)
    
    QtGui.QMainWindow.__init__(self)
    #self.voices = [90, 91, 92, 93]
    self.midi = midi
    self.wii = wii
    #self.notes = [False] * 4
    if self.wii is not None:
      self.wii.enable(cwiid.FLAG_MESG_IFC)
      self.wii.enable(cwiid.FLAG_NONBLOCK)
      self.wii.rpt_mode = cwiid.RPT_IR
    
    ## add central widget and horizontal layout
    self.cw = QtGui.QWidget()
    self.setCentralWidget(self.cw)
    hl1 = QtGui.QHBoxLayout()
    self.cw.setLayout(hl1)
    
    ## add graphics view
    self.view = QtGui.QGraphicsView()
    hl1.addWidget(self.view)
    
    ## Add grid
    gb1 = QtGui.QWidget()
    hl1.addWidget(gb1)
    gl1 = QtGui.QGridLayout()
    gb1.setLayout(gl1)
    
    gl1.addWidget(QtGui.QLabel('instrument'), 0, 1)
    gl1.addWidget(QtGui.QLabel('center note'), 0, 2)
    gl1.addWidget(QtGui.QLabel('note range'), 0, 3)
    self.instrumentSpins = []
    self.centerNoteSpins = []
    self.noteRangeSpins = []
    for i in range(0, len(self.regions)):
      gl1.addWidget(QtGui.QLabel('%d' % (i+1)), i+1, 0)
      ins = QtGui.QSpinBox()
      gl1.addWidget(ins, i+1, 1)
      note = QtGui.QSpinBox()
      gl1.addWidget(note, i+1, 2)
      bend = QtGui.QSpinBox()
      gl1.addWidget(bend, i+1, 3)
      
      ins.setMinimum(0)
      ins.setMaximum(127)
      ins.setValue(self.regions[i]['voice'])
      note.setMinimum(0)
      note.setMaximum(127)
      note.setValue(self.regions[i]['center'])
      bend.setMinimum(0)
      bend.setMaximum(127)
      bend.setValue(self.regions[i]['range'])
      
      self.regions[i]['voiceSpin'] = ins
      self.regions[i]['centerSpin'] = note
      self.regions[i]['rangeSpin'] = bend
    gl1.setRowStretch(5, 10)
      
    self.scene = QtGui.QGraphicsScene()
    self.view.setScene(self.scene)
    self.box = QtGui.QGraphicsRectItem(0, 0, 1024, 768)
    self.scene.addItem(self.box)
    self.spots = []
    self.labels = []
    for i in range(0, 4):
      self.spots.append(QtGui.QGraphicsEllipseItem(-1, -1, 0.1, 0.1))
      self.labels.append(QtGui.QGraphicsTextItem('%d' % i))
      p = QtGui.QPen(QtGui.QColor(0,0,0))
      p.setWidth(3)
      self.spots[-1].setPen(p)
      self.scene.addItem(self.spots[i])
      self.scene.addItem(self.labels[i])
    for r in self.regions:
      self.scene.addItem(QtGui.QGraphicsRectItem(r['area'][0], r['area'][1], r['area'][2]-r['area'][0], r['area'][3]-r['area'][1]))
    
    #self.noteBend = [0x2000] * 4
    
    self.show()
    
    self.timer = QtCore.QTimer()
    self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.updateSpots)
    self.timer.start(10)
    
    
  def __del__(self):
    for i in range(0, 4):
      if self.notes[i]:
        self.midi.WriteShort(0x8 + i,60,0)
    self.midi.WriteShort(0xB0, 120, 0)
    
    
  def permuteList(self, vals):
    pms = []
    if len(vals) == 1:
      return [vals]
    for v in vals:
      v2 = vals[:]
      v2.remove(v)
      pm2 = self.permuteList(v2)
      for p in pm2:
        pms.append([v] + p)
    return pms  
    
  def updateSpots(self):
    if self.wii is None:
      return
    
    ## get message from wiimote
    m = None
    while True:
      m1 = self.wii.get_mesg()
      if m1 is None:
        break
      m = m1
    
    if m is None:
      return
    
    
    print "============================================"
    
    ## Pull out new point locations
    pts = m[0][1]
    newPts = []
    for i in range(0, len(pts)):
      if pts[i] is not None:
        newPts.append({'size': pts[i]['size'], 'pos': [1023-pts[i]['pos'][0], 767-pts[i]['pos'][1]]})
    
    print "New points:", newPts
    
    ## Make a table of distances between old and new points
    maxPts = max(len(newPts), len(self.notes))
    dists = []
    for i in range(0, maxPts):
      dists.append(([100] * maxPts)[:])
    for i in range(0, maxPts):
      for j in range(0, maxPts):
        if i < len(newPts) and j < len(self.notes):
          dx = self.notes[j]['pos'][0] - newPts[i]['pos'][0]
          dy = self.notes[j]['pos'][1] - newPts[i]['pos'][1]
          dists[i][j] = math.sqrt(dx**2 + dy**2)
          
    print "Distance table:", dists
          
    ## find lowest-score point matching
    inds = self.permuteList(range(0, maxPts))
    minInd = None
    minVal = None
    for i in inds:
      val = 0.0
      for j in range(0, len(i)):
        val += dists[j][i[j]]
      if minVal is None or val < minVal:
        minVal = val
        minInd = i
        
    print "Assignment order:", minInd
        
    ## Assign new point values
    for i in range(0, maxPts):
      print "Updating point %d:" % minInd[i]
      if i >= len(newPts):
        print "  point disappeared, stopping note"
        ## Stop note 
        note = self.notes[minInd[i]]
        self.midi.WriteShort(0x80+note['channel'], note['center'], 0)
        self.channels.append(note['channel'])
        
        ## remove old point
        self.notes.pop(minInd[i])
        #self.spots[i].setRect(-1, -1, 0.1, 0.1)
        self.spots[minInd[i]].pen().setColor(QtGui.QColor(100, 100, 100))
      
      elif minInd[i] >= len(self.notes):
        print "  new point, creating structure"
        ## add new point
        newNote = newPts[i]
        if len(self.notes) > 0:
          newId = self.notes[-1]['id'] + 1
        else:
          newId = 0
        self.notes.append({'pos': newNote['pos'], 'size': newNote['size'], 'channel': self.channels.pop(0), 'center': None, 'region': None, 'id': newId})
      else:
        print "  updating position"
        note = self.notes[minInd[i]]
        newNote = newPts[i]
        
        ## update old point
        note['pos'] = newNote['pos']
        note['size'] = newNote['size']
        self.spots[i].setRect(note['pos'][0], note['pos'][1], note['size']*4, note['size']*4)
        self.spots[i].pen().setColor(QtGui.QColor(0, 0, 0))
        self.labels[i].setPos(note['pos'][0]-5, note['pos'][1])
        self.labels[i].setPlainText('%d' % note['id'])
    
    ## Handle changes to all points
    for n in self.notes:
      print "Updating note:", n
      ## determine new region
      rgn = None
      rgnInd = None
      for i in range(0, len(self.regions)):
        r = self.regions[i]
        if n['pos'][0] > min(r['area'][0], r['area'][2]) and n['pos'][0] < max(r['area'][0], r['area'][2]) and  n['pos'][1] > min(r['area'][1], r['area'][3]) and n['pos'][1] < max(r['area'][1], r['area'][3]):
          rgn = r
          rgnInd = i
          break
      print "  new rgn:", rgnInd
      
      if rgn is not None:
        ## determine/set new volume
        vol = float(n['pos'][1] - rgn['area'][3]) / (rgn['area'][1] - rgn['area'][3]) * 127.
        self.midi.WriteShort(0xB0+n['channel'], 0x07, int(vol))
        print "  volume:", vol
        
        ## determine/set new pitch
        bPos = (float((n['pos'][0]) - rgn['area'][0]) / (rgn['area'][2] - rgn['area'][0]) * 0x4000) - 0x2000
        bendRange = rgn['rangeSpin'].value()
        quant = 0x4000 / (bendRange*2)
        pitch = int((round(bPos / quant) * quant) + 0x2000)
        if n['region'] is not None:
          n['pitch'] = n['pitch'] * 0.8 + pitch * 0.2
        else:
          n['pitch'] = pitch
        self.midi.WriteShort(0xE0+n['channel'], int(n['pitch']) % 128, int(n['pitch']) / 128)
        print "  pitch:", n['pitch']
        
        
      ## (re)start note if region has changed
      if n['region'] != rgnInd:
        print "  region changed.."
        if n['region'] is not None:
          print "    stop note"
          self.midi.WriteShort(0x80+n['channel'], n['center'], 0)
        if rgn is not None:
          print "    start note"
          n['region'] = rgnInd
          
          ## Set pitch bend sensitivity
          self.midi.WriteShort(0xB0+n['channel'], 0x65, 0x0)
          self.midi.WriteShort(0xB0+n['channel'], 0x64, 0x0)
          self.midi.WriteShort(0xB0+n['channel'], 0x6, bendRange)
      
          ## Set patch
          self.midi.WriteShort(0xC0+n['channel'], rgn['voiceSpin'].value())
          
          ## Start note
          n['center'] = rgn['centerSpin'].value()
          self.midi.WriteShort(0x90+n['channel'], n['center'], 127)
      ## 
    
    
    
    
    
    
    
    #pts = m[0][1]
    #for i in range(0, len(pts)):
      #noteCenter = self.centerNoteSpins[i].value()
      
      #if pts[i] is None:
        #self.spots[i].setRect(-1, -1, 0.1, 0.1)
        #if self.notes[i]:
          #print "Stopping note on channel", i
          #self.midi.WriteShort(0x80+i, noteCenter, 0)
        #self.notes[i] = False
          
        
      #else:
        #s = pts[i]['size']
        #s2 = s * 0.5
        #pos = pts[i]['pos']
        #self.spots[i].setRect(pos[0]-s2, pos[1]-s2, s, s)
        
        #bendRange = self.noteRangeSpins[i].value()
        #note = float((1023-pos[0]) * 0x4000 / 1024) - 0x2000
        #quant = 0x4000 / (bendRange*2)
        #qNote = int((round(note / quant) * quant) + 0x2000)
        
        #if self.notes[i]:
          #self.noteBend[i] = self.noteBend[i] * 0.8 + qNote * 0.2
        #else:
          #self.noteBend[i] = qNote
        
        
        ##note = int(pos[0] * 127. / 1024.)
        #volume = int(pos[1] * 127. / 768.)
        #print "set channel %d to %f, %d" % (i, note, volume)
        ##self.midi.WriteShort(0xE0+i, note)
        
        ### select coarse tuning parameter
        ##self.midi.WriteShort(0xB0+i, 0x65, 0x0)
        ##self.midi.WriteShort(0xB0+i, 0x64, 0x2)
        ### set channel tuning
        ##self.midi.WriteShort(0xB0+i, 0x6, note)
        
        
        
        
        #self.midi.WriteShort(0xE0+i, int(self.noteBend[i]) % 128, int(self.noteBend[i]) / 128)
        #self.midi.WriteShort(0xB0+i, 0x07, volume)
        
        #if not self.notes[i]:
          #print "Starting note on channel", i
          ### Set pitch bend sensitivity
          #self.midi.WriteShort(0xB0+i, 0x65, 0x0)
          #self.midi.WriteShort(0xB0+i, 0x64, 0x0)
          #self.midi.WriteShort(0xB0+i, 0x6, bendRange)
          ##self.midi.WriteShort(0xB0+i, 0x26, 127)
          
          ### Select bank
          ##self.midi.WriteShort(0xB0+i, 0x0, 0x2)
          
          ### Set patch
          #self.midi.WriteShort(0xC0+i, self.instrumentSpins[i].value())
          
          ### Start note
          #self.midi.WriteShort(0x90+i, noteCenter, 100)
          #self.notes[i] = True
        

    ##self.view.fitInView(self.box.rect())




win = Window(wii, midi)
app.exec_()




## Midi status codes:
# 0x80 note off
# 0x90 note on
# 0xB0 control change
#    120, 0: all off
#    07, v: volume
#    
# 0xE0 pitch bend (2 bytes, 0x2000 is center)
