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(); /// /// Conditionally copy all machine information from another item /// /// Existing item to copy information from /// /// The cases when Machine data is updated: /// - Current machine is a clone of the other machine /// - Current machine is a rom of the other machine /// public void ConditionalUpdateMachine(DatItem item) { // Get the machines for the two items Machine? selfMachine = GetFieldValue(DatItem.MachineKey); Machine? itemMachine = item.GetFieldValue(DatItem.MachineKey); // If either machine is missing if (selfMachine == null || itemMachine == null) return; // Get the required strings string? selfCloneOf = selfMachine.GetStringFieldValue(Models.Metadata.Machine.CloneOfKey); string? selfRomOf = selfMachine.GetStringFieldValue(Models.Metadata.Machine.RomOfKey); string? otherMachineName = itemMachine.GetStringFieldValue(Models.Metadata.Machine.NameKey); // If the other machine has no name if (otherMachineName == null) return; // If the current machine is a child of the new machine, use the new machine instead if (selfCloneOf == otherMachineName) { CopyMachineInformation(item); SetName(item.GetName()); } else if (selfRomOf == otherMachineName) { CopyMachineInformation(item); SetName(item.GetName()); } } /// /// Conditionally copy all source information from another item /// /// Existing item to copy information from /// /// The cases when Source data is updated: /// - Current source data has an index higher than the other item /// public void ConditionalUpdateSource(DatItem item) { // Get the sources for comparison Source? selfSource = GetFieldValue(DatItem.SourceKey); Source? itemSource = item.GetFieldValue(DatItem.SourceKey); // If either source is missing if (selfSource == null || itemSource == null) return; // Use the new source if less than if (selfSource.Index > itemSource.Index) { SetFieldValue(DatItem.SourceKey, itemSource.Clone() as Source); CopyMachineInformation(item); SetName(item.GetName()); return; } } /// /// Copy all machine information over in one shot /// /// Existing item to copy information from public void CopyMachineInformation(DatItem item) { 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) { try { if (GetName() == other?.GetName()) return Equals(other) ? 0 : 1; return string.Compare(GetName(), other?.GetName()); } catch { return 1; } } /// /// 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 we don't have a matched type, return false if (GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue() != other?.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue()) 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; // If the duplicate is external already or should be, set it #if NET20 || NET35 if ((lastItem.GetFieldValue(DatItem.DupeTypeKey) & DupeType.External) != 0 || lastItem?.GetFieldValue(DatItem.SourceKey)?.Index != GetFieldValue(DatItem.SourceKey)?.Index) #else if (lastItem.GetFieldValue(DatItem.DupeTypeKey).HasFlag(DupeType.External) || lastItem?.GetFieldValue(DatItem.SourceKey)?.Index != GetFieldValue(DatItem.SourceKey)?.Index) #endif { var currentMachine = GetFieldValue(DatItem.MachineKey); var lastMachine = lastItem?.GetFieldValue(DatItem.MachineKey); if (lastMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) == currentMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) && lastItem?.GetName() == GetName()) output = DupeType.External | DupeType.All; else output = DupeType.External | DupeType.Hash; } // Otherwise, it's considered an internal dupe else { var currentMachine = GetFieldValue(DatItem.MachineKey); var lastMachine = lastItem?.GetFieldValue(DatItem.MachineKey); if (lastMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) == currentMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) && lastItem?.GetName() == GetName()) output = DupeType.Internal | DupeType.All; else output = DupeType.Internal | 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 GetDuplicateStatus(Source? source, DatItem? lastItem, Source? lastSource) { DupeType output = 0x00; // If we don't have a duplicate at all, return none if (!Equals(lastItem)) return output; // If the duplicate is external already or should be, set it #if NET20 || NET35 if ((lastItem.GetFieldValue(DatItem.DupeTypeKey) & DupeType.External) != 0 || lastSource?.Index != source?.Index) #else if (lastItem.GetFieldValue(DatItem.DupeTypeKey).HasFlag(DupeType.External) || lastSource?.Index != source?.Index) #endif { var currentMachine = GetFieldValue(DatItem.MachineKey); var lastMachine = lastItem?.GetFieldValue(DatItem.MachineKey); if (lastMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) == currentMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) && lastItem?.GetName() == GetName()) output = DupeType.External | DupeType.All; else output = DupeType.External | DupeType.Hash; } // Otherwise, it's considered an internal dupe else { var currentMachine = GetFieldValue(DatItem.MachineKey); var lastMachine = lastItem?.GetFieldValue(DatItem.MachineKey); if (lastMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) == currentMachine?.GetStringFieldValue(Models.Metadata.Machine.NameKey) && lastItem?.GetName() == GetName()) output = DupeType.Internal | DupeType.All; else output = DupeType.Internal | 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 passes the filter, false otherwise public bool PassesFilter(FilterRunner filterRunner) { if (!GetFieldValue(DatItem.MachineKey)!.PassesFilter(filterRunner)) return false; return filterRunner.Run(_internal); } /// /// Remove a field from the DatItem /// /// Field to remove /// True if the removal was successful, false otherwise public bool RemoveField(string? fieldName) => FieldManipulator.RemoveField(_internal, fieldName); /// /// Replace a field from another DatItem /// /// DatItem to replace field from /// Field to replace /// True if the replacement was successful, false otherwise public bool ReplaceField(DatItem? other, string? fieldName) => FieldManipulator.ReplaceField(other?._internal, _internal, fieldName); /// /// Set a field in the DatItem from a mapping string /// /// Field to set /// String representing the value to set /// True if the removal was successful, false otherwise /// This only performs minimal validation before setting public bool SetField(string? fieldName, string value) => FieldManipulator.SetField(_internal, fieldName, value); #endregion #region Sorting and Merging /// /// 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}"; if (lower) key = key.ToLowerInvariant(); 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; 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 GetKey(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}"; if (lower) key = key.ToLowerInvariant(); 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; 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 (NameKey != null) return GetStringFieldValue(NameKey); return null; } /// public override void SetName(string? name) { if (NameKey != null) SetFieldValue(NameKey, name); } /// /// Get a clone of the current internal model /// public T GetInternalClone() => (_internal.Clone() as T)!; #endregion #region Cloning Methods /// /// Clone the DatItem /// /// Clone of the DatItem 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; } #endregion #region Comparision Methods /// public int CompareTo(DatItem? other) { try { if (GetName() == other?.GetName()) return Equals(other) ? 0 : 1; return string.Compare(GetName(), other?.GetName()); } catch { return 1; } } /// /// 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 we don't have a matched type, return false if (GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue() != other?.GetStringFieldValue(Models.Metadata.DatItem.TypeKey).AsEnumValue()) return false; // Compare the internal models return _internal.EqualTo(other._internal); } #endregion } }