using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using SabreTools.Core.Tools;
using SabreTools.DatFiles;
using SabreTools.DatItems;
using SabreTools.Logging;
[assembly: InternalsVisibleTo("SabreTools.Test")]
namespace SabreTools.Filtering
{
///
/// Represents the cleaning operations that need to be performed on a set of items, usually a DAT
///
public class Cleaner
{
#region Fields
///
/// Clean all names to WoD standards
///
public bool Clean { get; set; }
///
/// Deduplicate items using the given method
///
public DedupeType DedupeRoms { get; set; }
///
/// Set Machine Description from Machine Name
///
public bool DescriptionAsName { get; set; }
///
/// Keep machines that don't contain any items
///
public bool KeepEmptyGames { get; set; }
///
/// Enable "One Rom, One Region (1G1R)" mode
///
public bool OneGamePerRegion { get; set; }
///
/// Ordered list of regions for "One Rom, One Region (1G1R)" mode
///
public List? RegionList { get; set; }
///
/// Ensure each rom is in their own game
///
public bool OneRomPerGame { get; set; }
///
/// Remove all unicode characters
///
public bool RemoveUnicode { get; set; }
///
/// Include root directory when determing trim sizes
///
public string? Root { get; set; }
///
/// Remove scene dates from the beginning of machine names
///
public bool SceneDateStrip { get; set; }
///
/// Change all machine names to "!"
///
public bool Single { get; set; }
///
/// Trim total machine and item name to not exceed NTFS limits
///
public bool Trim { get; set; }
#endregion
#region Logging
///
/// Logging object
///
private readonly Logger logger = new();
#endregion
#region Running
///
/// Apply cleaning methods to the DatFile
///
/// Current DatFile object to run operations on
/// True if the error that is thrown should be thrown back to the caller, false otherwise
/// True if cleaning was successful, false on error
public bool ApplyCleaning(DatFile datFile, bool throwOnError = false)
{
InternalStopwatch watch = new("Applying cleaning steps to DAT");
try
{
// Perform item-level cleaning
CleanDatItems(datFile);
CleanDatItemsDB(datFile);
// Bucket and dedupe according to the flag
if (DedupeRoms == DedupeType.Full)
{
datFile.Items.BucketBy(ItemKey.CRC, DedupeRoms);
datFile.ItemsDB.BucketBy(ItemKey.CRC, DedupeRoms);
}
else if (DedupeRoms == DedupeType.Game)
{
datFile.Items.BucketBy(ItemKey.Machine, DedupeRoms);
datFile.ItemsDB.BucketBy(ItemKey.Machine, DedupeRoms);
}
// Process description to machine name
if (DescriptionAsName == true)
{
datFile.Items.MachineDescriptionToName(throwOnError);
datFile.ItemsDB.MachineDescriptionToName(throwOnError);
}
// If we are removing scene dates, do that now
if (SceneDateStrip == true)
{
datFile.Items.StripSceneDatesFromItems();
datFile.ItemsDB.StripSceneDatesFromItems();
}
// Run the one rom per game logic, if required
if (OneGamePerRegion == true && RegionList != null)
{
datFile.Items.SetOneGamePerRegion(RegionList);
datFile.ItemsDB.SetOneGamePerRegion(RegionList);
}
// Run the one rom per game logic, if required
if (OneRomPerGame == true)
{
datFile.Items.SetOneRomPerGame();
datFile.ItemsDB.SetOneRomPerGame();
}
// Remove all marked items
datFile.Items.ClearMarked();
datFile.ItemsDB.ClearMarked();
// We remove any blanks, if we aren't supposed to have any
if (KeepEmptyGames == false)
{
datFile.Items.ClearEmpty();
datFile.ItemsDB.ClearEmpty();
}
}
catch (Exception ex) when (!throwOnError)
{
logger.Error(ex);
return false;
}
finally
{
watch.Stop();
}
return true;
}
///
/// Clean individual items based on the current filter
///
/// Current DatFile object to run operations on
internal void CleanDatItems(DatFile datFile)
{
List keys = [.. datFile.Items.Keys];
foreach (string key in keys)
{
// For every item in the current key
var items = datFile.Items[key];
if (items == null)
continue;
foreach (DatItem item in items)
{
// If we have a null item, we can't clean it it
if (item == null)
continue;
// Run cleaning per item
CleanDatItem(item);
}
// Assign back for caution
datFile.Items[key] = items;
}
}
///
/// Clean individual items based on the current filter
///
/// Current DatFile object to run operations on
internal void CleanDatItemsDB(DatFile datFile)
{
List keys = [.. datFile.ItemsDB.SortedKeys];
foreach (string key in keys)
{
// For every item in the current key
var items = datFile.ItemsDB.GetDatItemsForBucket(key);
if (items == null)
continue;
foreach ((long, DatItem) item in items)
{
// If we have a null item, we can't clean it it
if (item.Item2 == null)
continue;
// Run cleaning per item
CleanDatItemDB(datFile.ItemsDB, item);
}
}
}
///
/// Clean a DatItem according to the cleaner
///
/// DatItem to clean
internal void CleanDatItem(DatItem datItem)
{
// If we're stripping unicode characters, strip machine name and description
if (RemoveUnicode)
{
datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, TextHelper.RemoveUnicodeCharacters(datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)));
datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.DescriptionKey, TextHelper.RemoveUnicodeCharacters(datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)));
datItem.SetName(TextHelper.RemoveUnicodeCharacters(datItem.GetName()));
}
// If we're in cleaning mode, sanitize machine name and description
if (Clean)
{
datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, TextHelper.NormalizeCharacters(datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)));
datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.DescriptionKey, TextHelper.NormalizeCharacters(datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)));
}
// If we are in single game mode, rename the machine
if (Single)
datItem.GetFieldValue(DatItem.MachineKey)!.SetFieldValue(Models.Metadata.Machine.NameKey, "!");
// If we are in NTFS trim mode, trim the item name
if (Trim && datItem.GetName() != null)
{
// Windows max name length is 260
int usableLength = 260 - datItem.GetFieldValue(DatItem.MachineKey)!.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.Length - (Root?.Length ?? 0);
if (datItem.GetName()!.Length > usableLength)
{
string ext = Path.GetExtension(datItem.GetName()!);
datItem.SetName(datItem.GetName()!.Substring(0, usableLength - ext.Length) + ext);
}
}
}
///
/// Clean a DatItem according to the cleaner
///
/// ItemDictionaryDB to get machine information from
/// DatItem to clean
internal void CleanDatItemDB(ItemDictionaryDB db, (long, DatItem) datItem)
{
// Get the machine associated with the item, if possible
var machine = db.GetMachineForItem(datItem.Item1);
if (machine.Item2 == null)
return;
// If we're stripping unicode characters, strip machine name and description
if (RemoveUnicode)
{
machine.Item2.SetFieldValue(Models.Metadata.Machine.NameKey, TextHelper.RemoveUnicodeCharacters(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)));
machine.Item2.SetFieldValue(Models.Metadata.Machine.DescriptionKey, TextHelper.RemoveUnicodeCharacters(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)));
datItem.Item2.SetName(TextHelper.RemoveUnicodeCharacters(datItem.Item2.GetName()));
}
// If we're in cleaning mode, sanitize machine name and description
if (Clean)
{
machine.Item2.SetFieldValue(Models.Metadata.Machine.NameKey, TextHelper.NormalizeCharacters(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)));
machine.Item2.SetFieldValue(Models.Metadata.Machine.DescriptionKey, TextHelper.NormalizeCharacters(machine.Item2.GetStringFieldValue(Models.Metadata.Machine.DescriptionKey)));
}
// If we are in single game mode, rename the machine
if (Single)
machine.Item2.SetFieldValue(Models.Metadata.Machine.NameKey, "!");
// If we are in NTFS trim mode, trim the item name
if (Trim && datItem.Item2.GetName() != null)
{
// Windows max name length is 260
int usableLength = 260 - machine.Item2.GetStringFieldValue(Models.Metadata.Machine.NameKey)!.Length - (Root?.Length ?? 0);
if (datItem.Item2.GetName()!.Length > usableLength)
{
string ext = Path.GetExtension(datItem.Item2.GetName()!);
datItem.Item2.SetName(datItem.Item2.GetName()!.Substring(0, usableLength - ext.Length) + ext);
}
}
}
#endregion
}
}