local SCRIPT_TITLE = 'Move time V1.0'

--[[

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

Move times notes with all parameters & tempo in all tracks
	Version 1 : Creation
			2 : Add multitrack
			
!! Version for Synthesizer V 1 ONLY !!

2026 - JF AVILES
--]]

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

-- Generated by JFA TranslateScripts.lua
function getArrayLanguageStrings()
	return {
		["en-us"] = {
			{"Shift project to the left/right (time bars)", "Shift project to the left/right (time bars)"},
			{"Click OK button to start!", "Click OK button to start!"},
			{"for 1 bar, shift time in seconds: ", "for 1 bar, shift time in seconds: "},
			{"Notes cannot be lower than the starting track!", "Notes cannot be lower than the starting track!"},
			{"Tempo markings are affected; pay attention to the time-stretching behavior!", "Tempo markings are affected; pay attention to the time-stretching behavior!"},
		},
	}
end

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

-- Define a class  "NotesObject"
NotesObject = {
	project = nil,
	projectFileName = "",
	notesFileName = "",
	numTracks = 0,
	currentSeconds = "",
	newGroupRef = {},
	BPM = 120,
	newTimeGap = 1,
	timeGapMinValue = -4,
	timeGapMaxValue = 4,
	timeGapInterval = 1,
	timeGapDefaultValue = 1,
	infos = ""
}

-- Constructor method for the NotesObject class
function NotesObject:new()
    local notesObject = {}
    setmetatable(notesObject, self)
    self.__index = self
	
    self.project = SV:getProject()
	self.numTracks = self.project:getNumTracks()
	self.projectFileName = self.project:getFileName()
	
    return self
end

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

-- Trim string
function NotesObject:trim(s)
	return s:match'^()%s*$' and '' or s:match'^%s*(.*%S)'
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 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

-- 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

-- Move time notes
function NotesObject:moveNotes(newTime)
	
	for iTrack = 1, self.numTracks do
		local track = self:getProject():getTrack(iTrack)
		local numGroup = track:getNumGroups()
		local newTimeInBlicks = self:getTimeAxis():getBlickFromSeconds(newTime)
		self.newGroupRef = {}

		-- Loop groups
		for iGroupNote = 1, track:getNumGroups() do
			local groupRef = track:getGroupReference(iGroupNote)
			local noteGroup = groupRef:getTarget()
			numNotes = noteGroup:getNumNotes()
			if numNotes > 0 then
				if not groupRef:isMain() then
					local newTimeBlicks = groupRef:getOnset() + newTimeInBlicks
					
					local newGrouptRef = SV:create("NoteGroupReference", noteGroup)
					newGrouptRef:setTimeOffset(newTimeBlicks)
					table.insert(self.newGroupRef, newGrouptRef)
				else
					local notesToUpdate = {}
					-- Get notes
					for iNote = 1, numNotes do
						local note = noteGroup:getNote(iNote)
						table.insert(notesToUpdate, {note = note, newTime = note:getOnset() + newTimeInBlicks})
					end
					-- Update notes
					for iNote = 1, #notesToUpdate do
						notesToUpdate[iNote].note:setOnset(notesToUpdate[iNote].newTime)
					end
				end
			end
		end
		
		-- Loop groups to remove old ones (except main)
		local iOldGroups = track:getNumGroups()
		while iOldGroups > 1 do
			local groupRef = track:getGroupReference(iOldGroups)
			local index = groupRef:getIndexInParent()
			if groupRef ~= nil and not groupRef:isMain() then
				track:removeGroupReference(index)
				iOldGroups = track:getNumGroups()
			end
		end
		
		-- Add new groups
		for iGroupRef = 1, #self.newGroupRef do
			local newGroupRef = self.newGroupRef[iGroupRef]
			track:addGroupReference(newGroupRef)
		end
	end
	
	self:moveTempoMarks(newTime)

end

-- Is new group added
function NotesObject:isNewGroupRef(groupRef)
	local result = false
	for iGroupRef = 1, #self.newGroupRef do
		if groupRef == self.newGroupRef[iGroupRef] then
			result = true
		end
	end
	return result
end

-- Get first group position
function NotesObject:getFirstGroup()
	local result = -1
	local previousResult = -1
	
	for iTrack = 1,  self.numTracks do
		local track = self:getProject():getTrack(iTrack)
		local numGroup = track:getNumGroups()
		
		-- Loop groups
		for iGroupNote = 1, track:getNumGroups() do
			local groupRef = track:getGroupReference(iGroupNote)		
			if not groupRef:isMain() then
				result = groupRef:getOnset()
				break
			end
		end
		if result > -1 and previousResult > -1 and result > previousResult then
			result = previousResult
		end
		previousResult = result
	end
	return result
end

-- Get first note position
function NotesObject:getFirstNoteInMain()
	local result = -1
	local previousResult = -1
	
	for iTrack = 1, self.numTracks do
		local track = self:getProject():getTrack(iTrack)
		local numGroup = track:getNumGroups()
		
		-- Loop groups
		for iGroupNote = 1, track:getNumGroups() do
			local groupRef = track:getGroupReference(iGroupNote)
			local noteGroup = groupRef:getTarget()
			numNotes = noteGroup:getNumNotes()
			if numNotes > 0 then
				if groupRef:isMain() then
					result = noteGroup:getNote(1):getOnset()
					break
				end
			end
		end	
		if result > -1 and previousResult > -1 and result > previousResult then
			result = previousResult
		end
		previousResult = result
	end	
	return result
end

-- Get first time tempo marks
function NotesObject:getFirstTempoMarks()
	local tempoMarks = self:getTimeAxis():getAllTempoMarks()
	local tempoMarkPos = -1
	local tempoSeconds = -1
	for iTempo = 1, #tempoMarks do
		local tempoMark = tempoMarks[iTempo]
		if tempoMark ~= nil and iTempo > 1 then
			tempoPos = tempoMark.position
			tempoSeconds = tempoMark.positionSeconds
			tempoBpm = tempoMark.bpm
			tempoMarkPos = tempoPos
			break
		end
	end
	return tempoMarkPos, tempoSeconds
end

-- Move time tempo marks
function NotesObject:moveTempoMarks(newTime)
	local tempoMarks = self:getTimeAxis():getAllTempoMarks()
	for iTempo = 1, #tempoMarks do
		local tempoMark = tempoMarks[iTempo]
		if tempoMark ~= nil and iTempo > 1 then
			tempoPos = tempoMark.position
			tempoSeconds = tempoMark.positionSeconds
			tempoBpm = tempoMark.bpm
			self:getTimeAxis():removeTempoMark(tempoPos)
			local newPos = tempoPos + self:getTimeAxis():getBlickFromSeconds(newTime)
			self:getTimeAxis():addTempoMark(newPos, tempoBpm)
		end
	end
end

-- Get current project tempo
function NotesObject:getProjectTempo(seconds)
	local tempoActive = 120
	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

-- Show custom dialog box
function NotesObject:showForm(title, infos)
	local form = {
		title = SV:T(SCRIPT_TITLE),
		message = title,
		buttons = "OkCancel",
		widgets = {
			{
				name = "infos", type = "TextArea", label = infos, height = 0
			},
			{
				name = "newTimeGap", type = "Slider",
				label = SV:T("Shift project to the left/right (time bars)"),
				format = "%3.0f",
				minValue = self.timeGapMinValue,
				maxValue = self.timeGapMaxValue, 
				interval = self.timeGapInterval, 
				default = self.timeGapDefaultValue
			},
			{
				name = "separator", type = "TextArea", label = "", height = 0
			}
		}
	}
	self.dialogTitle = title
	return SV:showCustomDialog(form)
end

-- Start project notes processing
function NotesObject:start()
	local title = SV:T("Click OK button to start!")
	
	self.BPM = self:getProjectTempo(0)
	local BPM_ratio = 240 / self.BPM
	self.currentSeconds = BPM_ratio
	-- local measureBarTime = BPM_ratio / 8 -- 1/16
	
	local secondsInfo = "BPM: " .. tostring(self.BPM)
	secondsInfo = secondsInfo .. ", " .. SV:T("for 1 bar, shift time in seconds: ") .. self:secondsToClock(self.currentSeconds)
	
	local userInput = self:showForm(title, secondsInfo)
	if userInput.status then
		SV:getProject():newUndoRecord()		
		-- self.infos = userInput.answers.infos
		self.newTimeGap = userInput.answers.newTimeGap
		
		self.currentSeconds = self.newTimeGap * self.currentSeconds
		local currentSecondsInBlick = self:getTimeAxis():getBlickFromSeconds(self.currentSeconds)

		local firstNotePos = self:getFirstNoteInMain()
		local firstNotePosSeconds = 0
		local diffNotesPosSeconds = 0
		if firstNotePos >= 0 then
			firstNotePosSeconds = self:getTimeAxis():getSecondsFromBlick(firstNotePos)
			diffNotesPosSeconds = firstNotePosSeconds + self.currentSeconds
		end
		
		local firstGroupPos = self:getFirstGroup()
		local firstGroupPosSeconds = 0
		local diffGroupPosSeconds = 0
		if firstGroupPos >= 0 then
			firstGroupPosSeconds = self:getTimeAxis():getSecondsFromBlick(firstGroupPos)
			diffGroupPosSeconds = firstGroupPosSeconds + self.currentSeconds
		end		 
		
		if (firstNotePos >= 0 and diffNotesPosSeconds < 0) or (firstGroupPos >= 0 and diffGroupPosSeconds < 0) then
			self:show(SV:T("Notes cannot be lower than the starting track!"))
		else
			local firstTempoMark, firstTempoMarkSeconds = self:getFirstTempoMarks()
			if self.currentSeconds > 0 and self.currentSeconds > firstTempoMarkSeconds then
				self:show(SV:T("Tempo markings are affected; pay attention to the time-stretching behavior!"))
			end
			self:moveNotes(self.currentSeconds)
		end
	end
end

-- Main processing task	
function main()	
	local notesObject = NotesObject:new()
	notesObject:start()
	
	-- End of script
	SV:finish()
end
