using System; using System.Reflection; using System.Xml.Serialization; using Newtonsoft.Json; using SabreTools.Core; using SabreTools.Core.Filter; using SabreTools.Core.Tools; using SabreTools.DatItems.Formats; using SabreTools.Hashing; using SabreTools.IO.Logging; namespace SabreTools.DatItems { /// /// Base class for all items included in a set /// [JsonObject("datitem"), XmlRoot("datitem")] [XmlInclude(typeof(Adjuster))] [XmlInclude(typeof(Analog))] [XmlInclude(typeof(Archive))] [XmlInclude(typeof(BiosSet))] [XmlInclude(typeof(Blank))] [XmlInclude(typeof(Chip))] [XmlInclude(typeof(Condition))] [XmlInclude(typeof(Configuration))] [XmlInclude(typeof(ConfLocation))] [XmlInclude(typeof(ConfSetting))] [XmlInclude(typeof(Control))] [XmlInclude(typeof(DataArea))] [XmlInclude(typeof(Device))] [XmlInclude(typeof(DeviceRef))] [XmlInclude(typeof(DipLocation))] [XmlInclude(typeof(DipSwitch))] [XmlInclude(typeof(DipValue))] [XmlInclude(typeof(Disk))] [XmlInclude(typeof(DiskArea))] [XmlInclude(typeof(Display))] [XmlInclude(typeof(Driver))] [XmlInclude(typeof(Extension))] [XmlInclude(typeof(Feature))] [XmlInclude(typeof(Info))] [XmlInclude(typeof(Input))] [XmlInclude(typeof(Instance))] [XmlInclude(typeof(Media))] [XmlInclude(typeof(Part))] [XmlInclude(typeof(PartFeature))] [XmlInclude(typeof(Port))] [XmlInclude(typeof(RamOption))] [XmlInclude(typeof(Release))] [XmlInclude(typeof(Rom))] [XmlInclude(typeof(Sample))] [XmlInclude(typeof(SharedFeat))] [XmlInclude(typeof(Slot))] [XmlInclude(typeof(SlotOption))] [XmlInclude(typeof(SoftwareList))] [XmlInclude(typeof(Sound))] public abstract class DatItem : ModelBackedItem, IEquatable, IComparable, ICloneable { #region Constants /// /// Duplicate type when compared to another item /// public const string DupeTypeKey = "DUPETYPE"; /// /// Machine associated with the item /// public const string MachineKey = "MACHINE"; /// /// Flag if item should be removed /// public const string RemoveKey = "REMOVE"; /// /// Source information /// public const string SourceKey = "SOURCE"; #endregion #region Fields /// /// Item type for the object /// protected abstract ItemType ItemType { get; } #endregion #region Logging /// /// Static logger for static methods /// [JsonIgnore, XmlIgnore] protected static readonly Logger _staticLogger = new(); #endregion #region Accessors /// /// Gets the name to use for a DatItem /// /// Name if available, null otherwise public virtual string? GetName() => null; /// /// Sets the name to use for a DatItem /// /// Name to set for the item public virtual void SetName(string? name) { } #endregion #region Cloning Methods /// /// Clone the DatItem /// /// Clone of the DatItem public abstract object Clone(); /// /// Copy all machine information over in one shot /// /// Existing item to copy information from public void CopyMachineInformation(DatItem item) { // If there is no machine if (!item._internal.ContainsKey(DatItem.MachineKey)) return; var machine = item.GetFieldValue(DatItem.MachineKey); CopyMachineInformation(machine); } /// /// Copy all machine information over in one shot /// /// Existing machine to copy information from public void CopyMachineInformation(Machine? machine) { if (machine == null) return; if (machine.Clone() is Machine cloned) SetFieldValue(DatItem.MachineKey, cloned); } #endregion #region Comparision Methods /// public int CompareTo(DatItem? other) { // If the other item doesn't exist if (other == null) return 1; // Get the names to avoid changing values string? selfName = GetName(); string? otherName = other.GetName(); // If the names are equal if (selfName == otherName) return Equals(other) ? 0 : 1; // If `otherName` is null, Compare will return > 0 // If `selfName` is null, Compare will return < 0 return string.Compare(selfName, otherName, StringComparison.Ordinal); } /// public override bool Equals(ModelBackedItem? other) { // If other is null if (other == null) return false; // If the type is mismatched if (other is not DatItem otherItem) return false; // Compare internal models return _internal.Equals(otherItem); } /// public override bool Equals(ModelBackedItem? other) { // If other is null if (other == null) return false; // If the type is mismatched if (other is not DatItem otherItem) return false; // Compare internal models return _internal.Equals(otherItem); } /// /// Determine if an item is a duplicate using partial matching logic /// /// DatItem to use as a baseline /// True if the items are duplicates, false otherwise public virtual bool Equals(DatItem? other) { // If the other item is null if (other == null) return false; // Get the types for comparison ItemType selfType = GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); ItemType otherType = other.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); // If we don't have a matched type, return false if (selfType != otherType) return false; // Compare the internal models return _internal.EqualTo(other._internal); } /// /// Return the duplicate status of two items /// /// DatItem to check against /// The DupeType corresponding to the relationship between the two public DupeType GetDuplicateStatus(DatItem? lastItem) { DupeType output = 0x00; // If we don't have a duplicate at all, return none if (!Equals(lastItem)) return output; // Get the sources for comparison var selfSource = GetFieldValue(DatItem.SourceKey); var lastSource = lastItem.GetFieldValue(DatItem.SourceKey); // Get the machines for comparison var selfMachine = GetFieldValue(DatItem.MachineKey); string? selfMachineName = selfMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey); var lastMachine = lastItem.GetFieldValue(DatItem.MachineKey); string? lastMachineName = lastMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey); // If the duplicate is external already #if NET20 || NET35 if ((lastItem.GetFieldValue(DatItem.DupeTypeKey) & DupeType.External) != 0) #else if (lastItem.GetFieldValue(DatItem.DupeTypeKey).HasFlag(DupeType.External)) #endif output |= DupeType.External; // If the duplicate should be external else if (lastSource?.Index != selfSource?.Index) output |= DupeType.External; // Otherwise, it's considered an internal dupe else output |= DupeType.Internal; // If the item and machine names match if (lastMachineName == selfMachineName && lastItem.GetName() == GetName()) output |= DupeType.All; // Otherwise, hash match is assumed else output |= DupeType.Hash; return output; } /// /// Return the duplicate status of two items /// /// Source associated with this item /// DatItem to check against /// Source associated with the last item /// The DupeType corresponding to the relationship between the two public DupeType GetDuplicateStatusDB(Source? selfSource, DatItem? lastItem, Source? lastSource) { DupeType output = 0x00; // If we don't have a duplicate at all, return none if (!Equals(lastItem)) return output; // TODO: Fix this since machines are determined in a different way // Get the machines for comparison var selfMachine = GetFieldValue(DatItem.MachineKey); string? selfMachineName = selfMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey); var lastMachine = lastItem.GetFieldValue(DatItem.MachineKey); string? lastMachineName = lastMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey); // If the duplicate is external already #if NET20 || NET35 if ((lastItem.GetFieldValue(DatItem.DupeTypeKey) & DupeType.External) != 0) #else if (lastItem.GetFieldValue(DatItem.DupeTypeKey).HasFlag(DupeType.External)) #endif output |= DupeType.External; // If the duplicate should be external else if (lastSource?.Index != selfSource?.Index) output |= DupeType.External; // Otherwise, it's considered an internal dupe else output |= DupeType.Internal; // If the item and machine names match if (lastMachineName == selfMachineName && lastItem.GetName() == GetName()) output |= DupeType.All; // Otherwise, hash match is assumed else output |= DupeType.Hash; return output; } #endregion #region Manipulation /// /// Runs a filter and determines if it passes or not /// /// Filter runner to use for checking /// True if the item and its machine passes the filter, false otherwise public bool PassesFilter(FilterRunner filterRunner) { var machine = GetFieldValue(DatItem.MachineKey); if (machine != null && !machine.PassesFilter(filterRunner)) return false; return filterRunner.Run(_internal); } /// /// Runs a filter and determines if it passes or not /// /// Filter runner to use for checking /// True if the item passes the filter, false otherwise public bool PassesFilterDB(FilterRunner filterRunner) => filterRunner.Run(_internal); #endregion #region Sorting and Merging /// /// Get unique duplicate suffix on name collision /// /// String representing the suffix public virtual string GetDuplicateSuffix() => _internal.GetDuplicateSuffix(); /// /// Get the dictionary key that should be used for a given item and bucketing type /// /// ItemKey value representing what key to get /// True if the key should be lowercased (default), false otherwise /// True if games should only be compared on game and file name, false if system and source are counted /// String representing the key to be used for the DatItem public virtual string GetKey(ItemKey bucketedBy, bool lower = true, bool norename = true) { // Set the output key as the default blank string string key = string.Empty; // Now determine what the key should be based on the bucketedBy value switch (bucketedBy) { case ItemKey.CRC: key = ZeroHash.CRC32Str; break; case ItemKey.Machine: string sourceString = string.Empty; if (!norename) { var source = GetFieldValue(DatItem.SourceKey); if (source != null) sourceString = source.Index.ToString().PadLeft(10, '0') + "-"; } string machineString = "Default"; var machine = GetFieldValue(DatItem.MachineKey); if (machine != null) { var machineName = machine.GetStringFieldValue(Models.Metadata.Machine.NameKey); if (!string.IsNullOrEmpty(machineName)) machineString = machineName!; } key = $"{sourceString}{machineString}"; break; case ItemKey.MD2: key = ZeroHash.GetString(HashType.MD2); break; case ItemKey.MD4: key = ZeroHash.GetString(HashType.MD4); break; case ItemKey.MD5: key = ZeroHash.MD5Str; break; case ItemKey.SHA1: key = ZeroHash.SHA1Str; break; case ItemKey.SHA256: key = ZeroHash.SHA256Str; break; case ItemKey.SHA384: key = ZeroHash.SHA384Str; break; case ItemKey.SHA512: key = ZeroHash.SHA512Str; break; case ItemKey.SpamSum: key = ZeroHash.SpamSumStr; break; } // Double and triple check the key for corner cases key ??= string.Empty; if (lower) key = key.ToLowerInvariant(); return key; } /// /// Get the dictionary key that should be used for a given item and bucketing type /// /// ItemKey value representing what key to get /// Source associated with the item for renaming /// True if the key should be lowercased (default), false otherwise /// True if games should only be compared on game and file name, false if system and source are counted /// String representing the key to be used for the DatItem public virtual string GetKeyDB(ItemKey bucketedBy, Source? source, bool lower = true, bool norename = true) { // Set the output key as the default blank string string key = string.Empty; // Now determine what the key should be based on the bucketedBy value switch (bucketedBy) { case ItemKey.CRC: key = ZeroHash.CRC32Str; break; case ItemKey.Machine: string sourceString = string.Empty; if (!norename && source != null) sourceString = source.Index.ToString().PadLeft(10, '0') + "-"; string machineString = "Default"; var machine = GetFieldValue(DatItem.MachineKey); if (machine != null) { var machineName = machine.GetStringFieldValue(Models.Metadata.Machine.NameKey); if (!string.IsNullOrEmpty(machineName)) machineString = machineName!; } key = $"{sourceString}{machineString}"; break; case ItemKey.MD2: key = ZeroHash.GetString(HashType.MD2); break; case ItemKey.MD4: key = ZeroHash.GetString(HashType.MD4); break; case ItemKey.MD5: key = ZeroHash.MD5Str; break; case ItemKey.SHA1: key = ZeroHash.SHA1Str; break; case ItemKey.SHA256: key = ZeroHash.SHA256Str; break; case ItemKey.SHA384: key = ZeroHash.SHA384Str; break; case ItemKey.SHA512: key = ZeroHash.SHA512Str; break; case ItemKey.SpamSum: key = ZeroHash.SpamSumStr; break; } // Double and triple check the key for corner cases key ??= string.Empty; if (lower) key = key.ToLowerInvariant(); return key; } #endregion } /// /// Base class for all items included in a set that are backed by an internal model /// public abstract class DatItem : DatItem, IEquatable>, IComparable>, ICloneable where T : Models.Metadata.DatItem { #region Fields /// /// Key for accessing the item name, if it exists /// protected abstract string? NameKey { get; } #endregion #region Constructors /// /// Create a default, empty object /// public DatItem() { _internal = Activator.CreateInstance(); SetName(string.Empty); SetFieldValue(Models.Metadata.DatItem.TypeKey, ItemType); SetFieldValue(DatItem.MachineKey, new Machine()); } /// /// Create an object from the internal model /// public DatItem(T item) { _internal = item; SetFieldValue(Models.Metadata.DatItem.TypeKey, ItemType); SetFieldValue(DatItem.MachineKey, new Machine()); } #endregion #region Accessors /// public override string? GetName() { if (!string.IsNullOrEmpty(NameKey)) return GetStringFieldValue(NameKey); return null; } /// public override void SetName(string? name) { if (!string.IsNullOrEmpty(NameKey)) SetFieldValue(NameKey, name); } #endregion #region Cloning Methods /// /// Clone the DatItem /// /// Clone of the DatItem /// /// Throws an exception if there is a DatItem implementation /// that is not a part of this library. /// public override object Clone() { var concrete = Array.Find(Assembly.GetExecutingAssembly().GetTypes(), t => !t.IsAbstract && t.IsClass && t.BaseType == typeof(DatItem)); var clone = Activator.CreateInstance(concrete!); (clone as DatItem)!._internal = _internal?.Clone() as T ?? Activator.CreateInstance(); return clone; } /// /// Get a clone of the current internal model /// /// TODO: Make virtual so individuals can handle conversion public T GetInternalClone() => (_internal.Clone() as T)!; #endregion #region Comparision Methods /// public int CompareTo(DatItem? other) { // If the other item doesn't exist if (other == null) return 1; // Get the names to avoid changing values string? selfName = GetName(); string? otherName = other.GetName(); // If the names are equal if (selfName == otherName) return Equals(other) ? 0 : 1; // If `otherName` is null, Compare will return > 0 // If `selfName` is null, Compare will return < 0 return string.Compare(selfName, otherName, StringComparison.Ordinal); } /// /// Determine if an item is a duplicate using partial matching logic /// /// DatItem to use as a baseline /// True if the items are duplicates, false otherwise public virtual bool Equals(DatItem? other) { // If the other value is null if (other == null) return false; // Get the types for comparison ItemType selfType = GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); ItemType otherType = other.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue(); // If we don't have a matched type, return false if (selfType != otherType) return false; // Compare the internal models return _internal.EqualTo(other._internal); } #endregion } }