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

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This application 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 logging; logger = logging.getLogger(__name__); logger.info("import")


#Standard Library Modules
from typing import Tuple
import os.path

#Third Party Modules
from calfbox import cbox

#Our template modules
from ..start import PATHS
from . import sequencer
from ..helper import cache_unlimited, flatList
from .duration import D1024 #to add a note off spacer.


class Metronome(object):
    """
    A metronome uses calfbox to generate a click track.
    All calfbox handling and midi generation are internally.
           
    The metronome has multiple components, each can be switched on and off on creation:
     -stereo audio out (stereo for cbox reasons) 
     -midi out     
     -midi in                
    
    You can configure the midi notes representing a stressed and normal tick. 
    There is no half-stressed signal. 
    
    Stressing can be switched off.
    
    The metronome is a real midi track, not generated on the fly. Therefore it needs to be set
    with new data when music changes, which happens quite often. In general you want you metronome
    to be as long as your song.
    
    You can choose to loop the last measure, which makes simple "give me 4/4 at 120" possible.    
    
    """

    def __init__(self, parentData, normalMidiNote=77, stressedMidiNote=76, midiChannel=9):
        self.parentData = parentData
        #self.sequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome")        
        self.sfzInstrumentSequencerInterface = sequencer.SfzInstrumentSequencerInterface(parentTrack=self, name="metronome", absoluteSfzPath=os.path.join(PATHS["templateShare"], "metronome", "metronome.sfz"))
        #testing: self.sfzInstrumentSequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome"); self.sfzInstrumentSequencerInterface.setEnabled(True) needs activating below!!!
        self._soundStresses = True #Change through soundStresses function     
        self._normalTickMidiNote = normalMidiNote #no changing after instance got created
        self._stressedTickMidiNote = stressedMidiNote  #no changing after instance got created
        self._midiChannel = midiChannel #GM drums #1-16  #no changing after instance got created
        self._cachedData = None #once we have a track it gets saved here so the midi output can be regenerated in place.
        self.label = "" #E.g. current Track Name, but can be anything.
        self.setEnabled(False) #TODO: save load               
            
    def soundStresses(self, value:bool):        
        self._soundStresses = value
        self.generate(self._cachedData)

    def generate(self, data, label:str):
        """Data is ordered: Iterable of (positionInTicks, isMetrical, treeOfMetricalInstructions)
        as tuple. Does not check if we truly need an update
        
        Label typically is the track name"""                
        assert not data is None        
        self.label = label
        self._cachedData = data                       
        result = []        
        for position, isMetrical, treeOfMetricalInstructions in data:
            isMetrical = False if not self._soundStresses else True
            blob, length = self.instructionToCboxMeasure(isMetrical, treeOfMetricalInstructions)                
            result.append((blob, position, length))                                   
        self.sfzInstrumentSequencerInterface.setTrack(result)                    
        #we skip over instructions which have no proto-measure, metrical or not. This basically creates a zone without a metronome. 
        #This might be difficult to use when thinking of Laborejo alone but in combination with a real time audio recording in another program this becomes very useful.
        #TODO: repeat last instruction as loop
    
    @cache_unlimited
    def instructionToCboxMeasure(self, isMetrical, metricalInstruction:tuple)->Tuple[bytes,int]:
        """Convert a metrical instruction to a metronome measure"""
        measureBlob = bytes()
        workingTicks = 0
        ranOnce = False        
        for duration in flatList(metricalInstruction):
            if ranOnce or not isMetrical: #normal tick. Always normal if this measure has no stressed positions, in other words it is not metrical
                measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 77, 127) #full velocity
                measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 77, 127) #note off is the shortest value possible in this program.
            else: #Measure "one"
                measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 76, 127) #different pitch
                measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 76, 127)
                ranOnce = True
            workingTicks += duration        
        return (measureBlob, workingTicks)
    
    @property
    def enabled(self)->bool:
        return self.sfzInstrumentSequencerInterface.enabled        
        
    def setEnabled(self, value:bool):
        self.sfzInstrumentSequencerInterface.enable(value)

    def export(self)->dict:        
        return {
        #       "sequencerInterface" : self.sequencerInterface.export(),
                "enabled" : self.enabled,
                "label" : self.label,
            }   


