#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
Midi3 light version for soundt/Jamidi/clapt
v0.7.0

Midi Handler : 

- Hook to the MIDI host
- Enumerate connected midi devices and spawn a process/device to handle incoming events

by Sam Neurohack 
from /team/laser

Midi conversions from https://github.com/craffel/pretty-midi

"""

import time
from threading import Thread

import rtmidi
from rtmidi.midiutil import open_midiinput 
from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF,
                                  PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE, TIMING_CLOCK, SONG_CONTINUE, SONG_START, SONG_STOP)
import mido
from mido import MidiFile

import traceback
import weakref
import sys
from sys import platform
import os
import re
from collections import deque
import log


from queue import Queue
from OSC3 import OSCServer, OSCClient, OSCMessage


print("")

midiname = ["Name"] * 16
midiport = [rtmidi.MidiOut() for i in range(16) ]

OutDevice = [] 
InDevice = []

midisync = True

# max 16 midi port array 

midinputsname = ["Name"] * 16
midinputsqueue = [Queue() for i in range(16) ]
midinputs = []

debug  = 1
#Mser = False

MidInsNumber = 0

serverIP = "127.0.0.1"
OSCPORT = 9001

clock = mido.Message(type="clock")
start = mido.Message(type ="start")
stop = mido.Message(type ="stop")
ccontinue = mido.Message(type ="continue")
reset = mido.Message(type ="reset")
songpos = mido.Message(type ="songpos")

#mode = "maxwell"

'''
print "clock",clock)
print "start",start)
print "continue", ccontinue)
print "reset",reset)
print "sonpos",songpos)
'''

try:
    input = raw_input
except NameError:
    # Python 3
    Exception = Exception


STATUS_MAP = {
    'noteon': NOTE_ON,
    'noteoff': NOTE_OFF,
    'programchange': PROGRAM_CHANGE,
    'controllerchange': CONTROLLER_CHANGE,
    'pitchbend': PITCH_BEND,
    'polypressure': POLY_PRESSURE,
    'channelpressure': CHANNEL_PRESSURE
}


def SendTCview(oscaddress,oscargs=''):
        
    oscmsg = OSCMessage()
    oscmsg.setAddress(oscaddress)
    oscmsg.append(oscargs)
    
    osclientc = OSCClient()
    osclientc.connect((serverIP, OSCPORT)) 

    #print("MIDI Aurora sending UI :", oscmsg, "to",TouchOSCIP,":",TouchOSCPort)
    try:
        osclientc.sendto(oscmsg, (serverIP, OSCPORT))
        oscmsg.clearData()
    except:
        log.err('Connection to TCview refused : died ?')
        pass
    #time.sleep(0.001


def GetTime():
  return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())

# /cc cc number value
def cc(midichannel, ccnumber, value, mididest):

    if debug>0:
        print("Midix sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest)

    MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest)



#
# MIDI Startup and handling
#
      
mqueue  = Queue()
inqueue = Queue()
bpm = 0
running = True
samples = deque()
last_clock = None


#
# Events from Generic MIDI Handling
#

def MidinProcess(inqueue, portname):

    inqueue_get = inqueue.get
    bpm = 0
    samples = deque()
    last_clock = None

    while True:
        time.sleep(0.001)
        msg = inqueue_get()
        #print("")
        #print("Generic from", portname,"msg : ", msg)
        

        # NOTE ON message on all midi channels   
        if NOTE_ON -1 < msg[0] < 160 and msg[2] !=0 :

            MidiChannel = msg[0]-144
            MidiNote = msg[1]
            MidiVel = msg[2]
            print()
            print("NOTE ON :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel)
            #print("Midi in process send /aurora/noteon "+str(msg[1])+" "+str(msg[2]))
            SendTCview("/noteon",[MidiChannel, msg[1], msg[2]])

            '''
            # Sampler mode : note <63 launch snare.wav / note > 62 kick.wav 
            if MidiNote < 63 and MidiVel >0:
        
                if platform == 'darwin':
                    os.system("afplay snare.wav")
                else:
                    os.system("aplay snare.wav")
        
        
            if MidiNote > 62 and MidiVel >0:
              
                if platform == 'darwin':
                    os.system("afplay kick.wav")
                else:
                    os.system("aplay kick.wav")
            '''   
                
        # NOTE OFF or Note with 0 velocity on all midi channels
        if NOTE_OFF -1 < msg[0] < 145 or (NOTE_OFF -1 < msg[0] < 160 and msg[2] == 0):

            print("NOTE_off :",NOTE_OFF)
            if msg[0] > 143:
                MidiChannel = msg[0]-144
            else:
                MidiChannel = msg[0]-128
            #print("NOTE OFF :", MidiNote, "Channel", MidiChannel)
            #print("Midi in process send /aurora/noteoff "+str(msg[1]))
            SendTCview("/noteoff",[MidiChannel, msg[1]])


        # # CC on all Midi Channels 
        if CONTROLLER_CHANGE -1 < msg[0] < 192:

            MidiChannel = msg[0]-175
            print()
            #print("channel", MidiChannel, "CC :", msg[1], msg[2])
            print("Midi in process send /aurora/rawcc "+str(msg[0]-175-1)+" "+str(msg[1])+" "+str(msg[2]))
            SendTCview("/rawcc",[msg[0]-175-1, msg[1], msg[2]])
            


        if msg[0] == TIMING_CLOCK:
            now = time.time()

            if last_clock is not None:
                samples.append(now - last_clock)
            last_clock = now

            if len(samples) > 24:
                samples.popleft()

            if len(samples) >= 2:
                #bpm = 2.5 / (sum(samples) / len(samples))
                #print("%.2f bpm" % bpm)

                bpm = round(2.5 / (sum(samples) / len(samples)))        # Against BPM lot very tiny change :
                sync = True
                # print("MIDI BPM", bpm)
            
            #print("Midi clock : BPM", bpm)
            SendTCview("/MIDIBPM",[bpm])



        if msg[0] in (SONG_CONTINUE, SONG_START):
            running = True
            #print("START/CONTINUE received.")
            #print("Midi in process send /aurora/start")
            SendTCview("/start",[])


        if msg[0] == SONG_STOP:
            running = False
            #print("STOP received.")
            #print("Midi in process send /aurora/stop")
            SendTCview("/stop",[])

        '''
        # other midi message  
        if msg[0] != NOTE_OFF and  msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE:
            pass
            
            print("from", portname,"other midi message")
            MidiMsg(msg[0],msg[1],msg[2],mididest)
        '''

#def NoteOn(note, color, mididest):
#https://pypi.org/project/python-rtmidi/0.3a/
# NOTE_ON=#90 et NOTE_OFF=#80 on ajoute le channel (0 le premier) pour envoyer effectivement sur le channel
def NoteOn(note, color, mididest, midichannel=0):
    global MidInsNumber

    if debug >0:
        print("Sending", note, color, "to", mididest, "on channel", midichannel)

    for port in range(MidInsNumber):

        # To mididest
        if midiname[port].find(mididest) == 0:
            midiport[port].send_message([NOTE_ON+midichannel, note, color])

        # To All 
        elif mididest == "all" and midiname[port].find(mididest) != 0:
            midiport[port].send_message([NOTE_ON+midichannel, note, color])




def NoteOff(note, mididest):
    global MidInsNumber


    for port in range(MidInsNumber):

        # To mididest
        if midiname[port].find(mididest) != -1:
            midiport[port].send_message([NOTE_OFF, note, 0])

        # To All 
        elif mididest == "all" and midiname[port].find(mididest) == -1:
                midiport[port].send_message([NOTE_OFF, note, 0])


       
# Generic call back : new msg forwarded to queue 
class AddQueue(object):
    def __init__(self, portname, port):
        self.portname = portname
        self.port = port
        #print "AddQueue", port)
        self._wallclock = time.time()

    def __call__(self, event, data=None):
        message, deltatime = event
        self._wallclock += deltatime
        #print "inqueue : [%s] @%0.6f %r" % ( self.portname, self._wallclock, message))
        message.append(deltatime)
        midinputsqueue[self.port].put(message)


#    
# MIDI OUT Handling
#

class OutObject():

    _instances = set()
    counter = 0

    def __init__(self, name, kind, port):

        self.name = name
        self.kind = kind
        self.port = port
        
        self._instances.add(weakref.ref(self))
        OutObject.counter += 1

        print("Adding OutDevice name", self.name, "kind", self.kind, "port", self.port)

    @classmethod
    def getinstances(cls):
        dead = set()
        for ref in cls._instances:
            obj = ref()
            if obj is not None:
                yield obj
            else:
                dead.add(ref)
        cls._instances -= dead

    def __del__(self):
        OutObject.counter -= 1



def OutConfig():
    global midiout, MidInsNumber
    
    # 
    if len(OutDevice) == 0:
        print("")
        log.info("MIDIout...")
        print("List and attach to available devices on host with IN port :")
    
        # Display list of available midi IN devices on the host, create and start an OUT instance to talk to each of these Midi IN devices 
        midiout = rtmidi.MidiOut()
        available_ports = midiout.get_ports()
    
        for port, name in enumerate(available_ports):
    
            midiname[port]=name
            midiport[port].open_port(port)
            #print )
            #print "New OutDevice [%i] %s" % (port, name))

            OutDevice.append(OutObject(name, "generic", port))
    
        #print "")      
        print(len(OutDevice), "Out devices")
        #ListOutDevice()
        MidInsNumber = len(OutDevice)+1

def ListOutDevice():

    for item in OutObject.getinstances():

        print(item.name)

def FindOutDevice(name):

    port = -1
    for item in OutObject.getinstances():
        #print "searching", name, "in", item.name)
        if name == item.name:
            #print 'found port',item.port)
            port = item.port
    return port


def DelOutDevice(name):

    Outnumber = Findest(name)
    print('deleting OutDevice', name)

    if Outnumber != -1:
        print('found OutDevice', Outnumber)
        delattr(OutObject, str(name))
        print("OutDevice", Outnumber,"was removed")
    else:
        print("OutDevice was not found")



#    
# MIDI IN Handling 
# Create processing thread and queue for each device
#

class InObject():

    _instances = set()
    counter = 0

    def __init__(self, name, kind, port, rtmidi):

        self.name = name
        self.kind = kind
        self.port = port
        self.rtmidi = rtmidi
        self.queue = Queue()
        
        self._instances.add(weakref.ref(self))
        InObject.counter += 1

        print("Adding InDevice name", self.name, "kind", self.kind, "port", self.port)

    @classmethod
    def getinstances(cls):
        dead = set()
        for ref in cls._instances:
            obj = ref()
            if obj is not None:
                yield obj
            else:
                dead.add(ref)
        cls._instances -= dead

    def __del__(self):
        InObject.counter -= 1


def InConfig():

    print("")
    log.info("MIDIin...")
    

    print("List and attach to available devices on host with OUT port :")

    if platform == 'darwin':
        mido.set_backend('mido.backends.rtmidi/MACOSX_CORE')

    genericnumber = 0

    for port, name in enumerate(mido.get_input_names()):


        outport = FindOutDevice(name)
        midinputsname[port]=name
        
        #print "name",name, "Port",port, "Outport", outport)
        # print "midinames", midiname)
        
        #ListInDevice()

        try:
            #print name, name.find("RtMidi output"))
            if name.find("RtMidi output") > -1:
                print("No thread started for device", name)
            else:
                portin = object
                port_name = ""
                portin, port_name = open_midiinput(outport)

                if midisync == True:
                    portin.ignore_types(timing=False)

                #midinputs.append(portin)
                InDevice.append(InObject(name, "generic", outport, portin))
                
                thread = Thread(target=MidinProcess, args=(midinputsqueue[port],port_name))
                thread.setDaemon(True)
                thread.start() 

                #print "Thread launched for midi port", port, "portname", port_name, "Inname", midiname.index(port_name)
                #print "counter", InObject.counter
                #midinputs[port].set_callback(AddQueue(name),midinputsqueue[port])
                #midinputs[port].set_callback(AddQueue(name))
                #genericnumber += 1
                InDevice[InObject.counter-1].rtmidi.set_callback(AddQueue(name,port))

        except Exception:
            traceback.print_exc()
                    
    #print "")      
    print(InObject.counter, "In devices")
    #ListInDevice()


def ListInDevice():

    #print "known IN devices :"
    for item in InObject.getinstances():

        print(item.name)
    print("")

def FindInDevice(name):

    port = -1
    for item in InObject.getinstances():
        #print "searching", name, "in", item.name)
        if name in item.name:
            #print 'found port',item.port)
            port = item.port
    return port


def DelInDevice(name):

    Innumber = Findest(name)
    print('deleting InDevice', name)

    if Innumber != -1:
        print('found InDevice', Innumber)
        delattr(InObject, str(name))
        print("InDevice", Innumber,"was removed")
    else:
        print("InDevice was not found")



def End():
    global midiout
    
    #midiin.close_port()
    midiout.close_port()
  

# mididest : all or specifiname, won't be sent to launchpad or Bhoreal.
def MidiMsg(midimsg, mididest):
    
   
    desterror = -1

    print("tcview got midimsg", midimsg, "for", mididest)

    for port in range(len(OutDevice)):
        # To mididest
        if midiname[port].find(mididest) != -1:
            if debug>0:
                print("tcview sending to name", midiname[port], "port", port, ":", midimsg)
            midiport[port].send_message(midimsg)
            desterror = 0

    if desterror == -1:
        print("mididest",mididest, ": ** This midi destination doesn't exists **")



def listdevice(number):
	
	return midiname[number]
	
def check():

    OutConfig()
    InConfig()
    

	
