VERSION = '2019-10-14'
print("DRUMSHEET "  .. VERSION)

require "iuplua"
require "iupluacontrols"
require "midiwire"

-- now, this can actually play Blue monday!
-- see https://www.youtube.com/watch?v=Y_hk5P1X7sc&t=164s

LEN = 33
step = 1
mout = midiwire.open_out(0)
active_pat = 1
chan = 10

instruments = {
	[1] = {"clap", 39},
	[2] = {"open hat", 46},
	[3] = {"snare", 40},
	[4] = {"bass", 36},
}

patterns = {
	{},
	{},
	{},
	{}
}

-- GUI definition

pat_a = iup.button{title="PAT A", fontstyle="Bold"}
pat_b = iup.button{title="PAT B"}
pat_c = iup.button{title="PAT C"}
pat_d = iup.button{title="PAT D"}
pat_rec = iup.button{title="REC"}
pat_clean = iup.button{title="ZAP"}
pat_load = iup.button{title="load"}
pat_save = iup.button{title="save"}

matrix = iup.matrix{numcol=32, numlin=9, widthdef=10}
matrix.resizematrix = "YES"

function print_instruments()
	for i=1,32 do
		matrix:setcell(0,i,i)
	end
	for i=1,8 do
		matrix:setcell(i,0,"")
	end
	for i=1,#instruments do
		matrix:setcell(i,0,instruments[i][1])
	end
end

matrix:setcell(0,0,"DRUMsheet")
print_instruments()

playbtn = iup.button{title="play"}
stopbtn = iup.button{title="stop"}

midi_setup = iup.button{title="MIDI output"}

-- TODO - maskovat tyhle věci a validovat ať to necrashuje

bpm = iup.text{spin="YES", value="120", spinmin=30, spinmax=240} -- todo: min max sensible
factor = iup.text{spin="YES", value="4", spinmin=1, spinmax=8}
len = iup.text{spin="YES", value="32", spinmin=8, spinmax=32}

dlg = iup.dialog{
	iup.vbox{
		iup.hbox{pat_a, pat_b, pat_c, pat_d, pat_rec, pat_clean, pat_load, pat_save; gap=5},
		iup.hbox{matrix},
		iup.hbox{playbtn, stopbtn, iup.label{title="tempo: "} ,  bpm, iup.label{title="/"}, factor, iup.label{title=" || len: "}, len, midi_setup; alignment="ACENTER", gap="5"}
	}
	; title = "DRUMsheet " .. VERSION, size = "560x170"
}

-- GUI actions

timer = iup.timer{time=500}
timer.run="NO"

function timer:action_cb()
	-- ONE STEP of sequencer
	if step==1 then
		matrix:setcell(9, LEN-1, "")
	else
		matrix:setcell(9, step-1, "") -- clean from previous step
	end
	matrix:setcell(9, step, "^")

	for i=1,#instruments do
		if matrix:getcell(i, step)=="x" then
			midiwire.note_on(mout, instruments[i][2], 100, chan)
		end
		if matrix:getcell(i, step)=="*" then
			midiwire.note_on(mout, instruments[i][2], 127, chan)
		end
	end
	
	step = ((step + 1) % LEN)
	if step==0 then 
		step=1 
	end
	matrix.redraw="YES"
end

function silence()
	for i=1,#instruments do
		midiwire.note_off(mout, instruments[i][2], chan)
	end
end

function playbtn:action()
	bpm.active="NO"
	factor.active="NO"
	len.active="NO"
	timer.run="YES"
end

function stopbtn:action()
	bpm.active="YES"
	factor.active="YES"
	len.active="YES"
	timer.run="NO"
	-- todo  - tohle lépe
	silence()
	if step==1 then
		matrix:setcell(9, LEN-1, "")
	else
		matrix:setcell(9, step-1, "") -- clean from previous step
	end
	matrix.redraw="YES"
	step = 1
end

function adjust_time(bpm, factor)
	timer.time = math.floor((1000*60) / (bpm*factor) )
end

function bpm:valuechanged_cb()
	if tonumber(bpm.value) and tonumber(factor.value) then
		adjust_time(tonumber(bpm.value), tonumber(factor.value))
	end
end

function factor:valuechanged_cb()
	if tonumber(bpm.value) and tonumber(factor.value) then
		adjust_time(tonumber(bpm.value), tonumber(factor.value))
	end
end

function adjust_len(len)
    LEN = len + 1
    for i=1,LEN do
    	matrix:setcell(0, i, i)
    end
    if LEN<33 then
    	for i=LEN,32 do
        	matrix:setcell(0, i, "")
    	end
    end
end

function len:valuechanged_cb()
	if tonumber(len.value) then
		adjust_len(tonumber(len.value))
		matrix.redraw="YES"
	end
end

-- TODO - tohle přepsat na jedno okno
function midi_setup:action()
	local devices , sel2id = {}, {}
	for i=0,midiwire.device_count()-1 do
		if midiwire.name(i) then
			devices[#devices+1] = midiwire.name(i)
			sel2id[#devices] = i
		end
	end
	
	local selected = iup.ListDialog(1, "select MIDI output", #devices, devices, 1, 1, #devices)
	if selected>-1 and sel2id[selected+1] then
		midiwire.close_out(mout)
		if sel2id[selected+1]==0 then
			-- Microsoft GS Wavetable Synth
			mout = midiwire.open_out(sel2id[selected+1])
			chan = 10
		else
			mout = midiwire.open_out(sel2id[selected+1])
			chan = iup.Scanf("Select port\nchannel number%2.2%d\n", 1)
		end
	end
end



-- easier editing of patterns

function matrix:action_cb(key, x, y, edition, value)
	if key==iup.K_SP then
		matrix:setcell(x, y, "")
		matrix.redraw="YES"
		return iup.IGNORE
	elseif key==iup.K_X or key==iup.K_x then
		matrix:setcell(x, y, "x")
		matrix.redraw="YES"
		return iup.IGNORE
	end
end


-- patterns changing, saving and loading

function pat_rec:action()
	patterns[active_pat] = {}
	for x=1,8 do
		patterns[active_pat][x] = {}
		for y=1,32 do
			patterns[active_pat][x][y] = matrix:getcell(x,y) or ""
		end
		-- print(table.concat(patterns[active_pat][x], ","))
	end
end

function pat_clean:action()
	for x=1,8 do
		for y=1,32 do
			matrix:setcell(x,y, "")
		end
	end
	matrix.redraw = "YES"
end

function pat_recall(patno)
	if not patterns[patno] then return end
	local was_running = timer.run == "YES"
	timer.run="NO" -- pause while recalling to not glitch
	silence()
	for x=1,8 do
		patterns[patno][x] = patterns[patno][x] or {}
		for y=1,32 do
			matrix:setcell(x, y, patterns[patno][x][y] or "")
		end
		-- print(table.concat(patterns[active_pat][x], ","))
	end
	active_pat = patno
	if was_running then
		step = 1
		timer.run="YES"
	end
	matrix.redraw="YES"
end

function pat_a:action()
	pat_a.fontstyle="Bold"; pat_b.fontstyle=""; pat_c.fontstyle=""; pat_d.fontstyle=""
	pat_recall(1)
end

function pat_b:action()
	pat_a.fontstyle=""; pat_b.fontstyle="Bold"; pat_c.fontstyle=""; pat_d.fontstyle=""
	pat_recall(2)
end

function pat_c:action()
	pat_a.fontstyle=""; pat_b.fontstyle=""; pat_c.fontstyle="Bold"; pat_d.fontstyle=""
	pat_recall(3)
end


function pat_d:action()
	pat_a.fontstyle=""; pat_b.fontstyle=""; pat_c.fontstyle=""; pat_d.fontstyle="Bold"
	pat_recall(4)
end

-- saving & loading

FILEFORMAT_VERSION = 1

function _pat_write(fh, patno)
	fh:write("PAT,"..patno.."\n")
	for i=1,#patterns[patno] do
		fh:write(table.concat(patterns[patno][i],",") .. "\n")
	end
end

function pat_save:action()
	local fname, status = iup.GetFile("./*.csv")
	if status > -1 then
		local fh = io.open(fname, "w+")
		fh:write("DRUMSHEET," .. FILEFORMAT_VERSION .. "," .. VERSION .. "\n")
		fh:write("bpm,"..bpm.value.."\n")
		fh:write("factor,"..factor.value.."\n")
		fh:write("len,"..len.value.."\n")
		fh:write("INSTRUMENTS\n")
		for i=1,#instruments do
			fh:write(instruments[i][1] .. "," .. instruments[i][2] .. "\n")
		end
		-- todo pat_write nebo loop
		if #patterns[1]>1 then _pat_write(fh, 1) end
		if #patterns[2]>1 then _pat_write(fh, 2) end
		if #patterns[3]>1 then _pat_write(fh, 3) end
		if #patterns[4]>1 then _pat_write(fh, 4) end
		fh:close()
	end
end

function explode(div,str) -- credit: http://richard.warburton.it
	if (div=='') then return false end
	local pos,arr = 0,{}
	-- for each divider found
	for st,sp in function() return string.find(str,div,pos,true) end do
		table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
		pos = sp + 1 -- Jump past current divider
	end
	table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
	return arr
end

function sheet_read(fname)
	local data = { instruments={}, patterns = {} }
	local mode = ""
	local is_header = {bpm=true, factor=true, len=true}
	local i = 0
	local patno = 0
	for line in io.lines(fname) do
		local fields = explode(',', line)
		-- reads header:
		if mode=="DRUMSHEET" and is_header[fields[1]] then
			data[fields[1]] = tonumber(fields[2])
		end

		-- switch to PAT mode in INSTRUMENTS
		if mode=="INSTRUMENTS" and fields[1]=="PAT" then
			mode = "PAT"
		end

		-- reads instruments:
		if mode=="INSTRUMENTS" and fields[1]~="PAT" then
			i = i + 1
			data.instruments[i] = {fields[1], tonumber(fields[2])}
		end

		-- read patterns:
		if mode=="PAT" and fields[1]=="PAT" then
			patno = tonumber(fields[2])
			data.patterns[patno] = {}
			i = 0
		elseif mode=="PAT" then
			i = i + 1
			data.patterns[patno][i] = fields
		end

		if mode=="" and fields[1]=="DRUMSHEET" then
			-- todo - kontrolovat verzi drumsheetu
			if not fields[2] or not tonumber(fields[2]) then
				return nil, "File version not found!"
			end
			if tonumber(fields[2])>FILEFORMAT_VERSION then
				return nil, "File too new!"
			end
			mode = "DRUMSHEET"

		end
		if mode=="DRUMSHEET" and fields[1]=="INSTRUMENTS" then
			mode = "INSTRUMENTS"
			i = 0
		end
	end
	return data
end

function pat_load:action()
	local fname, status = iup.GetFile("./*.csv")
		if status > -1 then
		local data, error = sheet_read(fname)
		if error then
			iup.Message("File not loaded", error)
		else
			bpm.value = data.bpm
			factor.value = data.factor
			len.value = data.len
			adjust_time(data.bpm, data.factor)
			adjust_len(data.len)
			instruments = data.instruments or {}
			print_instruments()
			patterns = data.patterns or { {}, {}, {}, {} }
			if patterns[1] then
				pat_a.fontstyle="Bold"; pat_b.fontstyle=""; pat_c.fontstyle=""; pat_d.fontstyle=""
				pat_recall(1)
			end

			matrix.redraw = "YES"
		end
	end
end


-- main program

adjust_time(120, 4)
dlg:showxy( iup.CENTER, iup.CENTER )

if midiwire.device_count()<2 then
	midi_setup.active = "NO"
end

if (iup.MainLoopLevel()==0) then
  iup.MainLoop()
end
