--- sequins
--
-- The [norns script reference](https://monome.org/docs/norns/reference/)
-- has [examples for this module](https://monome.org/docs/norns/reference/lib/sequins).
--
-- @module lib.sequins
-- @alias S

S = {}

-- convert a string to a table of chars
local function totable(t)
  if type(t) == 'string' then
    local tmp = {}
    t:gsub('.', function(c) table.insert(tmp, c) end)
    return tmp
  end
  return t
end

function S.new(t)
  t = totable(t)   -- convert a string to a table of chars
  -- wrap a table in a sequins with defaults
  local s = {
    data = t
    ,
    length = #t             -- memoize for efficiency
    ,
    ix = 1
    ,
    qix = 1                 -- force 1st value to 1st step
    ,
    n = 1                   -- store the step val (can be a sequins)
    ,
    flw = {}                -- store any applied flow modifiers
    ,
    fun = {}                -- store a transformer function & any additional arguments
  }
  return setmetatable(s, S)
end

local function wrap_index(s, ix) return ((ix - 1) % s.length) + 1 end

function S:is_sequins() return getmetatable(self) == S end

function S:setdata(t)
  if S.is_sequins(t) then
    do     -- flow modifiers
      local src = t.flw
      local dst = self.flw

      -- update / remove existing
      for k, v in pairs(dst) do
        if src[k] then                   -- already exists
          dst[k].n = src[k].n            -- update value
        else
          dst[k] = nil
        end                              -- removed
      end

      -- add any new
      for k, v in pairs(src) do
        if not dst[k] then                        -- newly added
          dst[k] = { ix = v.ix, n = v.n }         -- manual copy
        end
      end
    end

    do     -- update data of transformers
      local src = t.fun
      local dst = self.fun

      if dst[1] then            -- already have a transformer
        dst[1] = src[1]         -- copy function
        if src[2] then
          for k, v in ipairs(src[2]) do
            if S.is_sequins(v) and dst[2] and S.is_sequins(dst[2][k]) then
              dst[2][k]:settable(v)               -- recurse nested sequins
            else
              dst[2][k] = v                       -- copy piecemeal
            end
          end
        end
      elseif src[1] then       -- no existing transformer, so we just pull in the new one
        print 'new transformer'
        self.fun = t.fun
      end
    end

    -- finally, place data over top of input sequins
    t = t.data     -- handle sequins data as input table
  end

  t = totable(t)   -- convert a string to a table of chars

  -- data swap
  for i = 1, #t do
    if S.is_sequins(t[i]) and S.is_sequins(self.data[i]) then
      self.data[i]:settable(t[i])       -- recurse nested sequins
    else
      self.data[i] = t[i]               -- copy table piecemeal
    end
  end
  self.data[#t + 1] = nil -- disregard any surplus data

  self.length = #t
  self.ix = wrap_index(self, self.ix)
end

function S:copy()
  if type(self) == 'table' then
    local cp = {}
    for k, v in pairs(self) do
      cp[k] = S.copy(v)
    end
    return setmetatable(cp, getmetatable(self))
  else
    return self
  end
end

function S:peek() return self.data[self.ix] end

function S:bake(n)
  n = n or #self   -- void call creates a table of length == the parent sequins
  local t = {}
  for i = 1, n do
    t[i] = self()
  end
  return S.new(t)
end

local function turtle(t, fn)
  -- apply fn to all nested sequins
  fn = fn or S.next                          -- default to S.next
  if S.is_sequins(t) then return fn(t) end   -- unwrap
  return t                                   -- literal value
end

------------------------------
--- transformers

function S:func(fn, ...)
  self.fun = { fn, { ... } } -- capture function & any args (sequins)
  return self
end

local function do_transform(s, v)
  if s.fun[1] then   -- implicitly handles a cleared function
    if #s.fun[2] > 0 then
      return s.fun[1](v, table.unpack(s.fun[2]))
    else
      return s.fun[1](v)
    end
  else
    return v
  end
end

-- support arithmetic via math operators
S._fns = {
  add = function(n, b) return n + turtle(b) end,
  sub = function(n, b) return n - turtle(b) end,
  mul = function(n, b) return n * turtle(b) end,
  div = function(n, b) return n / turtle(b) end,
  mod = function(n, b) return n % turtle(b) end,
}

-- assume 'a' is self of a sequins
S.__add = function(a, b) return S.func(a, S._fns.add, b) end
S.__sub = function(a, b) return S.func(a, S._fns.sub, b) end
S.__mul = function(a, b) return S.func(a, S._fns.mul, b) end
S.__div = function(a, b) return S.func(a, S._fns.div, b) end
S.__mod = function(a, b) return S.func(a, S._fns.mod, b) end

------------------------------
--- control flow execution

local function do_step(s)
  -- if .qix is set, it will be used, rather than incrementing by s.n
  local newix = wrap_index(s, s.qix or s.ix + turtle(s.n))
  -- pull data from new index (unwrap if it's a sequins)
  local retval, exec = turtle(s.data[newix])
  -- handle messaging from child sequins
  if exec ~= 'again' then
    s.ix = newix; s.qix = nil
  end
  -- FIXME add protection for list of dead sequins. for now we just recur, hoping for a live sequin in nest
  if retval == 'skip' or retval == 'dead' then return S.next(s) end
  return retval, exec
end

S.flows = {
  every = function(f, n) return (f.ix % n) ~= 0 end,
  times = function(f, n) return f.ix > n end,
  count = function(f, n)
    if f.ix < n then
      return 'again'
    else
      f.ix = 0
    end
  end
}

local function do_flow(s, k)
  local f = s.flw[k]   -- check if flow-mod exists
  if f then
    f.ix = f.ix + 1
    return S.flows[k](f, turtle(f.n))
  end
end

function S.next(s)
  if do_flow(s, 'every') then return 'skip' end
  if do_flow(s, 'times') then return 'dead' end
  local again = do_flow(s, 'count')
  if again then
    local e = s.flw.every
    if e then e.ix = e.ix - 1 end     -- undo every advance
  end
  return do_transform(s, do_step(s)), again
end

function S:step(n)
  self.n = n; return self
end

function S.flow(s, k, n)
  s.flw[k] = { n = n, ix = 0 }; return s
end

function S:every(n) return self:flow('every', n) end

function S:count(n) return self:flow('count', n) end

function S:times(n) return self:flow('times', n) end

function S:all() return self:flow('count', #self) end

function S:select(ix)
  rawset(self, 'qix', ix)   -- qix may be nil, hence rawset
  return self
end

function S:reset()
  self:select(1)
  for _, v in ipairs(self.data) do turtle(v, S.reset) end
  for _, v in pairs(self.flw) do
    v.ix = 0
    turtle(v.n, S.reset)
  end
  if self.fun[1] and #self.fun[2] > 0 then
    for _, v in pairs(self.fun[2]) do
      turtle(v, S.reset)
    end
  end
end

-- ---------------------------
--  metamethods

-- calling the sequins library will create a new sequins object (S:new)
-- calling a sequins object will produce a new value (S:next)
S.__call = function(self, ...)
  return (self == S) and S.new(...) or S.next(self)
end

S.metaix = {
  settable = S.setdata
  ,
  step = S.step
  ,
  flow = S.flow
  ,
  every = S.every
  ,
  times = S.times
  ,
  count = S.count
  ,
  all = S.all
  ,
  reset = S.reset
  ,
  select = S.select
  ,
  peek = S.peek
  ,
  copy = S.copy
  ,
  map = S.func
  ,
  bake = S.bake
}
S.__index = function(self, ix)
  if type(ix) == 'number' then
    return self.data[ix]
  else
    return S.metaix[ix]
  end
end

S.__newindex = function(self, ix, v)
  if type(ix) == 'number' then
    self.data[ix] = v
  elseif ix == 'n' then
    rawset(self, ix, v)
  end
end

S.__tostring = function(t)
  -- data
  local st = {}
  for i = 1, t.length do
    st[i] = tostring(t.data[i])
  end
  local s = string.format('s[%i]{%s}', t.qix or t.ix, table.concat(st, ','))

  -- modifiers
  for k, v in pairs(t.flw) do
    -- TODO do we need to print current counters?
    s = string.format('%s:%s[%i](%s)', s, k:sub(1, 1), v.ix, tostring(v.n))
  end

  -- transformer
  if #t.fun > 0 then
    local fns = { 'fn' }
    for i = 1, #t.fun[2] do
      fns[i + 1] = tostring(t.fun[2][i])
    end
    s = string.format('%s:map(%s)', s, table.concat(fns, ','))
  end

  return s
end

-- use memoized data length, aka #(t.data)
S.__len = function(t) return t.length end


setmetatable(S, S)




-- Global variable to store the previous octave
local previousOctave = 4

-- Table to map note names to their semitone offsets (relative to C)
local noteOffsets = {
  ["c"] = 0,
  ["c#"] = 1,
  ["d"] = 2,
  ["d#"] = 3,
  ["e"] = 4,
  ["f"] = 5,
  ["f#"] = 6,
  ["g"] = 7,
  ["g#"] = 8,
  ["a"] = 9,
  ["a#"] = 10,
  ["b"] = 11
}
-- Function to convert a note name to MIDI value
function note_to_midi(noteName)
  local note, octave

  -- Check if the note name includes an octave
  if noteName:match("%d$") then
    -- If it has an octave (e.g. "d4"), split the note and octave
    note, octave = noteName:match("([a-gA-G#b]+)(%d)$")
    octave = tonumber(octave)
  else
    -- If no octave is specified, use the previous octave
    note = noteName
    octave = previousOctave
  end

  -- If the note is not found, return an error
  local offset = noteOffsets[note:lower()]
  if offset == nil then
    return 0
  end
  local midiValue = 12 * (octave + 1) + offset
  previousOctave = octave
  return tonumber(midiValue)
end

aa = S { "c4", "e", "g", "a" }
bb = S{ S{0}:count(10),S{100}:count(5) }
for i = 1, 20 do
  print((aa:map(note_to_midi)()+bb())/12)
end

print(note_to_midi("c4"))
print(note_to_midi("d"))
