local SCRIPT_TITLE = 'DropToPos V1.1'

--[[

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

Version for Synthesizer V version >= 2.1.1

Drop audio to playhead position
1/ Waiting any newly created track
2/ Move imported audio into the current target position

Note: Stopping this script:
A/ If you want to STOP this script without the required drag&drop (multiple options): 
	A- STOP this script by creating a new note on the piano roll
	B- STOP this script by selecting a new existing note
	C- STOP this script by running this script again! 

Update: 0 - Creation
		1 - to be done
		
2025 - JF AVILES
--]]

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

-- Generated by JFA TranslateScripts.lua
function getArrayLanguageStrings()
	return {
		["en-us"] = {
			{"Tb", "Tb"},
			{"b", "b"},
			{"s", "s"},
			{"n", "n"},
			{"See other project keys", "See other project keys"},
			{"chars", "chars"},
			{"Scripts project keys found:", "Scripts project keys found:"},
			{"No other project keys", "No other project keys"},
			{"Error in saved parameters, try again!", "Error in saved parameters, try again!"},
		},
	}
end

function getClientInfo()
	return {
		name = SV:T(SCRIPT_TITLE),
		category = "_JFA_Tools",
		author = "JFAVILES",
		versionNumber = 1,
		minEditorVersion = 65540
	}
end

-- Define a class  "NotesObject"
NotesObject = {
	project = nil,
	timeAxis = nil,
	editor = nil,
	INITIAL_TRACK_NAME_REF = "initialTrackName",
	NUM_TRACKS_REF = "numTracks",
	CURRENT_TRACK_REF = "currentTrack",
	TRACK_TARGET_REF = "trackTarget",
	scriptDataKey = "SCRIPT_Project_DROP_TO_POS",
	scriptStopDataKey = "SCRIPT_Project_STOP_DROP",
	scriptDataKeys = {},
	scriptDataKeysList = {},
	trackTarget = nil,
	initialTrackName = "",
	scriptInstance = "",
	newAudioTrack = nil,
	newGrouptRef = nil,
	currentTrack = nil,
	numTracks = 0,
	selection = nil,
	selectedNotes = nil,
	numSelectedNotes = 0,
	playBack = nil,
	currentSeconds = 0,
	stopProcess = false,
	stopProcessOK = false,
	processing = 0,
	sepParam = "|"
}

-- Constructor method for the NotesObject class
function NotesObject:new()
    local notesObject = {}
    setmetatable(notesObject, self)
    self.__index = self
	
    self.project = SV:getProject()
    self.timeAxis = self.project:getTimeAxis()
    self.editor =  SV:getMainEditor()
	self.numTracks = self.project:getNumTracks()
	
	self.selection = self.editor:getSelection()
	self.selectedNotes = self.selection:getSelectedNotes()
	self.numSelectedNotes = #self.selection:getSelectedNotes()
	
	self.playBack = SV:getPlayback()
	self.currentSeconds = self.playBack:getPlayhead()
	self.currentTrack = self.editor:getCurrentTrack()
	self.initialTrackName = self.currentTrack:getName()
	self.trackTarget = self.currentTrack

    return self
end

--- Get last created track
function NotesObject:getLastTrack()
	return self.project:getTrack(self.project:getNumTracks())
end

-- Display message box
function NotesObject:show(message)
	SV:showMessageBox(SV:T(SCRIPT_TITLE), message)
end

-- Get string format from seconds
function NotesObject:secondsToClock(timestamp)
	return string.format("%02d:%06.3f", 
	  --math.floor(timestamp/3600), 
	  math.floor(timestamp/60)%60, 
	  timestamp%60):gsub("%.",",")
end

-- Get object properties
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			
			local varType = type(v)
			
			if varType == "table" then
				result = result .. SV:T("Tb") .. " " .. self:trim(tostring(k)) .. ": "
				-- result = result .. " (" .. #v .. "): " -- size
				if level < maxLevel then
					result = result .. self:getObjectProperties(v, level)
				-- else
					-- result = result .. "\r"
				end
			else
				local data = self:trim(tostring(v))
				local dataText = data
				if varType == "string" then
					dataText = dataText .. " (" .. #v .. ") " -- size
				end
				local levelInfo = ""
				-- if level > 1 then
					-- levelInfo = "(Lv" .. level .. ") "
				-- end
				if  #data > 0 then
					result = result .. levelInfo .. self:trim(tostring(k)) .. "=" .. dataText
				else
					result = result .. levelInfo .. self:trim(tostring(k))
				end
				
				if varType == "boolean" then
					result = result .. " ".. "(" .. SV:T("b") .. ")"
				elseif varType == "string" then
					result = result .. " ".. "(" .. SV:T("s") .. ")"
				elseif varType == "number" then
					result = result .. " ".. "(" .. SV:T("n") .. ")"
				else
					result = result .. " " .. "(" .. varType .. ")"
				end
				 result = result .. "\r"
			end
		end
	end
	return result
end

-- Set track name waiting
function NotesObject:setTrackNameWaiting()
	if self.trackTarget ~= nil then
		self.processing = self.processing  + 1
		if self.processing > 4 then
			self.processing = 0
		end
		local followString = string.rep(".", self.processing)
		local targetName = followString .. " " .. self.initialTrackName
		self.trackTarget:setName(targetName)
	end
end

-- Create group for internal data
function NotesObject:createInternalGroup()
	-- Create new group 
	local noteGroup = SV:create("NoteGroup")
	self.project:addNoteGroup(noteGroup)
	
	local newGrouptRef = SV:create("NoteGroupReference", noteGroup)
	
	return newGrouptRef, noteGroup
end

-- Main loop
function NotesObject:loop()
	local newSelectedNotes = #self.selection:getSelectedNotes()
	local cause = ""

	if self:isScriptProjectDataStopped() then
		self.stopProcess = true
	end
	
	if self.numSelectedNotes ~= newSelectedNotes then
		cause = "Selected notes: " .. self.numSelectedNotes .. "/" .. newSelectedNotes
		self.stopProcess = true
	end
	
	if self.stopProcess then
		-- self:show("cause: " .. cause)
		self:endOfScript()
	else
		-- if a new same script instance is running or track is deleted by another script
		if self.numTracks > self.project:getNumTracks() then
			self.stopProcess = true
			self:endOfScript()
		else
			-- Scan a new track
			self:scanNewTrack()
			
			if not self.stopProcess then
				SV:setTimeout(500, function() self:loop() end)
			else
				self:endOfScript()
			end
		end
	end	
end

-- Scan a new track
function NotesObject:scanNewTrack()
	SV:setTimeout(100, function() self:setTrackNameWaiting() end)
	
	self.currentSeconds = self.playBack:getPlayhead()
	local secondsInfo = self:secondsToClock(self.currentSeconds)
	
	-- Check if a new track is created
	if self.numTracks < self.project:getNumTracks() then
		self.newAudioTrack = self:getLastTrack()
		local numGroups = self.newAudioTrack:getNumGroups()
		
		if numGroups == 1 then
			local groupRef = self.newAudioTrack:getGroupReference(1)
			local groupTarget = groupRef:getTarget()
			if groupRef ~= nil then
				-- Audio?
				if groupRef:isInstrumental() then
					local groupOnset = groupRef:getOnset()
					local newStartPosition = self.timeAxis:getBlickFromSeconds(self.currentSeconds)
					local duration = groupRef:getDuration()
					if duration > 0 then
						groupRef:setTimeRange(newStartPosition, duration)
						groupRef:setTimeOffset(newStartPosition)					
						-- self.newAudioTrack:setName(groupTarget:getName())
						self.stopProcess = true		-- End of process
						self.stopProcessOK = true	-- End of process OK
						self:setTrackTarget()
					end
				end
			end
		end
	end
end

-- Stop script 
function NotesObject:stopScript()
	self.stopProcess = true
	SV:finish()
end

-- Set track target
function NotesObject:setTrackTarget()
	if self.trackTarget ~= nil then
		SV:setTimeout(10, function() self.trackTarget:setName(self.initialTrackName) end)
	end
end

-- Clear user project data
function NotesObject:clearProjectData()
	self.projectDataInfos = ""
	self.project:removeScriptData(self.scriptDataKey)
	self.project:removeScriptData(self.scriptStopDataKey)
end

-- Save user project data
function NotesObject:saveProjectData(data)
	self.project:setScriptData(self.scriptDataKey, data)
end

-- Get user project data
function NotesObject:getProjectData()
	local projectData = ""
	projectData = self.project:getScriptData(self.scriptDataKey) or ""
	return projectData
end

-- Get user stop project data
function NotesObject:getStopProjectData()
	local stopProjectData = ""
	stopProjectData = self.project:getScriptData(self.scriptStopDataKey) or ""
	return stopProjectData
end

-- Get user project data stopped
function NotesObject:isScriptProjectDataStopped()
	local result = false
	local stopProjectData = self:getStopProjectData()
	if #stopProjectData > 0 then 
		result = true
	end
	return result
end

-- Get script data keys
function NotesObject:getProjectScriptDataKeys()
	local projectDataInfos = ""
	local otherKeysCount = 0
	self.scriptDataKeys = self.project:getScriptDataKeys()
	self.scriptDataKeysList = {}
	if #self.scriptDataKeys > 1 then
		table.insert(self.scriptDataKeysList, SV:T("See other project keys"))
		if #self.projectFilename > 0 then
			for _, key in ipairs(self.scriptDataKeys) do
				if key ~= self.scriptDataKey then
					local val = self.project:getScriptData(key)
					projectDataInfos = projectDataInfos .. key .. " (" .. #val .. " " .. SV:T("chars") .. ")" .. "\r"
					otherKeysCount = otherKeysCount + 1
					table.insert(self.scriptDataKeysList, key)
				end
			end
		end
		if #projectDataInfos > 0 then
			projectDataInfos = SV:T("Scripts project keys found:") .. " " .. otherKeysCount .. "\r"
		end
	else
		table.insert(self.scriptDataKeysList, SV:T("No other project keys"))
	end
	return projectDataInfos
end

-- Display project script data
function NotesObject:displayProjectScriptData()
	local infos = ""
	self.projectDataInfos = self:getProjectScriptDataKeys()
	self:show(self.projectDataInfos)
end

-- End of script 
function NotesObject:endOfScript()
	self.stopProcess = true
	self:setTrackTarget()
	-- clean previous data
	SV:setTimeout(10, function() self:clearProjectData() end)
	SV:setTimeout(50, function() self:finishScriptProcess() end)
end

-- Finish script processing
function NotesObject:finishScriptProcess()
	-- End of script
	SV:finish()
end

-- trim string
function NotesObject:trim(s)
	  return s:match'^()%s*$' and '' or s:match'^%s*(.*%S)'
end

-- Get stored project data 
function NotesObject:getStoredProjectData()
	local result = false
	
	local data = self:getProjectData()

	if data ~= nil and #data > 0 then

		local paramSlitted = self:split(data, self.sepParam)
		for iLine = 1, #paramSlitted do
			local param = paramSlitted[iLine]
			local paramArray = self:split(param, "=")
			local paramKey = ""
			local paramValue = ""
			
			if paramArray[1] ~= nil then
				paramKey = self:trim(paramArray[1])
			end
			
			if paramArray[2] ~= nil then
				paramValue = self:trim(paramArray[2])
			end
			self:setParametersFromStoredData(paramKey, paramValue)
		end		
		result = true
	end

	return result
end

-- Set parameters from stored data
function NotesObject:setParametersFromStoredData(paramName, value)
	if string.find(paramName, self.INITIAL_TRACK_NAME_REF) then
		self.initialTrackName = value
	end
	if string.find(paramName, self.NUM_TRACKS_REF) then
		self.numTracks = tonumber(value)
	end	
	if string.find(paramName, self.TRACK_TARGET_REF) then
		local iTrack = tonumber(value)
		if iTrack <= self.numTracks then
			self.trackTarget = self.project:getTrack(iTrack)
		else
			self:show(SV:T("Error in saved parameters, try again!"))
			self:stopScript()
		end
	end
	if string.find(paramName,self.CURRENT_TRACK_REF) then
		local iTrack = tonumber(value)
		if iTrack <= self.numTracks then
			self.currentTrack = self.project:getTrack(iTrack)
		else
			self:show(SV:T("Error in saved parameters, try again!"))
			self:stopScript()
		end
	end
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

-- Store data to project script data
function NotesObject:storeToProjectScriptData()
	
	local data = self.INITIAL_TRACK_NAME_REF	.. "=" .. self.initialTrackName					.. self.sepParam
		.. self.NUM_TRACKS_REF					.. "=" .. self.numTracks						.. self.sepParam
		.. self.TRACK_TARGET_REF				.. "=" .. self.trackTarget:getIndexInParent()	.. self.sepParam
		.. self.CURRENT_TRACK_REF				.. "=" .. self.currentTrack:getIndexInParent()
	self:saveProjectData(data)
	
	-- Get this stored project data
	self:getStoredProjectData()
end

-- Stop process
function NotesObject:stopCurrentProcess()
	self.stopProcess = true
	self.project:setScriptData(self.scriptStopDataKey, "STOP")
end

-- Start process
function NotesObject:startProcess()
	
	-- Is first script processing
	if not self:getStoredProjectData() then
		self.project:newUndoRecord()
		self:storeToProjectScriptData()
		SV:setTimeout(100, function() self:loop() end)
	else
		self:stopCurrentProcess()
		SV:setTimeout(100, function() self:endOfScript() end)
	end
end

-- Main processing task	
function main()
	local notesObject = NotesObject:new()
	notesObject:startProcess()
end
