// (c) Copyright 2005-2007  Adobe Systems, Incorporated.  All rights reserved.

/*
@@@BUILDINFO@@@ StackSupport.jsx 1.0.0.0
*/

//
// Support routines for Stack based imaging scripts.  These
// routines mostly fill in for holes in the Document Object Model,
// and ideally should be replaced with DOM equivalents someday.
//

// debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning)
$.level = 0;
//$.level = (Window.version.search("d") != -1) ? 1 : 0;	// This doesn't work from Bridge

// debugger; // launch debugger on next line

// on localized builds we pull the $$$/Strings from a .dat file
$.localize = true;

$.evalFile(g_StackScriptFolderPath + "Terminology.jsx");

// Handy debugging function helps you figure out what descriptor keywords are.
function numToOSType( number )
{
	function decodeByte(num)
	{
		const hex = '0123456789ABCDEF';
		num = num & 0xFF;
		var hi = hex[(num >> 4)];
		var lo = hex[num & 0xF];
		return decodeURI( "%" + hi + lo );
	}

	return decodeByte(number >> 24) + decodeByte(number >> 16) + decodeByte(number >> 8) + decodeByte(number);
}

// Returns true if the active layer has a layer mask
function hasLayerMask()
{
	ref = new ActionReference();
	args = new ActionDescriptor();
	ref.putProperty( classProperty, keyUserMaskEnabled );
	ref.putEnumerated( classLayer, typeOrdinal, enumTarget );
	args.putReference( keyTarget, ref );

	var resultDesc = executeAction( eventGet, args, DialogModes.NO );
	return resultDesc.hasKey( keyUserMaskEnabled );
}

// Create a layer mask in the active document.	Leaves layer mask the active channel
function createLayerMask()
{
	if (hasLayerMask()) return;
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	desc.putClass( keyNew, typeChannel );
	ref.putEnumerated( typeChannel, typeChannel, classMask );
	desc.putReference( keyAt, ref );
	desc.putEnumerated( keyUsing, keyUserMaskEnabled, enumRevealAll );
	executeAction( eventMake, desc, DialogModes.NO );
}

// Apply the layer mask, deleting it in the process.
function applyLayerMask()
{
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putEnumerated( typeChannel, typeOrdinal, enumTarget );
	desc.putReference( typeNULL, ref );
	desc.putBoolean( keyApply, true );
	executeAction( eventDelete, desc, DialogModes.NO );
}

// Returns the name of the layer mask of the active layer
// (or "Alpha" if none...in English at least...)
function layerMaskName()
{
	ref = new ActionReference();
	args = new ActionDescriptor();
	ref.putProperty( classProperty, keyChannelName );	// keyChannelName
	ref.putEnumerated( classChannel, typeOrdinal, enumMask );
	args.putReference( keyTarget, ref );

	var resultDesc = executeAction( eventGet, args, DialogModes.NO );
	return resultDesc.getString( keyChannelName );
}

// This is the same as app.activeDocument.activeLayer.translate( dx, dy ),
// which seems to be broken at the moment.
// Potential DOM FIX
function translateActiveLayer( deltaX, deltaY )
{
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putEnumerated( classLayer, typeOrdinal, enumTarget );
	desc.putReference( typeNULL, ref );
	var coords = new ActionDescriptor();
	coords.putUnitDouble( enumHorizontal, unitPixels, deltaX );
	coords.putUnitDouble( keyVertical, unitPixels, deltaY );
	desc.putObject( keyTo, keyOffset, coords );
	executeAction( eventMove, desc, DialogModes.NO );
}

// Gradient Fill operator. 
// Note this is hardwired to Darken mode (among other things)
// Potential DOM FIX
function gradientFillLayerMask( fromPoint, toPoint )
{
	function pointDesc( pt )
	{
		var desc = new ActionDescriptor();
		desc.putUnitDouble( keyHorizontal, unitPixels, pt.fX );
		desc.putUnitDouble( keyVertical, unitPixels, pt.fY );
		return desc;
	}
	
	function stopDesc( location, midPoint )
	{
		var desc = new ActionDescriptor();
		desc.putInteger( keyLocation, location );
		desc.putInteger( keyMidpoint, midPoint );
		return desc;
	}
	
	function grayDesc( grayValue )
	{
		var desc = new ActionDescriptor();
		desc.putDouble( enumGray, grayValue );
		return desc;
	}

	var args = new ActionDescriptor();
	args.putObject( keyFrom, classPoint, pointDesc( fromPoint ) );
	args.putObject( keyTo, classPoint, pointDesc( toPoint ) );
	args.putEnumerated( keyMode, typeBlendMode, enumDarken );
	args.putEnumerated( keyType, typeGradientType, enumLinear );
	args.putBoolean( keyDither, true );
	args.putBoolean( keyUseMask, true );
	args.putBoolean( keyReverse, true );
	
	var gradDesc = new ActionDescriptor();
	gradDesc.putString( keyName, "White, Black" );
	gradDesc.putEnumerated( typeGradientForm, typeGradientForm, enumCustomStops );
	gradDesc.putDouble( keyInterpolation, 4096.000000 );
	
	var colorList = new ActionList();
	var stop = stopDesc( 0, 50 );
//	stop.putEnumerated( keyType, typeColorStopType, enumForegroundColor );
	stop.putObject( classColor, classGrayscale, grayDesc( 100 ) );
	stop.putEnumerated( keyType, typeColorStopType, enumUserStop );
	colorList.putObject( classColorStop, stop );
	stop = stopDesc( 4096, 50 );
//	stop.putEnumerated( keyType, typeColorStopType, enumBackgroundColor );
	stop.putObject( classColor, classGrayscale, grayDesc( 0 ) );
	stop.putEnumerated( keyType, typeColorStopType, enumUserStop );
	colorList.putObject( classColorStop, stop );
	gradDesc.putList( typeColors, colorList );
	
	var xferList = new ActionList();
	var xfer = stopDesc( 0, 50 );
	xfer.putUnitDouble( keyOpacity, unitPercent, 100.000000 );
	xferList.putObject( keyTransferSpec, xfer );
	xfer = stopDesc( 4096, 50 );
	xfer.putUnitDouble( keyOpacity, unitPercent, 100.000000 );
	xferList.putObject( keyTransferSpec, xfer );
	
	gradDesc.putList( keyTransparency, xferList );
	args.putObject( keyGradient, classGradient, gradDesc );
	executeAction( eventGradient, args, DialogModes.NO );
}

// Create polygon selection.  The polygon is an array of point arrays,
// e.g. [[20,30][40,30][40,60][20,30]]
// Potential DOM FIX
function createPolygonSelection( polygon )
{
	var i;
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	var listDesc = new ActionDescriptor();
	var pointList = new ActionList();
	
	ref.putProperty( typeChannel, charIDToTypeID('fsel') );	// What's fsel?
	desc.putReference( typeNULL, ref );

	for (i in polygon)
	{
		var pointDesc = new ActionDescriptor();
		pointDesc.putUnitDouble( keyHorizontal, unitPixels, polygon[i].fX );
		pointDesc.putUnitDouble( keyVertical, unitPixels, polygon[i].fY );
		pointList.putObject( classPoint, pointDesc );
	}

	listDesc.putList( keyPoints, pointList );
	desc.putObject( keyTo, classPolygon, listDesc );
	desc.putBoolean( keyAntiAlias, true );
	executeAction( eventSet, desc, DialogModes.NO );
}

function duplicateDocument( newName )
{
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putEnumerated( classDocument, typeOrdinal, enumFirst );
	desc.putReference( typeNULL, ref );
	desc.putString( keyName, newName );
	executeAction( eventDuplicate, desc, DialogModes.NO );
}

// Set the exposure of the frontmost document
// Potential DOM FIX
function setFrontmostExposure( exposure, gamma )
{
	if (typeof(gamma) == "undefined")
		gamma = 1.0;

	args = new ActionDescriptor();
	args.putInteger( classVersion, 3 );
	args.putEnumerated( keyMethod, app.stringIDToTypeID( 'hdrToningMethodType' ), 
								   app.stringIDToTypeID( 'hdrtype2' ) );
	args.putDouble( keyExposure, exposure );
	args.putDouble( keyGamma, gamma );

	executeAction( app.stringIDToTypeID('32BitPreviewOptions'), args );
}

// Since you can't assign to doc.resolution
// Potential DOM FIX
function setFrontmostResolution( dpi )
{
	var desc = new ActionDescriptor();
	desc.putUnitDouble( keyResolution, unitDensity, dpi );
	executeAction( eventImageSize, desc, DialogModes.NO );
}

// Forces the frontmost document to save in Photoshop (.psd) format.
function resetFrontmostDocumentFormat()
{
	var desc = new ActionDescriptor();
	executeAction( kresetDocumentFormatStr, desc, DialogModes.NO );
}

// Scale the view on the frontmost document so it fits on the screen
function fitViewOnScreen()
{
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putEnumerated( classMenuItem, typeMenuItem, enumFitOnScreen );
	desc.putReference( typeNULL, ref );
	executeAction( eventSelect, desc, DialogModes.NO );
}

// Undo the last operation
// Potential DOM FIX
function undoLastEvent()
{
	executeAction( eventUndo, undefined, DialogModes.NO );
}

// Purge this history states for the active document
// Potential DOM FIX
function purgeHistoryStates()
{
	var desc = new ActionDescriptor();
	desc.putEnumerated( typeNULL, typePurgeItem, enumHistory );
	executeAction( eventPurge, desc, DialogModes.NO );
}

// Get / set the "Prefer Adobe Camera Raw for JPEG Files" flag.
// This feature was added at the last minute, and thus left out of app.preferences
// Potential DOM FIX
function setUseCameraRawJPEGPreference( flag )
{
    var saveDesc = new ActionDescriptor();
    var ref = new ActionReference();
    ref.putProperty( classProperty, keyFileSavePrefs );
    ref.putEnumerated( classApplication, typeOrdinal, enumTarget );
    saveDesc.putReference( typeNULL, ref );
    var rawDesc = new ActionDescriptor();
    rawDesc.putBoolean( kcameraRawJPEGStr, flag );
    saveDesc.putObject( keyTo, classFileSavePrefs, rawDesc );
	executeAction( eventSet, saveDesc, DialogModes.NO );
}

function getUseCameraRawJPEGPreference()
{
    var ref = new ActionReference();
    ref.putProperty( classProperty, keyFileSavePrefs );
    ref.putEnumerated( classApplication, typeOrdinal, enumTarget );
	// Descriptor with the actual prefs is buried one deep...
	var result = app.executeActionGet( ref );
	var desc = result.getObjectValue( result.getKey(0) );
	return desc.getBoolean( kcameraRawJPEGStr );
}

// Filters for the open dialog 
function winFileSelection( f )
{
	var suffix = f.name.match(/[.](\w+)$/);
	var t;
	
	if (suffix && suffix.length == 2)
	{
		suffix = suffix[1].toUpperCase();
		for (t in app.windowsFileTypes)
			if (suffix == app.windowsFileTypes[t])
			{
				// Ignore mac-generated system thumbnails
				if (f.name.slice(0,2) != "._")
					return true;
			}
	}
	return false;
}

function macFileSelection( f )
{
	var t;
	for (t in app.macintoshFileTypes)
		if (f.type == app.macintoshFileTypes[t])
			return true;
	
	// Also check windows suffixes...
	return winFileSelection( f );	
}

function isValidImageFile( f )
{
	return ((File.fs == "Macintosh") && macFileSelection(f)) || ((File.fs == "Windows") && winFileSelection(f));
}

// Simple utilities for those lovely four-char-codes
function intToOStype(n)
{
	return String.fromCharCode( (n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF);
}

function osTypeToInt(os)
{
	var n = 0;
	for (i = 0; i < os.length; i++)
		n |= os.charCodeAt(i) << ((3-i)*8);
	return n;
}

// Open Photoshop's FileOpen dialog, and return a list of filenames
// Potential DOM FIX
function photoshopFileOpenDialog()
{
	result = new Array();

	var nlist = app.openDialog();
	
	for (var i = 0; i < nlist.length; i++)
	{
		var s = decodeURI(nlist[i].toString());
		s = s.replace(/^file:\/\//, "");
		if ($.os.match(/^Windows.*/))	// Pull off ":" from drive letter
			s = s.replace(/^\/(.):\//, "/$1/");
		result.push(s);
	}
	
	return result;
}

// Apply a perspective transform to the current layer, with the
// corner TPoints given in newCorners (starts at top left, in clockwise order)
// Potential DOM fix
function transformActiveLayer( newCorners )
{
	function pxToNumber( px )
	{
		return px.as("px");
	}
	
	var saveUnits = app.preferences.rulerUnits;
	app.preferences.rulerUnits = Units.PIXELS;

	var i;
	var setArgs = new ActionDescriptor();
	var chanArg = new ActionReference();
	
	chanArg.putProperty( classChannel, keySelection );
	setArgs.putReference( keyNull, chanArg );
	
	var boundsDesc = new ActionDescriptor();
	var layerBounds = app.activeDocument.activeLayer.bounds;
	boundsDesc.putUnitDouble( keyTop, unitPixels, pxToNumber( layerBounds[1] ) );
	boundsDesc.putUnitDouble( keyLeft, unitPixels, pxToNumber( layerBounds[0] ) );
	boundsDesc.putUnitDouble( keyRight, unitPixels, pxToNumber( layerBounds[2] ) );
	boundsDesc.putUnitDouble( keyBottom, unitPixels, pxToNumber( layerBounds[3] ) );
	
	setArgs.putObject( keyTo, classRectangle, boundsDesc );
	executeAction( eventSet, setArgs );
	
	var result = new ActionDescriptor();
	var args = new ActionDescriptor();
	var quadRect = new ActionList();
	quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[0] ) );	// ActionList put is different from ActionDescriptor put
	quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[1] ) );
	quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[2] ) );
	quadRect.putUnitDouble( unitPixels, pxToNumber( layerBounds[3] ) );
	
	var quadCorners = new ActionList();
	for (i = 0; i < 4; ++i)
	{
		quadCorners.putUnitDouble( unitPixels, newCorners[i].fX );
		quadCorners.putUnitDouble( unitPixels, newCorners[i].fY );
	}
	args.putList( krectangleStr, quadRect );
	args.putList( kquadrilateralStr, quadCorners );
	executeAction( eventTransform, args );
	
	// Deselect
	deselArgs = new ActionDescriptor();
	deselRef = new ActionReference();
	deselRef.putProperty( classChannel, keySelection );
	deselArgs.putReference( keyNull, deselRef );
	deselArgs.putEnumerated( keyTo, typeOrdinal, enumNone );
	executeAction( eventSet, deselArgs );
	app.preferences.rulerUnits = saveUnits;
}

const kAlignAuto			= "Auto";
const kAlignCylindrical	= "cylindrical";
const kAlignPerspective	= "Prsp";
const kAlignTranslation	= "translation";

// Convert plain string to the alignment key.
// Avoids clients having to load Terminology.jsx
function stringToAlignmentKey( alignMethod )
{
	// Today's JavaScript lesson:  You can not use the constants above in the table below, because
	// the parser interns the symbol on the left of the ":" into a new ID, whether or not it's already
	// defined in scope.  The new interned symbols only work as object field IDs (table.xyz) not
	// array indicies (table[xyz])
	var table = {"interactive":kinteractiveStr, "Prsp":keyPerspectiveIndex, "Auto":keyAuto, "cylindrical":kcylindricalStr, "translation":ktranslationStr};
	if (typeof(alignMethod) == "string")
		alignMethod = table[alignMethod];
	return alignMethod;
}

// Align selected layers in the active document by content 
// (uses SIFT registration in Photoshop core)
// Potential DOM FIX
function getActiveDocAlignmentInfo( alignmentKey, doTransform, highQuality )
{
	const kUserCancelledError = 8007;

	function getUnitPoint( pointDesc, key )
	{
		var desc = pointDesc.getObjectValue( key );
		var xType = desc.getUnitDoubleType( keyHorizontal ); 
		var yType = desc.getUnitDoubleType( keyVertical );
		var x = desc.getUnitDoubleValue( keyHorizontal );
		var y = desc.getUnitDoubleValue( keyVertical );
		return new TPoint( x, y );
	}

	alignmentKey = stringToAlignmentKey( alignmentKey );

	var layerInfo = new Array();
	var returnInfo = null;
	var projection, numGroups = 1;
	
	// Must match enums in UAlignment.h, AlignContentInfo::transformation_type
	const kXfmAuto			=-1;
	const kXfmProjective	= 0;
	const kXfmCylindrical	= 1;
	const kXfmTranslate		= 3;
	const kXfmEuclidean		= 4;
	const kXfmSimilarity	= 5;
	const kXfmAffine		= 6;
	const kXfmReposition	= 8;
	
	try {
		var saveUnits = app.preferences.rulerUnits;
		app.preferences.rulerUnits = Units.PIXELS;

		var i,j, desc = new ActionDescriptor();
		var result, ref = new ActionReference();
		ref.putEnumerated( classLayer, typeOrdinal, enumTarget );
		desc.putReference( typeNULL, ref );
		desc.putEnumerated( keyUsing, typeAlignDistributeSelector, kADSContentStr );
		if (alignmentKey)
			desc.putEnumerated( keyApply, kprojectionStr, alignmentKey );
		if ((typeof(doTransform) != "undefined") && doTransform)
			desc.putBoolean( kgeometryRecordStr, true );
		else
			desc.putBoolean( kgeometryOnlyStr, true );
		if ((typeof(highQuality) != "undefined") && highQuality)
			desc.putBoolean( khighQualityStr, true );
		result = executeAction( keyAlignment, desc, DialogModes.NO );
		
//		projection = result.getEnumerationValue( kprojectionStr );
		if (result.hasKey( kgroupStr ))
			numGroups = result.getInteger( kgroupStr );

		// Pick apart the data returned by the alignment engine.
		// Clues are found in ULayerCommand.cpp, PostLayoutCommand.
		var layerList = result.getList( klayerTransformationStr );
		for (i = 0; i < layerList.count; ++i)
		{
			var layerXformDesc = layerList.getObjectValue( i );
			var layerID = layerXformDesc.getInteger( klayerIDStr );
			var groupNum = layerXformDesc.getInteger( kgroupStr );
			// Note xformType here is the xform actually used (vs. what was requested)
			var xformType = layerXformDesc.getInteger( ktransformStr );	
			var baseFlag = layerXformDesc.hasKey( kignoreStr ) ? layerXformDesc.getBoolean( kignoreStr ) : false;
			var pts = new Array();
			// JavaScript note 1: ignore the ":1"s below, it's just a way to get a set membership test.
			// JavaScript note 2: Using symbolic constants for this fails.
//			if (xformType in {kXfmProjective:1, kXfmTranslate:1, kXfmEuclidean:1, kXfmSimilarity:1, kXfmAffine:1, kXfmReposition:1})
			if (xformType in {0:1, 3:1, 4:1, 5:1, 6:1, 8:1})
			{
				var quadType = layerXformDesc.getObjectType( keyTo );
				var quadDescriptor = layerXformDesc.getObjectValue( keyTo );
				pts[0] = getUnitPoint( quadDescriptor, kquadCorner0Str );
				pts[1] = getUnitPoint( quadDescriptor, kquadCorner1Str );
				pts[2] = getUnitPoint( quadDescriptor, kquadCorner2Str );
				pts[3] = getUnitPoint( quadDescriptor, kquadCorner3Str );
			}
			// Note we depend on stackElement's order matching the sheet list.
			layerInfo[i] = {"baseFlag":baseFlag, "corners":pts, "groupNum":groupNum, "layerID":layerID, "xformType":xformType};
		}
	}
	catch (e) 
	{
		app.preferences.rulerUnits = saveUnits;
		numGroups = 0;
		if (e.number == kUserCancelledError)
			throw e;
	}
	app.preferences.rulerUnits = saveUnits;
	if (numGroups == 0)
		return null;
	else
		return {"numGroups":numGroups, "layerInfo":layerInfo}
}

// Convert a 32 bit HDR document to eight bit (brings up the dialog)
// Potential DOM FIX						   
function convertFromHDR( newDepth )
{
	args = new ActionDescriptor();
	args.putInteger( keyDepth, newDepth );
	executeAction( eventConvertMode, args, DialogModes.ALL );
}

// Use the core graph-cut facility to merge the layers.  Assumes
// layers to be merged are selected.
// Potential DOM FIX
function advancedMergeLayers()
{
	executeAction( kmergeAlignedLayersStr, undefined, DialogModes.NO );
}

// Select all of the layers, except the one named pluginName.  
// Currently the DOM only allows for a single layer selected at one time.
// Potential DOM FIX
function selectAllLayers(doc, upToLayer)
{
	var i;
	app.activeDocument = doc;
	// Select the first layer...
	doc.activeLayer = doc.layers[0];
	
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	if (typeof(upToLayer) == 'undefined')
		upToLayer = 1;
	// Note - the event indexing is the -reverse- of the JavaScript indexing,
	// so "2" refers to the next to the last.
	ref.putIndex( classLayer, upToLayer );
	desc.putReference( typeNULL, ref );
	desc.putEnumerated( kselectionModifierStr, kselectionModifierTypeStr, kaddToSelectionContinuousStr );
	desc.putBoolean( keyMakeVisible, false );
	executeAction( eventSelect, desc, DialogModes.NO );
}

// Select one layer.  Native JS is broken if multiple layers are
// already selected. 
// Potential DOM FIX
function selectOneLayer(doc, layer)
{
	app.activeDocument = doc;
	if (typeof(layer) != "string")
		layer = layer.name;
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putName( classLayer, layer );
	desc.putReference( typeNULL, ref );
	desc.putBoolean( keyMakeVisible, false );
	executeAction( eventSelect, desc, DialogModes.NO );
}

// Align selected layers by content (uses SIFT registration in Photoshop core)
// Potential DOM FIX
function alignLayersByContent( alignMethod )
{
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	
	alignMethod = stringToAlignmentKey( alignMethod );
	if (! alignMethod)
		alignMethod = keyPerspectiveIndex;

	ref.putEnumerated( classLayer, typeOrdinal, enumTarget );
	desc.putReference( typeNULL, ref );
	desc.putEnumerated( keyUsing, typeAlignDistributeSelector, kADSContentStr );
	desc.putEnumerated( keyApply, kprojectionStr, alignMethod );
		
	executeAction( keyAlignment, desc, DialogModes.NO );
}

// Invoke the graph-cut based blending method
// Potential DOM FIX
function graphCutMergeLayers()
{
	var desc = new ActionDescriptor();
	var ref = new ActionReference();
	ref.putEnumerated( classLayer, typeOrdinal, enumTarget );
	desc.putReference( typeNULL, ref );
	executeAction( kmergeAlignedLayersStr, desc, DialogModes.NO );
}

// This is sleazy - check if a char/string ID is present by seeing if
// a "new" one is made when we ask for it.
// Note *THIS ONLY WORKS ONCE*.  The second time it's called, it'll report
// the ID as valid even if it's not, since the act of testing it defines it.

function isIDDefined(idStr)
{
	var d = new Date();
	var bogusID = app.stringIDToTypeID( "s" + d.getTime().toString() );
	var testID = app.stringIDToTypeID( idStr );
	// If bogus and test are one apart, then they both defined new IDs.
	// Otherwise, the ID already existed.
	return !(testID - bogusID == 1)
}