{
	Purpose: Export actors default outfits from xEdit to a PapyrusUtils .json file. Is used in game resetting outfits back to default. Also includes whether they are part of the zbfSlave faction.
	Game: The Elder Scrolls V: Skyrim
	Author: Kaxat
	Version: 1.5
}

unit UserScript;

var 
	tTsvRows, tActorBases, tOutfits, tSleepOutfits, tHasFaction, tFileLines, tCommentsActorBases, tCommentsOutfits, tCommentsSleepOutfits: TwbFastStringList;
	OutputDebug, OutputTSV, OutputJSON, OutputSkipped, OutputZbfSlave: Boolean;
	OutputFaction, FilterGender: String;
	OutfitsFound: Integer;

uses
  StrUtils;

function Initialize: Integer;
begin
	OutputTSV := 0; // Can also output as a TSV so one can inspect the data.
	OutputJSON := 1;
	OutputSkipped := 1; // Also output all actors it skipped over and why.
	OutputDebug := 0; // Debug mode messaging.
	OutputFaction := 'zbfFactionSlave';

	// Filters that limit which NPCs to return. @TODO Add toggles for Encounter NPCs, and race check.
	FilterGender := 'Female';

	tTsvRows := TwbFastStringList.Create;
	tActorBases := TwbFastStringList.Create;
	tOutfits := TwbFastStringList.Create;
	tSleepOutfits := TwbFastStringList.Create;
	tHasFaction := TwbFastStringList.Create;
	tFileLines := TStringList.Create;
	
	tCommentsActorBases := TStringList.Create;
	tCommentsOutfits := TStringList.Create;
	tCommentsSleepOutfits := TStringList.Create;
	
	OutfitsFound := 0;
	
	AddMessage('--------------------------------------');
	AddMessage('Starting search for valid actors.');
	
	Result := 0;
end;


// Traverse template heirarchy until it finds the race for this NPC
function GetRace(e: IInterface; fallbackValue: IInterface): IInterface;
var
	xTemplate, xActorRace: IInterface;

begin
	xTemplate := LinksTo(ElementByPath(e, 'TPLT'));
	xActorRace := fallbackValue;
	
	// Loads race from template. Template might also load race from its template. Recursive call.
	if (GetElementEditValues(e, 'ACBS\Template Flags\Use Traits') = '1') and not (GetEditValue(xTemplate) = '') then begin
		xActorRace := ElementByPath(xTemplate, 'RNAM');
		
		if OutputDebug then AddMessage('GetRace()	Found Template	e=' + GetEditValue(e) + ';	currentRNAM=' + GetEditValue(xActorRace) + ';	currentTPLT=' + GetEditValue(xTemplate));
		
		// No race so use fallbackValue value
		if (GetEditValue(xActorRace) = '') then xActorRace := fallbackValue;
		
		xActorRace := GetRace(xTemplate, xActorRace);
	end;
	
	Result := xActorRace;
end;


// Traverse template heirarchy until it finds the gender for this NPC
// Returns Male, Female, or Random for level lists
function GetGender(e: IInterface): String;
var
	xTemplate, xLeveledList, xLeveledItem: IInterface;
	sActorGender: String;

begin
	xTemplate := LinksTo(ElementByPath(e, 'TPLT'));
	
	if ( GetElementEditValues(e, 'ACBS\Flags\Female') = '1' ) then 
		sActorGender := 'Female'
	else 
		sActorGender := 'Male';

	if OutputDebug then AddMessage(
		'GetGender()	Begin e=' + GetEditValue(e)
		+ ';	sActorGender=' + sActorGender
		+ ';	xTemplate=' + GetEditValue(xTemplate)
	);
	
	// Loads Gender from template. Template might also load Gender from its template. Recursive call.
	if  (GetElementEditValues(e, 'ACBS\Template Flags\Use Traits') = '1') and not (GetEditValue(xTemplate) = '') then begin
		if OutputDebug then AddMessage(
			'GetGender() 	Found Template e=' + GetEditValue(e)
			+ ';	Template=' + GetEditValue(xTemplate)
			+ ';	Template Signature=' + Signature(xTemplate)
		);
		
		if (Signature(xTemplate) = 'LVLN') then
			// Gender is pulled from leveled list. Impossible to be sure what it is.
			sActorGender := 'Random'
		else 
			// Valid template record. Iterate deeper.
			sActorGender := GetGender(xTemplate);
	end;
	
	if OutputDebug then AddMessage(
		'GetGender()	End    e=' + GetEditValue(e)
		+ ';	sActorGender=' + sActorGender
		+ ';	xTemplate=' + GetEditValue(xTemplate)
	);
	
	Result := sActorGender;
end;


// Traverse template heirarchy until it finds the outfit for this NPC
function GetOutfit(e: IInterface; fallbackValue: IInterface; sOutfitPath: String): IInterface;
var
	xTemplate, xActorOutfit, xLeveledList, xLeveledItem: IInterface;

begin
	xTemplate := LinksTo(ElementByPath(e, 'TPLT'));
	xActorOutfit := fallbackValue;

	if OutputDebug then AddMessage(
		'GetOutfit()	Begin e=' + GetEditValue(e)
		+ ';	xActorOutfit=' + GetEditValue(xActorOutfit)
		+ ';	xTemplate=' + GetEditValue(xTemplate)
		+ ';	sOutfitPath=' + sOutfitPath
	);
	
	// Template can be a leveled list. A list of templates. Get first one.
	if (Signature(e) = 'LVLN') then begin
		xLeveledList := ElementByName(e, 'Leveled List Entries');
		
		if OutputDebug then AddMessage(
			'GetOutfit()	Found Leveled List e=' + GetEditValue(e)
			+ ';	xLeveledList=' + GetEditValue(xLeveledList)
		);
		
		if Assigned(xLeveledList) then begin

			xLeveledItem := ElementByPath(ElementByIndex(xLeveledList, 0), 'LVLO\Reference');
			
			if OutputDebug then AddMessage(
				'GetOutfit()	Found Leveled Item e=' + GetEditValue(e)
				+ ';	xLeveledItem Signature=' + Signature(xLeveledItem)
				+ ';	xLeveledItem=' + GetEditValue(xLeveledItem)
			);

			if (GetEditValue(xLeveledItem) = '') then 
				xActorOutfit := fallbackValue
			else 
				xActorOutfit := GetOutfit(LinksTo(xLeveledItem), fallbackValue, sOutfitPath);
		end;

	end;
	
	// Loads outfit from template. Template might also load outfit from its template. Recursive call.
	if  (GetElementEditValues(e, 'ACBS\Template Flags\Use Inventory') = '1') and not (GetEditValue(xTemplate) = '') then begin
		xActorOutfit := ElementByPath(xTemplate, sOutfitPath);
		
		if OutputDebug then AddMessage(
			'GetOutfit() 	Found Template e=' + GetEditValue(e)
			+ ';	Template=' + GetEditValue(xTemplate)
			+ ';	Template Signature=' + Signature(xTemplate)
			+ ';	Template ' + sOutfitPath + '=' + GetEditValue(xActorOutfit)
		);
		
		if (Signature(xTemplate) = 'LVLN') then
			// Level list. Have to iterate deeper.
			xActorOutfit := GetOutfit(xTemplate, LinksTo(xActorOutfit), sOutfitPath)
		else if (GetEditValue(xActorOutfit) = '') and not (GetEditValue(fallbackValue) = '') then 
			// Regular NPC_ record but lacks an outfit. Use fallback unless fallback is empty.
			xActorOutfit := fallbackValue
		else 
			// Valid template record. Iterate deeper.
			xActorOutfit := GetOutfit(xTemplate, LinksTo(xActorOutfit), sOutfitPath);
	end;
	
	if OutputDebug then AddMessage(
		'GetOutfit()	End    e=' + GetEditValue(e)
		+ ';	xActorOutfit=' + GetEditValue(xActorOutfit)
		+ ';	xTemplate=' + GetEditValue(xTemplate)
		+ ';	sOutfitPath=' + sOutfitPath
	);
	
	Result := xActorOutfit;
end;


// Traverse template heirarchy until it finds if the NPC has this faction
function HasFaction(e: IInterface; sFactionEdid: String; iDefaultValue: Integer): Integer;
var
	xTemplate, xActorFaction, xActorFactions: IInterface;
	i, iOutput: Integer;
	iUseTemplate, iHasTemplate: Boolean;

begin
	xTemplate := LinksTo(ElementByPath(e, 'TPLT'));
	iOutput := iDefaultValue;
	
	if OutputDebug then AddMessage(
		'HasFaction()	Begin	e=' + GetEditValue(e)
		+ ';	xActorFaction=' + GetEditValue(xActorFaction)
	);
	
	// Iterate over factions and compare
	xActorFactions := ElementByName(e, 'Factions');
	for i := 0 to Pred(ElementCount(xActorFactions)) do begin
		xActorFaction := LinksTo(ElementByName(ElementByIndex(xActorFactions, i), 'Faction'));

		if (GetEditValue(ElementByPath(xActorFaction, 'EDID')) = sFactionEdid) then
			iOutput := 1;
	end;

	
	// Loads from template. Template might also load from its template. Recursive call.
	if (GetElementEditValues(e, 'ACBS\Template Flags\Use Factions') = '1') and not (GetEditValue(xTemplate) = '') then begin
		if OutputDebug then AddMessage(
			'HasFaction()	Found Template	e=' + GetEditValue(e)
			+ ';	xTemplate=' + GetEditValue(xTemplate)
		);

		iOutput := HasFaction(xTemplate, sFactionEdid, iOutput);
	end;
	
	if OutputDebug then AddMessage(
		'HasFaction()	End   e=' + GetEditValue(e)
		+ ';	xTemplate=' + GetEditValue(xTemplate)
		+ ';	sFactionEdid=' + sFactionEdid
		+ ';	iOutput=' + IntToStr(iOutput)
	);
	
	Result := iOutput;
end;



// The form ID relative to the plugin i.e. 00000801
function GetLocalID(e: IInterface): Integer;
begin
	Result := FileFormIDtoLoadOrderFormID(FileByIndex(0), FormID(e));
end;

// Plugin file that created this record
function GetPluginFile(e: IInterface): String;
begin
	Result := GetFileName(GetFile(MasterOrSelf(e)));
end;

// Json Form string formatted for PapyrusUtils version 3.3
function GetJSONFormString(e: IInterface): String;
begin
	Result := '"0x' + IntToHex(GetLocalID(e),1) + '|' + GetPluginFile(e) + '"';
end;

// Outputs why an actor was skipped
procedure LogInvalid(sMessage: String);
begin
	if OutputSkipped then AddMessage('Skipped NPC. ' + sMessage)
end;

function ProcessNPC(e: IInterface): integer;
var
	xActorRace, xActorOutfit, xActorSleepOutfit: IInterface;
	sOutputMessage, sActorBaseJSON, sActorGender: string;
	tRaceRegex, tEdidRegex: TPerlRegEx;

begin
	// If not a follower do many extra checks. Followers can always have outfits.
	// Sometimes followers have ignored prefixes like dun (dunDarklightIllia for example)
	if (HasFaction(e, 'PotentialFollowerFaction', 0) = 0) then begin

		// Check gender if gender filter is set
		if not (FilterGender = '') then begin
			sActorGender := GetGender(e);
			
			if not (sActorGender = FilterGender) and not (sActorGender = 'Random') then begin
				LogInvalid('Invalid Gender: ' + sActorGender + '	' + Name(e));
				Result := 0;
				exit;
			end;
		end;
	
		xActorRace := GetRace(e, ElementByPath(e, 'RNAM'));
		tRaceRegex := TPerlRegEx.Create;
		tRaceRegex.Subject := GetEditValue(xActorRace);
		
		// Matches various clothes wearing races + the default FoxRace.
		tRaceRegex.RegEx := '(Fox|Default|Argonian|Khajiit|Orc|DarkElf|HighElf|SnowElf|WoodElf|Breton|Elder|Imperial|Nord|Redguard)Race';
		tRaceRegex.Options := [];
		
		// Not a race we care about. Return.
		if not tRaceRegex.Match then begin
			LogInvalid('Invalid Race: ' + GetEditValue(xActorRace) + '	' + Name(e));
			Result := 0;
			exit;
		end;

		tEdidRegex := TPerlRegEx.Create;
		tEdidRegex.Subject := GetEditValue(ElementByPath(e, 'EDID'));
		tEdidRegex.Options := [];
		
		// Prefixes that indiciate a spawned NPC outside of a city.
//		tEdidRegex.RegEx := '^(DLC\d)?([Dd]un|[Ee]nc|[L]vl)[A-Z]';

		// Prefix that indiciates a temporary NPC. An encounter.
		tEdidRegex.RegEx := '^(DLC\d)?([Ee]nc)[A-Z]';
		
		// Not a race we care about
		if tEdidRegex.Match then begin
			
			LogInvalid('Invalid EDID prefix: ' + GetEditValue(ElementByPath(e, 'EDID')) + '	' + Name(e));
			Result := 0;
			exit;
		end;
		
		// Invalid when it contains: Template, Corpse, Test, Demo, Dead
		// These terms must be followed by a string end or snakeCase capital letter. That way Demo doesn't rule out Demolition
		tEdidRegex.RegEx := '(Template|Corpse|Test|Demo|Dead)[A-Z\W]';
		
		// Not a race we care about
		if tEdidRegex.Match then begin
			LogInvalid('Invalid EDID contains: ' + GetEditValue(ElementByPath(e, 'EDID')) + '	' + Name(e));
			Result := 0;
			exit;
		end;
	end;
		
	xActorOutfit := GetOutfit(e, LinksTo(ElementByPath(e, 'DOFT')), 'DOFT');

	// Lacks an outfit
	if (GetEditValue(xActorOutfit) = '') then begin
		LogInvalid('No Outfit:	' + Name(e));
		Result := 0;
		exit;
	end;

	xActorSleepOutfit := GetOutfit(e, LinksTo(ElementByPath(e, 'SOFT')), 'SOFT');
	
	if OutputDebug then AddMessage(
		'End   GetOutfit() e=' + GetEditValue(e)
		+ ';	xActorOutfit=' + GetEditValue(xActorOutfit)
		+ ';	xActorSleepOutfit=' + GetEditValue(xActorSleepOutfit)
	);
	
	sActorBaseJSON := GetJSONFormString(e);
	
	// Duplicate of existing.
	if not (tActorBases.IndexOf(sActorBaseJSON) = -1) then begin
		LogInvalid('Entry already exists, this is probably an override:	' + Name(e));
		Result := 0;
		exit;
	end;
	
	if OutputTSV then begin
		sOutputMessage := BaseName(e) + '	' + sActorBaseJSON + '	' +
		+ Name(xActorOutfit) + '	' + GetJSONFormString(xActorOutfit);
		
		// Frequently no sleep outfit is set. For these do an empty column and a None JSON value.
		if (GetEditValue(xActorSleepOutfit) = '') then
			sOutputMessage := sOutputMessage + '		null,'
		else
			sOutputMessage := sOutputMessage + '	' + Name(xActorSleepOutfit) + '	' + GetJSONFormString(xActorSleepOutfit);
		
		// Setting include whether or not NPC has a faction
		if not (OutputFaction = '') then begin
			sOutputMessage := sOutputMessage + '		' + IntToStr(HasFaction(e, OutputFaction, 0)) + ','
		end;
		
		// Only add unique rows
		if (tTsvRows.IndexOf(sOutputMessage) = -1) then
			tTsvRows.Add(sOutputMessage);
	end;
	
	if OutputJSON then begin
		tActorBases.Add(sActorBaseJSON);
		tCommentsActorBases.Add(GetEditValue(ElementByPath(e,'EDID')) + ' Dec.' + IntToStr(GetLocalID(e)));
		
		
		tOutfits.Add(GetJSONFormString(xActorOutfit));
		tCommentsOutfits.Add(GetEditValue(ElementByPath(xActorOutfit,'EDID')) + ' Dec.' + IntToStr(GetLocalID(xActorOutfit)));
		
		if (GetEditValue(xActorSleepOutfit) = '') then begin
			tSleepOutfits.Add('null');
			tCommentsSleepOutfits.Add('');
		end
		else begin
			tSleepOutfits.Add(GetJSONFormString(xActorSleepOutfit));
			tCommentsSleepOutfits.Add(GetEditValue(ElementByPath(xActorSleepOutfit,'EDID')) + ' Dec.' + IntToStr(GetLocalID(xActorSleepOutfit)));
		end;
			
			
		// Setting include whether or not NPC has a faction
		if not (OutputFaction = '') then begin
			tHasFaction.Add(IntToStr(HasFaction(e, OutputFaction, 0)))
		end;
	end;
	
	Inc(OutfitsFound);

	Result := 1;
end;


procedure OutputJSONArray(sArrayName: String; tJsonItems: TwbFastStringList; bTrailingComma: Boolean);
var
	i: int;
begin
	tFileLines.Add('		"' + sArrayName + '" : [');
	for i := 0 to (tJsonItems.Count - 1) do 
		if i < (tJsonItems.Count - 1) then
			tFileLines.Add('			' + tJsonItems[i] + ',')
		else
			tFileLines.Add('			' + tJsonItems[i]);
			
	if (bTrailingComma = 1) then
		tFileLines.Add('		],')
	else
		tFileLines.Add('		]');
	
end;


procedure OutputJSONArrayWithComments(sArrayName: String; tJsonItems: TwbFastStringList; bTrailingComma: Boolean; tJsonComments: TwbFastStringList);
var
	i: int;
	sRow: String;
begin
	tFileLines.Add('		"' + sArrayName + '" : [ ');
	for i := 0 to (tJsonItems.Count - 1) do begin
		if i < (tJsonItems.Count - 1) then
			sRow := '			' + tJsonItems[i] + ','
		else
			sRow := '			' + tJsonItems[i];
		
		if not (tJsonComments[i] = '') then
			sRow := sRow + '	/* ' + tJsonComments[i] + ' */';

		tFileLines.Add(sRow);
	end;
			
	if (bTrailingComma = 1) then
		tFileLines.Add('		],')
	else
		tFileLines.Add('		]');
	
end;



function Process(e: IInterface): integer;
begin
	
	if (Signature(e) = 'NPC_') and not (GetElementEditValues(e, 'ACBS\Flags\Is CharGen Face Preset') = '1') then begin
		if (ReferencedByCount(e) > 0) then
			ProcessNPC(WinningOverride(e))
		else
			LogInvalid('No references to:	' + Name(e));

	end;
	
end;

function Finalize: integer;
var
	i: Integer;
	sOutputDir, sOutputTsvFile, sOutputJsonFile: String;
begin
	
	if (OutfitsFound > 0) then begin
		AddMessage('--------------------------------------');
		AddMessage('Found ' + IntToStr(OutfitsFound) + ' matching NPCs.');
		AddMessage('--------------------------------------');
		AddMessage('	');
		
		if OutputTSV or OutputJSON then begin
			AddMessage('Exporting results.');
			sOutputDir := SelectDirectory('Select destination directory', '', ProgramPath, nil);
		end;

		if OutputTSV then begin
			sOutputTsvFile := sOutputDir + '\SlaverunNPCOutfitList.tsv';
			
			tFileLines.Add('ActorBase	ActorBase JSON	Outfit	Outfit JSON	Sleep Outfit	Sleep Outfit JSON		zbfSlave');
			//for i := tTsvRows.Count - 1 downto 0 do tFileLines.Add(tTsvRows[i]);
			for i := 0 to (tTsvRows.Count - 1) do tFileLines.Add(tTsvRows[i]);
			
			tFileLines.SaveToFile(sOutputTsvFile);
			
			AddMessage('Finished exporting ' + IntToStr(OutfitsFound) + ' TSV rows to: ' + sOutputTsvFile);
			AddMessage('	');
			AddMessage('	This file is for informational purposes. It can be viewed in a spreadsheet program.');
			AddMessage('	');
		end;
		
		tFileLines.Clear;

		if OutputJSON then begin
			
			sOutputJsonFile := sOutputDir + '\SES_Outfits_NPC_Defaults.json';
			
			tFileLines.Add('{');
			
			tFileLines.Add('	"formList" : {');
			OutputJSONArrayWithComments('actorbases', tActorBases, 1, tCommentsActorBases);
			OutputJSONArrayWithComments('outfits', tOutfits, 1, tCommentsOutfits);
			OutputJSONArrayWithComments('sleepoutfits', tSleepOutfits, 0, tCommentsSleepOutfits);
			tFileLines.Add('	},');

			tFileLines.Add('	"intList" : {');
			OutputJSONArray('zbfslave', tHasFaction, 0);
			tFileLines.Add('	}');
			
			tFileLines.Add('}');
			
			tFileLines.SaveToFile(sOutputJsonFile);

			AddMessage('Finished exporting ' + IntToStr(OutfitsFound) + ' NPCs to: ' + sOutputJsonFile);
			AddMessage('	');
			AddMessage('	Copy that json file into a zip file then install the zip with your mod manager.');
			AddMessage('	The location within the zip should be:');
			AddMessage('	[zip file]\SKSE\Plugins\Slaverun\SES_Outfits_NPC_Defaults.json');
			AddMessage('	');
		end;
	end
	else begin
		AddMessage('--------------------------------------');
		AddMessage('Found 0 matching NPCs. Nothing to export.');
	end;
	
	AddMessage('--------------------------------------');

	tTsvRows.Free;
	tActorBases.Free;
	tOutfits.Free;
	tSleepOutfits.Free;
	tHasFaction.Free;
	tFileLines.Free;
	
	tCommentsActorBases.Free;
	tCommentsOutfits.Free;
	tCommentsSleepOutfits.Free;
	
	Result := 0;
end;

end.
