--- Softcut module.
--
-- API for controlling the "softcut" buffer processor
-- includes low-level setters and mid-level utilities
--
-- IMPORTANT: all indices are 1-based, per lua convention!
-- this applies to indices for selecting voice, ADC/DAC channel, buffer, &c.
-- however, _quantities_ (including units of time) can still start at zero.
--
-- @module softcut
-- @alias SC

local SC = {}

local controlspec = require 'core/controlspec'

-------------------------------
-- @section constants

-- @field number of voices
SC.VOICE_COUNT = 6
-- @field length of buffer in seconds
SC.BUFFER_SIZE = 16777216 / 48000

-------------------------------
-- @section setters

--- set output level of each voice
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.level = function(voice, amp)
  _norns.level_cut(voice, amp)
end

--- set pan position of each voice.
-- -1 == full left, +1 == full right, 0 == centered
-- @tparam int voice : voice index
-- @tparam number pos : position in [-1, 1]
SC.pan = function(voice, pos)
  _norns.pan_cut(voice, pos)
end

--- set input level to each voice/channel.
-- @tparam int ch : ADC channel index
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.level_input_cut = function(ch, voice, amp)
  _norns.level_input_cut(ch, voice, amp)
end

--- set mix matrix, voice output to voice input.
-- @tparam number src : source voice index
-- @tparam number dst : destination voice index
-- @tparam number amp : linear amplitude
SC.level_cut_cut = function(src, dst, amp)
  _norns.level_cut_cut(src, dst, amp)
end

--- set play status.
-- @tparam int voice : voice index
-- @tparam int state : off/on (0,1)
SC.play = function(voice,state) _norns.cut_param("play_flag",voice,state) end

--- set playback rate.
-- @tparam int voice : voice index
-- @tparam number rate : speed of read/write head (unitless; 1 == normal)
SC.rate = function(voice,rate) _norns.cut_param("rate",voice,rate) end

--- set loop start.
-- @tparam int voice : voice index
-- @tparam number pos : loop start position in seconds
SC.loop_start = function(voice,pos) _norns.cut_param("loop_start",voice,pos) end

--- set loop end.
-- @tparam int voice : voice index
-- @tparam number pos : loop end position in seconds
SC.loop_end = function(voice,pos) _norns.cut_param("loop_end",voice,pos) end

--- set loop mode.
-- "0" indicates one-shot mode: voice will play to the loop endpoint, fadeout and stop.
-- "1" indicates crossfaded looping mode.
-- @tparam int voice : voice index
-- @tparam int state : off/on (0,1)
SC.loop = function(voice,state) _norns.cut_param("loop_flag",voice,state) end

--- set fade time.
-- @tparam int voice : voice index
-- @tparam number fade_time : crossfade time in seconds
SC.fade_time = function(voice,fade_time) _norns.cut_param("fade_time",voice,fade_time) end

--- set record level.
-- this sets the realtime-modulated record level,
-- by which incoming signal is scaled before writing to the buffer
-- `recpre` slew level applies
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude of new signal
SC.rec_level = function(voice,amp) _norns.cut_param("rec_level",voice,amp) end

--- set pre level (overdub preserve.)
-- this sets the realtime-modulated "preserve" level,
-- by which existing material is scaled on each pass of the write head.
-- `recpre` slew level applies
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude of preserved signal
SC.pre_level = function(voice,amp) _norns.cut_param("pre_level",voice,amp) end

--- set record state.
-- @tparam int voice : voice number (1-?)
-- @tparam int state : off/on (0,1)
SC.rec = function(voice,state) _norns.cut_param("rec_flag",voice,state) end
--- set record head offset
SC.rec_offset = function(voice,value) _norns.cut_param("rec_offset",voice,value) end
--- set play position
SC.position = function(voice,value) _norns.cut_param("position",voice,value) end

--- specify buffer used by voice.
-- @tparam int i : voice number
-- @tparam int b : buffer number (1,2)
SC.buffer = function(i,b) _norns.cut_param_ii("buffer",i,b) end

--- synchronize two voices.
--- position of "dst" will be immediately set to that of "source"
-- @tparam int dst : destination voice index
-- @tparam int src : source voice index
-- @tparam number offset : additional offset in seconds
SC.voice_sync = function(dst, src, offset) _norns.cut_param_iif("voice_sync",dst,src,offset) end

--- set pre_filter cutoff frequency.
--- @tparam int voice : voice index
--- @tparam number fc : cutoff frequency in Hz
SC.pre_filter_fc = function(voice,fc) _norns.cut_param("pre_filter_fc",voice,fc) end

--- set pre_filter amount of rate modulation.
-- this parameter controls the amount by which the current rate affects filter cutoff frequency
-- (always in a negative direction, towards zero.)
-- with mod == 1, setting rate = 0 will also fully close the filter.
-- this can be useful as a crude anti-aliasing method...
--- @tparam int voice : voice index
--- @tparam number amount : modulation amount in [0, 1]
SC.pre_filter_fc_mod = function(voice,amount) _norns.cut_param("pre_filter_fc_mod",voice,amount) end

--- set pre_filter reciprocal of Q-factor.
-- the reciprocal of the filter's Q-factor is a measure of bandwidth,
-- that is independent of center frequency.
-- RQ ~= 0 will result in self-oscillation;
-- RQ == 4 gives a bandwidth of 2 octaves.
-- @tparam int voice : voice index
-- @tparam number rq : reciprocal of filter Q-factor for voice
SC.pre_filter_rq = function(voice,rq) _norns.cut_param("pre_filter_rq",voice,rq) end

--- set pre_filter lowpass output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.pre_filter_lp = function(voice,amp) _norns.cut_param("pre_filter_lp",voice,amp) end

--- set pre-filter highpass output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.pre_filter_hp = function(voice,amp) _norns.cut_param("pre_filter_hp",voice,amp) end

--- set pre-filter bandpass output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.pre_filter_bp = function(voice,amp) _norns.cut_param("pre_filter_bp",voice,amp) end

--- set pre_filter band-reject output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.pre_filter_br = function(voice,amp) _norns.cut_param("pre_filter_br",voice,amp) end

--- set pre_filter dry output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.pre_filter_dry = function(voice,amp) _norns.cut_param("pre_filter_dry",voice,amp) end


---------------------
-- wrappers around pre_filter, for backwards compatibility
SC.filter_fc = function(voice,value) SC.pre_filter_fc(voice, value) end
SC.filter_fc_mod = function(voice,value) SC.pre_filter_fc_mod(voice, value) end
SC.filter_rq = function(voice,value) SC.pre_filter_rq(voice, value) end
SC.filter_lp = function(voice,value) SC.pre_filter_lp(voice, value) end
SC.filter_hp = function(voice,value) SC.pre_filter_hp(voice, value) end
SC.filter_bp = function(voice,value) SC.pre_filter_bp(voice, value) end
SC.filter_br = function(voice,value) SC.pre_filter_br(voice, value) end
SC.filter_dry = function(voice,value) SC.pre_filter_dry(voice, value) end

--- set post-filter cutoff
-- @tparam int voice : voice index
-- @tparam number value : cutoff frequency in Hz
SC.post_filter_fc = function(voice,value) _norns.cut_param("post_filter_fc",voice,value) end

--- set post-filter reciprocal of Q
-- the reciprocal of the filter's Q factor is a measure of bandwidth,
-- that is independent of center frequency.
-- RQ ~= 0 will result in self oscillation;
-- RQ == 4 gives a bandwidth of 2 octaves.
-- @tparam int voice : voice index
-- @tparam number rq : reciprocal of filter Q-factor for voice
SC.post_filter_rq = function(voice,rq) _norns.cut_param("post_filter_rq",voice,rq) end


--- set post_filter lowpass output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.post_filter_lp = function(voice,amp) _norns.cut_param("post_filter_lp",voice,amp) end

--- set post-filter highpass output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.post_filter_hp = function(voice,amp) _norns.cut_param("post_filter_hp",voice,amp) end

--- set post-filter bandpass output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.post_filter_bp = function(voice,amp) _norns.cut_param("post_filter_bp",voice,amp) end

--- set post_filter band-reject output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.post_filter_br = function(voice,amp) _norns.cut_param("post_filter_br",voice,amp) end

--- set post_filter dry output level.
-- @tparam int voice : voice index
-- @tparam number amp : linear amplitude
SC.post_filter_dry = function(voice,amp) _norns.cut_param("post_filter_dry",voice,amp) end

--- set level-slew time
-- this slew time applies to level at all mix points: ADC->voice, voice->voice, &c.
-- @tparam int voice : voice index
-- @tparam number time : exponential slew time in seconds (-60db convergence)
SC.level_slew_time = function(voice,time) _norns.cut_param("level_slew_time",voice,time) end

--- set pan slew time
-- @tparam int voice : voice index
-- @tparam number time : exponential slew time in seconds (-60db convergence)
SC.pan_slew_time = function(voice,time) _norns.cut_param("pan_slew_time",voice,time) end

--- set recpre slew time
-- affects slew time for record and pre levels
-- @tparam int voice : voice index
-- @tparam number time : exponential slew time in seconds (-60db convergence)
SC.recpre_slew_time = function(voice,time) _norns.cut_param("recpre_slew_time",voice,time) end

--- set rate slew time
-- affects slew time for rate
-- @tparam int voice : voice index
-- @tparam number time : exponential slew time in seconds (-60db convergence)
SC.rate_slew_time = function(voice,time) _norns.cut_param("rate_slew_time",voice,time) end

--- set phase poll quantum
-- e.g. 0.25 will produce 4 updates per second with rate=1
-- judicious use of this parameter is preferable to using a very fast poll (for performance,)
-- or polling at arbitrary rate (for accuracy when rate is slewed.)
-- @tparam int voice : voice index
-- @tparam number quantum : phase reporting interval, in seconds
SC.phase_quant = function(voice,quantum) _norns.cut_param("phase_quant",voice,quantum) end

--- set phase poll offset in frames
-- @tparam int voice : voice index
-- @tparam number offset : phase poll offset in seconds
SC.phase_offset = function(voice,offset) _norns.cut_param("phase_offset",voice,offset) end

--- start phase poll
SC.poll_start_phase = function() _norns.poll_start_cut_phase() end

--- stop phase poll
SC.poll_stop_phase = function() _norns.poll_stop_cut_phase() end


--- set voice enable
-- disabled voices have no effect and consume basically zero CPU
-- @tparam int voice : voice number (1-?)
-- @tparam int state : off/on (0,1)
SC.enable = function(voice, state) _norns.cut_enable(voice, state) end

-- - TODO: complete function doc comments below here!

--- clear all buffers completely
SC.buffer_clear = function() _norns.cut_buffer_clear() end

--- clear one buffer completely
-- @tparam int channel : buffer channel index (1-based)
SC.buffer_clear_channel = function(channel) _norns.cut_buffer_clear_channel(channel) end

--- clear region (both channels)
-- @tparam number start : start point in seconds
-- @tparam number dur : duration in seconds
-- @tparam number fade_time : crossfade time in seconds
-- @tparam number preserve : level of existing material
SC.buffer_clear_region = function(start, dur, fade_time, preserve)
  _norns.cut_buffer_clear_region(start, dur or -1, fade_time or 0, preserve or 0)
end

--- clear region of single channel
-- @tparam int ch : buffer channel index (1-based)
-- @tparam number start : start point in seconds
-- @tparam number dur : duration in seconds
-- @tparam number fade_time : crossfade time in seconds
-- @tparam number preserve : level of existing material
SC.buffer_clear_region_channel = function(ch, start, dur, fade_time, preserve)
  _norns.cut_buffer_clear_region_channel(ch, start, dur or -1, fade_time or 0, preserve or 0)
end

--- copy region from one point in a buffer to another
-- @tparam int src_ch : source buffer index (1-based)
-- @tparam int dst_ch : destination buffer index (1-based)
-- @tparam number start_src : start point in source, in seconds
-- @tparam number start_dst : start point in destination, in seconds
-- @tparam number dur : duration in seconds. if -1, copy as much as possible.
-- @tparam number fade_time : crossfade time in seconds
-- @tparam number preserve : level of existing material
-- @tparam int reverse : nonzero to reverse while copying. when reversing, overlap between source and destination regions is not handled.
SC.buffer_copy_mono = function(src_ch, dst_ch, start_src, start_dst, dur, fade_time, preserve, reverse)
  _norns.cut_buffer_copy_mono(src_ch, dst_ch, start_src, start_dst, dur or -1, fade_time or 0, preserve or 0, reverse or 0)
end

--- copy region of both buffers to another point
-- @tparam number start_src : start point in source, in seconds
-- @tparam number start_dst : start point in destination, in seconds
-- @tparam number dur : duration in seconds. if -1, copy as much as possible.
-- @tparam number fade_time : crossfade time in seconds
-- @tparam number preserve : level of existing material
-- @tparam int reverse : nonzero to reverse while copying.
SC.buffer_copy_stereo = function(start_src, start_dst, dur, fade_time, preserve, reverse)
  _norns.cut_buffer_copy_stereo(start_src, start_dst, dur or -1, fade_time or 0, preserve or 0, reverse or 0)
end

--- read mono soundfile to arbitrary region of single buffer
-- @tparam string file : input file path
-- @tparam number start_src : start point in source, in seconds
-- @tparam number start_dst : start point in destination, in seconds
-- @tparam number dur : duration in seconds. if -1, read as much as possible.
-- @tparam int ch_src : soundfile channel to read
-- @tparam int ch_dst : buffer channel to write
-- @tparam number preserve : level of existing material
-- @tparam number mix : level of new material
SC.buffer_read_mono = function(file, start_src, start_dst, dur, ch_src, ch_dst, preserve, mix)
  _norns.cut_buffer_read_mono(file, start_src or 0, start_dst or 0, dur or -1, ch_src or 1, ch_dst or 1, preserve or 0, mix or 1)
end

--- read stereo soundfile to an arbitrary region in both buffers
-- @tparam string file : input file path
-- @tparam number start_src : start point in source, in seconds
-- @tparam number start_dst : start point in destination, in seconds
-- @tparam number dur : duration in seconds. if -1, read as much as possible
-- @tparam number preserve : level of existing material
-- @tparam number mix : level of new material
SC.buffer_read_stereo = function(file, start_src, start_dst, dur, preserve, mix)
  _norns.cut_buffer_read_stereo(file, start_src or 0, start_dst or 0, dur or -1, preserve or 0, mix or 1)
end

--- write an arbitrary buffer region to soundfile (mono)
-- @tparam string file : output file path
-- @tparam number start : start point in seconds
-- @tparam number dur : duration in seconds. if -1, read as much as possible
-- @tparam int ch : buffer channel index (1-based)
SC.buffer_write_mono = function(file, start, dur, ch)
  _norns.cut_buffer_write_mono(file, start or 0, dur or -1, ch or 1)
end

--- write an arbitrary region from both buffers to stereo soundfile
-- @tparam string file : output file path
-- @tparam number start : start point in seconds
-- @tparam number dur : duration in seconds. if -1, read as much as possible
SC.buffer_write_stereo = function(file, start, dur)
  _norns.cut_buffer_write_stereo(file, start or 0, dur or -1)
end

--- set function for phase poll
-- @tparam function func : callback function. this function should take two parameters  (voice, phase)
SC.event_phase = function(func) _norns.softcut_phase = func end

--- request snapshot of buffer content for region.
-- @tparam integer ch : buffer channel index (1-based)
-- @tparam number start : beginning of region in seconds
-- @tparam number dur : length of region in seconds
-- @tparam integer samples : max number of samples to retrieve. if less than the number of frames in the region, content will be downsampled
SC.render_buffer = function(ch, start, dur, samples)
  _norns.cut_buffer_render(ch, start, dur, samples)
end

--- set function for render callback. use render_buffer to request contents.
-- @tparam function func : called when buffer content is ready. args: (ch, start, sec_per_sample, samples)
SC.event_render = function(func)
  _norns.softcut_render = func
end

--- query playback position
-- @tparam integer i : which softcut voice
SC.query_position = function(i) _norns.cut_query_position(i) end

--- set function for query callback. use query_position to request contents.
-- @tparam function func : called when index and position is returned
SC.event_position = function(func)
  _norns.softcut_position = func
end


-------------------------------
-- @section utilities

--- reset state of softcut process on backend.
-- this should correspond to the values returned by the `defaults()` function above.
function SC.reset()
   _norns.cut_reset()
  SC.event_phase(norns.none)
end

--- get the default state of the softcut system
-- this returns a table for each voice,
-- in which each key corresponds to the name of one of the setter functions defined above.
-- for parameters with one value per voice, the corresponding entry is also a single value.
-- for parameters with multiple values (e.g. matrix indices), the entry is a table.
-- NB: these values are synchronized by hand with those specified in the softcut cpp sources
-- @treturn table table of parameter states for each voice
function SC.defaults()
   zeros = {}

   for i=1,SC.VOICE_COUNT do
      zeros[i] = 0
   end

   local state = {}
   for i=1,SC.VOICE_COUNT do
      state[i] = {}

     state[i].enable = 0
     state[i].play = 0
     state[i].record = 0

     state[i].buffer = (i%2 + 1)
     state[i].level =0
     state[i].pan = 0

     state[i].level_input_cut = {0,0}
     state[i].level_cut_cut = zeros

     state[i].rate = 1
     state[i].loop_start = (i-1)*2
     state[i].loop_end = (i-1)*2+1
     state[i].loop = 1

     state[i].fade_time =  0.0005
     state[i].rec_level = 0
     state[i].pre_level = 0
     state[i].rec = 0
     state[i].rec_offset = -0.00015
     state[i].position = 0

     state[i].pre_filter_fc = 16000
     state[i].pre_filter_dry = 0
     state[i].pre_filter_lp = 1
     state[i].pre_filter_hp = 0
     state[i].pre_filter_bp = 0
     state[i].pre_filter_br = 0
     state[i].pre_filter_fc_mod = 1

     state[i].post_filter_fc = 12000
     state[i].post_filter_dry = 0
     state[i].post_filter_lp = 0
     state[i].post_filter_hp = 0
     state[i].post_filter_bp = 0
     state[i].post_filter_br = 0

     state[i].level_slew_time = 0.001
     state[i].rate_slew_time = 0.001
     state[i].phase_quant = 1
     state[i].phase_offset = 0
  end
  return state
end


--- controlspec factory
-- each table contains an entry for each softcut parameter.
-- each entry is a parameter argument list configured for that voice+param
-- @return an array of tables, one per voice.
function SC.params()
  -- @fixme should memoize
  local specs = {}
  local voice=1
  while voice <= SC.VOICE_COUNT do
    local spec = {
      -- voice enable
      enable = { type="number", min=0, max=1, default=0 },
      -- levels
      -- @fixme: use dB / taper?
      level = { type="control", controlspec=controlspec.new(0, 0, 'lin', 0, 0.25, "") },
      pan = { type="control", controlspec=controlspec.new(-1, 1, 'lin', 0, 0, "") },
      level_input_cut = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0.5, "") },
      level_cut_cut = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      -- timing
      rate = { type="control", controlspec=controlspec.new(-8, 8, 'lin', 0, 0, "") },
      loop_start = { type="control", controlspec=controlspec.new(0, SC.BUFFER_SIZE, 'lin', 0, voice*2.5, "sec") },
      loop_end = { type="control", controlspec=controlspec.new(0, SC.BUFFER_SIZE, 'lin', 0, voice*2.5 + 2, "sec") },
      loop = { type="number", min=0, max=1, default=1},
      fade_time = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      -- recording parameters
      rec_level = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      pre_level = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      play = { type="number", min=0, max=1, default=1},
      rec = { type="number", min=0, max=1, default=1},
      rec_offset = { type="number", min=-100, max=100, default=-8},
      -- jump to position
      position = { type="control", controlspec=controlspec.new(0, SC.BUFFER_SIZE, 'lin', 0, voice*2.5, "sec") },
      -- pre filter
      pre_filter_fc = { type="control", controlspec=controlspec.new(10, 12000, 'exp', 1, 12000, "Hz") },
      pre_filter_fc_mod = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 1, "") },
      pre_filter_rq = { type="control", controlspec=controlspec.new(0.0005, 8.0, 'exp', 0, 2.0, "") },
      -- @fixme use dB / taper?
      pre_filter_lp = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 1, "") },
      pre_filter_hp = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      pre_filter_bp = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      pre_filter_br = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      pre_filter_dry = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      -- post filter
      post_filter_fc = { type="control", controlspec=controlspec.new(10, 12000, 'exp', 1, 12000, "Hz") },
      post_filter_rq = { type="control", controlspec=controlspec.new(0.0005, 8.0, 'exp', 0, 2.0, "") },
      -- @fixme use dB / taper?
      post_filter_lp = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 1, "") },
      post_filter_hp = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      post_filter_bp = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      post_filter_br = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },
      post_filter_dry = { type="control", controlspec=controlspec.new(0, 1, 'lin', 0, 0, "") },

      -- slew times
      level_slew_time = { type="control", controlspec=controlspec.new(0, 8, 'lin', 0, 0, "") },
      rate_slew_time = { type="control", controlspec=controlspec.new(0, 8, 'lin', 0, 0, "") },
      -- poll quantization unit
      phase_quant = { type="control", controlspec=controlspec.new(0, 8, 'lin', 0, 0.125, "") },
    }
    -- assign name, id and action
    for k,v in pairs(spec) do
      local z = voice
      spec[k].id = k
      spec[k].name = "cut"..z..k
      local act = SC[k]
      if act == nil then
        print("warning: didn't find SoftCut voice method: "..k)
      end
      spec[k].action = function(x) act(z, x) end
    end
    specs[voice] = spec
    voice = voice + 1
  end

  return specs
end

return SC
