// smedge_render_queue.jsx
//
// Submits all queued renders from After Effects as Smedge Jobs
//
// Smedge
// Copyright Uberware. All Rights Reserved


// If you run the script directly, it will create a floating panel window. You can also embed this as a panel
// that can be attached to your workspace. For this to work, you must copy this script into the UI Scripts folder
// of your After Effects distribution, and access it from the Window menu.

{

var SMEDGE = 'Smedge';

// helper to check for previous settings
function LoadSetting( name, defValue )
{
	if( app.settings.haveSetting( SMEDGE, name ) )
	{
		var val = app.settings.getSetting( SMEDGE, name );
		return val;
	}
	else
		return defValue;
}


// converts the string value to a boolean
String.prototype.bool = function()
{
	return (/^true$/i).test(this);
};


// trims whitespace and new lines from a string
String.prototype.trim = function()
{
	return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};


// generates an rfc 4122 version 4 compliant UUID
function GenerateUID()
{
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, function( c ) {
		var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r&0x3|0x8);
		return v.toString( 16 );
	} );
}


// settings
var settings = new Object();
settings.useComp = parseInt( LoadSetting( 'UseComp', 1 ) );
settings.packetSize = parseInt( LoadSetting( 'PacketSize', 4 ) );
settings.cores = parseInt( LoadSetting( 'Cores', 0 ) );
settings.priority = parseInt( LoadSetting( 'Priority', 50 ) );
settings.paused = LoadSetting( 'Paused', false ) === 'true' ? true : false;
settings.pool = parseInt( LoadSetting( 'Pool', 1 ) );
settings.note = LoadSetting( 'Note', '' );
settings.path = LoadSetting( 'SmedgePath', '' );
settings.creator = LoadSetting( 'Creator', '' );
settings.extra = LoadSetting( 'Extra', '' );
// guess an appropriate default program folder
if( settings.path.length == 0 )
{
	if( system.osName == 'MacOS' )
		settings.path = '/Applications';
	else
		settings.path = 'C:\\Program Files (x86)\\Smedge';
}


// gets the actual CLI executable path (different on Mac than the root path stored in the settings
function ExePath()
{
	var path = settings.path;
	if( system.osName == 'MacOS' )
		path += '/Smedge.app/Contents/MacOS';
	return path;
}


// fills in the pool selector with the currently available pools
function FillPools( pool )
{
	// disable the onChange handler
	var onChange = pool.onChange;
	pool.onChange = null;
	// ensure that the list is empty first
	pool.removeAll();
	// add the two default pools
	pool.add( 'item', 'Whole System' );
	pool.add( 'item', 'My Computer' );
	// process the result to get the pools
	var pools = LoadSetting( 'Pools', '' ).split( '\n' );
	if( pools.length > 0 )
		pool.add( 'separator' );
	for( var i = 0; i < pools.length; i++ )
	{
		// trim whitespace
		var name = pools[ i ].trim();
		// add this name
		pool.add( 'item', name );
	}
	// restore the onChange handler
	pool.onChange = onChange;
	// restore the last selected pool
	pool.selection = settings.pool;
}


// figure out which pool the user selected
function GetPool( pool )
{
	switch( pool )
	{
	case 0:
		// whole system
		return 'Whole System';
	case 1:
		// my computer
		return 'My Computer';
	default:
		// get pool by index
		var pools = LoadSetting( 'Pools', '' ).split( '\n' );
		return pools[ pool - 3 ].trim();
	}
}


// check that it is safe to run the script
function SafeToRun()
{
	// check that there is a project open
	if( app.project == null )
		alert( 'A project must be open to run this script.' );
	// check that the project is saved to a file on disk
	else if( app.project.file == null )
		alert( 'Please save your Project before submitting the project to Smedge' );
	// check that scripts have permission to run
	else if( app.preferences.getPrefAsLong( 'Main Pref Section', 'Pref_SCRIPTING_FILE_NETWORK_SECURITY' ) != 1 )
		alert( 'This script requires the scripting security preference to be set.\n' +
				'Go to the \'General\' panel of your application preferences,\n' +
				'and make sure that \'Allow Scripts to Write Files and Access Network\' is checked.' );
	// check the render queue and make certain at least one item is queued
	else
	{
		for( i = 1; i <= app.project.renderQueue.numItems; ++i )
			if( app.project.renderQueue.item( i ).status == RQItemStatus.QUEUED )
				return true;
		alert ( 'You do not have any items set to render.' );
	}
	return false;
}


// the actual callback to submit the job(s) to Smedge
function DoSubmit()
{
	// get the path to the Submit executable
	var PathToSubmit = ExePath() + '/Submit';
	if( PathToSubmit.search( ' ' ) != -1 )
		PathToSubmit = '"' + PathToSubmit + '"';

	// check for a project and file
	if( SafeToRun() )
	{
		// save if requested
		if( confirm( 'Save the project now?' ) )
			app.project.save();
			
		// Create a log file with all of the commands and their results
		var logPath = new Folder( app.project.file.fsName + ' Logs' );
		logPath.create();
		var log = new File( logPath.fsName + '\\__SmedgeSubmit__.log' );
		log.open( 'w' );
		log.writeln( 'Submitting all queued renders to Smedge' );
		var time = new Date();
		log.writeln( time.toString() );
		log.writeln( '' );
		
		// get the project filename
		var file = app.project.file;
		var scene = file.fsName;
		// generate a name for the SJ file we will submit
		var SJ = new File( scene + '.sj' );
		SJ.open( 'w' );
		log.writeln( 'Creating SJ file: ' + SJ.fsName );
		
		// Submit To Smedge on button click
		function AddToJobFile( render )
		{
			// get the comp name
			var comp = render.comp.name;
			// generate a name for the job
			var name = File.decode( file.name );
			// get the frame range
			var FPS        = render.comp.frameRate;
			var StartFrame = Math.round( (render.comp.displayStartTime + render.timeSpanStart) * FPS );
			var EndFrame   = Math.round( (render.comp.displayStartTime + render.timeSpanStart + render.timeSpanDuration) * FPS ) - 1;
			// get the output path info
			var finalPath = render.outputModules[ 1 ].file;
			var finalFile = File.decode( finalPath.name );
			var finalDir = finalPath.parent.fsName;
			var frameStart = finalFile.indexOf( '[' );
			var frameEnd = finalFile.indexOf( ']' );
			var rootName = finalFile.substring( 0, frameStart );
			var tailName = finalFile.substr( frameEnd + 1 );
			var padding  = frameEnd - frameStart - 1;
			var fileFormat = finalDir + '\\' + rootName + '%0' + padding.toString() + 'd' + tailName;

			// now generate the SJ file with the data for rendering this comp
			var id = GenerateUID();
			SJ.writeln( '\t[' + id + ']' );
			SJ.writeln( '' );
			SJ.writeln( 'Type = EF7F0373-3542-4d1d-80D0-BB8599FA4C63' );
			if( settings.useComp )
			{
				name += ': ' + comp;
				SJ.writeln( 'Range = ' + StartFrame + '-' + EndFrame );
				SJ.writeln( 'Comp = "' + comp + '"' );
				SJ.writeln( 'PacketSize = ' + settings.packetSize );
			}
			else
			{
				SJ.writeln( 'Range = 1 - ' + settings.packetSize );
				SJ.writeln( 'PacketSize = 1' );
			}
			SJ.writeln( 'Name = "' + name+ '"' );
			SJ.writeln( 'Scene = ' + scene );
			SJ.writeln( 'Priority = ' + settings.priority );
			SJ.writeln( 'Status = ' + (settings.paused ? '-1' : '0') );
			SJ.writeln( 'CPUs = ' + settings.cores );
			SJ.writeln( 'Pool = ' + GetPool( settings.pool ) );
			SJ.writeln( 'Note = "' + settings.note + '"' );
			if( settings.creator.length > 0 )
				SJ.writeln( 'Creator = "' + settings.creator + '"' );
			SJ.writeln( 'InitImageFormat = ' + fileFormat );
			SJ.writeln( 'DetectImageFormat = 0' );
			SJ.writeln( 'CheckImages = 0' );
			SJ.writeln( 'OvertimeKill = 4' );
			SJ.writeln( '\n' );
			log.writeln( '\t' + comp + ' added to SJ file with ID: ' + id );
		}
	
		// send each queued render
		var queued = 0;
		var count = app.project.renderQueue.numItems;
		for( i = 1; i <= count; i++ )
		{
			var render = app.project.renderQueue.item( i );
			if( render.status == RQItemStatus.QUEUED )
			{
				// queue this render
				AddToJobFile( render );
				++queued;
			}
		}
		
		// Finished making the SJ file, time to submit it
		SJ.close();
		log.writeln( 'Added ' + queued + ' jobs to to the file' );
		log.writeln( '' );
		// assemble the command string
		var cmd = PathToSubmit + ' -FromFile "' + SJ.fsName + '"';
		// add any additional parameters requested
		cmd += ' ' + settings.extra;
		// send the jobs
		log.writeln( cmd );
		log.writeln( '' );
		// execute the command line and log the result
		try
		{
			var result = system.callSystem( cmd );
			log.writeln( result );
			// was it successful?
			var success = (result.match( /Successfully sent new Job/g ) || [] ).length;
			if( success > 0 )
				alert( 'Successfully sent ' + success + ' jobs' );
			else
				log.writeln( 'FAILED: Did not see success message in output' );
		}
		catch( e )
		{
			log.writeln( 'FAILED: Exception running command' );
		}

		// finish the log file
		log.writeln( '' );
		log.writeln( 'All Finished' );
		log.close();
	}
}


// Build the interface
function BuildUI( parent )
{
	// default layout sizes and settings
	var LABEL_SIZE = [ 0, 0, 50, 20 ];
	var LABEL2_SIZE = [ 0, 0, 80, 0 ];
	var VALUE_SIZEb= [ 0, 0, 300, 20 ];
	var NO_MARGIN = [ 0, 0, 0, 0 ];
	var MARGIN = [ 5, 5, 5, 5 ];
	var MIN_CHARS = 6;
	var SPACING = 5;
	var FILL = [ 'fill', 'top' ];

	// adds a new group to the window
	function DoUIGroup( panel, label, size )
	{
		var row = panel.add( 'group' );
		row.orientation = 'row';
		row.margins = NO_MARGIN;
		row.add( 'statictext', size, label );
		row = row.add( 'group' );
		row.alignChildren = FILL;
		row.alignment = FILL;
		row.orientation = 'row';
		row.margins = NO_MARGIN;
		return row;
	}
	// shortcut for UI groups in main window
	function UIGroup( panel, label )
	{
		return DoUIGroup( panel, label, LABEL_SIZE );
	}
	// shortcut for UI groups in settings window
	function UIGroup2( panel, label )
	{
		return DoUIGroup( panel, label, LABEL2_SIZE );
	}

	// opens the Settings window to set the Smedge path
	function SettingsWindow( parent )
	{
		// make a new window
		var win = new Window( 'dialog', 'Submit to Smedge Settings', undefined, { resizable:true } );
		if( win == null )
			return;
		win.alignChildren = FILL;
		win.spacing = SPACING;
		win.margins = MARGIN;
		win.onResizing = function() { this.layout.resize(); }
		win.onShow = function() { win.layout.layout(); win.minimumSize = win.preferredSize; }

		// the smedge path
		win.add( 'statictext', undefined, 'The folder where you installed Smedge' );
		var row = UIGroup2( win, 'Smedge Path:' );
		var smedgePath = row.add( 'edittext', undefined, settings.path );
		row.add( 'button', undefined, '...' ).onClick = function()
		{
			var current = new Folder( smedgePath.text );
			smedgePath.text = current.selectDlg( 'Select the Smedge program folder' ).fsName;
		}

		// the creator
		win.add( 'statictext', undefined, 'The creator name to use. If blank, it uses the current username' );
		row = UIGroup2( win, 'Creator:' );
		var creator = row.add( 'edittext', undefined, settings.creator );

		// the extra parameters
		win.add( 'statictext', undefined, 'Additional command line parameters to send to Submit' );
		row = UIGroup2( win, 'Parameters:' );
		var extra = row.add( 'edittext', undefined, settings.extra );

		// the buttons
		row = win.add( 'group' );
		row.alignChildren = FILL;
		row.orientation = 'column';
		row.margins = [ 0, 8, 0, 0 ];
		var line = row.add( 'panel' );
		line.minimumSize.height = line.maximumSize.height = 2;
		row = row.add( 'group' );
		row.alignChildren = FILL;
		row.orientation = 'row';
		row.add( 'button', undefined, 'OK' ).onClick = function()
		{
			// update and save the settings
			settings.path = smedgePath.text; app.settings.saveSetting( SMEDGE, 'SmedgePath', settings.path );
			settings.creator = creator.text; app.settings.saveSetting( SMEDGE, 'Creator', settings.creator );
			settings.extra = extra.text; app.settings.saveSetting( SMEDGE, 'Extra', settings.extra );
			// close the settigns window
			win.close();
		}
		row.add( 'button', undefined, 'Cancel' );

		// show the dialog
		win.show();
	}


	// create window
	var panel = (parent instanceof Panel) ? parent : new Window( 'palette', 'Submit to Smedge', undefined, { resizeable:true } );
	if( panel == null )
		// failed to create a panel to hold the controls!
		return panel;
	// set window overall settings
	panel.alignChildren = FILL;
	panel.spacing = SPACING;
	panel.margins = MARGIN;
	// window callbacks for dealing with sizing correctly
	panel.onResizing = function() { this.layout.resize(); }
	panel.onShow = function() { panel.layout.layout(); panel.minimumSize = panel.preferredSize; }

	// comp mode or project mode
	var row = UIGroup( panel, 'Mode:' );
	row.alignChildren = [ 'right', 'top' ];
	var useComp = row.add( 'dropdownlist' );
	useComp.add( 'item', 'Project Mode' );
	useComp.add( 'item', 'Comp Mode' );
	useComp.alignment = FILL;
	useComp.selection = settings.useComp;

	// the packet size (number of frames to render at one time)
	row = UIGroup( panel, 'Division:' );
	var packet = row.add( 'edittext', undefined, settings.packetSize );
	packet.onChange = function()
	{
		// get the value
		settings.packetSize = parseInt( packet.text );
		// validate it
		if( settings.packetSize < 1 )
			settings.packetSize = 1;
		// save it as an option for next time
		app.settings.saveSetting( SMEDGE, 'PacketSize', settings.packetSize );
	}
	var packetLabel = row.add( 'statictext', undefined, 'frames' );
	packetLabel.alignment = [ 'right', 'top' ];

	// now we can set the onChange handler for the useComp mode selector
	function ModeChange()
	{
		// which mode are we in?
		settings.useComp = useComp.selection.index;
		app.settings.saveSetting( SMEDGE, 'UseComp', settings.useComp );
		// adjust the controls appropriately
		packetLabel.text = settings.useComp ? 'frames' : 'workers';
	}
	useComp.onChange = ModeChange;
	ModeChange();

	// the cores per render
	var hideRow2 = UIGroup( panel, 'Cores:' );
	hideRow2.alignChildren = 'left';
	var cpu0 = hideRow2.add( 'radiobutton', undefined, 'All' );
	var cpu1 = hideRow2.add( 'radiobutton', undefined, 'One' );
	var cpu2 = hideRow2.add( 'radiobutton', undefined, 'Custom:' );
	var cpuN = hideRow2.add( 'edittext', undefined, '0' );
	cpuN.alignment = FILL;
	cpuN.characters = MIN_CHARS;
	function SetCores( cores ) { settings.cores = cores; app.settings.saveSetting( SMEDGE, 'Cores', settings.cores ); }
	cpu0.onClick = function() { SetCores( 0 ); cpuN.enalbed = false; }
	cpu1.onClick = function() { SetCores( 1 ); cpuN.enabled = false; }
	cpu2.onClick = function()
	{
		// get the value of the text control
		var value = parseInt( cpuN.text );
		// if it's < 2, just use the other radio buttons
		if( value < 2 )
		{
			value = 2;
			cpuN.text = value;
		}
		// save the value
		SetCores( value );
		// enable the edit control
		cpuN.enabled = true;
	}
	cpuN.onChange = function() { settings.cores = Number( this.text ); app.settings.saveSetting( SMEDGE, 'Cores', settings.cores ); }
	// set the current value and enable the edit control if needed
	switch( settings.cores )
	{
	case 0:
		cpu0.value = true;
		cpuN.enabled = false;
		break;
	case 1:
		cpu1.value = true;
		cpuN.enabled = false;
		break;
	default:
		cpu2.value = true;
		cpuN.text = settings.cores;
		cpuN.enabled = true;
		break;
	}

	// the priority
	row = UIGroup( panel, 'Priority:' );
	var priorityValue = row.add( 'edittext', undefined, settings.priority );
	priorityValue.alignment = 'left';
	priorityValue.characters = 3;
	priorityValue.onChanging = function()
	{
		// get and validate the value
		var value = parseInt( priorityValue.text );
		if( value < 0 )
			value = 0;
		else if (value > 100 )
			value = 100;
		// also update the slider and settings
		settings.priority = prioritySlider.value = value;
		// save it as an option for next time
		app.settings.saveSetting( SMEDGE, 'Priority', settings.priority );
	}
	var prioritySlider = row.add( 'slider', undefined, 50, 0, 100 );
	prioritySlider.alignment = FILL;
	prioritySlider.value = settings.priority;
	prioritySlider.onChanging = prioritySlider.onChanged = function()
	{
		// get the new value
		settings.priority = priorityValue.text = prioritySlider.value = parseInt( prioritySlider.value );
		// save it as an option for next time
		app.settings.saveSetting( SMEDGE, 'Priority', settings.priority );
	}

	// paused?
	row = UIGroup( panel, 'Paused:' );
	var paused = row.add( 'checkbox', undefined, '' );
	paused.value = settings.paused;
	paused.onClick = function()
	{
		// get the new value
		settings.paused = paused.value;
		// save it as an option for next time
		app.settings.saveSetting( SMEDGE, 'Paused', settings.paused );
	}

	// the pool
	row = UIGroup( panel, 'Pool:' );
	row.alignChildren = [ 'right', 'top' ];
	var pool = row.add( 'dropdownlist' );
	FillPools( pool );
	pool.alignment = FILL;
	pool.onChange = function() { settings.pool = pool.selection.index; app.settings.saveSetting( SMEDGE, 'Pool', settings.pool ); }
	var getPools = row.add( 'button', undefined, 'Update' );
	getPools.onClick = function()
	{
		// build the command line
		var cmd = ExePath() + '/PoolManager';
		if( cmd.search( ' ' ) != -1 )
			cmd = '"' + cmd + '"';
		cmd += ' List Name';
		// run it
		var result = system.callSystem( cmd );
		// save the result in the options
		app.settings.saveSetting( SMEDGE, 'Pools', result );
		// refill the pool names in the control
		FillPools( pool );
	}


	// the note
	row = UIGroup( panel, 'Note:' );
	var note = row.add( 'edittext' );
	note.text = settings.note;
	note.onChange = function() { settings.note = note.text; app.settings.saveSetting( SMEDGE, 'Note', settings.note ); }

	// the buttons
	row = panel.add( 'group' );
	row.alignChildren = FILL;
	row.orientation = 'column';
	row.margins = [ 0, 8, 0, 0 ];
	var line = row.add( 'panel' );
	line.minimumSize.height = line.maximumSize.height = 2;
	row = row.add( 'group' );
	row.alignChildren = FILL;
	row.orientation = 'row';
	var cmdSubmit = row.add( 'button', undefined, 'Submit' );
	cmdSubmit.onClick = function()
	{
		// submit the job(s)
		cmdSubmit.enabled = false;
		app.beginUndoGroup( 'Submit to Smedge' );
		DoSubmit();
		app.endUndoGroup();
		cmdSubmit.enabled = true;
	}
	var cmdShowGUI = row.add( 'button', undefined, 'Open GUI' );
	cmdShowGUI.onClick = function()
	{
		// build the command for the correct platform
		// make sure that the command starts asynchronously so AE is not blocked by this
		var cmd = settings.path;
		if( system.osName == 'MacOS' )
			cmd += '/Smedge.app';
		else
			cmd += '\\SmedgeGui.exe';
		var exe = new File( cmd );
		// start the process.
		exe.execute();
	}
	var cmdSettings = row.add( 'button', undefined, 'Settings' );
	cmdSettings.onClick = function() { SettingsWindow( this ); }

	// layout the window
	panel.layout.layout(true);
	panel.layout.resize();
	// show the window
	if (panel instanceof Window)
	{
		panel.center();
		panel.show();
	}
	else
	{
		panel.layout.layout(true);
	}
}

// main entry point is to build the UI
BuildUI( this );


}
