# This scipt is for creating and using as many counters as you need.
#
# Written by HighMoon - Malcolm Greene
# Current Version : v1.3
# Changes in v1.1:
#   Updated save and load functions to preserve the deicmals, was previouse using int instead of double
# Changes in v1.2:
#   Updated the output function due to formatting issues, also added a property so you can specify the number of decimal places
#   Also added locale function to keep number local to user.
# Changes in v1.3:
#   Added sounds for hotkey presses - They are optional

import obspython as obs
import math
import locale
import sys

script_ver = "1.3"
sysDecimal = "."

totalCounters = 1
counters = []

class counterClass:
    #Class Variables
    hotKeyInc = obs.OBS_INVALID_HOTKEY_ID
    hotKeyDec = obs.OBS_INVALID_HOTKEY_ID
    hotKeyRst = obs.OBS_INVALID_HOTKEY_ID
    
    displaySource = ""
    name = "counterName"

    count = 0
    minVal = 0
    maxVal = 99
    stepAmount = 1

    placesAfter = 2
    padChar = "0"
    
    incVol = 1.0
    decVol = 1.0
    rstVol = 1.0
    
    incSound = ""
    decSound = ""
    rstSound = ""
    
    def __init__(self, counterName):
        #initalize counter with given counterName
        self.name = counterName
        
    def saveCounter(self, settings):
        #Save Increment hotkey
        hotKeySaveArray = obs.obs_hotkey_save(self.hotKeyInc)
        obs.obs_data_set_array(settings, self.name+"_inc", hotKeySaveArray)
        obs.obs_data_array_release(hotKeySaveArray)
        
        #Save Decrement hotkey
        hotKeySaveArray = obs.obs_hotkey_save(self.hotKeyDec)
        obs.obs_data_set_array(settings, self.name+"_dec", hotKeySaveArray)
        obs.obs_data_array_release(hotKeySaveArray)
        
        #Save Reset hotkey
        hotKeySaveArray = obs.obs_hotkey_save(self.hotKeyRst)
        obs.obs_data_set_array(settings, self.name+"_rst", hotKeySaveArray)
        obs.obs_data_array_release(hotKeySaveArray)

        #Save source on which we display the counter
        obs.obs_data_set_string(settings,self.name+"_display_source",self.displaySource)
        #Save variables for the count
        obs.obs_data_set_double(settings,self.name+"_count",self.count)
        obs.obs_data_set_double(settings, self.name+"_min_val", self.minVal)
        obs.obs_data_set_double(settings, self.name+"_max_val", self.maxVal)
        obs.obs_data_set_double(settings, self.name+"_step_val", self.stepAmount)
        #Save formatting variables
        obs.obs_data_set_int(settings, self.name+"_places_after", self.placesAfter)
        obs.obs_data_set_string(settings,self.name+"_pad_char", self.padChar)
        #Save increment sound and volume
        obs.obs_data_set_string(settings, self.name+"_inc_sound", self.incSound)
        obs.obs_data_set_double(settings, self.name+"_inc_vol", self.incVol)
        #Save decrement sound and volume
        obs.obs_data_set_string(settings, self.name+"_dec_sound", self.decSound)
        obs.obs_data_set_double(settings, self.name+"_dec_vol", self.decVol)
        #Save reset sound and volume
        obs.obs_data_set_string(settings, self.name+"_rst_sound", self.rstSound)
        obs.obs_data_set_double(settings, self.name+"_rst_vol", self.rstVol)
    
    def loadCounter(self, settings, counterName):
        #Load counter - setting name to given counterName - These are generated to be easy to access
        self.name = counterName
        self.displaySource = obs.obs_data_get_string(settings, counterName+"_display_source")
        #Load count variables
        self.count = obs.obs_data_get_double(settings, counterName+"_count")
        self.minVal = obs.obs_data_get_double(settings, counterName+"_min_val")
        self.maxVal = obs.obs_data_get_double(settings, counterName+"_max_val")
        self.stepAmount = obs.obs_data_get_double(settings, counterName+"_step_val")
        #Load formatting variables
        self.placesAfter = obs.obs_data_get_int(settings, counterName+"_places_after")
        self.padChar = obs.obs_data_get_string(settings, counterName+"_pad_char")
        #Load increment sound and volume
        self.incSound = obs.obs_data_get_string(settings, counterName+"_inc_sound")
        self.incVol = obs.obs_data_get_double(settings, counterName+"_inc_vol")
        #Load decrement sound and volume
        self.decSound = obs.obs_data_get_string(settings, counterName+"_dec_sound")
        self.decVol = obs.obs_data_get_double(settings, counterName+"_dec_vol")
        #Load reset sound and volume
        self.rstSound = obs.obs_data_get_string(settings, counterName+"_rst_sound")
        self.rstVol = obs.obs_data_get_double(settings, counterName+"_rst_vol")
        
        #Function to handle counter increment
        def inc_counter(pressed):
            if pressed:
                self.count = self.count + self.stepAmount
                if self.count > self.maxVal:
                    self.count = self.maxVal
                self.updateText()
                play_sound(self.incSound, self.incVol)
        
        #Register increment hotkey function with OBS - this will load saved keys 
        self.hotKeyInc = obs.obs_hotkey_register_frontend(counterName+"_inc", "Increment Counter "+counterName, inc_counter)
        hotKeySaveArray = obs.obs_data_get_array(settings, counterName+"_inc")
        obs.obs_hotkey_load(self.hotKeyInc, hotKeySaveArray)
        obs.obs_data_array_release(hotKeySaveArray)
        
        #Function to handle counter decrement
        def dec_counter(pressed):
            if pressed:
                self.count = self.count - self.stepAmount
                if self.count < self.minVal:
                    self.count = self.minVal
                self.updateText()
                play_sound(self.decSound, self.decVol)
        
        #Register increment hotkey function with OBS - this will load saved keys                 
        self.hotKeyDec = obs.obs_hotkey_register_frontend(counterName+"_dec", "Decrement Counter "+counterName, dec_counter)
        hotKeySaveArray = obs.obs_data_get_array(settings, counterName+"_dec")
        obs.obs_hotkey_load(self.hotKeyDec, hotKeySaveArray)
        obs.obs_data_array_release(hotKeySaveArray)
        
        #Function to handle counter reset
        def rst_counter(pressed):
            if pressed:
                self.count = self.minVal
                self.updateText()
                play_sound(self.rstSound, self.rstVol)
        
        #Register reset hotkey function with OBS - this will load saved keys 
        self.hotKeyRst = obs.obs_hotkey_register_frontend(counterName+"_rst", "Reset Counter "+counterName, rst_counter)
        hotKeySaveArray = obs.obs_data_get_array(settings, counterName+"_rst")
        obs.obs_hotkey_load(self.hotKeyRst, hotKeySaveArray)
        obs.obs_data_array_release(hotKeySaveArray)
        
        return settings
    
    #Function to format and output the counter text
    def updateText(self):
        settings = obs.obs_data_create()
        pad=""
        maxLen = 0
        minLen = 0
        numLen = 0
        output = ""
        
        #Grab display source object
        source = obs.obs_get_source_by_name(self.displaySource)
        
        #Check if we need to have a decimal at all
        if hasDecimalVal(self.count) or hasDecimalVal(self.maxVal) or hasDecimalVal(self.minVal) or hasDecimalVal(self.stepAmount) or self.placesAfter > 0:
            #Format output with a decimal according to settings
            output = locale.format_string("%."+str(self.placesAfter)+"f", self.count)
        else:
            #Format output without any decimals
            output = locale.format_string("%.0f", self.count)
        
        #Calculate length of number before the decimal place
        numLen = len(str(getIntPart(self.count, self.placesAfter)))
        #Calculate the max length of the number based on maximum value setting
        maxLen = len(str(getIntPart(self.maxVal, self.placesAfter)))
        
        #Make padding string
        pad = self.padChar * (maxLen - numLen)
            
        #Set text of source to the pad + formated output
        obs.obs_data_set_string(settings, "text", pad+output)
        #Update source
        obs.obs_source_update(source, settings)
        #Release Data
        obs.obs_data_release(settings)
        #Release Source
        obs.obs_source_release(source)    

# This function runs when the script loads
# We uses it to initalize the counters array, and load any saved counter information.    
def script_load(settings):
    global totalCounters
    global counters
    global sysDecimal
    global script_ver
    
    #Retrieve the total counters variable
    totalCounters = obs.obs_data_get_int(settings,"total_counters")
    #Make as many counters as the totalCounters variable, we then update their output
    for prepCounter in range(totalCounters):
        counter = counterClass("counter_"+str(prepCounter+1))
        counters.append(counter)
        counters[prepCounter].loadCounter(settings, "counter_"+str(prepCounter+1))
        counters[prepCounter].updateText()
    
    #Set locale to user default
    locale.setlocale(locale.LC_ALL,'')
    #Get system decimal point - this is an attempt at international compatability
    sysDecimal = locale.localeconv()["decimal_point"]
    #Log that the script has loaded
    obs.script_log(obs.LOG_INFO, "Counters script v" + script_ver + " - By HighMoon(Malcolm Greene) - Loaded!!!")

# This function is called to save the script
# We use it to save all the counters in the counters array
def script_save(settings):
    global counters
    for counter in counters:
        counter.saveCounter(settings)

def script_unload():
    obs.script_log(obs.LOG_INFO, "Counters script v" + script_ver + " - By HighMoon(Malcolm Greene) - Unloaded!!!")

# This function is called when the settings page has changed settings on it.
def script_update(settings):
    global totalCounters
    global counters
    #Track old total number of counters
    oldTotal = totalCounters
    #Get current setting value
    totalCounters = obs.obs_data_get_int(settings,"total_counters")
    
    #Count up to the total number of counters
    for cntNum in range(totalCounters):
        #If the current counter number is larger than the old total
        # we need to make a new counter entry.
        if cntNum >= oldTotal:
            #Initalize counter and set name
            counter = counterClass("counter_"+str(cntNum+1))
            #Set display source name
            counter.displaySource = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_display_source")
            #Set count variables
            counter.count = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_count")
            counter.minVal = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_min_val")
            counter.maxVal = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_max_val")
            counter.stepAmount = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_step_val")
            #Set formatting variables
            counter.placesAfter = obs.obs_data_get_int(settings, "counter_"+str(cntNum+1)+"_places_after")
            counter.padChar = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_pad_char")
            #Set increment sound and volume variables
            counter.incSound = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_inc_sound")
            counter.incVolume = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_inc_vol")
            #Set decrement sound and volume variables
            counter.decSound = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_dec_sound")
            counter.decVolume = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_dec_vol")
            #Set reset sound and volume variables
            counter.rstSound = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_rst_sound")
            counter.rstVolume = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_rst_vol")
            #Add counter to coutners array
            counters.append(counter)
            #update counter's output
            counter.updateText()
            
        #Otherwise we can just updated the settings of the existing entry.
        else:
            #Set counter name - maybe redundant
            counters[cntNum].name = "counter_"+str(cntNum+1)
            #Set display source name
            counters[cntNum].displaySource = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_display_source")
            #Set count variables
            counters[cntNum].count = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_count")
            counters[cntNum].minVal = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_min_val")
            counters[cntNum].maxVal = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_max_val")
            counters[cntNum].stepAmount = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_step_val")
            #Set formatting variables
            counters[cntNum].placesAfter = obs.obs_data_get_int(settings, "counter_"+str(cntNum+1)+"_places_after")
            counters[cntNum].padChar = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_pad_char")
            #Set increment sound and volume variables
            counters[cntNum].incSound = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_inc_sound")
            counters[cntNum].incVolume = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_inc_vol")
            #Set decrement sound and volume variables
            counters[cntNum].decSound = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_dec_sound")
            counters[cntNum].decVolume = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_dec_vol")
            #Set reset sound and volume variables
            counters[cntNum].rstSound = obs.obs_data_get_string(settings, "counter_"+str(cntNum+1)+"_rst_sound")
            counters[cntNum].rstVolume = obs.obs_data_get_double(settings, "counter_"+str(cntNum+1)+"_rst_vol")
            #update counter's output
            counters[cntNum].updateText()

# This function adds properties to the OBS script page
# it will add a copy for each counter in the total counter field.
# it does require a refresh after changing the total.
def script_properties():
    global totalCounters
    
    #Create properties object
    props = obs.obs_properties_create()
    #Add total counters property
    obs.obs_properties_add_int(props, "total_counters", "Total Number of Counters", 1, 2147483647, 1)
    
    #Add property sections for each counter
    for makeCounter in range(totalCounters):
        
        #Add Property Seperator
        obs.obs_properties_add_text(props, "counter_"+str(makeCounter+1)+"_title","Settings for counter_"+str(makeCounter+1), obs.OBS_TEXT_INFO)
        
        #Add list of possible display sources
        p = obs.obs_properties_add_list(props, "counter_"+str(makeCounter+1)+"_display_source", "Text Source to display count: ", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
        
        sources = obs.obs_enum_sources()
        if sources is not None:
            for source in sources:
                source_id = obs.obs_source_get_id(source)
                if source_id == "text_gdiplus" or source_id == "text_ft2_source" or source_id == "text_gdiplus_v3":
                    name = obs.obs_source_get_name(source)
                    obs.obs_property_list_add_string(p, name, name)
        
        obs.source_list_release(sources)
        #Add count properties
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_count", "Counter current value: ", -2147483647, 2147483647, 1)
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_min_val", "Counter minimum value: ", -2147483647, 2147483647, 1)
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_max_val", "Counter maximum value: ", -2147483647, 2147483647, 1)
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_step_val", "Counter step value: ", -2147483647, 2147483647, 1)
        #Add formatting properties
        obs.obs_properties_add_int(props, "counter_"+str(makeCounter+1)+"_places_after", "Number of decimal places: ", 0, 2147483647, 1)
        obs.obs_properties_add_text(props, "counter_"+str(makeCounter+1)+"_pad_char", "Pad character: ",  obs.OBS_TEXT_DEFAULT)
        #Add increment sound file and volume properties
        obs.obs_properties_add_path(props, "counter_"+str(makeCounter+1)+"_inc_sound", "File to play when counter increments: ", obs.OBS_PATH_FILE, "Audio Files (*.wav *.mp3)", "")
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_inc_vol", "Volume of increment sound:", 0, 1, 0.125)
        #Add decrement sound file and volume properties
        obs.obs_properties_add_path(props, "counter_"+str(makeCounter+1)+"_dec_sound", "File to play when counter decrements: ", obs.OBS_PATH_FILE, "Audio Files (*.wav *.mp3)", "")
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_dec_vol", "Volume of decrement sound:", 0, 1, 0.125)
        #Add reset sound file and volume properties
        obs.obs_properties_add_path(props, "counter_"+str(makeCounter+1)+"_rst_sound", "File to play when counter resets: ", obs.OBS_PATH_FILE, "Audio Files (*.wav *.mp3)", "")
        obs.obs_properties_add_float(props, "counter_"+str(makeCounter+1)+"_rst_vol", "Volume of reset sound:", 0, 1, 0.125)
    
    return props

# This function is used by OBS to place at the top of the script property page.
def script_description():
    return "Counter system that can handle many counters. \n Press the script refresh button after changing counter count. \n You can also hit refresh after changing other options force an update."
    
#Fuction to check if the number has a decimal part
def hasDecimalVal(numToCheck):
    checkString = locale.format_string("%f", numToCheck)
    decPos = checkString.index(sysDecimal)
    decVal = int(checkString[decPos+1:])
    
    if decVal > 0:
        return True
        
    return False

#Returns just the integer part of a number, i.e. left of the decimal place
def getIntPart(numToSplit, decPlaces):
    if decPlaces <= 0:
        return numToSplit
    numString = locale.format_string("%."+str(decPlaces)+"f", numToSplit)
    decPos = numString.index(sysDecimal)
    intVal = int(numString[:decPos])
    return intVal

#Function to create a media player then play a sound
def play_sound(soundFile, volume):
    
    if soundFile == "":
        return
    obs.script_log(obs.LOG_INFO, "playSound: " + soundFile)
    soundSettings = obs.obs_data_create()
    obs.obs_data_set_string(soundSettings, "local_file", soundFile)
    mediaSource = obs.obs_source_create_private("ffmpeg_source", "play_sound_"+soundFile, soundSettings)
    obs.obs_source_set_volume(mediaSource,volume)
    obs.obs_source_set_muted(mediaSource, False)
    obs.obs_source_set_monitoring_type(mediaSource, obs.OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)
    obs.obs_data_release(soundSettings)
    obs.obs_set_output_source(63, mediaSource)
    obs.obs_source_release(mediaSource)    