-- See LICENSE for more info about your rights as a person to be
-- rightfully persecuted

export script_name        = "Aegisub-Motion"
export script_description = "A set of tools for simplifying the process of creating and applying motion tracking data with Aegisub."
export script_author      = "torque"
export script_version     = "1.0.9"
export script_namespace   = "a-mo.Aegisub-Motion"

local interface, setProgress, setTask
local versionRecord, clipboard, json, ConfigHandler, DataWrapper
local LineCollection, log, Math, MotionHandler, Statistics, TrimHandler, Tags

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

if haveDepCtrl
	versionRecord = DependencyControl {
		url: 'https://github.com/TypesettingTools/Aegisub-Motion'
		feed: 'https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json'
		{
			'aegisub.clipboard'
			'json'
			{ 'a-mo.ConfigHandler',  version: '1.1.4'  }
			{ 'a-mo.DataWrapper',    version: '1.0.2'    }
			{ 'a-mo.LineCollection', version: '1.3.0' }
			{ 'a-mo.Log' ,           version: '1.0.0'            }
			{ 'a-mo.Math' ,          version: '1.0.0'           }
			{ 'a-mo.MotionHandler',  version: '1.1.8'  }
			{ 'a-mo.Statistics' ,    version: '0.1.3'     }
			{ 'a-mo.TrimHandler',    version: '1.0.5'    }
			{ 'a-mo.Tags',           version: '1.3.4'           }
		}
	}
	clipboard, json, ConfigHandler, DataWrapper, LineCollection, log, Math, MotionHandler, Statistics, TrimHandler, Tags = versionRecord\requireModules!

else
	clipboard      = require 'aegisub.clipboard'
	json           = require 'json'
	ConfigHandler  = require 'a-mo.ConfigHandler'
	DataWrapper    = require 'a-mo.DataWrapper'
	LineCollection = require 'a-mo.LineCollection'
	log            = require 'a-mo.Log'
	Math           = require 'a-mo.Math'
	MotionHandler  = require 'a-mo.MotionHandler'
	Statistics     = require 'a-mo.Statistics'
	TrimHandler    = require 'a-mo.TrimHandler'
	Tags           = require 'a-mo.Tags'

statsTemplate = {
	apply: {
		longestTrack: 0
		lines: { largestInput: 0, largestOutput: 0, totalOutput: 0 }
		bytes: { largestInput: 0, largestOutput: 0, totalOutput: 0 }
		runCount: 0
	}
	trim: {
		runCount: 0
		clipsCreated: 0
	}
	trimEach: {
		runCount: 0
	}
	revert: {
		lines: { total: 0 }
		bytes: { total: 0 }
		runCount: 0
	}
	uuid: 0
}

initStats = ->
	stats = Statistics statsTemplate, "aegisub-motion.stats.json"
	if 0 == stats\getValue "uuid"
		stats\setValue "uuid", Math.uuid!
		stats\write!

	return stats

initializeInterface = ->
	-- Set up interface tables.
	interface = {
		main: {
			-- mnemonics: xyOCSBuRWen + G\A + Wl\A
			dataLabel: { class: "label",    x: 0, y: 0,  width: 10, height: 1,                                  label: "                 Paste data or enter a filepath." }
			data:      { class: "textbox",  x: 0, y: 1,  width: 10, height: 4,                                                     hint: "Paste data or the path to a file containing it. No quotes or escapes." }

			-- optLabel:  { class: "label",    x: 0, y: 5,  width: 5,  height: 1,                                  label: "Data to be applied:" }
			xPosition: { class: "checkbox", x: 0, y: 5,  width: 1,  height: 1, config: true, label: "&x",            value: true,  hint: "Apply x position data to the selected lines." }
			yPosition: { class: "checkbox", x: 1, y: 5,  width: 1,  height: 1, config: true, label: "&y",            value: true,  hint: "Apply y position data to the selected lines." }
			origin:    { class: "checkbox", x: 2, y: 5,  width: 2,  height: 1, config: true, label: "&Origin",       value: false, hint: "Move the origin along with the position." }
			absPos:    { class: "checkbox", x: 4, y: 5,  width: 2,  height: 1, config: true, label: "Absolut&e",     value: false, hint: "Set position to exactly that of the tracking data with no processing." }

			xScale:    { class: "checkbox", x: 0, y: 6,  width: 2,  height: 1, config: true, label: "&Scale",        value: true,  hint: "Apply scaling data to the selected lines." }
			border:    { class: "checkbox", x: 2, y: 6,  width: 2,  height: 1, config: true, label: "&Border",       value: true,  hint: "Scale border with the line (only if Scale is also selected)." }
			shadow:    { class: "checkbox", x: 4, y: 6,  width: 2,  height: 1, config: true, label: "&Shadow",       value: true,  hint: "Scale shadow with the line (only if Scale is also selected)." }
			blur:      { class: "checkbox", x: 4, y: 7,  width: 2,  height: 1, config: true, label: "Bl&ur",         value: true,  hint: "Scale blur with the line (only if Scale is also selected, does not scale \\be)." }
			blurScale: { class:"floatedit", x: 7, y: 7,  width: 2,  height: 1, config: true, step: 0.01,             value: 1,     hint: "Factor to attenuate (or amplify) blur values by." }

			zRotation: { class: "checkbox", x: 0, y: 7,  width: 3,  height: 1, config: true, label: "&Rotation",     value: false, hint: "Apply rotation data to the selected lines." }

			writeConf: { class: "checkbox", x: 0, y: 10, width: 4,  height: 1, config: true, label: "&Write config", value: true,  hint: "Write current settings to the configuration file." }
			relative:  { class: "checkbox", x: 4, y: 10, width: 3,  height: 1, config: true, label: "Relat&ive",     value: true,  hint: "Start frame should be relative to the beginning of the selection rather than the beginning of the video." }
			startFrame:{ class: "intedit",  x: 7, y: 10, width: 2,  height: 1, config: true,                         value: 1,     hint: "Frame used as the starting point for the tracking data. \"-1\" corresponds to the last frame." }
			linear:    { class: "checkbox", x: 4, y: 11, width: 2,  height: 1, config: true, label: "Li&near",       value: false, hint: "Use transforms and \\move to create a linear transition, instead of frame-by-frame." }
			clipOnly:  { class: "checkbox", x: 0, y: 11, width: 3,  height: 1, config: true, label: "&Clip Only",    value: false, hint: "Only apply the main data to \\clips present in the line." }

			rectClip:  { class: "checkbox", x: 0, y: 8,  width: 3,  height: 1, config: true, label: "Rect C&lip",    value: true,  hint: "Apply tracking data to the rectangular clip contained in the line." }
			vectClip:  { class: "checkbox", x: 3, y: 8,  width: 3,  height: 1, config: true, label: "&Vect Clip",    value: true,  hint: "Apply tracking data to the vector clip contained in the line." }
			rcToVc:    { class: "checkbox", x: 6, y: 8,  width: 4,  height: 1, config: true, label: "Rect -> Vect",  value: false, hint: "Convert rectangular clips contained in the line to vector clips." }
			killTrans: { class: "checkbox", x: 0, y: 9,  width: 10, height: 1, config: true, label: "Interpolate &transforms", value: true, hint: "Attempt to interpolate transform value instead of just shifting transform times." }
		}
		clip: {
			-- mnemonics: xySRe + GCA
			dataLabel: { class: "label",    x: 0, y: 0, width: 10, height: 1, label:  "                     This stuff is for clips." }
			data:      { class: "textbox",  x: 0, y: 1, width: 10, height: 4,               name:  "data", hint: "Paste data or the path to a file containing it. No quotes or escapes." }

			optLabel:  { class: "label",    x: 0, y: 5, width: 5,  height: 1, label: "Data to be applied:" }
			xPosition: { class: "checkbox", x: 0, y: 6, width: 1,  height: 1, config: true,  label: "&x",            value: true,  hint: "Apply x position data to the selected clips in the selected lines." }
			yPosition: { class: "checkbox", x: 1, y: 6, width: 1,  height: 1, config: true,  label: "&y",            value: true,  hint: "Apply y position data to the selected clips in the selected lines." }
			xScale:    { class: "checkbox", x: 0, y: 7, width: 2,  height: 1, config: true,  label: "&Scale",        value: true,  hint: "Apply scaling data to the selected clips in the selected lines." }
			zRotation: { class: "checkbox", x: 0, y: 8, width: 3,  height: 1, config: true,  label: "&Rotation",     value: false, hint: "Apply rotation data to the selected clips in the selected lines." }

			rectClip:  { class: "checkbox", x: 0, y: 10, width: 3,  height: 1, config: true, label: "Rect C&lip",    value: true,  hint: "Apply tracking data to the rectangular clip contained in the line." }
			vectClip:  { class: "checkbox", x: 3, y: 10, width: 3,  height: 1, config: true, label: "&Vect Clip",    value: true,  hint: "Apply tracking data to the vector clip contained in the line." }
			rcToVc:    { class: "checkbox", x: 6, y: 10, width: 4,  height: 1, config: true, label: "Rect -> Vect",  value: false, hint: "Convert rectangular clips contained in the line to vector clips." }

			startLabel:{ class: "label",    x: 7, y: 5, width: 3,  height: 1, label: "Start Frame:" }
			startFrame:{ class: "intedit",  x: 7, y: 6, width: 3,  height: 1, config: true,                          value: 1,     hint: "Frame used as the starting point for the tracking data. \"-1\" corresponds to the last frame." }
		}
		trim: {
			pLabel:   { class: "label", x: 0, y: 0, width: 10, height: 1, label: [[
Prefix the encoded video is written. Useful values are ?video for the
directory of the currently loaded video and ?script for the directory
of the currently open script. Can be a hardcoded path, too.]] }
			psLabel:  { class: "label", x: 0, y: 3, width: 10, height: 1, label: [[
Encoding preset. Different presets may have different output.]] }
			eLabel:   { class: "label", x: 0, y: 5, width: 10, height: 1, label: [[
The currently selected encoding binary. Use the "Encoder..." button
below to set this. Manual edits will not be saved.]] }
			cLabel:   { class: "label", x: 0, y: 7, width: 10, height: 1, label: [[
If you want to use a custom encoding command, write it here. Leave this
blank to use the built-in presets (probably what you want).]] }
			prefix:   { config: true, value: "?video", class: "textbox",  x: 0, y: 1, width: 10, height: 1,                                                             hint: "Prefix the encoded video is written. Useful values are ?video for the directory of the currently loaded video and ?script for the directory of the currently open script." }
			makePfix: { config: true, value: true,     class: "checkbox", x: 0, y: 2, width: 10, height: 1, label: "Try to create prefix directory.",                   hint: "Try to create prefix directory." }
			preset:   { config: true, value: "x264",   class: "dropdown", x: 0, y: 4, width: 10, height: 1, label: "Sort lines by", items: TrimHandler.existingPresets, hint: "Choose an existing preset by name." }
			encBin:   { config: true, value: "",       class: "textbox",  x: 0, y: 6, width: 10, height: 1,                                                             hint: "The full path to your encoding binary (x264.exe if you're using the default preset)" }
			command:  { config: true, value: "",       class: "textbox",  x: 0, y: 8, width: 10, height: 4,                                                             hint: "If you want to use a custom encoding command, write it here." }
		}
	}

	if TrimHandler.windows
		interface.trim.writeLog = { config: true, value: true, class: "checkbox", x: 0, y: 12, width: 10, height: 1, label: "Write encode log.", hint: "Write encode log. Allows aegisub-motion to print information if an encode fails. Only matters on Windows." }

	interface

fetchDataFromClipboard = ->
	-- According to vague reports, Aegisub clipboard usage crashes it on
	-- Linux. Disable any clipboard use until I find a better way to
	-- handle this.
	dataString = (jit.os != "Linux") and clipboard.get!

	-- If there's nothing on the clipboard, clipboard.get returns nil.
	return dataString or ""

prepareConfig = ( config, mainData, clipData, lineCollection ) ->
	rectClipData, vectClipData = nil, nil

	totalFrames = lineCollection.totalFrames

	for field, data in pairs { main: mainData, clip: clipData }
		configField = config[field]

		if '' == configField.data or nil == configField.data
			for option in pairs configField
				-- Nuke everything because it doesn't matter at this point.
				configField[option] = false

		else
			-- Be extremely lazy and just re-parse data from scratch.
			unless data\bestEffortParsingAttempt configField.data, lineCollection.meta.PlayResX, lineCollection.meta.PlayResY
				log.windowError "You put something in the #{field} data box\nbut it is wrong in ways I can't imagine."
			unless data.dataObject\checkLength totalFrames
				log.windowError "The length of your #{field} data (#{data.dataObject.length} frames) doesn't match\nthe length of your lines (#{totalFrames} frames) and I quit."

			if configField.rcToVc
				configField.rectClip = true
				configField.vectClip = true
			if configField.rectClip
				rectClipData = data
			if configField.vectClip
				vectClipData = data

	unless config.main.data or config.clip.data
		log.windowError "As far as I can tell, you've forgotten to give me any motion data."

	-- Disable options that depend on scale.
	unless config.main.xScale
		config.main.border = false
		config.main.shadow = false
		config.main.blur   = false

	-- Nudge the start frames.
	if config.main.relative
		for context in *{ 'main', 'clip' }
			-- Have to check that the field exists (if the clip dialog wasn't
			-- opened it will be nil) to avoid comparison with nil errors.
			if config[context].startFrame
				if config[context].startFrame == 0
					config[context].startFrame = 1
				elseif config[context].startFrame < 0
					config[context].startFrame = totalFrames + config[context].startFrame + 1
	else
		for context in *{ 'main', 'clip' }
			if config[context].startFrame
				config[context].startFrame = config[context].startFrame - lineCollection.startFrame + 1
				if config[context].startFrame <= 0
					log.windowError "You have specified an out-of-range absolute\nstart frame and you have been judged."

	return rectClipData, vectClipData

-- Used in getMissingTags, for fade handling when killTrans is used
getMissingAlphas = ( block, properties ) ->
	-- makes sure every necessary alpha tag exists in the first block
	alpha = Tags.allTags.alpha
	alpha1, alpha2, alpha3, alpha4 = Tags.allTags.alpha1, Tags.allTags.alpha2, Tags.allTags.alpha3, Tags.allTags.alpha4
	outline, shadow = Tags.allTags.border, Tags.allTags.shadow

	if block\find alpha.pattern
		return ''
	if ( properties[alpha1] == 0 and properties[alpha2] == 0 and
		 properties[alpha3] == 0 and properties[alpha4] == 0 )
		return alpha\format 0

	alphas = { }
	if not block\find alpha1.pattern
		table.insert alphas, alpha1\format properties[alpha1]
	if ( not block\find alpha2.pattern ) and ( block\find Tags.allTags.karaoke.pattern )
		table.insert alphas, alpha2\format properties[alpha2]
	if ( not block\find alpha3.pattern ) and ( ( block\find "\\[xy]?bord([%d%.]+)" ) or ( properties[outline] > 0 ) )
		table.insert alphas, alpha3\format properties[alpha3]
	if ( not block\find alpha4.pattern ) and ( ( block\find "\\[xy]?shad([%d%.]+)" ) or ( properties[shadow] > 0 ) )
		table.insert alphas, alpha4\format properties[alpha4]

	return table.concat alphas, ''

-- This table is used to verify that style defaults are inserted at
-- the beginning the selected line(s) if the corresponding options are
-- selected. The structure is: tag = { opt:"opt", skip:val } where
-- "opt" is the option that must be enabled and skip specifies
-- not to write the tag if the style default is that value.
importantTags = {
	xscale: { opt: "xScale", skip: 0 }
	yscale: { opt: "xScale", skip: 0 }
	border: { opt: "border", skip: 0 }
	shadow: { opt: "shadow", skip: 0 }
	zrot:   { opt: "zRotation" }
}
-- A style table is passed to this function so that it can cope with
-- \r.
getMissingTags = ( block, options, properties ) ->
	result = { }
	for key, tab in pairs importantTags
		tag = Tags.allTags[key]
		if options[tab.opt]
			if not block\match tag.pattern
				if properties[tag] != tab.skip
					table.insert result, tag\format properties[tag]
	if options.killTrans
		table.insert result, getMissingAlphas block, properties
	return table.concat result, ''

rectClipToVectClip = ( clip ) ->
	if clip\match "[%-%d%.]+, *[%-%d%.]+"
		clip = clip\gsub "([%-%d%.]+), *([%-%d%.]+), *([%-%d%.]+), *([%-%d%.]+)", ( l, t, r, b ) ->
			return table.concat ({
				"m %g %g "\format l, t
				"l %g %g "\format r, t
				"%g %g "\format r, b
				"%g %g"\format l, b
			})

	return clip

convertClipToFP = ( clip ) ->
	-- only muck around with vector clips (convert scaling factor into floating point coordinates).
	unless clip\match "[%-%d%.]+, *[%-%d%.]+"
		-- Convert clip with scale into floating point coordinates.
		clip = clip\gsub "%((%d*),?(.-)%)", ( scaleFactor, points ) ->
			if scaleFactor ~= ""
				scaleFactor = tonumber scaleFactor

				points = points\gsub "([%.%d%-]+) ([%.%d%-]+)", ( x, y ) ->
					x = Math.round tonumber( x )/(2^(scaleFactor - 1)), 2
					y = Math.round tonumber( y )/(2^(scaleFactor - 1)), 2

					("%g %g")\format x, y
			return '(' .. points .. ')'
	return clip

prepareLines = ( lineCollection ) ->
	setProgress 0

	totalLines = #lineCollection.lines

	options = lineCollection.options
	-- remove the lines while ensuring new lines will be inserted in the
	-- correct place.
	lineCollection\deleteLines!

	-- Perform all of the manipulation that used to be performed in
	-- Line.moon but are actually fairly Aegisub-Motion specific.
	lineCollection\runCallback ( line, index ) =>
		log.checkCancellation!
		-- If a line already contains a-mo extradata, it is probably being
		-- tracked again after being tracked. There are some reasons to do
		-- this, but it results in a huge amount of extradata garbage if
		-- unchecked. Presumably 99% of tracking that isn't this case will
		-- be on lines without extradata. This isn't a perfect solution, but
		-- it is probably better than before. This doesn't change
		-- extradata's lack of resilience toward copying lines between
		-- scripts.
		if line\getExtraData 'a-mo'
			line.retrack = true
		else
			-- Add our signature extradata.
			line\setExtraData 'a-mo', { originalText: line.text, uuid: Math.uuid! }

		-- Get default style properties (will be used later if transform
		-- interpolation is enabled)
		line\getPropertiesFromStyle!

		-- need to brutalize fades before tokenizing transforms so that the
		-- produced transforms will be tokenized as well. Alternately (this
		-- may be more clean but also less efficient), tokenize transforms,
		-- dedup tags, detokenize transforms, brutalize fades, and then
		-- retokenize transforms.
		line\tokenizeTransforms!

		line\runCallbackOnOverrides ( tagBlock ) =>
			return tagBlock\gsub "\\fade?%((%d+),(%d+)%)", ( start, finish ) ->
				return "\\fade(255,0,255,0,%d,%d,%d)"\format start, @duration - finish, @duration

		-- retokenize the transforms to simplify later processing.
		line\dontTouchTransforms!
		line\tokenizeTransforms!

		-- Deduplicate all override tags.
		line\deduplicateTags!

		-- Collect alignment and position info for each line.
		styles = @styles
		lineStyle = line.styleRef
		unless line\extraMetrics lineStyle
			-- unless line\moveToPosition math.floor(0.5*(aegisub.ms_from_frame(lineCollection.startFrame + options.main.startFrame - 1) + aegisub.ms_from_frame(lineCollection.startFrame + options.main.startFrame))) - line.start_time
			line\ensureLeadingOverrideBlockExists!

			-- Note that we are repeatedly shadowing @, so in this function it
			-- refers to the line. This is interestingly the opposite of how
			-- fat arrow functions work in coffeescript.
			line\runCallbackOnFirstOverride ( tagBlock ) =>
				return tagBlock\gsub "{", ("{\\pos(%g,%g)")\format @xPosition, @yPosition

		-- Add any tags we need that are missing from the line.
		line\runCallbackOnFirstOverride ( tagBlock ) =>
			tags = getMissingTags tagBlock, options.main, line.properties
			return '{' .. tags .. tagBlock\sub( 2 )

		line\runCallbackOnOverrides ( tagBlock ) =>
			tagBlock\gsub "\\org%([%.%d%-]+,[%.%d%-]+%)", ->
				@hasOrg = true
				return nil

			savestyle = line.styleRef
			reset = false
			tagBlock = tagBlock\gsub "\\r([^\\}]*)([^}]*)", ( resetStyle, remainder ) ->
				if styles[resetStyle]
					line.styleRef = styles[resetStyle]
					line\getPropertiesFromStyle!
					reset = true
				tags = getMissingTags remainder, options.main, line.properties
				return '\\r' .. resetStyle .. tags .. remainder
			if reset
				line.styleRef = savestyle
				line\getPropertiesFromStyle!

			if options.main.rectClip or options.main.vectClip or options.clip.rectClip or options.clip.vectClip
				tagBlock = tagBlock\gsub "(\\i?clip%b())", ( clip ) ->
					@hasClip = true
					clip = convertClipToFP clip
					if options.main.rcToVc or options.clip.rcToVc
						clip = rectClipToVectClip clip
					return clip

			return tagBlock

		unless line.hasClip
			line\runCallbackOnFirstOverride ( tagBlock ) =>
				"{\\clip()" .. tagBlock\sub 2

		setProgress index/totalLines*100

postprocLines = ( lineCollection ) ->
	setProgress 0

	totalLines = #lineCollection.lines

	lineCollection\runCallback ( line, index ) =>
		if line.wasLinear
			line\dontTouchTransforms!
		else
			line\deduplicateTags!

		line\shiftKaraoke!
		line.text = line.text\gsub "{}", ""

		setProgress index/totalLines*100
		log.checkCancellation!

	-- No progress for this.
	lineCollection\combineIdenticalLines!

applyProcessor = ( subtitles, selectedLines ) ->
	setTask     = aegisub.progress.task
	setProgress = aegisub.progress.set

	setTask "Loading Interface"
	initializeInterface!

	math.randomseed tonumber tostring( os.time! )\reverse!\sub( 1, 8 )

	-- Initialize the configuration
	options = ConfigHandler interface, "aegisub-motion.json", true, script_version
	options\read!
	options\updateInterface { "main", "clip" }

	stats = initStats!

	lineCollection = LineCollection subtitles, selectedLines

	currentVideoFrame = aegisub.project_properties!.video_position

	rawInputData = fetchDataFromClipboard!

	-- Instantiate both of these so they can be passed by reference later.
	setTask "Checking Clipboard for Data"
	mainData = DataWrapper!
	clipData = DataWrapper!
	if mainData\bestEffortParsingAttempt rawInputData, lineCollection.meta.PlayResX, lineCollection.meta.PlayResY
		if mainData.dataObject\checkLength lineCollection.totalFrames
			interface.main.data.value = rawInputData
			interface.main.dataLabel.label = "                Data is the correct length."
		else
			interface.main.dataLabel.label = "Data had the wrong framecount. Expected: #{lineCollection.totalFrames}. Got: #{mainData.dataObject.length}"

	if options.configuration.main.relative
		relativeFrame = currentVideoFrame - lineCollection.startFrame + 1
		if relativeFrame > 0 and relativeFrame <= lineCollection.totalFrames
			interface.main.startFrame.value = relativeFrame
			interface.clip.startFrame.value = relativeFrame
	else
		interface.main.startFrame.value = currentVideoFrame
		interface.clip.startFrame.value = currentVideoFrame

	setTask "Launching Interface"

	-- cancel:Abort in the main dialog tells Esc key to abort the entire macro
	-- cancel:Back in \clip dialog tells Esc key to close it and go back to the main dialog
	buttons = {
		main: {
			list: {
				"&Go"
				"Track &\\clip separately"
				"&Quit"
			}
			namedList: {
				ok: "&Go"
				clip: "Track &\\clip separately"
				cancel: "&Quit"
			}
		}
		clip: {
			list: {
				"&Go"
				"&Back"
				"&Quit"
			}
			namedList: {
				ok: "&Go"
				close: "&Back"
				abort: "&Quit"
			}
		}
	}

	currentDialog = "main"
	config = { clip: { }, main: { } }

	while true
		button, config[currentDialog] = aegisub.dialog.display interface[currentDialog], buttons[currentDialog].list, buttons[currentDialog].namedList

		for k, v in pairs config[currentDialog]
			interface[currentDialog][k].value = v

		switch button
			when buttons.main.namedList.clip
				currentDialog = "clip"

			when false, buttons.clip.namedList.abort
				setTask "ABORT"
				aegisub.cancel!

			when buttons.clip.namedList.close
				currentDialog = "main"

			else
				break

	-- Update the persistent configuration before it gets (potentially)
	-- horribly mutilated in prepareConfig. Ensures that what the user saw
	-- last is what will be presented to them next time.
	if config.main.writeConf
		setTask "Updating Configuration"
		options\updateConfiguration config, { "main", "clip" }
	if config.main.writeConf or (options.configuration.main.writeConf != config.main.writeConf)
		options.configuration.main.writeConf = config.main.writeConf
		options\write!

	setTask "Preparing Configuration and Data"
	rectClipData, vectClipData = prepareConfig config, mainData, clipData, lineCollection
	lineCollection.options = config

	setTask "Preprocessing Lines"
	prepareLines lineCollection
	stats\setMax "apply.longestTrack", lineCollection.totalFrames
	stats\setMax "apply.lines.largestInput", #lineCollection.lines
	for line in *lineCollection.lines
		stats\setMax "apply.bytes.largestInput", #line.text

	if mainData.type and 'SRS' != mainData.type
		mainData.dataObject\addReferenceFrame config.main.startFrame
		mainData.dataObject\stripFields config.main
	if clipData.type and 'SRS' != clipData.type
		clipData.dataObject\addReferenceFrame config.clip.startFrame
		clipData.dataObject\stripFields config.clip

	setTask "Applying Data"
	motionHandler = MotionHandler lineCollection, mainData, rectClipData, vectClipData
	newLines = motionHandler\applyMotion!

	-- Postproc lines: detokenize transforms and combine identical lines.
	setTask "Postprocessing Lines"
	postprocLines newLines
	newLines\replaceLines!

	stats\setMax "apply.lines.largestOutput", #newLines.lines
	stats\incrementValue "apply.lines.totalOutput", #newLines.lines
	for line in *newLines.lines
		stats\setMax "apply.bytes.largestOutput", #line.text
		stats\incrementValue "apply.bytes.totalOutput", #line.text
	stats\incrementValue "apply.runCount"
	stats\write!

trimConfigDialog = ( options ) ->
	buttons = {
		{ "&Save", "&Encoder...", "&Cancel" },
		{ ok: "&Save", enc: "&Encoder...", cancel: "&Cancel" }
	}
	while true
		options\updateInterface "trim"
		button, config = aegisub.dialog.display interface.trim, buttons[1], buttons[2]
		if button == buttons[2].ok or button == buttons[2].enc
			-- only update encBin when the open dialog is shown.
			config.encBin = nil
			options\updateConfiguration config, "trim"
			if button == buttons[2].ok
				options\write!
				break
			else
				encoder = aegisub.dialog.open "Choose an Encoding Binary", "", "", "", false, true
				if encoder
					options\updateConfiguration { encBin: encoder }, "trim"
		else
			aegisub.cancel!

trimConfigurator = ->
	initializeInterface!
	options = ConfigHandler interface, "aegisub-motion.json", true, script_version
	options\read!
	trimConfigDialog options

trimProcessor = ( subtitles, selectedLines, activeLine, eachFlag ) ->
	setTask     = aegisub.progress.task
	setProgress = aegisub.progress.set

	initializeInterface!
	options = ConfigHandler interface, "aegisub-motion.json", true, script_version

	stats = initStats!
	options\read!

	-- Check if encBin has been set.
	if options.configuration.trim.encBin == ""
		interface.trim.pLabel.label = [[
You must specify the path to your encoding binary.

]] .. interface.trim.pLabel.label
		trimConfigDialog options

	trim = TrimHandler options.configuration.trim
	if eachFlag
		seenRanges = { }
		for lineIndex in *selectedLines
			lineCollection = LineCollection subtitles, { lineIndex }
			collectionRange = "#{lineCollection.startFrame}-#{lineCollection.endFrame}"
			unless seenRanges[collectionRange]
				seenRanges[collectionRange] = true
				trim\calculateTrimLength lineCollection
				trim\performTrim!
				stats\incrementValue "trim.clipsCreated"
		stats\incrementValue "trimEach.runCount"

	else
		lineCollection = LineCollection subtitles, selectedLines
		trim\calculateTrimLength lineCollection
		trim\performTrim!
		stats\incrementValue "trim.clipsCreated"
		stats\incrementValue "trim.runCount"

	stats\write!
	return selectedLines

trimProcessorEach = ( subtitles, selectedLines ) ->
	trimProcessor subtitles, selectedLines, nil, true

revertProcessor = ( subtitles, selectedLines ) ->
	setTask     = aegisub.progress.task
	setProgress = aegisub.progress.set
	stats = initStats!

	setTask "Collecting UUIDs"
	-- A table of all UUIDs found in the selected lines
	uuids = { }
	-- Indices of lines that need to be removed later (are part of a
	-- tracked line collection, but are not the first line in that
	-- collection)
	for index in *selectedLines
		with line = subtitles[index]
			if .extra['a-mo']
				data = json.decode .extra['a-mo']
				unless uuids[data.uuid]
					.text = data.originalText
					.number = index
					.extra = {}
					uuids[data.uuid] = line

	indicesToNuke = { }

	setTask "Gathering Matching Lines"
	totalLines = #subtitles

	for index = 1, totalLines
		with line = subtitles[index]
			if line.extra
				-- Catch lines containing our signature extradata.
				if .extra['a-mo']
					-- Decode our data, which is stored as json.
					data = json.decode .extra['a-mo']
					if uuids[data.uuid]
						oldLine = uuids[data.uuid]
						-- Check if we should change the start time.
						if .start_time < oldLine.start_time
							oldLine.start_time = .start_time
						-- Check if we should change the end time.
						if .end_time > oldLine.end_time
							oldLine.end_time = .end_time
						-- Mark the line for deletion.
						if index < oldLine.number
							oldLine.number = index
						elseif index > oldLine.number
							stats\incrementValue "revert.bytes.total", #line.text
							indicesToNuke[#indicesToNuke+1] = index

		setProgress index/totalLines*100

	setTask "Replacing Lines"
	-- Replace the lines.
	for _, line in pairs uuids
		subtitles[line.number] = line

	-- Delete the remainders.
	subtitles.delete indicesToNuke
	stats\incrementValue "revert.lines.total", #indicesToNuke

	stats\incrementValue "revert.runCount"
	stats\write!
	return nil

canRun = ( sub, selectedLines ) ->
	if not aegisub.frame_from_ms 0
		return false, "You must have a video loaded to run this macro."
	elseif 0 == #selectedLines
		return false, "You must have lines selected to use this macro."
	true

if haveDepCtrl
	versionRecord\registerMacros {
		{ "Apply", "Applies properly formatted motion tracking data to selected subtitles.", applyProcessor, canRun }
		{ "Revert", "Removes properly formatted motion tracking data from selected subtitles.", revertProcessor }
		{ "Trim", "Cuts and encodes the current scene for use with motion tracking software.", trimProcessor, canRun }
		{ "Trim Each", "Cuts and encodes selected scenes for use with motion tracking software.", trimProcessorEach, canRun }
		{ "Trim Settings", "Opens a gui to configure the trim tool.", trimConfigurator }
	}
else
	aegisub.register_macro "#{script_name}/Apply", "Applies properly formatted motion tracking data to selected subtitles.",
		applyProcessor, canRun

	aegisub.register_macro "#{script_name}/Revert", "Removes properly formatted motion tracking data from selected subtitles.",
		revertProcessor

	aegisub.register_macro "#{script_name}/Trim", "Cuts and encodes the current scene for use with motion tracking software.",
		trimProcessor, canRun

	aegisub.register_macro "#{script_name}/Trim Each", "Cuts and encodes selected scenes for use with motion tracking software.",
		trimProcessorEach, canRun

	aegisub.register_macro "#{script_name}/Trim Settings", "Opens a gui to configure the trim tool.",
		trimConfigurator
