local SCRIPT_TITLE = 'Lyrics to notes'

--[[

Synthesizer V Studio Pro Script
 
lua file name: Lyrics2NotesPanel.lua

Generates melodies based on scale, style, and rhythm
Includes variable spacing between notes based on rhythm and style
Fill lyrics into melody

Update: 1 - Creation

Notice: Works only with script panel 
		introduced with Synthesizer V version >= 2.1.2

2026 - JF AVILES
--]]

function getClientInfo()
	return {
		name = SV:T(SCRIPT_TITLE),
		-- category = "_JFA_Panels",
		author = "JFAVILES",
		versionNumber = 1,
		minEditorVersion = 131329,
		type = "SidePanelSection"
	}
end

-- Generated by JFA TranslateScripts.lua
function getTranslations(langCode)
	return getArrayLanguageStrings()[langCode]
end

-- Generated by JFA TranslateScripts.lua
function getArrayLanguageStrings()
	return {
		["en-us"] = {
			{"Version", "Version"},
			{"author", "author"},
			{"minEditorVersion", "minEditorVersion"},
			{"Lyrics to melodies", "Lyrics to melodies"},
			{"No lyrics in text area!", "No lyrics in text area!"},
			{"major", "major"},
			{"natural minor", "natural minor"},
			{"blues major", "blues major"},
			{"dorian", "dorian"},
			{"phrygian", "phrygian"},
			{"melodic minor", "melodic minor"},
			{"harmonic minor", "harmonic minor"},
			{"ionian", "ionian"},
			{"locrian", "locrian"},
			{"lydian", "lydian"},
			{"mixolydian", "mixolydian"},
			{"aeolian", "aeolian"},
			{"blues minor", "blues minor"},
			{"japanese", "japanese"},
			{"chinese", "chinese"},
			{"chinese 2", "chinese 2"},
			{"indian", "indian"},
			{"hungarian major", "hungarian major"},
			{"Style pop", "Style pop"},
			{"Style ballad", "Style ballad"},
			{"Style edm", "Style edm"},
			{"Style jazz", "Style jazz"},
			{"Speed fast", "Speed fast"},
			{"Speed medium", "Speed medium"},
			{"Speed slow", "Speed slow"},
			{"Length short", "Length short"},
			{"Length normal", "Length normal"},
			{"Length long", "Length long"},
			{"Octave down", "Octave down"},
			{"Octave base", "Octave base"},
			{"Octave up", "Octave up"},
			{"Note fixed: 1/16", "Note fixed: 1/16"},
			{"Note fixed: 1/32", "Note fixed: 1/32"},
			{"Note fixed: 1/64", "Note fixed: 1/64"},
			{"No lyrics found!", "No lyrics found!"},
			{"Apply", "Apply"},
			{"Select a key scale", "Select a key scale"},
			{"Select a scale type", "Select a scale type"},
			{"Select a style", "Select a style"},
			{"Select a rhythm", "Select a rhythm"},
			{"Reduce duration", "Reduce duration"},
			{"Select melody length", "Select melody length"},
			{"Select octave (down:-1, base:0, up:+1)", "Select octave (down:-1, base:0, up:+1)"},
			{"Note position to measure bar", "Note position to measure bar"},
		},
	}
end

-- Define a class "NotesObject"
NotesObject = {
	displayVersion = true,	-- display version
	displayAuthor = false,	-- display author
	currentSeconds = 0,
	defaultLyrics = "la",
	linefeed = "[^\r\n]+",
	words = "%S+",
	octaveRange = 1,
	quarterDivider = 4,
	measureBarVal = 8,		-- Place notes on starting measure bar 1/8
	minTimeSpacing = 1/8,	-- default minimum time(s) between notes
	errorMessages = {},
	currentPlayheadSeconds = 0,
	beginGroup = nil,
	endGroup = nil,
	hostinfo = nil,
	osType = "",
	osName = "",
	hostName = "",
	languageCode = "", 
	hostVersion = "",
	hostVersionNumber = 0,
	debug = false,
	saved_melodies = {},	-- Storage for generated melodies
	keyNames = {},			-- {"C", "Db", "D" ...
	note_names = {},		-- ["C"] = 60, ["Db"] = 61,
	allscalesActive = false, -- true to list all scales
	scales = {},
	styles = {},	
	stylesRef = {},			-- {"pop", "edm", "jazz", "ballad"}
	rhythms = {},
	rhythmsLyrics = {},
	melodyLength = {},
	octaveUpDown = {},
	measureBar = {},
	scalesList = {},
	stylesList = {},
	rhythmsList = {},
	melodyLengthList = {},
	octaveUpDownList = {},
	controls = {},				-- controls panel
	applyButtonValue = nil, 	-- button apply
	createProjectValue = nil, 	-- button create project
	statusTextValue = nil,   	-- text panel
	keyScaleChoice = {},
	styleChoice = {},
	measureBarChoice = {},
	infosToDisplay = "",
	logs = {},
	lyrics2NotesTextChoice = {},
	coefMinValue = 0.25,
	coefMaxValue = 1,
	coefInterval = 0.25,
	durationCoef = 0.75	-- Multiplier/divider for note duration (1 = normal, 0.5 = shorter)
}


-- Constructor method for the NotesObject class
function NotesObject:new()
    local notesObject = {}
    setmetatable(notesObject, self)
    self.__index = self
	
	self:getHostInformations()
	
	-- Get playhead first measure
	self.currentSeconds = self:getPlayhead()
	
	self.keyNames = {"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"}
	
	-- Note names with values
	self.note_names = {
		["C"] = 60, ["Db"] = 61, -- ["C#"] = 61, 
		["D"] = 62, ["Eb"] = 63, -- ["D#"] = 63, 
		["E"] = 64,
		["F"] = 65, ["Gb"] = 66, -- ["F#"] = 66, 
		["G"] = 67, ["Ab"] = 68, -- ["G#"] = 68, 
		["A"] = 69, ["Bb"] = 70, -- ["A#"] = 70,
		["B"] = 71
	}
	
	self.scales = self:getScalesData(self.allscalesActive) -- init activated scales or all
	self.styles = self:getStylesData()
	self.rhythms = self:getRhythmsData()
	-- self.rhythmsLyrics = {"sentimental", "powerful", "tragic", "emotional", "happy"}
	self.stylesRef = {"pop", "edm", "jazz", "ballad"}

	self.melodyLength = self:getMelodyLengthData()
	self.octaveUpDown = self:getOctaveUpDownData()
	self.measureBar = self:getMeasureBarData()

	self.controls = self:getControls()
	
	self:initializeControlsValues()
	self:setControlsCallback()
	
	self.applyButtonValue = SV:create("WidgetValue")
	self.lyrics2NotesTextValue = SV:create("WidgetValue")
	self.lyrics2NotesTextValue:setValue("")
	self.lyrics2NotesTextValue:setEnabled(false)
	self.createProjectValue = SV:create("WidgetValue")
	self.statusTextValue = SV:create("WidgetValue")
	self.statusTextValue:setValue("")
	self.statusTextValue:setEnabled(false)
	
	self:setButtonApplyControlCallback()

	-- load combox data
	self:getComboLists()
	
	local infos = getClientInfo()

	self.infosToDisplay = ""
	if self.displayVersion then
		self.infosToDisplay = self.infosToDisplay .. SV:T("Version") .. ": " ..  infos.versionNumber
		if self.displayAuthor then
			self.infosToDisplay = self.infosToDisplay .. " - " .. SV:T("author") .. ": " .. infos.author
		end
	end
	-- self.infosToDisplay = self.infosToDisplay .. SV:T("minEditorVersion") .. ": " ..  infos.minEditorVersion
	self:addTextPanel(self.infosToDisplay)
	self:addTextPanel(SV:T("Lyrics to melodies") .. "...")
	
    return self
end

-- Show message dialog
function NotesObject:show(message)
	SV:showMessageBox(SV:T(SCRIPT_TITLE), message)
end

-- Get selected groups
function NotesObject:getSelectedGroups()
	return SV:getArrangement():getSelection():getSelectedGroups()
end

-- Get current track
function NotesObject:getCurrentTrack()
	return SV:getMainEditor():getCurrentTrack()
end

-- Get timeAxis
function NotesObject:getTimeAxis()
	return self:getProject():getTimeAxis()
end

-- Get project
function NotesObject:getProject()
	return SV:getProject()
end

-- Add log into self.logs
function NotesObject:addLogs(message)
	table.insert(self.logs, message)
end

-- Display logs into panel
function NotesObject:addLogsInPanel()
	for i = 1, #self.logs do
		self:addTextPanel(self.logs[i])
	end
end

-- Display message box in panel
function NotesObject:addTextPanel(message)
	local old = self.statusTextValue:getValue()
	local sepLine = "\r"
	if #old > 0 then
		message = old .. sepLine .. message
	end
	self.statusTextValue:setValue(message)
end

-- Clear display message in panel
function NotesObject:clearTextPanel()
	self.statusTextValue:setValue("")
end

-- Store error message
function NotesObject:error(message)
	table.insert(self.errorMessages, message)
end

-- Get controls panel
function NotesObject:getControls()

	local controls = {
		durationCoef = {
			value = SV:create("WidgetValue"),
			defaultValue = self.durationCoef,
			paramKey = "durationCoef"
		},
		scaleKey = {
			value = SV:create("WidgetValue"),
			defaultValue = 0, -- C
			paramKey = "scaleKey"
		},
		scaleType = {
			value = SV:create("WidgetValue"),
			defaultValue = 0, -- major
			paramKey = "scale"
		},
		style = {
			value = SV:create("WidgetValue"),
			defaultValue = 0, -- pop
			paramKey = "style"
		},
		rhythm = {
			value = SV:create("WidgetValue"),
			defaultValue = 1, -- medium
			paramKey = "rhythm"
		},
		melodyLength = {
			value = SV:create("WidgetValue"),
			defaultValue = 1, -- normal = 16
			paramKey = "melodyLength"
		},
		octaveUpDown = {
			value = SV:create("WidgetValue"),
			defaultValue = 0, -- Octave down = 0
			paramKey = "octaveUpDown"
		},
		measureBar = {
			value = SV:create("WidgetValue"),
			defaultValue = 0, -- measureBar = 8
			paramKey = "measureBar"
		}
	}
	return controls
end

-- Initialize widget values
function NotesObject:initializeControlsValues()
	-- Initialize widget values
	for key, control in pairs(self.controls) do
		control.value:setValue(control.defaultValue)
		-- self:addLogs(key .. "=" .. tostring(control.defaultValue))
		-- self:addLogs(key .. "=" .. self:getObjectProperties(control))
	end
end

-- Set controls callback
function NotesObject:setControlsCallback()
	for key, control in pairs(self.controls) do
		control.value:setValueChangeCallback(function()
			-- do something may be
				-- self:addLogsInPanel()
				if control.paramKey == "rhythm" then
					local rhythm = self.rhythmsList[self.controls.rhythm.value:getValue() + 1]
					local rhythm_config = self:get_rhythm(rhythm).val
				end
			end
		)
	end
end

-- Display message
function NotesObject:displayMessage(message)
	self:clearTextPanel()
	self:addTextPanel(self.infosToDisplay)
	self:addTextPanel(message)
end

-- Get key position in Keynames
function NotesObject:getKeyPosInKeynames(keyNames, keyfound)
	local posKeyInScale = -1
	-- loop each scales
	for key = 1, #keyNames do
		if keyfound == keyNames[key] then
			posKeyInScale = key
			break
		end
	end
	return posKeyInScale
end

-- Get position in scale array from scale name
function NotesObject:getPosScaleFromScaleName(style)
	local pos = 0
	for i = 1, #self.scales do
		if self.scales[i].name == style then
			pos	= i
			break
		end
	end
	return pos
end

-- Set button Apply control callback
function NotesObject:setButtonApplyControlCallback()

	-- Button create melody
	self.applyButtonValue:setValueChangeCallback(function()
			self:getProject():newUndoRecord()
			
			local lyrics = self.lyrics2NotesTextValue:getValue()
			if #lyrics > 0 then
				self:lyrics2NotesGroup(lyrics)
			else
				self:show(SV:T("No lyrics in text area!"))
			end
		end
	)
end

-- Get next measure bar
function NotesObject:getNextMeasure(groupName, currentBlicksPos)
	local next = 0
	if currentBlicksPos == 0 then
		next = 0
	else
		local posSecond = self:getTimeAxis():getSecondsFromBlick(currentBlicksPos)
		local posSecondFloor = math.floor(posSecond) + 1
		local nextPosMod = (posSecondFloor % 2)
		local nextPos = posSecondFloor + nextPosMod
		next = self:getTimeAxis():getBlickFromSeconds(nextPos)
	end	
	return next
end

-- Get scales data
function NotesObject:getScalesData(allscalesActive)
	local scales = {}
	
	local scalesReference = {
		-- KeyScale type, Intervals
		{name = SV:T("major"), 			val = {0,2,4,5,7,9,11}, active = true},
		{name = SV:T("natural minor"),	val = {0,2,3,5,7,8,10}, active = true},
		{name = SV:T("blues major"),	val = {0,2,3,4,7,9}, 	active = true},
		{name = SV:T("dorian"),			val = {0,2,3,5,7,9,10}, active = true},
		{name = SV:T("phrygian"),		val = {0,1,3,5,7,8,10}, active = true},
		{name = SV:T("melodic minor"),	val = {0,2,3,5,7,9,11}, active = false},
		{name = SV:T("harmonic minor"),	val = {0,2,3,5,7,8,11}, active = false},
		{name = SV:T("ionian"),			val = {0,2,4,5,7,9,11}, active = false},
		{name = SV:T("locrian"),		val	= {0,1,3,5,6,8,10}, active = false},
		{name = SV:T("lydian"),			val = {0,2,4,6,7,9,11}, active = false},
		{name = SV:T("mixolydian"),		val	= {0,2,4,5,7,9,10}, active = false},
		{name = SV:T("aeolian"),		val	= {0,2,3,5,7,8,10}, active = false},
		{name = SV:T("blues minor"),	val	= {0,3,5,6,7,10}, 	active = false},
		{name = SV:T("japanese"),		val	= {0,1,5,7,8}, 		active = false},
		{name = SV:T("chinese"),		val	= {0,2,4,7,9}, 		active = false}, 
		{name = SV:T("chinese 2"),		val	= {0,4,6,7,11}, 	active = false},
		{name = SV:T("indian"),			val	= {0,1,3,4,7,8,10}, active = false},
		{name = SV:T("hungarian major"),val = {0,3,4,6,7,9,10}, active = false}
	}
	
	-- loop to active scales
	for i = 1, #scalesReference do
		if allscalesActive or scalesReference[i].active then
			table.insert(scales, scalesReference[i])
		end
	end

	return scales
end

-- Get styles data
function NotesObject:getStylesData()
	local styles = {}
	
	local stylesReference = { 
		{name = SV:T("Style pop"), key="pop", val = {
			scale_preference = "major",
			octave_range = 1,
			jump_probability = 0.3,
			repetition_chance = 0.4,
			rhythm_complexity = 0.5,
			base_spacing = 0.08,	-- Spacing configurations
			spacing_variation = 0.1,
			syncopation_chance = 0.2,
			staccato_chance = 0.4
		}},
		{name = SV:T("Style ballad"), key="ballad",  val = {
			scale_preference = "major",
			octave_range = 1,
			jump_probability = 0.2,
			repetition_chance = 0.5,
			rhythm_complexity = 0.3,
			base_spacing = 0.15,	-- Spacing configurations
			spacing_variation = 0.2,
			syncopation_chance = 0.1,
			staccato_chance = 0.2
		}},
		{name = SV:T("Style edm"), key="edm", val = {
			scale_preference = "major", -- Note names to MIDI mapping
			octave_range = 2,
			jump_probability = 0.4,
			repetition_chance = 0.3,
			rhythm_complexity = 0.7,
			base_spacing = 0.1,        -- Spacing configurations, Base silence between notes
			spacing_variation = 0.15,  -- Random variation in spacing
			syncopation_chance = 0.4,  -- Probability of syncopated rhythm
			staccato_chance = 0.6      -- Probability of short, detached notes
		}},
		{name = SV:T("Style jazz"), key="jazz", val = {
			scale_preference = "dorian",
			octave_range = 2,
			jump_probability = 0.6,
			repetition_chance = 0.2,
			rhythm_complexity = 0.8,
			base_spacing = 0.05,		-- Spacing configurations
			spacing_variation = 0.25,
			syncopation_chance = 0.7,
			staccato_chance = 0.3
		}}
	}
	
	-- loop into data
	for i = 1, #stylesReference do
			table.insert(styles, stylesReference[i])
	end
	
	return styles
end

-- Get rhythms data
function NotesObject:getRhythmsData()
	local rhythms = {}

	local rhythmsReference = {
		{name = SV:T("Speed fast"), key = "fast",
			val = {
			tempo = 140,
			durations = {0.5, 0.5, 0.5, 0.25, 0.5}, -- Rhythm configurations (note durations and spacing multipliers)
			spacing_multiplier = 0.8,	-- Faster = less spacing
			groove_factor = 1.2,		-- Slightly uneven timing
			speed = 8 					-- note divider
		}},
		{name = SV:T("Speed medium"), key = "medium",
			val = {
			tempo = 120,
			durations = {0.5, 1, 0.5, 1, 0.5},
			spacing_multiplier = 1.0,
			groove_factor = 1.1,
			speed = 6
		}},
		{name = SV:T("Speed slow"), key = "slow",
			val = {
			tempo = 70,
			durations = {1, 2, 1, 1.5, 2},
			spacing_multiplier = 1.5,	-- Slower = more breathing room
			groove_factor = 1.05,
			speed = 4
		}}
	}
	
	-- loop into data
	for i = 1, #rhythmsReference do
		table.insert(rhythms, rhythmsReference[i])
	end
	
	return rhythms
end

-- Get melodyLength data
function NotesObject:getMelodyLengthData()
	local melodyLength = {}
	
	local melodyLengthReference = {
		{name = SV:T("Length short"), val = 8 },
		{name = SV:T("Length normal"), val = 16 },
		{name = SV:T("Length long"), val = 32 }
	}
	
	-- loop into data
	for i = 1, #melodyLengthReference do
			table.insert(melodyLength, melodyLengthReference[i])
	end
	
	return melodyLength
end

-- Get octaveUpDown data
function NotesObject:getOctaveUpDownData()
	local octaveUpDown = {}
	
	local octaveUpDownReference = {
		{name = SV:T("Octave down"), val = -1 },
		{name = SV:T("Octave base"), val = 0 },
		{name = SV:T("Octave up"), val = 1 }
	}
	
	-- loop into data
	for i = 1, #octaveUpDownReference do
			table.insert(octaveUpDown, octaveUpDownReference[i])
	end
	
	return octaveUpDown
end

-- Get measure bar data
function NotesObject:getMeasureBarData()
	local measureBar = {}
	
	local measureBarReference = {
		{name = SV:T("Note fixed: 1/16"), val = 8 }, -- 8 in 1s (16 in 2s)
		{name = SV:T("Note fixed: 1/32"), val = 16 },
		{name = SV:T("Note fixed: 1/64"), val = 32 }
	}
	
	-- loop into data
	for i = 1, #measureBarReference do
			table.insert(measureBar, measureBarReference[i])
	end
	
	return measureBar
end

-- Get playhead position in first nearest measure
function NotesObject:getPlayhead()
	-- Get playhead first measure
	self.currentPlayheadSeconds = SV:getPlayback():getPlayhead()
	local currentBlick = self:getTimeAxis():getBlickFromSeconds(self.currentPlayheadSeconds)
	-- Get first measure
	local measureBlick = self:getFirstMesure(currentBlick, 0)
	local newPositionSeconds = self:getTimeAxis():getSecondsFromBlick(measureBlick)
	return newPositionSeconds
end

-- Display error messages
function NotesObject:displayErrors()
	local result = ""
	if #self.errorMessages > 0 then
		for _, m in pairs(self.errorMessages) do
			result = result .. m .. "\r"
		end
	end
	return result
end

-- Note to MIDI pitch conversion
function NotesObject:noteToPitch(noteName, octave)
    for i, name in ipairs(self.keyNames) do
        if name == noteName then
            return (octave + 1) * 12 + (i - 1)
        end
    end
    return 60 -- C4 default
end

-- Get host informations
function NotesObject:getHostInformations()
	self.hostinfo = SV:getHostInfo()
	self.osType = self.hostinfo.osType  -- "macOS", "Linux", "Unknown", "Windows"
	self.osName = self.hostinfo.osName
	self.hostName = self.hostinfo.hostName
	self.hostVersion = self.hostinfo.hostVersion
	self.hostVersionNumber = self.hostinfo.hostVersionNumber
end

-- Get scale from scale type
function NotesObject:get_scale(scale_type)
	local scale = nil
	for i, v in ipairs(self.scales) do
		if v.name == scale_type then
			scale = v
			break
		end
	end
	return scale
end

-- Get style from style name
function NotesObject:get_style(style_name)
	local style = nil
	for i, v in ipairs(self.styles) do
		if v.name == style_name then
			style = v
			break
		end
	end
	return style
end

-- Get rhythm from rhythm name
function NotesObject:get_rhythm(rhythm_name)
	local rhythm = nil
	for i, v in ipairs(self.rhythms) do
		if v.name == rhythm_name then
			rhythm = v
			break
		end
	end
	return rhythm
end

-- Get melodyLength from melodyLength name
function NotesObject:get_melodyLength(melodyLength_name)
	local melodyLength = nil
	for i, v in ipairs(self.melodyLength) do
		if v.name == melodyLength_name then
			melodyLength = v
			break
		end
	end
	return melodyLength
end

-- Get octaveUpDown from octaveUpDown name
function NotesObject:get_octaveUpDown(octaveUpDown_name)
	local octaveUpDown = nil
	for i, v in ipairs(self.octaveUpDown) do
		if v.name == octaveUpDown_name then
			octaveUpDown = v
			break
		end
	end
	return octaveUpDown
end

-- Get measureBar name
function NotesObject:get_measureBar(measureBar_name)
	local measureBar = nil
	for i, v in ipairs(self.measureBar) do
		if v.name == measureBar_name then
			measureBar = v
			break
		end
	end
	return measureBar
end

-- Get scale notes in MIDI format
function NotesObject:get_scale_notes(root_note, scale_type, octaveUpDown)
    local root_midi = self.note_names[root_note]
    if not root_midi then
        self:error("Invalid root note: " .. root_note)
    end
	
    local scale_intervals = self:get_scale(scale_type).val
    local scale_notes = {}
    
    -- Generate notes across multiple octaves
    for octave = 0, self.octaveRange do
        for _, interval in ipairs(scale_intervals) do
            table.insert(scale_notes, root_midi + (octaveUpDown * 12) + interval + (octave * 12))
        end
    end
    
    return scale_notes
end

-- Calculate spacing between notes
function NotesObject:calculate_spacing(style_config, rhythm_config)
    local base_spacing = style_config.base_spacing * rhythm_config.spacing_multiplier
    
    -- Add variation
    local variation = (math.random() - 0.5) * style_config.spacing_variation
    local spacing = base_spacing + variation
    
    -- Apply syncopation (occasional off-beat emphasis)
    if math.random() < style_config.syncopation_chance then
        spacing = spacing + (math.random() * 0.1 - 0.05)
    end
    
    -- Apply groove factor (slight timing imperfections that feel human)
    local groove_variation = (math.random() - 0.5) * 0.02 * rhythm_config.groove_factor
    spacing = spacing + groove_variation
    
    -- Ensure spacing is not negative
    return math.max(0, spacing)
end

-- Calculate note duration with style modifications
function NotesObject:calculate_note_duration(base_duration, style_config, rhythm_config)
    local duration = base_duration
    
    -- Apply staccato effect (shortens notes)
    if math.random() < style_config.staccato_chance then
        duration = duration * (0.6 + math.random() * 0.3)  -- 60-90% of original duration
    end
    
    -- Add slight duration variation based on rhythm complexity
    local variation = (math.random() - 0.5) * 0.1 * style_config.rhythm_complexity
    duration = duration * (1 + variation)
    
    -- Apply groove factor to duration
    local groove_variation = (math.random() - 0.5) * 0.05 * rhythm_config.groove_factor
    duration = duration * (1 + groove_variation)
    
    return math.max(0.1, duration)  -- Ensure minimum duration
end

-- Get next measure Position
function NotesObject:getNextMeasurePos(current_time, timeMeasureBar, melodyLength)
	local newtime =  current_time
	local current_time_Int = math.floor(current_time)
	
	local new_next_time = current_time_Int
	local maxTime = melodyLength * 10
	for i = 1, maxTime do
		if current_time <= new_next_time then
			newtime = new_next_time
			break
		end
		new_next_time = timeMeasureBar * (i + current_time_Int)
	end
	return newtime
end

-- Get current project tempo
function NotesObject:getProjectTempo(seconds)
	local tempoActive = 120 -- default
	local blicks = self:getTimeAxis():getBlickFromSeconds(seconds)
	local tempoMarks = self:getTimeAxis():getAllTempoMarks()
	for iTempo = 1, #tempoMarks do
		local tempoMark = tempoMarks[iTempo]
		if tempoMark ~= nil and blicks >= tempoMark.position then
			tempoActive = tempoMark.bpm
		end
	end
	return math.floor(tempoActive)
end

-- Get object properties (debug only)
function NotesObject:getObjectProperties(obj, level)
	local result = ""
	local level = level or 0
	local maxLevel = 3
	level = level + 1
	
	for k, v in pairs(obj) do
		if obj[k] ~= nil then
			result = result .. "(level: " .. level .. ") " .. k .. "=" .. tostring(v) .. "\r"
			if type(v) == "table" then
				-- result = result .. ", size:" .. #v .. ": "
				if level < maxLevel then
					result = result .. self:getObjectProperties(v, level) .. "\r"
				else
					result = result .. "\r"
				end
			end
		end
	end
	return result
end

-- Split string by sep char
function NotesObject:split(str, sep)
	local result = {}
	local regex = ("([^%s]+)"):format(sep)
	for each in str:gmatch(regex) do
		table.insert(result, each)
	end
	return result
end

-- Get first mesure after first note (or next position + n)
function NotesObject:getFirstMesure(notePos, nextPos)
	local measurePos = 0
	local measureBlick = 0
	local measureFirst = self:getTimeAxis():getMeasureAt(notePos) + nextPos
	local checkExistingMeasureMark = self:getTimeAxis():getMeasureMarkAt(measureFirst)
	
	if checkExistingMeasureMark ~= nil then
		if checkExistingMeasureMark.position == measureFirst then
			measurePos = checkExistingMeasureMark.position
			measureBlick = checkExistingMeasureMark.positionBlick
		else 
			self:getTimeAxis():addMeasureMark(measureFirst, 
						checkExistingMeasureMark.numerator, 
						checkExistingMeasureMark.denominator)
			local measureMark = self:getTimeAxis():getMeasureMarkAt(measureFirst)
			measurePos = measureMark.position
			measureBlick = measureMark.positionBlick
			self:getTimeAxis():removeMeasureMark(measureFirst)
		end
	else
		-- Temporary measure mark addition
		self:getTimeAxis():addMeasureMark(measureFirst, 4, 4)
		local measureMark = self:getTimeAxis():getMeasureMarkAt(measureFirst)
		measurePos = measureMark.position
		measureBlick = measureMark.positionBlick
		self:getTimeAxis():removeMeasureMark(measureFirst)
	end
	return measureBlick
end

-- lyrics to Notes to group
function NotesObject:lyrics2NotesGroup(lyrics)
	math.randomseed(os.time())
	local resultFunction = false	
	local scaleKeyCtrl = self.controls.scaleKey.value:getValue()
	local scaleTypeCtrl = self.controls.scaleType.value:getValue()
	local octaveUpDownCtrl = self.controls.octaveUpDown.value:getValue()
	local styleCtrl = self.controls.style.value:getValue()
	local rhythmCtrl = self.controls.rhythm.value:getValue()
	local measureBarCtrl = self.controls.measureBar.value:getValue()
	local newGrouptRef = {}
	local noteGroup = {}
	local result = ""

	local root_note = self.keyNames[scaleKeyCtrl + 1]
	local scale_type = self.scalesList[scaleTypeCtrl + 1 ]
	local octaveUpDown = self:get_octaveUpDown(self.octaveUpDownList[octaveUpDownCtrl + 1]).val
    local scale_notes = self:get_scale_notes(root_note, scale_type, octaveUpDown)
	local style_name = self.stylesList[styleCtrl + 1]
    local style_config = self:get_style(style_name).val
	local rhythm_name = self.rhythmsList[rhythmCtrl + 1]
    local rhythm_config = self:get_rhythm(rhythm_name).val
    local rhythm_pattern = rhythm_config.durations
	
	
	self.durationCoef = self.controls.durationCoef.value:getValue()
	-- measure bar default 8 for 1/16
	self.measureBarVal = self:get_measureBar(self.measureBarList[measureBarCtrl + 1]).val
	self.currentSeconds = SV:getPlayback():getPlayhead()
	self.BPM = self:getProjectTempo(self.currentSeconds)
	local BPM_ratio = 120 / self.BPM
	local measureBarTime = BPM_ratio / self.measureBarVal
	self.minTimeSpacing = measureBarTime
	
	if #lyrics == 0  then
		self:show(SV:T("No lyrics found!"))
	else
		local previousNoteOnset = nil
		local previousNoteDuration = 0
		local previousMidi_note = 48
		local previousWord = ""

		local matchSentences = string.gmatch(lyrics, self.linefeed)
		for lyricsLine in matchSentences do
		
			-- Create a note group
			local noteGroup = SV:create("NoteGroup")
			local newGrouptRef = SV:create("NoteGroupReference")
			local iNote = 0
			local noteOnset = nil
			local noteDuration = 0
			local noteGetEnd = 0
			local newNote = nil
			local matchWordsCount = 0
			local current_time = 0
			local new_next_time = 0
			local current_note_index = math.random(1, #scale_notes)
			
			-- Count words in line
			local matchWords = string.gmatch(lyricsLine, self.words)
			for word in matchWords do
				matchWordsCount = matchWordsCount + 1			
			end
			
			-- Get words from lyrics
			local matchWords = string.gmatch(lyricsLine, self.words)
			for word in matchWords do
				iNote = iNote + 1
				-- Get base duration from rhythm pattern
				local duration_index = ((iNote - 1) % #rhythm_pattern) + 1
				local base_duration = rhythm_pattern[duration_index] * self.durationCoef
				
				-- Calculate actual note duration with style modifications
				local note_duration = self:calculate_note_duration(base_duration, style_config, rhythm_config)
				
				if new_next_time > 0 then
					current_time = new_next_time
				end
			
				-- Create note event
				local midi_note = scale_notes[current_note_index]
				result = result .. "midi_note: " .. midi_note
				result = result .. ", current_time: " .. current_time
				result = result .. ", note_duration: " .. note_duration
				result = result .. "\r"
						
				-- Calculate spacing after this note
				local spacing = self:calculate_spacing(style_config, rhythm_config)
				
				if spacing < self.minTimeSpacing then
					spacing = 0
					result = result .. ", new: " .. spacing
				end
				-- Update current time (note duration + spacing)
				local next_time = current_time + note_duration + spacing
				result = result .. ", next_time: " .. next_time
				
				-- Place next note to next measure bar 1/16
				new_next_time = self:getNextMeasurePos(next_time, measureBarTime, matchWordsCount) -- Get next measure position
				result = result .. ", new_next_time: " .. new_next_time .. "\r"
				
				-- Force duration to join notes (without space)
				if new_next_time > next_time then
					-- note_duration = note_duration + new_next_time - next_time
					note_duration = new_next_time - current_time
				end
				
				-- current note recorded and created in next loop
				noteOnset = math.floor(self:getTimeAxis():getBlickFromSeconds(current_time))
				noteDuration = math.floor(self:getTimeAxis():getBlickFromSeconds(note_duration))
				
				if iNote == 1 then
				else
					-- New note for one	word (note from previous loop)
					newNote = SV:create("Note")
					newNote:setLyrics(previousWord)

					-- Set new duration if it is too long
					if iNote < matchWordsCount then					
						local previousNoteGetEnd = previousNoteOnset + previousNoteDuration
						if previousNoteGetEnd > noteOnset then
							previousNoteDuration = noteOnset - previousNoteOnset
							result = result .. ", previousNoteDuration: " .. previousNoteDuration .. "\r"
						end
					end
			
					newNote:setDuration(previousNoteDuration)
					newNote:setPitch(previousMidi_note)
					newNote:setOnset(previousNoteOnset)
					
					-- Add the note to the note group
					noteGroup:addNote(newNote)
				end
				previousNoteOnset = noteOnset
				previousNoteDuration = noteDuration
				previousMidi_note = midi_note
				previousWord = word				
				
				-- Determine next note
				if math.random() < style_config.repetition_chance then
					-- Repeat current note (do nothing)
				elseif math.random() < style_config.jump_probability then
					-- Make a jump
					local jump_size = math.random(-5, 5)
					current_note_index = math.max(1, math.min(#scale_notes, current_note_index + jump_size))
				else
					-- Step movement
					local step = math.random() < 0.5 and -1 or 1
					current_note_index = math.max(1, math.min(#scale_notes, current_note_index + step))
				end
			end
			
			-- New note for one	word (note from last loop)
			local newNote = SV:create("Note")
			newNote:setLyrics(previousWord)
			newNote:setDuration(previousNoteDuration)
			newNote:setPitch(previousMidi_note)
			newNote:setOnset(previousNoteOnset)
					
			-- Add the note to the note group
			noteGroup:addNote(newNote)
			
			noteGroup:setName(lyricsLine)			
			-- Add the note group to the project
			self:getProject():addNoteGroup(noteGroup)

			newGrouptRef:setTarget(noteGroup)
			
			self.currentSeconds = SV:getPlayback():getPlayhead()
			local checkGroup = self:getGroupRef(self:getCurrentTrack(), self.currentSeconds)
			if checkGroup ~= nil then
				self.currentSeconds = self:getNextEmptyPosition(self:getCurrentTrack(), checkGroup:getEnd())
				SV:getPlayback():seek(self.currentSeconds)
			else
				local newPositionBlicks = self:getTimeAxis():getBlickFromSeconds(self.currentSeconds)
				measureBlick = self:getFirstMesure(newPositionBlicks, 0)
				self.currentSeconds = self:getTimeAxis():getSecondsFromBlick(measureBlick)
				SV:getPlayback():seek(self.currentSeconds)
			end
			
			local startPositionBlicks = self:getTimeAxis():getBlickFromSeconds(self.currentSeconds)
			local firstNote = noteGroup:getNote(1)
			local lastNote = noteGroup:getNote(noteGroup:getNumNotes())
			
			local newTimeOffset, newDuration = self:getGroupNewTimeRange(startPositionBlicks, firstNote, lastNote)
			newGrouptRef:setTimeOffset(newTimeOffset)			
			newGrouptRef:setTimeRange(startPositionBlicks, newDuration) -- v2.1.1		
			
			self:getCurrentTrack():addGroupReference(newGrouptRef)
		end

		if self.debug then SV:setHostClipboard(result) end
		
		resultFunction = true
	end
	return result
end

-- Get group new time range
function NotesObject:getGroupNewTimeRange(startPositionBlicks, firstNote, lastNote)
	local endPositionBlicks = lastNote:getOnset() + lastNote:getDuration() - firstNote:getOnset()
	-- New end position to next bar
	local newEndPosition = self:getFirstMesure(endPositionBlicks, 1) - 10

	return	startPositionBlicks - firstNote:getOnset(), newEndPosition
end

-- Get next empty position on track
function NotesObject:getNextEmptyPosition(currentTrack, lastPositionBlicks)
	local measureBlick = self:getFirstMesure(lastPositionBlicks, 0)
	local seconds = self:getTimeAxis():getSecondsFromBlick(measureBlick)
	local checkGroup = self:getGroupRef(currentTrack, seconds)
	
	if checkGroup ~= nil then
		-- A group is found there
		-- Next measure bar
		measureBlick = self:getFirstMesure(checkGroup:getEnd(), 1)
		-- Recursive check to get empty place in track
		seconds = self:getNextEmptyPosition(currentTrack, measureBlick)
	end
	 
	return seconds
end

-- Get group reference in time position
function NotesObject:getGroupRef(track, time)
	local groupRefFound = nil
	local numGroups = track:getNumGroups()
	local blicksPos = self:getTimeAxis():getBlickFromSeconds(time)
	
	-- All groups except the main group
	for iGroup = 1, numGroups do
		local groupRef = track:getGroupReference(iGroup)
		if not groupRef:isMain() then
			local blickSeconds = self:getTimeAxis():getSecondsFromBlick(groupRef:getOnset())
			
			-- Get group on timing pos
			if blicksPos >= groupRef:getOnset() and blicksPos <= groupRef:getEnd() then
				groupRefFound = groupRef
				break
			end
		end
	end						
	return groupRefFound
end

-- Get combo box data
function NotesObject:getComboLists()
	self.scalesList = {}
	self.stylesList = {}
	self.rhythmsList = {}
	self.measureBarList = {}
	
	-- scales list
	for key, v in ipairs(self.scales) do
		table.insert(self.scalesList, self.scales[key].name)
	end
	-- styles list
	for key, v in ipairs(self.styles) do
		table.insert(self.stylesList, self.styles[key].name)
	end
	-- rhythms list
	for key, v in ipairs(self.rhythms) do
		table.insert(self.rhythmsList, self.rhythms[key].name)
	end
	-- melody length list and data
	for key, v in ipairs(self.melodyLength) do
		table.insert(self.melodyLengthList, self.melodyLength[key].name)
	end
	-- octave up or down list and data
	for key, v in ipairs(self.octaveUpDown) do
		table.insert(self.octaveUpDownList, self.octaveUpDown[key].name)
	end
	-- measure bar list and data
	for key, v in ipairs(self.measureBar) do
		table.insert(self.measureBarList, self.measureBar[key].name)
	end
end

-- Get section
function NotesObject:getSection()
	
	-- Define all ComboBox	
	self:setComboChoices()

	self.lyrics2NotesTextChoice = {
		type = "Container",
		columns = {
			{
				name = "lyrics2Notes",
				type = "TextArea",
				label = "Lyrics",
				height = 200,
				value = self.lyrics2NotesTextValue
			}
		}
	}
	

	-- Define CheckBox & button & textarea
	local section = {
		title = SV:T(SCRIPT_TITLE),
		rows = {
			self.lyrics2NotesTextChoice,
			self.keyScaleChoice,
			self.styleChoice,
			self.melodyLengthChoice,
			self.durationCoefChoice,
			self.measureBarChoice,
			{
				type = "Container",
				columns = {
					{
						type = "Button",
						text = SV:T("Apply"),
						width = 1.0,
						value = self.applyButtonValue
					}
				}
			}
		}
	}
	return section
end

-- Set ComboBox choices
function NotesObject:setComboChoices()
	-- Define ComboBox	
	
	self.keyScaleChoice = {
		type = "Container",
		columns = {
			{
				type = "ComboBox",
				text = SV:T("Select a key scale"),
				value = self.controls.scaleKey.value,
				choices = self.keyNames,
				width = 0.3
			},
			{
				type = "ComboBox",
				text = SV:T("Select a scale type"),
				value = self.controls.scaleType.value,
				choices = self.scalesList,
				width = 0.7
			}
		}
	}
	
	self.styleChoice = {
        type = "Container",
        columns = {
			{
				type = "ComboBox",
				text = SV:T("Select a style"),
				value = self.controls.style.value,
				choices = self.stylesList,
				width = 0.5
			},
			{
				type = "ComboBox",
				text = SV:T("Select a rhythm"),
				value = self.controls.rhythm.value,
				choices = self.rhythmsList,
				width = 0.5
			}
        }
    }

	self.durationCoefChoice = 
		{
			type = "Container",
			columns = {
				{
					type = "Slider",
					text = SV:T("Reduce duration"),
					format = "%1.2f coef",
					minValue = self.coefMinValue, 
					maxValue = self.coefMaxValue, 
					interval = self.coefInterval,
					value = self.controls.durationCoef.value,
					width = 1.0
				}
			}
		}
	
	self.melodyLengthChoice = {
        type = "Container",
        columns = {
			{
				type = "ComboBox",
				text = SV:T("Select melody length"),
				value = self.controls.melodyLength.value,
				choices = self.melodyLengthList,
				width = 0.5
			},
			{
				type = "ComboBox",
				text = SV:T("Select octave (down:-1, base:0, up:+1)"),
				value = self.controls.octaveUpDown.value,
				choices = self.octaveUpDownList,
				width = 0.5
			}
        }
    }
	
	self.measureBarChoice = {
        type = "Container",
        columns = {
			{
				type = "ComboBox",
				text = SV:T("Note position to measure bar"),
				value = self.controls.measureBar.value,
				choices = self.measureBarList,
				width = 1.0
			}
        }
    }
	
end

-- Get panel section state
function NotesObject:getPanelSectionState()

	self.applyButtonValue:setEnabled(true)
	self.createProjectValue:setEnabled(true)
	local errors = self:displayErrors()
	if #errors > 0 then
		self:addTextPanel(errors)
	end
	
	-- Get section data
	local section = self:getSection()

	return section
end

-- Initialize main internal object	
local notesObject = NotesObject:new()

-- Get panel section state called by Synthesizer V internal system
function getSidePanelSectionState()

	local section = notesObject:getPanelSectionState()

	return section
end