package provide dialog_audio 0.1

package require pdtcl_compat

namespace eval ::dialog_audio:: {
    namespace export pdtk_audio_dialog
    variable referenceconfig ""
    variable standalone_window ""
}
set ::audio_samplerate 48000
set ::audio_advance 25
set ::audio_blocksize 64
set ::audio_use_callback 0
set ::audio_can_multidevice 0

## audio devices (available and used)
# ::audio_*devlist: a list of available input devices (type:string)
# ::audio_*devices: currently used input devices (type:index)
# ::audio_*devcechannels: currently used channels per input device (type:int)
# the length of the 'currently used' lists is currently hardcoded to 4
# (to match Pd's "audio-dialog")
set ::audio_indevlist {}
set ::audio_indevices {-1 -1 -1 -1}
set ::audio_indevicechannels {0 0 0 0}
set ::audio_outdevlist {}
set ::audio_outdevices {-1 -1 -1 -1}
set ::audio_outdevicechannels {0 0 0 0}

# dialog vars
set ::dialog_audio::samplerate $::audio_samplerate
set ::dialog_audio::advance $::audio_advance
set ::dialog_audio::blocksize $::audio_blocksize
set ::dialog_audio::use_callback $::audio_use_callback
array set ::dialog_audio::in_enable {}
array set ::dialog_audio::in_device {}
array set ::dialog_audio::in_channels {}
array set ::dialog_audio::out_enable {}
array set ::dialog_audio::out_device {}
array set ::dialog_audio::out_channels {}

# TODO this panel really needs some reworking, it works but the code is very
# unreadable.  The panel could look a lot better too, like using menubuttons
# instead of regular buttons with tk_popup for pulldown menus.

####################### audio dialog ##################

# serialize the current configuration into a list so we can compare it with a reference
# indevice(*) inchannels(*) inenabled(*) outdevice(*) outchannels(*) outenabled(*) sr advance cb blocksize
proc ::dialog_audio::checkedval { reference_index varname {goodexpr_v true} {normalizeexpr_v {$v} } } {
    set fallback [lindex $::dialog_audio::referenceconfig ${reference_index}]
    upvar 0 ::dialog_audio::${varname} v

    if { $v eq {} } {
        # empty!
        set v ${fallback}
    } elseif [expr ${goodexpr_v}] {
        # all good, nothing to see here
    } else {
        # !good
        set v ${fallback}
    }
    return [set v [expr ${normalizeexpr_v}]]
}

proc ::dialog_audio::serialize_config { } {
    set reference $::dialog_audio::referenceconfig
    set result {}
    set idx -1

    foreach dir {in out} {
        for {set x 1} {$x <= 4} {incr x} {
            # deviceD must be >= -1
            lappend result [checkedval [incr idx] ${dir}_device($x) { [string is int $v ] && $v >= -1 }]
        }
        for {set x 1} {$x <= 4} {incr x} {
            # channels must be >= 0
            lappend result [checkedval [incr idx] ${dir}_channels($x) { [string is int $v ] && $v >= 0 }]
        }
        for {set x 1} {$x <= 4} {incr x} {
            # enable must be 0 or 1
            lappend result [checkedval [incr idx] ${dir}_enable($x) { [string is boolean $v] } { [ string is true $v ] }]
        }
    }

    lappend result [checkedval [incr idx] samplerate   { [string is double $v] && $v > 0 }]
    lappend result [checkedval [incr idx] advance      { [string is double $v] && $v >= 0 }]
    lappend result [checkedval [incr idx] use_callback { [string is boolean $v] } { [ string is true $v ] }]
    lappend result [checkedval [incr idx] blocksize    { [string is int $v] && $v > 0 }]

    return $result
}

proc ::dialog_audio::apply {mytoplevel {force ""}} {
    set config [serialize_config]
    if { $force ne "" || $config ne $::dialog_audio::referenceconfig} {
        set msg "pd audio-dialog"
        # indevices
        set msg [concat $msg [lrange $config 0 3]]
        # inchannels (negative is "disabled")
        set msg [concat $msg [lmap channels [lrange $config 4 7] enabled [lrange $config 8 11] {expr $channels * ( $enabled ? 1 : -1)}]]
        # outdevices
        set msg [concat $msg [lrange $config 12 15]]
        # outchannels (negative is "disabled")
        set msg [concat $msg [lmap channels [lrange $config 16 19] enabled [lrange $config 20 23] {expr $channels * ( $enabled ? 1 : -1)}]]

        # samplerate advance callbacks blocksize
        set msg [concat $msg [lrange $config 24 27]]

        pdsend ${msg}
    }
    set ::dialog_audio::referenceconfig ${config}
}

proc ::dialog_audio::cancel {mytoplevel} {
    destroy $mytoplevel
}

proc ::dialog_audio::ok {mytoplevel} {
    ::dialog_audio::apply $mytoplevel 1
    ::dialog_audio::cancel $mytoplevel
}


# check if the value has an 'unchangeable' marker (a '!'-prefix)
# returns the value (without the marker) and a boolean whether the marker was set
# e.g. '!44100' -> {44100 1}
proc ::dialog_audio::isfixed {value} {
    set fixed 0
    if { [string match "!*" ${value}] } {
        set fixed 1
    }
    list [string trimleft "${value}" "!"] $fixed
}

# enables/disables a number of widgets ('args')) based on 'state' (0/1)
proc ::dialog_audio::state2widgets {state args} {
    set s "normal"
    if { $state == 0 } {
        set s "disabled"
    }
    foreach w $args {
        if {[winfo exist $w.menu]} {
            if {[$w.menu index last] < 1} {
                $w configure -state disabled
                continue
            }
        }
        $w configure -state $s
    }

}

proc ::dialog_audio::fill_frame_device {frame direction index} {
    ## side-effects:
    ## ro: ::audio_${direction}devlist
    ## rw: ::dialog_audio::${direction}_device(${index})
    ##
    ## rW: ::dialog_audio::${direction}_device(${index})
    ## rW: ::dialog_audio::${direction}_enable(${index})
    ## rW: ::dialog_audio::${direction}_channels(${index})
    upvar ::dialog_audio::${direction}_device(${index}) x
    upvar ::audio_${direction}devlist devlist
    upvar ::dialog_audio::${direction}_enable(${index}) enabled
    set device [lindex $devlist $x]
    if { "${device}" eq "" } {set device [format "(%s)" [_ "no device" ]] }

    checkbutton $frame.x0 -variable ::dialog_audio::${direction}_enable(${index}) \
        -text "${index}:" -anchor e \
        -command "::dialog_audio::state2widgets \$::dialog_audio::${direction}_enable(${index})  $frame.x1 $frame.x2"

    if { $::audio_can_multidevice < 1 && $direction eq "out" } {
        label $frame.x1 -text [_ "(same as input device)..."]
    } else {
        menubutton $frame.x1 -indicatoron 1 -menu $frame.x1.menu \
            -relief raised -highlightthickness 1 -anchor c \
            -direction flush \
            -text $device
        menu $frame.x1.menu -tearoff 0
        set idx 0
        foreach input $devlist {
            $frame.x1.menu add radiobutton \
                -label "${input}" \
                -value ${idx} -variable ::dialog_audio::${direction}_device(${index}) \
                -command "$frame.x1 configure -text \"${input}\""
            incr idx
        }
    }
    label $frame.l2 -text [_ "Channels:"]
    foreach {dummy v} [array get ::dialog_audio::${direction}_channels ${index}] {break}
    spinbox $frame.x2 \
        -width 3 \
        -values {1 2 4 6 8 10 12 14 16 24 64 128} \
        -textvariable ::dialog_audio::${direction}_channels(${index})
    # spinbox's '-values' option resets the textvariable, so we need to restore it
    array set ::dialog_audio::${direction}_channels [list ${index} ${v}]

    set mytoplevel [winfo toplevel $frame]
    if {[winfo exists $mytoplevel.buttonframe.ok]} {
        # this is being called from the standalone audio dialog
        ::preferencewindow::entryfocus $frame.x2 $mytoplevel.buttonframe.ok "::dialog_audio::ok $mytoplevel"
    } else {
        # this is being called from the tabbed preferences
        ::preferencewindow::entryfocus $frame.x2
    }

    pack $frame.x0 -side left
    pack $frame.x1 -side left -fill x -expand 1
    pack $frame.x2 $frame.l2 -side right


    ::dialog_audio::state2widgets $enabled $frame.x1  $frame.x2
}
proc ::dialog_audio::fill_frame_devices {frame direction maxdevs} {
    # side effects:
    ## ro: devlist_::audio_${direction}devlist

    upvar ::audio_${direction}devlist devlist
    set numdevs [llength $devlist]
    if { $maxdevs > $numdevs } { set maxdevs $numdevs }
    for {set i 1} {$i <= $maxdevs} {incr i} {
        frame ${frame}.${i}f
        pack ${frame}.${i}f -side top -fill x
        fill_frame_device ${frame}.${i}f ${direction} ${i}
    }
}

proc ::dialog_audio::fill_frame_iodevices {frame maxdevs longform} {
    # side effects: none

    if {[winfo exists $frame]} {
        destroy $frame
    }
    frame $frame
    pack  $frame -side top -fill x -anchor n -expand 1

    set showdevs $maxdevs
    if {$longform == 0} {
        set showdevs 1
    }
    # input devices
    labelframe $frame.inputs -text [_ "Input Devices"] -padx 5 -pady 5 -borderwidth 1
    pack $frame.inputs -side top -fill x -pady 5
    ::dialog_audio::fill_frame_devices $frame.inputs in ${showdevs}

    # output devices
    labelframe $frame.outputs -text [_ "Output Devices"] -padx 5 -pady 5 -borderwidth 1
    pack $frame.outputs -side top -fill x -pady 5
    ::dialog_audio::fill_frame_devices $frame.outputs out ${showdevs}

    # If not the "long form" but if "multi" is 2, make a button to
    # restart with longform set.
    if {$longform == 0 && $maxdevs > 1} {
        set mytoplevel [winfo toplevel $frame]

        frame $frame.longbutton
        pack $frame.longbutton -side right -fill x
        button $frame.longbutton.b -text [_ "Use Multiple Devices"] \
            -command  "::dialog_audio::fill_frame_iodevices $frame $maxdevs 1"
        if {[winfo exists $mytoplevel.buttonframe.ok]} {
            # this is being called from the standalone audio dialog
            ::preferencewindow::buttonfocus $frame.longbutton.b $mytoplevel.buttonframe.ok "::dialog_audio::ok $mytoplevel"
        } else {
            # this is being called from the tabbed preferences
            ::preferencewindow::buttonfocus $frame.longbutton.b
        }
        pack $frame.longbutton.b -expand 1 -ipadx 10 -pady 5
    }

    frame $frame.refreshbutton
    pack $frame.refreshbutton -side bottom -fill x
    button $frame.refreshbutton.b -text [_ "Rescan Devices"] \
        -command {pdsend {pd rescan-audio} }
    pack $frame.refreshbutton.b -expand 1 -ipadx 10 -pady 5
}



proc ::dialog_audio::fill_frame {frame {include_backends 1}} {
    # side-effects:
    # rW: ::dialog_audio::samplerate
    # rW: ::dialog_audio::advance
    # rW: ::dialog_audio::use_callback
    ::dialog_audio::init_devicevars

    if { $include_backends != 0 && [llength $::audio_apilist] > 1} {
        labelframe $frame.backend -text [_ "Audio System"] -padx 5 -pady 5 -borderwidth 1
        pack $frame.backend -side top -fill x -pady 5

        menubutton $frame.backend.api -indicatoron 1 -menu $frame.backend.api.menu \
            -relief raised -highlightthickness 1 -anchor c \
            -direction flush \
            -text [_ "Audio System" ]
        menu $frame.backend.api.menu -tearoff 0

        foreach api $::audio_apilist {
            foreach {api_name api_id} $api {
                $frame.backend.api.menu add radiobutton \
                    -label "${api_name}" \
                    -value ${api_id} -variable ::pd_whichapi \
                    -command {pdsend [list pd audio-setapi $::pd_whichapi]}
                if { ${api_id} == ${::pd_whichapi} } {
                    $frame.backend.api configure -text "${api_name}"
                }
            }
        }
        pack $frame.backend.api -side left -fill x -expand 1
    }

    # settings
    labelframe $frame.settings -text [_ "Settings"] -padx 5 -pady 5 -borderwidth 1
    pack $frame.settings -side top -fill x -pady 5

    frame $frame.settings.srd
    pack $frame.settings.srd -side top -fill x
    label $frame.settings.srd.sr_label -text [_ "Sample rate:"]
    entry $frame.settings.srd.sr_entry -textvariable ::dialog_audio::samplerate -width 8
    ::preferencewindow::entryfocus $frame.settings.srd.sr_entry
    label $frame.settings.srd.d_label -text [_ "Delay (msec):"]
    entry $frame.settings.srd.d_entry -textvariable ::dialog_audio::advance -width 4
    ::preferencewindow::entryfocus $frame.settings.srd.d_entry

    pack $frame.settings.srd.sr_label $frame.settings.srd.sr_entry -side left
    pack $frame.settings.srd.d_entry $frame.settings.srd.d_label -side right
    frame $frame.settings.bsc
    pack $frame.settings.bsc -side top -fill x
    button $frame.settings.bsc.rate1 -text [_ "48k"] \
        -command "set ::dialog_audio::samplerate 48000"
    button $frame.settings.bsc.rate2 -text [_ "44.1k"] \
        -command "set ::dialog_audio::samplerate 44100"
    button $frame.settings.bsc.rate3 -text [_ "96k"] \
        -command "set ::dialog_audio::samplerate 96000"
    pack $frame.settings.bsc.rate1 \
        $frame.settings.bsc.rate2 \
        $frame.settings.bsc.rate3 \
         -side left

    label $frame.settings.bsc.bs_label -text [_ "Block size:"]
    set blocksizes {64 128 256 512 1024 2048}
    set bsmenu \
        [eval tk_optionMenu $frame.settings.bsc.bs_popup ::dialog_audio::blocksize $blocksizes]
    pack $frame.settings.bsc.bs_popup -side right
    pack $frame.settings.bsc.bs_label -side right -padx {0 10}


    frame $frame.settings.callback
    pack $frame.settings.callback -side bottom -fill x
    checkbutton $frame.settings.callback.c_button -variable ::dialog_audio::use_callback \
        -text [_ "Use callbacks"]
    pack $frame.settings.callback.c_button


    if {$::dialog_audio::isfixed_samplerate} {
        $frame.settings.srd.sr_entry config -state "disabled"
        $frame.settings.bsc.rate1 config -state "disabled"
        $frame.settings.bsc.rate2 config -state "disabled"
        $frame.settings.bsc.rate3 config -state "disabled"
    }

    if {$::dialog_audio::isfixed_advance} {
        $frame.settings.srd.d_entry config -state "disabled"
    }

    if {$::dialog_audio::isfixed_blocksize} {
        $frame.settings.bsc.bs_popup config -state "disabled"
    }

    if {$::dialog_audio::isfixed_use_callback} {
        $frame.settings.callback.c_button config -state "disabled"
    }


    # FIXXME: longform+multi -> 4 else 1
    if { $::audio_can_multidevice <= 1 } {
        set maxdevs 1
    } else {
        set maxdevs 4
    }
    # FIXXME: use longform by default if...
    set longform 0

    frame $frame.iodevf
    pack  $frame.iodevf -side top -anchor n -fill x -expand 1
    ::dialog_audio::fill_frame_iodevices $frame.iodevf.contentf $maxdevs $longform
}


proc ::dialog_audio::init_devicevars {} {
    foreach direction {in out} {
        # input vars
        upvar ::audio_${direction}devices devices
        upvar ::audio_${direction}devicechannels channels
        for {set i 1} {$i <= 4} {incr i} {
            # output vars
            upvar ::dialog_audio::${direction}_device(${i}) dev
            upvar ::dialog_audio::${direction}_channels(${i}) chan
            upvar ::dialog_audio::${direction}_enable(${i}) enabled

            if { [llength $devices] > 0 } {
                set dev [lindex $devices ${i}-1]
            } else {
                set dev -1
            }
            set c [lindex $channels ${i}-1]
            set chan [expr ( $c > 0 ? $c : -$c ) ]
            set enabled [expr $c > 0 ]
        }
    }
    # resolve immutable variables (prepended with '!')
    foreach v {samplerate blocksize advance use_callback} {
        upvar ::dialog_audio::${v} var
        upvar ::dialog_audio::isfixed_${v} fixed
        upvar ::audio_${v} invar
        foreach {var fixed} [isfixed $invar] {}
    }
    set ::dialog_audio::referenceconfig [serialize_config]

}

proc ::dialog_audio::set_configuration { \
        available_inputs indevices inchannels \
        available_outputs outdevices outchannels \
        samplerate blocksize advance \
        use_callback can_multidevice} {
    set ::audio_indevlist ${available_inputs}
    set ::audio_indevices [lmap _ ${indevices} {expr int("${_}")}]
    set ::audio_indevicechannels [lmap _ ${inchannels} {expr int("${_}")}]

    set ::audio_outdevlist ${available_outputs}
    set ::audio_outdevices  [lmap _ ${outdevices} {expr int("${_}")}]
    set ::audio_outdevicechannels [lmap _ ${outchannels} {expr int("${_}")}]

    set ::audio_samplerate ${samplerate}
    set ::audio_blocksize ${blocksize}
    set ::audio_advance ${advance}
    set ::audio_use_callback ${use_callback}
    set ::audio_can_multidevice ${can_multidevice}
}

proc ::dialog_audio::refresh_ui {} {
    # check if we're in the tabbed preferences
    if {[winfo exists ${::dialog_preferences::audio_frame}]} {
        ::preferencewindow::removechildren ${::dialog_preferences::audio_frame}
        ::dialog_audio::fill_frame ${::dialog_preferences::audio_frame}
    }
    # or in the standalone audio dialog
    if {[winfo exists $::dialog_audio::standalone_window]} {
        destroy $::dialog_audio::standalone_window
        ::dialog_audio::create $::dialog_audio::standalone_window
    }
}

proc ::dialog_audio::create {mytoplevel} {
    # check if there's already an open gui-preference
    # where we can splat the new audio preferences into
    if {[winfo exists ${::dialog_preferences::audio_frame}]} {
        ::preferencewindow::removechildren ${::dialog_preferences::audio_frame}
        ::dialog_audio::fill_frame ${::dialog_preferences::audio_frame}
        return {}
    }

    # destroy leftover dialogs
    destroy $mytoplevel
    # create a dialog window
    toplevel $mytoplevel -class DialogWindow
    wm withdraw $mytoplevel
    wm title $mytoplevel [_ "Audio Settings"]
    wm group $mytoplevel .
    wm resizable $mytoplevel 0 0
    wm transient $mytoplevel
    wm minsize $mytoplevel 380 320
    ::pd_menus::menubar_for_dialog $mytoplevel
    $mytoplevel configure -padx 10 -pady 5
    ::pd_bindings::dialog_bindings $mytoplevel "audio"

    # add the preference widgets to the dialog
    ::dialog_audio::fill_frame $mytoplevel 0

    # save all settings button
    button $mytoplevel.saveall -text [_ "Save All Settings"] \
        -command "::dialog_audio::apply $mytoplevel 1; pdsend {pd save-preferences}"
    pack $mytoplevel.saveall -side top -expand 1 -ipadx 10 -pady 5

    # buttons
    frame $mytoplevel.buttonframe
    pack $mytoplevel.buttonframe -side top -after $mytoplevel.saveall -pady 2m
    button $mytoplevel.buttonframe.cancel -text [_ "Cancel"] \
        -command "::dialog_audio::cancel $mytoplevel"
    pack $mytoplevel.buttonframe.cancel -side left -expand 1 -fill x -padx 15 -ipadx 10
    button $mytoplevel.buttonframe.apply -text [_ "Apply"] \
        -command "::dialog_audio::apply $mytoplevel 1"
    pack $mytoplevel.buttonframe.apply -side left -expand 1 -fill x -padx 15 -ipadx 10
    button $mytoplevel.buttonframe.ok -text [_ "OK"] \
        -command "::dialog_audio::ok $mytoplevel" -default active
    pack $mytoplevel.buttonframe.ok -side left -expand 1 -fill x -padx 15 -ipadx 10


    ::preferencewindow::entryfocus $mytoplevel.settings.srd.sr_entry $mytoplevel.buttonframe.ok "::dialog_audio::ok $mytoplevel"
    ::preferencewindow::entryfocus $mytoplevel.settings.srd.d_entry $mytoplevel.buttonframe.ok "::dialog_audio::ok $mytoplevel"

    # for focus handling on OSX
    if {$::windowingsystem eq "aqua"} {
        # remove cancel button from focus list since it's not activated on Return
        $mytoplevel.buttonframe.cancel config -takefocus 0

        # show active focus on the ok button as it *is* activated on Return
        $mytoplevel.buttonframe.ok config -default normal
        bind $mytoplevel.buttonframe.ok <FocusIn> "$mytoplevel.buttonframe.ok config -default active"
        bind $mytoplevel.buttonframe.ok <FocusOut> "$mytoplevel.buttonframe.ok config -default normal"

        # since we show the active focus, disable the highlight outline
        $mytoplevel.buttonframe.ok config -highlightthickness 0
        $mytoplevel.buttonframe.cancel config -highlightthickness 0
    }

    # show active focus on multiple device button
    if {[winfo exists $mytoplevel.longbutton.b]} {
        ::preferencewindow::buttonfocus $mytoplevel.longbutton.b $mytoplevel.buttonframe.ok "::dialog_audio::ok $mytoplevel"
    }

    # show active focus on save settings button
    ::preferencewindow::buttonfocus $mytoplevel.saveall $mytoplevel.buttonframe.ok "::dialog_audio::ok $mytoplevel"


    # set min size based on widget sizing & pos over pdwindow
    wm minsize $mytoplevel [winfo reqwidth $mytoplevel] [winfo reqheight $mytoplevel]
    position_over_window $mytoplevel .pdwindow
    raise $mytoplevel
    set ::dialog_audio::standalone_window $mytoplevel
}

# legacy proc forthe audio-dialog
# (LATER: just remove it)
#
# start a dialog window to select audio devices and settings.  "multi"
# is 0 if only one device is allowed; 1 if one apiece may be specified for
# input and output; and 2 if we can select multiple devices.  "longform"
# (which only makes sense if "multi" is 2) asks us to make controls for
# opening several devices; if not, we get an extra button to turn longform
# on and restart the dialog.
#
# sr, advance, callback and blocksize can be prefixed with '!', indicating
# that these values must not be changed by the GUI
proc ::dialog_audio::pdtk_audio_dialog {mytoplevel \
        indev1 indev2 indev3 indev4 \
        inchan1 inchan2 inchan3 inchan4 \
        outdev1 outdev2 outdev3 outdev4 \
        outchan1 outchan2 outchan3 outchan4 \
        sr advance multi callback \
        longform blocksize} {
    # initialize variables
    ::dialog_audio::set_configuration \
        ${::audio_indevlist} \
        [list $indev1 $indev2 $indev3 $indev4] \
        [list $inchan1 $inchan2 $inchan3 $inchan4] \
        ${::audio_outdevlist} \
        [list $outdev1 $outdev2 $outdev3 $outdev4] \
        [list $outchan1 $outchan2 $outchan3 $outchan4] \
        $sr $blocksize $advance $callback $multi

    ::dialog_audio::create $mytoplevel
}

# for focus handling on OSX
proc ::dialog_audio::rebind_return {mytoplevel} {
    bind $mytoplevel <KeyPress-Return> "::dialog_audio::ok $mytoplevel"
    focus $mytoplevel.buttonframe.ok
    return 0
}

# for focus handling on OSX
proc ::dialog_audio::unbind_return {mytoplevel} {
    bind $mytoplevel <KeyPress-Return> break
    return 1
}
