#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""


import re
from calfbox import cbox  #use the globally installed calfbox
from asyncio import get_event_loop
from sys import stdout, maxsize
import os, signal

D1024 =210 * 2**0 # = 210. The lcm of 2, 3, 5, 7 . according to www.informatics.indiana.edu/donbyrd/CMNExtremes.htm this is the real world limit.
D512 = 210 * 2**1
D256 = 210 * 2**2
D128 = 210 * 2**3
D64  = 210 * 2**4
D32  = 210 * 2**5
D16  = 210 * 2**6   #16th     13440 ticks
D8   = 210 * 2**7   #eigth    26880 ticks
D4   = 210 * 2**8   #quarter  53760 ticks
D2   = 210 * 2**9   #half     107520 ticks
D1   = 210 * 2**10  #whole    215040 ticks
DB   = 210 * 2**11  #brevis   430080 ticks
DL   = 210 * 2**12  #longa
DM   = 210 * 2**13  #maxima
#MAXIMUM = 0x7FFFFFFF # 31bit. maximum number of calfbox ticks allowed for its timeline, for example for the song duration
MAXIMUM = 100 * D1
#max_pulses = min(2**31, 2**31 * ppqn * bpm / (60 * sample_rate))

ly2pitch = {
    "ceses,,," : 00,
    "ces,,," : 10,
    "c,,," : 20,
    "cis,,," : 30,
    "cisis,,," : 40,
    "deses,,," : 50,
    "des,,," : 60,
    "d,,," : 70,
    "dis,,," : 80,
    "disis,,," : 90,
    "eeses,,," : 100,
    "ees,,," : 110,
    "e,,," : 120,
    "eis,,," : 130,
    "eisis,,," : 140,
    "feses,,," : 150,
    "fes,,," : 160,
    "f,,," : 170,
    "fis,,," : 180,
    "fisis,,," : 190,
    "geses,,," : 200,
    "ges,,," : 210,
    "g,,," : 220,
    "gis,,," : 230,
    "gisis,,," : 240,
    "aeses,,," : 250,
    "aes,,," : 260,
    "a,,," : 270,
    "ais,,," : 280,
    "aisis,,," : 290,
    "beses,,," : 300,
    "bes,,," : 310,
    "b,,," : 320,
    "bis,,," : 330,
    "bisis,,," : 340,
    "ceses,," : 350,
    "ces,," : 360,
    "c,," : 370,
    "cis,," : 380,
    "cisis,," : 390,
    "deses,," : 400,
    "des,," : 410,
    "d,," : 420,
    "dis,," : 430,
    "disis,," : 440,
    "eeses,," : 450,
    "ees,," : 460,
    "e,," : 470,
    "eis,," : 480,
    "eisis,," : 490,
    "feses,," : 500,
    "fes,," : 510,
    "f,," : 520,
    "fis,," : 530,
    "fisis,," : 540,
    "geses,," : 550,
    "ges,," : 560,
    "g,," : 570,
    "gis,," : 580,
    "gisis,," : 590,
    "aeses,," : 600,
    "aes,," : 610,
    "a,," : 620,
    "ais,," : 630,
    "aisis,," : 640,
    "beses,," : 650,
    "bes,," : 660,
    "b,," : 670,
    "bis,," : 680,
    "bisis,," : 690,
    "ceses," : 700,
    "ces," : 710,
    "c," : 720,
    "cis," : 730,
    "cisis," : 740,
    "deses," : 750,
    "des," : 760,
    "d," : 770,
    "dis," : 780,
    "disis," : 790,
    "eeses," : 800,
    "ees," : 810,
    "e," : 820,
    "eis," : 830,
    "eisis," : 840,
    "feses," : 850,
    "fes," : 860,
    "f," : 870,
    "fis," : 880,
    "fisis," : 890,
    "geses," : 900,
    "ges," : 910,
    "g," : 920,
    "gis," : 930,
    "gisis," : 940,
    "aeses," : 950,
    "aes," : 960,
    "a," : 970,
    "ais," : 980,
    "aisis," : 990,
    "beses," : 1000,
    "bes," : 1010,
    "b," : 1020,
    "bis," : 1030,
    "bisis," : 1040,
    "ceses" : 1050,
    "ces" : 1060,
    "c" : 1070,
    "cis" : 1080,
    "cisis" : 1090,
    "deses" : 1100,
    "des" : 1110,
    "d" : 1120,
    "dis" : 1130,
    "disis" : 1140,
    "eeses" : 1150,
    "ees" : 1160,
    "e" : 1170,
    "eis" : 1180,
    "eisis" : 1190,
    "feses" : 1200,
    "fes" : 1210,
    "f" : 1220,
    "fis" : 1230,
    "fisis" : 1240,
    "geses" : 1250,
    "ges" : 1260,
    "g" : 1270,
    "gis" : 1280,
    "gisis" : 1290,
    "aeses" : 1300,
    "aes" : 1310,
    "a" : 1320,
    "ais" : 1330,
    "aisis" : 1340,
    "beses" : 1350,
    "bes" : 1360,
    "b" : 1370,
    "bis" : 1380,
    "bisis" : 1390,
    "ceses'" : 1400,
    "ces'" : 1410,
    "c'" : 1420,
    "cis'" : 1430,
    "cisis'" : 1440,
    "deses'" : 1450,
    "des'" : 1460,
    "d'" : 1470,
    "dis'" : 1480,
    "disis'" : 1490,
    "eeses'" : 1500,
    "ees'" : 1510,
    "e'" : 1520,
    "eis'" : 1530,
    "eisis'" : 1540,
    "feses'" : 1550,
    "fes'" : 1560,
    "f'" : 1570,
    "fis'" : 1580,
    "fisis'" : 1590,
    "geses'" : 1600,
    "ges'" : 1610,
    "g'" : 1620,
    "gis'" : 1630,
    "gisis'" : 1640,
    "aeses'" : 1650,
    "aes'" : 1660,
    "a'" : 1670,
    "ais'" : 1680,
    "aisis'" : 1690,
    "beses'" : 1700,
    "bes'" : 1710,
    "b'" : 1720,
    "bis'" : 1730,
    "bisis'" : 1740,
    "ceses''" : 1750,
    "ces''" : 1760,
    "c''" : 1770,
    "cis''" : 1780,
    "cisis''" : 1790,
    "deses''" : 1800,
    "des''" : 1810,
    "d''" : 1820,
    "dis''" : 1830,
    "disis''" : 1840,
    "eeses''" : 1850,
    "ees''" : 1860,
    "e''" : 1870,
    "eis''" : 1880,
    "eisis''" : 1890,
    "feses''" : 1900,
    "fes''" : 1910,
    "f''" : 1920,
    "fis''" : 1930,
    "fisis''" : 1940,
    "geses''" : 1950,
    "ges''" : 1960,
    "g''" : 1970,
    "gis''" : 1980,
    "gisis''" : 1990,
    "aeses''" : 2000,
    "aes''" : 2010,
    "a''" : 2020,
    "ais''" : 2030,
    "aisis''" : 2040,
    "beses''" : 2050,
    "bes''" : 2060,
    "b''" : 2070,
    "bis''" : 2080,
    "bisis''" : 2090,
    "ceses'''" : 2100,
    "ces'''" : 2110,
    "c'''" : 2120,
    "cis'''" : 2130,
    "cisis'''" : 2140,
    "deses'''" : 2150,
    "des'''" : 2160,
    "d'''" : 2170,
    "dis'''" : 2180,
    "disis'''" : 2190,
    "eeses'''" : 2200,
    "ees'''" : 2210,
    "e'''" : 2220,
    "eis'''" : 2230,
    "eisis'''" : 2240,
    "feses'''" : 2250,
    "fes'''" : 2260,
    "f'''" : 2270,
    "fis'''" : 2280,
    "fisis'''" : 2290,
    "geses'''" : 2300,
    "ges'''" : 2310,
    "g'''" : 2320,
    "gis'''" : 2330,
    "gisis'''" : 2340,
    "aeses'''" : 2350,
    "aes'''" : 2360,
    "a'''" : 2370,
    "ais'''" : 2380,
    "aisis'''" : 2390,
    "beses'''" : 2400,
    "bes'''" : 2410,
    "b'''" : 2420,
    "bis'''" : 2430,
    "bisis'''" : 2440,
    "ceses''''" : 2450,
    "ces''''" : 2460,
    "c''''" : 2470,
    "cis''''" : 2480,
    "cisis''''" : 2490,
    "deses''''" : 2500,
    "des''''" : 2510,
    "d''''" : 2520,
    "dis''''" : 2530,
    "disis''''" : 2540,
    "eeses''''" : 2550,
    "ees''''" : 2560,
    "e''''" : 2570,
    "eis''''" : 2580,
    "eisis''''" : 2590,
    "feses''''" : 2600,
    "fes''''" : 2610,
    "f''''" : 2620,
    "fis''''" : 2630,
    "fisis''''" : 2640,
    "geses''''" : 2650,
    "ges''''" : 2660,
    "g''''" : 2670,
    "gis''''" : 2680,
    "gisis''''" : 2690,
    "aeses''''" : 2700,
    "aes''''" : 2710,
    "a''''" : 2720,
    "ais''''" : 2730,
    "aisis''''" : 2740,
    "beses''''" : 2750,
    "bes''''" : 2760,
    "b''''" : 2770,
    "bis''''" : 2780,
    "bisis''''" : 2790,
    "ceses'''''" : 2800,
    "ces'''''" : 2810,
    "c'''''" : 2820,
    "cis'''''" : 2830,
    "cisis'''''" : 2840,
    "deses'''''" : 2850,
    "des'''''" : 2860,
    "d'''''" : 2870,
    "dis'''''" : 2880,
    "disis'''''" : 2890,
    "eeses'''''" : 2900,
    "ees'''''" : 2910,
    "e'''''" : 2920,
    "eis'''''" : 2930,
    "eisis'''''" : 2940,
    "feses'''''" : 2950,
    "fes'''''" : 2960,
    "f'''''" : 2970,
    "fis'''''" : 2980,
    "fisis'''''" : 2990,
    "geses'''''" : 3000,
    "ges'''''" : 3010,
    "g'''''" : 3020,
    "gis'''''" : 3030,
    "gisis'''''" : 3040,
    "aeses'''''" : 3050,
    "aes'''''" : 3060,
    "a'''''" : 3070,
    "ais'''''" : 3080,
    "aisis'''''" : 3090,
    "beses'''''" : 3100,
    "bes'''''" : 3110,
    "b'''''" : 3120,
    "bis'''''" : 3130,
    "bisis'''''" : 3140,
    #"r" : float('inf'), a rest is not a pitch
    }

def plain(pitch):
    """ Extract the note from a note-number, without any octave but with the tailing zero.
    This means we double-use the lowest octave as abstract version."""
    #Dividing through the octave, 350, results in the number of the octave and the note as remainder.
    return divmod(pitch, 350)[1]

def octave(pitch):
    """Return the octave of given note. Lowest 0 is X,,,"""
    return divmod(pitch, 350)[0]

def halfToneDistanceFromC(pitch):
    """Return the half-tone step distance from C. The "sounding" interval"""
    return {
        #00 : 10, # ceses,,, -> bes
        #10 : 11, # ces,,, -> b

        00 : -2, # ceses,,, -> bes
        10 : -1, # ces,,, -> b
        20 : 0, # c,,,
        30 : 1, # cis,,,
        40 : 2, # cisis,,, -> d ...
        50 : 0, # deses,,,
        60 : 1, # des,,,
        70 : 2, # d,,,
        80 : 3, # dis,,,
        90 : 4, # disis,,,
        100 : 2, # eeses,,,
        110 : 3, # ees,,,
        120 : 4, # e,,,
        130 : 5, # eis,,,
        140 : 6, # eisis,,,
        150 : 3, # feses,,,
        160 : 4, # fes,,,
        170 : 5, # f,,,
        180 : 6, # fis,,,
        190 : 7, # fisis,,,
        200 : 5, # geses,,,
        210 : 6, # ges,,,
        220 : 7, # g,,,
        230 : 8, # gis,,,
        240 : 9, # gisis,,,
        250 : 7, # aeses,,,
        260 : 8, # aes,,,
        270 : 9, # a,,,
        280 : 10, # ais,,,
        290 : 11, # aisis,,,
        300 : 9, # beses,,,
        310 : 10, # bes,,,
        320 : 11, # b,,,
        330 : 12, # bis,,,
        340 : 13, # bisis,,,
        #330 : 0, # bis,,,
        #340 : 1, # bisis,,,
        }[plain(pitch)]


lyToMidi = {} #filled for all pitches on startup, below
for ly, pitch in ly2pitch.items():
    octOffset = (octave(pitch) +1) * 12 #twelve tones per midi octave
    lyToMidi[ly] = octOffset +  halfToneDistanceFromC(pitch)


lyToTicks = {
    "16" : D16,
     "8" : D8,
     "4" : D4,
     "2" : D2,
     "1" : D1,
    }



def ly(lilypondString):
    """Take string of simple lilypond notes, return midi pitches as generator of (pitch, ticks)"""
    lastDur = "4"
    for lyNote in lilypondString.split(" "):
        try:
            lyPitch, lyDur = re.split(r'(\d+)', lyNote)[0:2]
            lastDur = lyDur
        except ValueError:
            lyPitch = re.split(r'(\d+)', lyNote)[0]
            lyDur = lastDur

        yield (lyToMidi[lyPitch], lyToTicks[lyDur])


def ly2cbox(lilypondString):
    """Return (pbytes, durationInTicks)
    a python byte data type with midi data for cbox"""
    #cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity))
    pblob = bytes()
    startTick = 0
    for midiPitch, durationInTicks in ly(lilypondString):
        endTick = startTick + durationInTicks - 1  #-1 ticks to create a small logical gap. This is nothing compared to our tick value dimensions, but it is enough for the midi protocol to treat two notes as separate ones. Imporant to say that this does NOT affect the next note on. This will be mathematically correct anyway.
        pblob += cbox.Pattern.serialize_event(startTick, 0x90, midiPitch, 100) # note on
        pblob += cbox.Pattern.serialize_event(endTick  , 0x80, midiPitch, 100) # note off
        startTick = startTick + durationInTicks #no -1 for the next note
    return pblob, startTick


cboxTracks = {}  #trackName:(cboxTrack,cboxMidiOutUuid)
def cboxSetTrack(trackName, durationInTicks, pattern):
    """Creates or resets calfbox tracks including jack connections
    Keeps jack connections alive.

    pattern is most likely a single pattern created through cbox.Document.get_song().pattern_from_blob
    But it can also be a list of such patterns. In this case all patterns must be the same duration
    and the parameter durationInTicks is the length of ONE pattern.
    """

    if not trackName in cboxTracks:
        cboxMidiOutUuid = cbox.JackIO.create_midi_output(trackName)
        calfboxTrack = cbox.Document.get_song().add_track()
        cboxTracks[trackName] = (calfboxTrack, cboxMidiOutUuid)
    else:
        calfboxTrack, cboxMidiOutUuid = cboxTracks[trackName]
        calfboxTrack.delete()

    calfboxTrack = cbox.Document.get_song().add_track()
    calfboxTrack.set_external_output(cboxMidiOutUuid)
    cbox.JackIO.rename_midi_output(cboxMidiOutUuid, trackName)
    calfboxTrack.set_name(trackName)

    if type(pattern) is cbox.DocPattern:
        calfboxTrack.add_clip(0, 0, durationInTicks, pattern)  #pos, offset, length(and not end-position, but is the same for the complete track), pattern
    else: #iterable
        assert iter(pattern)
        #durationInTicks is the length of ONE pattern.
        for i, pat in enumerate(pattern):
            calfboxTrack.add_clip(i*durationInTicks, 0, durationInTicks, pat) #pos, offset, length, pattern.
            calfboxTrack.add_clip(i*durationInTicks, 0, durationInTicks, pat) #pos, offset, length, pattern.

    return calfboxTrack


def ly2Track(trackName, lyString):
    """Convert a simple string of lilypond notes to a cbox track and add that to the score"""
    music = lyString
    cboxBlob, durationInTicks = ly2cbox(music)
    pattern = cbox.Document.get_song().pattern_from_blob(cboxBlob, durationInTicks)
    cboxSetTrack(trackName, durationInTicks, pattern)

def getLongestTrackDuationInTicks():
    return max( max(clip.pos + clip.length for clip in cboxTrack.track.status().clips) for cboxTrack in cbox.Document.get_song().status().tracks if cboxTrack.track.status().clips)
    #for cboxTrack in cbox.Document.get_song().status().tracks:
    #    print (cboxTrack.track)
    #TODO: These are different than the one above. It is more. Why?
    #for cboxTrack, cboxMidiOutUuid in cboxTracks.values():
    #    print (cboxTrack.status())


def cboxLoop(eventLoop):
    cbox.call_on_idle()
    assert eventLoop.is_running()

    #it is not that simple. status = "[Running]" if cbox.Transport.status().playing else "[Stopped]"
    if cbox.Transport.status().playing == 1:
        status = "[Running]"
    elif cbox.Transport.status().playing == 0:
        status = "[Stopped]"
    elif cbox.Transport.status().playing == 2:
        status = "[Stopping]"
    elif cbox.Transport.status().playing is None:
        status = "[Uninitialized]"
    else:
        raise ValueError("Unknown playback status: {}".format(cbox.Transport.status().playing))

    stdout.write("                                           \r") #it is a hack but it cleans the line from old artefacts
    stdout.write('{}: {}\r'.format(status, cbox.Transport.status().pos_ppqn))
    stdout.flush()
    eventLoop.call_later(0.1, cboxLoop, eventLoop) #100ms delay

eventLoop = get_event_loop()
def initCbox(clientName, internalEventProcessor=True, commonMidiInput=True):
    cbox.init_engine("")
    cbox.Config.set("io", "client_name", clientName)
    cbox.Config.set("io", "enable_common_midi_input", commonMidiInput) #the default "catch all" midi input
    cbox.start_audio()
    scene = cbox.Document.get_engine().new_scene()
    scene.clear()
    cbox.do_cmd("/master/set_ppqn_factor", None, [D4]) #quarter note has how many ticks?

    cbox.Transport.stop()
    cbox.Transport.seek_ppqn(0)
    cbox.Transport.set_tempo(120.0) #must be float

    if internalEventProcessor:
        eventLoop.call_soon(cboxLoop, eventLoop)
    return scene, cbox, eventLoop


def connectPhysicalKeyboards(port="midi"):
    midiKeyboards = cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL)
    ourMidiInPort = cbox.Config.get("io", "client_name",) + ":" + port
    for keyboard in midiKeyboards:
        cbox.JackIO.port_connect(keyboard, ourMidiInPort)

def start(autoplay = False, userfunction = None, tempo = 120):
    def ask_exit():
        print()
        eventLoop.stop()
        shutdownCbox()

    try:
        dur = getLongestTrackDuationInTicks()
        print("Starting with supplied music data. Setting sond duration to longest track")
        assert dur > 0
        cbox.Document.get_song().set_loop(dur, dur) #set playback length for the entire score.
    except ValueError:
        print ("Starting without a track. Setting song duration to a high value to generate recording space")
        cbox.Document.get_song().set_loop(MAXIMUM, MAXIMUM) #set playback length for the entire score.

    cbox.Transport.set_tempo(float(tempo))
    cbox.Document.get_song().update_playback()

    for signame in ('SIGINT', 'SIGTERM'):
        eventLoop.add_signal_handler(getattr(signal, signame), ask_exit)

    if userfunction:
        print ("Send SIGUSR1 with following command to trigger user function")
        print ("kill -10 {}".format(os.getpid()))
        print ()
        eventLoop.add_signal_handler(getattr(signal, "SIGUSR1"), userfunction)

    print ("Use jack transport to control playback")
    print ("Press Ctrl+C to abort")
    print("pid %s: send SIGINT or SIGTERM to exit." % os.getpid())

    try:
        eventLoop.run_forever()
    finally:
        eventLoop.close()


def shutdownCbox():
    cbox.Transport.stop()
    cbox.Transport.seek_ppqn(0)
    cbox.stop_audio()
    cbox.shutdown_engine()
