local log, Line, LineCollection, Math, tags, Transform
version = '1.1.8'

haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl'

if haveDepCtrl
	version = DependencyControl {
		name: 'MotionHandler'
		:version
		description: 'A class for applying motion data to a LineCollection.'
		author: 'torque'
		url: 'https://github.com/TypesettingTools/Aegisub-Motion'
		moduleName: 'a-mo.MotionHandler'
		feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
		{
			{ 'a-mo.Log',            version: '1.0.0'            }
			{ 'a-mo.Line',           version: '1.5.3'           }
			{ 'a-mo.LineCollection', version: '1.3.0' }
			{ 'a-mo.Math',           version: '1.0.0'           }
			{ 'a-mo.Tags',           version: '1.3.4'           }
			{ 'a-mo.Transform',      version: '1.2.4'      }
		}
	}
	log, Line, LineCollection, Math, tags, Transform = version\requireModules!

else
	Line           = require 'a-mo.Line'
	LineCollection = require 'a-mo.LineCollection'
	log            = require 'a-mo.Log'
	Math           = require 'a-mo.Math'
	tags           = require 'a-mo.Tags'
	Transform      = require 'a-mo.Transform'

class MotionHandler
	@version: version

	new: ( @lineCollection, mainData, rectClipData = { }, vectClipData = { } ) =>
		-- Create a local reference to the options table.
		@options = @lineCollection.options
		@lineTrackingData = mainData.dataObject
		@rectClipData = rectClipData.dataObject
		@vectClipData = vectClipData.dataObject
		@xDelta = 0
		@yDelta = 0

		@callbacks = { }

		-- Do NOT perform any normal callbacks if mainData is shake
		-- rotoshape. In theory it would be possible to do plain translation
		-- because the SRS data contains a center_x and center_y field for
		-- each frame.
		unless 'SRS' == mainData.type or @options.main.clipOnly
			if @options.main.xPosition or @options.main.yPosition or @options.main.xScale or @options.main.zRotation

				if @options.main.absPos
					@callbacks["(\\pos)%(([%-%d%.]+,[%-%d%.]+)%)"] = absolutePosition
				else
					@callbacks["(\\pos)%(([%-%d%.]+,[%-%d%.]+)%)"] = position

			if @options.main.origin
				@callbacks["(\\org)%(([%-%d%.]+,[%-%d%.]+)%)"] = origin

			if @options.main.xScale then
				@callbacks["(\\fsc[xy])([%d%.]+)"] = scale
				if @options.main.border
					@callbacks["(\\[xy]?bord)([%d%.]+)"] = scale
				if @options.main.shadow
					@callbacks["(\\[xy]?shad)([%-%d%.]+)"] = scale
				if @options.main.blur
					@callbacks["(\\blur)([%d%.]+)"] = blur

			if @options.main.zRotation
				@callbacks["(\\frz?)([%-%d%.]+)"] = rotate

		-- Don't support SRS for rectangular clips.
		if @rectClipData and 'SRS' != rectClipData.type
			@callbacks['(\\i?clip)(%([%-%d%.]+,[%-%d%.]+,[%-%d%.]+,[%-%d%.]+%))'] = rectangularClip

		if @vectClipData
			if 'SRS' == vectClipData.type
				@callbacks['(\\i?clip)%(([^,]-)%)'] = vectorClipSRS
			else
				@callbacks['(\\i?clip)(%([^,]-%))'] = vectorClip

		@resultingCollection = LineCollection @lineCollection.sub
		@resultingCollection.shouldInsertLines = true
		@resultingCollection.options = @options
		-- This has to be copied over for clip interpolation
		@resultingCollection.meta = @lineCollection.meta
		for line in *@lineCollection.lines
			if @options.main.linear and not (@options.main.origin and line.hasOrg) and not ((@rectClipData or @vectClipData) and line.hasClip)
				line.method = linear
			else
				line.method = nonlinear

	applyMotion: =>
		setProgress = aegisub.progress.set
		setProgress 0

		totalLines = #@lineCollection.lines
		-- The lines are collected in reverse order in LineCollection so
		-- that we don't need to do things in reverse here.
		insertNumber = @lineCollection.lines[totalLines].number
		for index = 1, totalLines
			with line = @lineCollection.lines[index]

				-- start frame of line relative to start frame of tracked data
				.relativeStart = .startFrame - @lineCollection.startFrame + 1
				-- end frame of line relative to start frame of tracked data
				.relativeEnd = .endFrame - @lineCollection.startFrame
				.number = insertNumber
				.method @, line

			setProgress index/totalLines*100

		return @resultingCollection

	linear = ( line ) =>
		moveTag = tags.allTags.move
		posTag = tags.allTags.pos
		with line
			startFrameTime = aegisub.ms_from_frame aegisub.frame_from_ms .start_time
			frameAfterStartTime = aegisub.ms_from_frame aegisub.frame_from_ms( .start_time ) + 1
			frameBeforeEndTime = aegisub.ms_from_frame aegisub.frame_from_ms( .end_time ) - 1
			endFrameTime = aegisub.ms_from_frame aegisub.frame_from_ms .end_time
			-- Calculates the time length (in ms) from the start of the first
			-- subtitle frame to the actual start of the line time.
			beginTime = math.floor 0.5*(startFrameTime + frameAfterStartTime) - .start_time
			-- Calculates the total length of the line plus the difference
			-- (which is negative) between the start of the last frame the
			-- line is on and the end time of the line.
			endTime = math.floor 0.5*(frameBeforeEndTime + endFrameTime) - .start_time

			if .move
				.text = .text\gsub moveTag.pattern, ->
					move = .move
					progress = (.start_time - move.start)/(move.end - move.start)
					return posTag\format moveTag\interpolate {move.x1, move.y1}, {move.x2, move.y2}, progress

			for pattern, callback in pairs @callbacks
				log.checkCancellation!
				.text = .text\gsub pattern, ( tag, value ) ->
					values = { }
					for frame in *{ .relativeStart, .relativeEnd }
						@lineTrackingData\calculateCurrentState frame
						values[#values+1] = callback @, value, frame
					("%s%s\\t(%d,%d,%s%s)")\format tag, values[1], beginTime, endTime, tag, values[2]

			if @options.main.xPosition or @options.main.yPosition
				.text = .text\gsub "\\pos(%b())\\t%((%d+,%d+),\\pos(%b())%)", ( start, time, finish ) ->
					"\\move" .. start\sub( 1, -2 ) .. ',' .. finish\sub( 2, -2 ) .. ',' .. time .. ")"

			@resultingCollection\addLine Line( line, nil, { wasLinear: true } ), nil, true, true

	nonlinear = ( line ) =>
		moveTag = tags.allTags.move
		posTag = tags.allTags.pos
		for frame = line.relativeEnd, line.relativeStart, -1
			with line

				log.checkCancellation!

				newStartTime = math.floor(math.max(0, aegisub.ms_from_frame( @lineCollection.startFrame + frame - 1 ))/10)*10
				newEndTime   = math.floor(aegisub.ms_from_frame( @lineCollection.startFrame + frame )/10)*10

				timeDelta = newStartTime - math.floor(math.max(0,aegisub.ms_from_frame( @lineCollection.startFrame + .relativeStart - 1 ))/10)*10

				local newText
				if @options.main.killTrans
					newText = \interpolateTransformsCopy timeDelta, newStartTime
				else
					newText = \detokenizeTransformsCopy timeDelta

				fadeTag = tags.allTags.fade
				if @options.main.killTrans
					local fade
					newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub fadeTag.pattern, ( value ) ->
						fade = fadeTag\convert value
						return ""

					if fade
						-- multiplies every alpha tag by a scaling factor determined by the fade envelope
						local fadeFactor
						-- Piecewise function done with logicals :)
						-- probably should go in fadeTag\interpolate, but I dunno how that whole section is supposed to work
						f = { k, tonumber v for k, v in pairs fade }
						fadeFactor = (
							(timeDelta < f.t1) and f.a1 or
							(timeDelta < f.t2) and f.a1 + (f.a2 - f.a1) * (timeDelta - f.t1) / (f.t2 - f.t1) or
							(timeDelta < f.t3) and f.a2 or
							(timeDelta < f.t4) and f.a2 + (f.a3 - f.a2) * (timeDelta - f.t3) / (f.t4 - f.t3) or
							f.a3
						)
						-- factor between 0 and 1 representing opacity of fade line
						fadeFactor = (255 - fadeFactor) / 255

						-- apply the opacity factor to all alpha tags
						newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub "(\\[1234]?a[lpha]-)&H(%x%x)&", ( alpha, value ) ->
								value = Math.round( 255 - ( fadeFactor * (255 - tonumber value, 16) ) )
								return alpha .. "&H%02X&"\format value

				else
					-- modified each fade tag in every override block
					newText = newText\gsub "({.-})", ( tagBlock ) -> tagBlock\gsub fadeTag.pattern, ( value ) ->
						fade = fadeTag\convert value
						-- Erroneous fade tags are being ignored and left sitting around as long as they doesn't have 2 or 7 arguments.
						-- if t1 == nil
						-- 	message = "There is a malformed \\fade you must fix.\n\\fade requires 7 integer arguments.\nLine: #{.number}, tag: \\fade(#{fade})."
						-- 	if fade\match("(%d+),(%d+)")
						-- 		message ..= "\nPerhaps you meant to use \\fad."
						-- 	log.windowError message
						for i = 4, 7  -- t1 - t4
							fade[i] -= timeDelta
						return fadeTag\format fade

				if .move
					newText = newText\gsub moveTag.pattern, ->
						move = .move
						progress = (timeDelta - move.start)/(move.end - move.start)
						return posTag\format moveTag\interpolate {move.x1, move.y1}, {move.x2, move.y2}, progress

				-- In theory, this is more optimal if we loop over the frames on
				-- the outside loop and over the lines on the inside loop, as
				-- this only needs to be calculated once for each frame, whereas
				-- currently it is being calculated for each frame for each
				-- line. However, if the loop structure is changed, then
				-- inserting lines into the resultingCollection would need to be
				-- more clever to compensate for the fact that lines would no
				-- longer be added to it in order.
				@lineTrackingData\calculateCurrentState frame

				-- iterate through the necessary operations
				for pattern, callback in pairs @callbacks
					newText = newText\gsub pattern, ( tag, value ) ->
						tag .. callback @, value, frame

				newLine = Line line, @resultingCollection, {
					text: newText,
					start_time: newStartTime,
					end_time: newEndTime,
					transformsAreTokenized: false,
				}
				newLine.karaokeShift = (newStartTime - .start_time)*0.1

				@resultingCollection\addLine newLine, nil, true, true

	position = ( pos, frame ) =>
		x, y = pos\match "([%-%d%.]+),([%-%d%.]+)"
		x, y = positionMath x, y, @lineTrackingData
		("(%g,%g)")\format Math.round( x, 2 ), Math.round( y, 2 )

	positionMath = ( x, y, data ) ->
		x = (tonumber( x ) - data.xStartPosition)*data.xRatio
		y = (tonumber( y ) - data.yStartPosition)*data.yRatio
		radius = math.sqrt( x^2 + y^2 )
		alpha  = Math.dAtan( y, x )
		x = data.xCurrentPosition + radius*Math.dCos( alpha - data.zRotationDiff )
		y = data.yCurrentPosition + radius*Math.dSin( alpha - data.zRotationDiff )
		return x, y

	absolutePosition = ( pos, frame ) =>
		x, y = pos\match "([%-%d%.]+),([%-%d%.]+)"
		@xDelta = @lineTrackingData.xPosition[frame] - x
		@yDelta = @lineTrackingData.yPosition[frame] - y
		("(%g,%g)")\format Math.round( @lineTrackingData.xPosition[frame], 2 ), Math.round( @lineTrackingData.yPosition[frame], 2 )

	-- Needs to be fixed.
	origin = ( origin, frame ) =>
		ox, oy = origin\match("([%-%d%.]+),([%-%d%.]+)")
		ox, oy = positionMath ox, oy, @lineTrackingData
		("(%g,%g)")\format Math.round( ox, 2 ), Math.round( oy, 2 )

	scale = ( scale, frame ) =>
		scale *= @lineTrackingData.xRatio
		tostring Math.round scale, 2

	blur = ( blur, frame ) =>
		ratio = @lineTrackingData.xRatio
		ratio = 1 - (1 - ratio)*@options.main.blurScale

		tostring Math.round blur*ratio, 2

	rotate = ( rotation, frame ) =>
		rotation += @lineTrackingData.zRotationDiff
		tostring Math.round rotation, 2

	rectangularClip = ( clip, frame ) =>
		@rectClipData\calculateCurrentState frame
		@rectClipData.zRotationDiff = 0

		return clip\gsub "([%.%d%-]+),([%.%d%-]+)", ( x, y ) ->
			x, y = x + @xDelta, y + @yDelta
			x, y = positionMath x, y, @rectClipData
			("%g,%g")\format Math.round( x, 2 ), Math.round( y, 2 )

	vectorClip = ( clip, frame ) =>
		-- This is redundant if vectClipData is the same as
		-- lineTrackingData.
		@vectClipData\calculateCurrentState frame

		return clip\gsub "([%.%d%-]+) ([%.%d%-]+)", ( x, y ) ->
			x, y = x + @xDelta, y + @yDelta
			x, y = positionMath x, y, @vectClipData
			("%g %g")\format Math.round( x, 2 ), Math.round( y, 2 )

	vectorClipSRS = ( clip, frame ) =>
		return '(' .. clip .. ' ' .. @vectClipData.data[frame]\sub( 1, -2 ) .. ')'

if haveDepCtrl
	return version\register MotionHandler
else
	return MotionHandler
