using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using MPF.Core.Converters;
using MPF.Core.Data;
using MPF.Core.Utilities;
using SabreTools.RedumpLib.Data;

#pragma warning disable IDE0051 // Remove unused private members

namespace MPF.Core.Modules.Redumper
{
    /// <summary>
    /// Represents a generic set of Redumper parameters
    /// </summary>
    /// <remarks>
    /// TODO: Redumper natively supports multiple, chained commands but MPF
    /// doesn't have an easy way of dealing with that right now. Maybe have
    /// BaseCommand be a space-deliminated list of the commands?
    /// TODO: With chained commands, how do we tell what parameters are
    /// actually supported? Do we go one by one and if it doesn't match
    /// any, then it's incorrect?
    /// </remarks>
    public class Parameters : BaseParameters
    {
        #region Generic Dumping Information

        /// <inheritdoc/>
#if NET48
        public override string InputPath => DriveValue;
#else
        public override string? InputPath => DriveValue;
#endif

        /// <inheritdoc/>
#if NET48
        public override string OutputPath => Path.Combine(ImagePathValue?.Trim('"') ?? string.Empty, ImageNameValue?.Trim('"') ?? string.Empty) + GetDefaultExtension(this.Type);
#else
        public override string? OutputPath => Path.Combine(ImagePathValue?.Trim('"') ?? string.Empty, ImageNameValue?.Trim('"') ?? string.Empty) + GetDefaultExtension(this.Type);
#endif

        /// <inheritdoc/>
        public override int? Speed => SpeedValue;

        #endregion

        #region Metadata

        /// <inheritdoc/>
        public override InternalProgram InternalProgram => InternalProgram.Redumper;

        #endregion

        #region Flag Values

        /// <summary>
        /// List of all modes being run
        /// </summary>
#if NET48
        public List<string> ModeValues { get; set; }
#else
        public List<string>? ModeValues { get; set; }
#endif

        #region General

        /// <summary>
        /// Drive to use, first available drive with disc, if not provided
        /// </summary>
#if NET48
        public string DriveValue { get; set; }
#else
        public string? DriveValue { get; set; }
#endif

        /// <summary>
        /// Drive read speed, optimal drive speed will be used if not provided
        /// </summary>
        public int? SpeedValue { get; set; }

        /// <summary>
        /// Number of sector retries in case of SCSI/C2 error (default: 0)
        /// </summary>
        public int? RetriesValue { get; set; }

        /// <summary>
        /// Dump files base directory
        /// </summary>
#if NET48
        public string ImagePathValue { get; set; }
#else
        public string? ImagePathValue { get; set; }
#endif

        /// <summary>
        /// Dump files prefix, autogenerated in dump mode, if not provided
        /// </summary>
#if NET48
        public string ImageNameValue { get; set; }
#else
        public string? ImageNameValue { get; set; }
#endif

        #endregion

        #region Drive Configuration

        /// <summary>
        /// Override drive type, possible values: GENERIC, PLEXTOR, LG_ASUS
        /// </summary>
#if NET48
        public string DriveTypeValue { get; set; }
#else
        public string? DriveTypeValue { get; set; }
#endif

        /// <summary>
        /// Override drive read offset
        /// </summary>
        public int? DriveReadOffsetValue { get; set; }

        /// <summary>
        /// Override drive C2 shift
        /// </summary>
        public int? DriveC2ShiftValue { get; set; }

        /// <summary>
        /// Override drive pre-gap start LBA
        /// </summary>
        public int? DrivePregapStartValue { get; set; }

        /// <summary>
        /// Override drive read method, possible values: BE, D8, BE_CDDA
        /// </summary>
#if NET48
        public string DriveReadMethodValue { get; set; }
#else
        public string? DriveReadMethodValue { get; set; }
#endif

        /// <summary>
        /// Override drive sector order, possible values: DATA_C2_SUB, DATA_SUB_C2
        /// </summary>
#if NET48
        public string DriveSectorOrderValue { get; set; }
#else
        public string? DriveSectorOrderValue { get; set; }
#endif

        #endregion

        #region Offset

        /// <summary>
        /// Override offset autodetection and use supplied value
        /// </summary>
        public int? ForceOffsetValue { get; set; }

        /// <summary>
        /// Maximum absolute sample value to treat it as silence (default: 32)
        /// </summary>
        public int? AudioSilenceThresholdValue { get; set; }

        #endregion

        #region Split

        /// <summary>
        /// Fill byte value for skipped sectors (default: 0x55)
        /// </summary>
        public byte? SkipFillValue { get; set; }

        #endregion

        #region Miscellaneous

        /// <summary>
        /// LBA to start dumping from
        /// </summary>
        public int? LBAStartValue { get; set; }

        /// <summary>
        /// LBA to stop dumping at (everything before the value), useful for discs with fake TOC
        /// </summary>
        public int? LBAEndValue { get; set; }

        /// <summary>
        /// LBA ranges of sectors to skip
        /// </summary>
#if NET48
        public string SkipValue { get; set; }
#else
        public string? SkipValue { get; set; }
#endif

        /// <summary>
        /// Number of sectors to read at once on initial dump, DVD only (Default 32)
        /// </summary>
        public int? DumpReadSizeValue { get; set; }

        /// <summary>
        /// Maximum number of lead-in retries per session (Default 4)
        /// </summary>
        public int? PlextorLeadinRetriesValue { get; set; }

        #endregion

        #endregion

        /// <inheritdoc/>
#if NET48
        public Parameters(string parameters) : base(parameters) { }
#else
        public Parameters(string? parameters) : base(parameters) { }
#endif

        /// <inheritdoc/>
#if NET48
        public Parameters(RedumpSystem? system, MediaType? type, string drivePath, string filename, int? driveSpeed, Options options)
#else
        public Parameters(RedumpSystem? system, MediaType? type, string? drivePath, string filename, int? driveSpeed, Options options)
#endif
            : base(system, type, drivePath, filename, driveSpeed, options)
        {
        }

        #region BaseParameters Implementations

        /// <inheritdoc/>
        public override (bool, List<string>) CheckAllOutputFilesExist(string basePath, bool preCheck)
        {
            var missingFiles = new List<string>();

            switch (this.Type)
            {
                case MediaType.CDROM:
                    if (!File.Exists($"{basePath}.cue"))
                        missingFiles.Add($"{basePath}.cue");
                    if (!File.Exists($"{basePath}.scram") && !File.Exists($"{basePath}.scrap"))
                        missingFiles.Add($"{basePath}.scram");

                    if (!File.Exists($"{basePath}_logs.zip") || !preCheck)
                    {
                        if (!File.Exists($"{basePath}.fulltoc"))
                            missingFiles.Add($"{basePath}.fulltoc");
                        if (!File.Exists($"{basePath}.log"))
                            missingFiles.Add($"{basePath}.log");
                        else if (GetDatfile($"{basePath}.log") == null)
                            missingFiles.Add($"{basePath}.dat");
                        if (!File.Exists($"{basePath}.state"))
                            missingFiles.Add($"{basePath}.state");
                        if (!File.Exists($"{basePath}.subcode"))
                            missingFiles.Add($"{basePath}.subcode");
                        if (!File.Exists($"{basePath}.toc"))
                            missingFiles.Add($"{basePath}.toc");
                    }

                    // Removed or inconsistent files
                    //{
                    //    // Depends on the disc
                    //    if (!File.Exists($"{basePath}.cdtext"))
                    //        missingFiles.Add($"{basePath}.cdtext");
                    //}

                    break;

                case MediaType.DVD:
                    if (!File.Exists($"{basePath}_logs.zip") || !preCheck)
                    {
                        if (!File.Exists($"{basePath}.log"))
                            missingFiles.Add($"{basePath}.log");
                        else if (GetDatfile($"{basePath}.log") == null)
                            missingFiles.Add($"{basePath}.dat");
                        if (!File.Exists($"{basePath}.manufacturer") && !File.Exists($"{basePath}.1.manufacturer") && !File.Exists($"{basePath}.2.manufacturer"))
                            missingFiles.Add($"{basePath}.manufacturer");
                        if (!File.Exists($"{basePath}.physical") && !File.Exists($"{basePath}.physical") && !File.Exists($"{basePath}.1.physical") && !File.Exists($"{basePath}.2.physical"))
                            missingFiles.Add($"{basePath}.physical");
                        if (!File.Exists($"{basePath}.state"))
                            missingFiles.Add($"{basePath}.state");
                    }

                    break;

                default:
                    missingFiles.Add("Media and system combination not supported for Redumper");
                    break;
            }

            return (!missingFiles.Any(), missingFiles);
        }

        /// <inheritdoc/>
#if NET48
        public override void GenerateSubmissionInfo(SubmissionInfo info, Options options, string basePath, Drive drive, bool includeArtifacts)
#else
        public override void GenerateSubmissionInfo(SubmissionInfo info, Options options, string basePath, Drive? drive, bool includeArtifacts)
#endif
        {
            // Ensure that required sections exist
            info = SubmissionInfoTool.EnsureAllSections(info);

            // Get the dumping program and version
#if NET48
            info.DumpingInfo.DumpingProgram = $"{EnumConverter.LongName(this.InternalProgram)} {GetVersion($"{basePath}.log") ?? "Unknown Version"}";
#else
            info.DumpingInfo!.DumpingProgram = $"{EnumConverter.LongName(this.InternalProgram)} {GetVersion($"{basePath}.log") ?? "Unknown Version"}";
#endif
            info.DumpingInfo.DumpingDate = InfoTool.GetFileModifiedDate($"{basePath}.log")?.ToString("yyyy-MM-dd HH:mm:ss");

            // Fill in the hardware data
            if (GetHardwareInfo($"{basePath}.log", out var manufacturer, out var model, out var firmware))
            {
                info.DumpingInfo.Manufacturer = manufacturer;
                info.DumpingInfo.Model = model;
                info.DumpingInfo.Firmware = firmware;
            }

            switch (this.Type)
            {
                case MediaType.CDROM:
#if NET48
                    info.Extras.PVD = GetPVD($"{basePath}.log") ?? "Disc has no PVD";
                    info.TracksAndWriteOffsets.ClrMameProData = GetDatfile($"{basePath}.log");
#else
                    info.Extras!.PVD = GetPVD($"{basePath}.log") ?? "Disc has no PVD";
                    info.TracksAndWriteOffsets!.ClrMameProData = GetDatfile($"{basePath}.log");
#endif
                    info.TracksAndWriteOffsets.Cuesheet = GetFullFile($"{basePath}.cue") ?? string.Empty;

                    // Attempt to get the write offset
                    string cdWriteOffset = GetWriteOffset($"{basePath}.log") ?? string.Empty;
#if NET48
                    info.CommonDiscInfo.RingWriteOffset = cdWriteOffset;
#else
                    info.CommonDiscInfo!.RingWriteOffset = cdWriteOffset;
#endif
                    info.TracksAndWriteOffsets.OtherWriteOffsets = cdWriteOffset;

                    // Attempt to get the error count
                    long errorCount = GetErrorCount($"{basePath}.log");
                    info.CommonDiscInfo.ErrorsCount = (errorCount == -1 ? "Error retrieving error count" : errorCount.ToString());

                    // Attempt to get multisession data
                    string cdMultiSessionInfo = GetMultisessionInformation($"{basePath}.log") ?? string.Empty;
                    if (!string.IsNullOrWhiteSpace(cdMultiSessionInfo))
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.Multisession] = cdMultiSessionInfo;
#else
                        info.CommonDiscInfo.CommentsSpecialFields![SiteCode.Multisession] = cdMultiSessionInfo;
#endif

                    // Attempt to get the universal hash, if it's an audio disc
                    if (this.System.IsAudio())
                    {
                        string universalHash = GetUniversalHash($"{basePath}.log") ?? string.Empty;
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.UniversalHash] = universalHash;
#else
                        info.CommonDiscInfo.CommentsSpecialFields![SiteCode.UniversalHash] = universalHash;
#endif
                    }

                    // Attempt to get the non-zero data start, if it's an audio disc
                    if (this.System.IsAudio())
                    {
                        string ringNonZeroDataStart = GetRingNonZeroDataStart($"{basePath}.log") ?? string.Empty;
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.RingNonZeroDataStart] = ringNonZeroDataStart;
#else
                        info.CommonDiscInfo.CommentsSpecialFields![SiteCode.RingNonZeroDataStart] = ringNonZeroDataStart;
#endif
                    }

                    break;

                case MediaType.DVD:
#if NET48
                    info.Extras.PVD = GetPVD($"{basePath}.log") ?? "Disc has no PVD";
                    info.TracksAndWriteOffsets.ClrMameProData = GetDatfile($"{basePath}.log");
#else
                    info.Extras!.PVD = GetPVD($"{basePath}.log") ?? "Disc has no PVD";
                    info.TracksAndWriteOffsets!.ClrMameProData = GetDatfile($"{basePath}.log");
#endif

                    // Get the individual hash data, as per internal
                    if (InfoTool.GetISOHashValues(info.TracksAndWriteOffsets.ClrMameProData, out long size, out var crc32, out var md5, out var sha1))
                    {
#if NET48
                        info.SizeAndChecksums.Size = size;
#else
                        info.SizeAndChecksums!.Size = size;
#endif
                        info.SizeAndChecksums.CRC32 = crc32;
                        info.SizeAndChecksums.MD5 = md5;
                        info.SizeAndChecksums.SHA1 = sha1;
                    }

                    string layerbreak = GetLayerbreak($"{basePath}.log") ?? string.Empty;
#if NET48
                    info.SizeAndChecksums.Layerbreak = !string.IsNullOrEmpty(layerbreak) ? Int64.Parse(layerbreak) : default;
#else
                    info.SizeAndChecksums!.Layerbreak = !string.IsNullOrEmpty(layerbreak) ? Int64.Parse(layerbreak) : default;
#endif
                    break;
            }

            switch (this.System)
            {
                case RedumpSystem.AppleMacintosh:
                case RedumpSystem.EnhancedCD:
                case RedumpSystem.IBMPCcompatible:
                case RedumpSystem.RainbowDisc:
                case RedumpSystem.SonyElectronicBook:
#if NET48
                    info.CopyProtection.SecuROMData = GetSecuROMData($"{basePath}.log") ?? string.Empty;
#else
                    info.CopyProtection!.SecuROMData = GetSecuROMData($"{basePath}.log") ?? string.Empty;
#endif
                    break;

                case RedumpSystem.DVDAudio:
                case RedumpSystem.DVDVideo:
#if NET48
                    info.CopyProtection.Protection = GetDVDProtection($"{basePath}.log") ?? string.Empty;
#else
                    info.CopyProtection!.Protection = GetDVDProtection($"{basePath}.log") ?? string.Empty;
#endif
                    break;

                case RedumpSystem.KonamiPython2:
                    if (InfoTool.GetPlayStationExecutableInfo(drive?.Name, out var pythonTwoSerial, out Region? pythonTwoRegion, out var pythonTwoDate))
                    {
                        // Ensure internal serial is pulled from local data
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = pythonTwoSerial ?? string.Empty;
#else
                        info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = pythonTwoSerial ?? string.Empty;
#endif
                        info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? pythonTwoRegion;
                        info.CommonDiscInfo.EXEDateBuildDate = pythonTwoDate;
                    }

#if NET48
                    info.VersionAndEditions.Version = InfoTool.GetPlayStation2Version(drive?.Name) ?? string.Empty;
#else
                    info.VersionAndEditions!.Version = InfoTool.GetPlayStation2Version(drive?.Name) ?? string.Empty;
#endif
                    break;

                case RedumpSystem.MicrosoftXbox:
                    // TODO: Support DMI and additional file information when generated
                    break;

                case RedumpSystem.MicrosoftXbox360:
                    // TODO: Support DMI and additional file information when generated
                    break;

                case RedumpSystem.NamcoSegaNintendoTriforce:
                    // TODO: Support header information and GD-ROM info when generated
                    break;

                case RedumpSystem.SegaMegaCDSegaCD:
#if NET48
                    info.Extras.Header = GetSegaCDHeader($"{basePath}.log", out var scdBuildDate, out var scdSerial, out _) ?? string.Empty;
                    info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = scdSerial ?? string.Empty;
#else
                    info.Extras!.Header = GetSegaCDHeader($"{basePath}.log", out var scdBuildDate, out var scdSerial, out _) ?? string.Empty;
                    info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = scdSerial ?? string.Empty;
#endif
                    info.CommonDiscInfo.EXEDateBuildDate = scdBuildDate ?? string.Empty;
                    // TODO: Support region setting from parsed value
                    break;

                case RedumpSystem.SegaChihiro:
                    // TODO: Support header information and GD-ROM info when generated
                    break;

                case RedumpSystem.SegaDreamcast:
                    // TODO: Support header information and GD-ROM info when generated
                    break;

                case RedumpSystem.SegaNaomi:
                    // TODO: Support header information and GD-ROM info when generated
                    break;

                case RedumpSystem.SegaNaomi2:
                    // TODO: Support header information and GD-ROM info when generated
                    break;

                case RedumpSystem.SegaSaturn:
#if NET48
                    info.Extras.Header = GetSaturnHeader($"{basePath}.log") ?? string.Empty;
#else
                    info.Extras!.Header = GetSaturnHeader($"{basePath}.log") ?? string.Empty;
#endif

                    // Take only the first 16 lines for Saturn
                    if (!string.IsNullOrEmpty(info.Extras.Header))
                        info.Extras.Header = string.Join("\n", info.Extras.Header.Split('\n').Take(16));

                    if (GetSaturnBuildInfo(info.Extras.Header, out var saturnSerial, out var saturnVersion, out var buildDate))
                    {
                        // Ensure internal serial is pulled from local data
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = saturnSerial ?? string.Empty;
                        info.VersionAndEditions.Version = saturnVersion ?? string.Empty;
#else
                        info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = saturnSerial ?? string.Empty;
                        info.VersionAndEditions!.Version = saturnVersion ?? string.Empty;
#endif
                        info.CommonDiscInfo.EXEDateBuildDate = buildDate ?? string.Empty;
                    }

                    break;

                case RedumpSystem.SonyPlayStation:
                    if (InfoTool.GetPlayStationExecutableInfo(drive?.Name, out var playstationSerial, out Region? playstationRegion, out var playstationDate))
                    {
                        // Ensure internal serial is pulled from local data
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = playstationSerial ?? string.Empty;
#else
                        info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = playstationSerial ?? string.Empty;
#endif
                        info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? playstationRegion;
                        info.CommonDiscInfo.EXEDateBuildDate = playstationDate;
                    }

#if NET48
                    info.CopyProtection.AntiModchip = GetPlayStationAntiModchipDetected($"{basePath}.log").ToYesNo();
                    info.EDC.EDC = GetPlayStationEDCStatus($"{basePath}.log").ToYesNo();
#else
                    info.CopyProtection!.AntiModchip = GetPlayStationAntiModchipDetected($"{basePath}.log").ToYesNo();
                    info.EDC!.EDC = GetPlayStationEDCStatus($"{basePath}.log").ToYesNo();
#endif
                    info.CopyProtection.LibCrypt = GetPlayStationLibCryptStatus($"{basePath}.log").ToYesNo();
                    info.CopyProtection.LibCryptData = GetPlayStationLibCryptData($"{basePath}.log");
                    break;

                case RedumpSystem.SonyPlayStation2:
                    if (InfoTool.GetPlayStationExecutableInfo(drive?.Name, out var playstationTwoSerial, out Region? playstationTwoRegion, out var playstationTwoDate))
                    {
                        // Ensure internal serial is pulled from local data
#if NET48
                        info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = playstationTwoSerial ?? string.Empty;
#else
                        info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = playstationTwoSerial ?? string.Empty;
#endif
                        info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? playstationTwoRegion;
                        info.CommonDiscInfo.EXEDateBuildDate = playstationTwoDate;
                    }

#if NET48
                    info.VersionAndEditions.Version = InfoTool.GetPlayStation2Version(drive?.Name) ?? string.Empty;
#else
                    info.VersionAndEditions!.Version = InfoTool.GetPlayStation2Version(drive?.Name) ?? string.Empty;
#endif
                    break;

                case RedumpSystem.SonyPlayStation3:
#if NET48
                    info.VersionAndEditions.Version = InfoTool.GetPlayStation3Version(drive?.Name) ?? string.Empty;
                    info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = InfoTool.GetPlayStation3Serial(drive?.Name) ?? string.Empty;
#else
                    info.VersionAndEditions!.Version = InfoTool.GetPlayStation3Version(drive?.Name) ?? string.Empty;
                    info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = InfoTool.GetPlayStation3Serial(drive?.Name) ?? string.Empty;
#endif
                    break;

                case RedumpSystem.SonyPlayStation4:
#if NET48
                    info.VersionAndEditions.Version = InfoTool.GetPlayStation4Version(drive?.Name) ?? string.Empty;
                    info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = InfoTool.GetPlayStation4Serial(drive?.Name) ?? string.Empty;
#else
                    info.VersionAndEditions!.Version = InfoTool.GetPlayStation4Version(drive?.Name) ?? string.Empty;
                    info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = InfoTool.GetPlayStation4Serial(drive?.Name) ?? string.Empty;
#endif
                    break;

                case RedumpSystem.SonyPlayStation5:
#if NET48
                    info.VersionAndEditions.Version = InfoTool.GetPlayStation5Version(drive?.Name) ?? string.Empty;
                    info.CommonDiscInfo.CommentsSpecialFields[SiteCode.InternalSerialName] = InfoTool.GetPlayStation5Serial(drive?.Name) ?? string.Empty;
#else
                    info.VersionAndEditions!.Version = InfoTool.GetPlayStation5Version(drive?.Name) ?? string.Empty;
                    info.CommonDiscInfo!.CommentsSpecialFields![SiteCode.InternalSerialName] = InfoTool.GetPlayStation5Serial(drive?.Name) ?? string.Empty;
#endif
                    break;
            }

            // Fill in any artifacts that exist, Base64-encoded, if we need to
            if (includeArtifacts)
            {
#if NET48
                if (info.Artifacts == null) info.Artifacts = new Dictionary<string, string>();
#else
                info.Artifacts ??= new Dictionary<string, string>();
#endif

                if (File.Exists($"{basePath}.cdtext"))
                    info.Artifacts["cdtext"] = GetBase64(GetFullFile($"{basePath}.cdtext")) ?? string.Empty;
                if (File.Exists($"{basePath}.cue"))
                    info.Artifacts["cue"] = GetBase64(GetFullFile($"{basePath}.cue")) ?? string.Empty;
                if (File.Exists($"{basePath}.fulltoc"))
                    info.Artifacts["fulltoc"] = GetBase64(GetFullFile($"{basePath}.fulltoc")) ?? string.Empty;
                if (File.Exists($"{basePath}.log"))
                    info.Artifacts["log"] = GetBase64(GetFullFile($"{basePath}.log")) ?? string.Empty;
                if (File.Exists($"{basePath}.manufacturer"))
                    info.Artifacts["manufacturer"] = GetBase64(GetFullFile($"{basePath}.manufacturer")) ?? string.Empty;
                if (File.Exists($"{basePath}.1.manufacturer"))
                    info.Artifacts["manufacturer1"] = GetBase64(GetFullFile($"{basePath}.1.manufacturer")) ?? string.Empty;
                if (File.Exists($"{basePath}.2.manufacturer"))
                    info.Artifacts["manufacturer2"] = GetBase64(GetFullFile($"{basePath}.2.manufacturer")) ?? string.Empty;
                if (File.Exists($"{basePath}.physical"))
                    info.Artifacts["physical"] = GetBase64(GetFullFile($"{basePath}.physical")) ?? string.Empty;
                if (File.Exists($"{basePath}.1.physical"))
                    info.Artifacts["physical1"] = GetBase64(GetFullFile($"{basePath}.1.physical")) ?? string.Empty;
                if (File.Exists($"{basePath}.2.physical"))
                    info.Artifacts["physical2"] = GetBase64(GetFullFile($"{basePath}.2.physical")) ?? string.Empty;
                // if (File.Exists($"{basePath}.scram"))
                //     info.Artifacts["scram"] = GetBase64(GetFullFile($"{basePath}.scram")) ?? string.Empty;
                // if (File.Exists($"{basePath}.scrap"))
                //     info.Artifacts["scrap"] = GetBase64(GetFullFile($"{basePath}.scrap")) ?? string.Empty;
                if (File.Exists($"{basePath}.state"))
                    info.Artifacts["state"] = GetBase64(GetFullFile($"{basePath}.state")) ?? string.Empty;
                if (File.Exists($"{basePath}.subcode"))
                    info.Artifacts["subcode"] = GetBase64(GetFullFile($"{basePath}.subcode")) ?? string.Empty;
                if (File.Exists($"{basePath}.toc"))
                    info.Artifacts["toc"] = GetBase64(GetFullFile($"{basePath}.toc")) ?? string.Empty;
            }
        }

        /// <inheritdoc/>
        /// <remarks>
        /// Redumper is unique in that the base command can be multiple
        /// modes all listed together. It is also unique in that "all
        /// flags are supported for everything" and it filters out internally
        /// </remarks>
        public override string GenerateParameters()
        {
            var parameters = new List<string>();

#if NET48
            if (ModeValues == null)
                ModeValues = new List<string> { CommandStrings.NONE };
#else
            ModeValues ??= new List<string> { CommandStrings.NONE };
#endif

            // Modes
            parameters.AddRange(ModeValues);

            #region General

            // Help
            if (this[FlagStrings.HelpLong] == true)
                parameters.Add(FlagStrings.HelpLong);

            // Verbose
            if (this[FlagStrings.Verbose] == true)
                parameters.Add(FlagStrings.Verbose);

            // Debug
            if (this[FlagStrings.Debug] == true)
                parameters.Add(FlagStrings.Debug);

            // Drive
            if (this[FlagStrings.Drive] == true)
            {
                if (DriveValue != null)
                    parameters.Add($"{FlagStrings.Drive}={DriveValue}");
            }

            // Speed
            if (this[FlagStrings.Speed] == true)
            {
                if (SpeedValue != null)
                    parameters.Add($"{FlagStrings.Speed}={SpeedValue}");
            }

            // Retries
            if (this[FlagStrings.Retries] == true)
            {
                if (RetriesValue != null)
                    parameters.Add($"{FlagStrings.Retries}={RetriesValue}");
            }

            // Image Path
            if (this[FlagStrings.ImagePath] == true)
            {
                if (ImagePathValue != null)
                    parameters.Add($"{FlagStrings.ImagePath}={ImagePathValue}");
            }

            // Image Name
            if (this[FlagStrings.ImageName] == true)
            {
                if (ImageNameValue != null)
                    parameters.Add($"{FlagStrings.ImageName}={ImageNameValue}");
            }

            // Overwrite
            if (this[FlagStrings.Overwrite] == true)
                parameters.Add(FlagStrings.Overwrite);

            #endregion

            #region Drive Configuration

            // Drive Type
            if (this[FlagStrings.DriveType] == true)
            {
                if (DriveTypeValue != null)
                    parameters.Add($"{FlagStrings.DriveType}={DriveTypeValue}");
            }

            // Drive Read Offset
            if (this[FlagStrings.DriveReadOffset] == true)
            {
                if (DriveReadOffsetValue != null)
                    parameters.Add($"{FlagStrings.DriveReadOffset}={DriveReadOffsetValue}");
            }

            // Drive C2 Shift
            if (this[FlagStrings.DriveC2Shift] == true)
            {
                if (DriveC2ShiftValue != null)
                    parameters.Add($"{FlagStrings.DriveC2Shift}={DriveC2ShiftValue}");
            }

            // Drive Pregap Start
            if (this[FlagStrings.DrivePregapStart] == true)
            {
                if (DrivePregapStartValue != null)
                    parameters.Add($"{FlagStrings.DrivePregapStart}={DrivePregapStartValue}");
            }

            // Drive Read Method
            if (this[FlagStrings.DriveReadMethod] == true)
            {
                if (DriveReadMethodValue != null)
                    parameters.Add($"{FlagStrings.DriveReadMethod}={DriveReadMethodValue}");
            }

            // Drive Sector Order
            if (this[FlagStrings.DriveSectorOrder] == true)
            {
                if (DriveSectorOrderValue != null)
                    parameters.Add($"{FlagStrings.DriveSectorOrder}={DriveSectorOrderValue}");
            }

            #endregion

            #region Drive Specific

            // Plextor Leadin Skip
            if (this[FlagStrings.PlextorLeadinSkip] == true)
                parameters.Add(FlagStrings.PlextorLeadinSkip);

            // Plextor Leadin Retries
            if (this[FlagStrings.PlextorLeadinRetries] == true)
            {
                if (PlextorLeadinRetriesValue != null)
                    parameters.Add($"{FlagStrings.PlextorLeadinRetries}={PlextorLeadinRetriesValue}");
            }

            // Asus Skip Leadout
            if (this[FlagStrings.AsusSkipLeadout] == true)
                parameters.Add(FlagStrings.AsusSkipLeadout);

            #endregion

            #region Offset

            // Force Offset
            if (this[FlagStrings.ForceOffset] == true)
            {
                if (ForceOffsetValue != null)
                    parameters.Add($"{FlagStrings.ForceOffset}={ForceOffsetValue}");
            }

            // Audio Silence Threshold
            if (this[FlagStrings.AudioSilenceThreshold] == true)
            {
                if (AudioSilenceThresholdValue != null)
                    parameters.Add($"{FlagStrings.AudioSilenceThreshold}={AudioSilenceThresholdValue}");
            }

            // Correct Offset Shift
            if (this[FlagStrings.CorrectOffsetShift] == true)
                parameters.Add(FlagStrings.CorrectOffsetShift);

            // Offset Shift Relocate
            if (this[FlagStrings.OffsetShiftRelocate] == true)
                parameters.Add(FlagStrings.OffsetShiftRelocate);

            #endregion

            #region Split

            // Force Split
            if (this[FlagStrings.ForceSplit] == true)
                parameters.Add(FlagStrings.ForceSplit);

            // Leave Unchanged
            if (this[FlagStrings.LeaveUnchanged] == true)
                parameters.Add(FlagStrings.LeaveUnchanged);

            // Force QTOC
            if (this[FlagStrings.ForceQTOC] == true)
                parameters.Add(FlagStrings.ForceQTOC);

            // Skip Fill
            if (this[FlagStrings.SkipFill] == true)
            {
                if (SkipFillValue != null)
                    parameters.Add($"{FlagStrings.SkipFill}={SkipFillValue:x}");
            }

            // ISO9660 Trim
            if (this[FlagStrings.ISO9660Trim] == true)
                parameters.Add(FlagStrings.ISO9660Trim);

            #endregion

            #region Miscellaneous

            // LBA Start
            if (this[FlagStrings.LBAStart] == true)
            {
                if (LBAStartValue != null)
                    parameters.Add($"{FlagStrings.LBAStart}={LBAStartValue}");
            }

            // LBA End
            if (this[FlagStrings.LBAEnd] == true)
            {
                if (LBAEndValue != null)
                    parameters.Add($"{FlagStrings.LBAEnd}={LBAEndValue}");
            }

            // Refine Subchannel
            if (this[FlagStrings.RefineSubchannel] == true)
                parameters.Add(FlagStrings.RefineSubchannel);

            // Skip
            if (this[FlagStrings.Skip] == true)
            {
                if (!string.IsNullOrWhiteSpace(SkipValue))
                    parameters.Add($"{FlagStrings.Skip}={SkipValue}");
            }

            // Dump Read Size
            if (this[FlagStrings.DumpReadSize] == true)
            {
                if (DumpReadSizeValue != null && DumpReadSizeValue > 0)
                    parameters.Add($"{FlagStrings.DumpReadSize}={DumpReadSizeValue}");
            }

            // Overread Leadout
            if (this[FlagStrings.OverreadLeadout] == true)
                parameters.Add(FlagStrings.OverreadLeadout);

            #endregion

            return string.Join(" ", parameters);
        }

        /// <inheritdoc/>
        /// <remarks>Command support is irrelevant for redumper</remarks>
        public override Dictionary<string, List<string>> GetCommandSupport()
        {
            return new Dictionary<string, List<string>>()
            {
                [CommandStrings.NONE] = new List<string>
                {
                    // General
                    FlagStrings.HelpLong,
                    FlagStrings.HelpShort,
                    FlagStrings.Verbose,
                    FlagStrings.Debug,
                    FlagStrings.Drive,
                    FlagStrings.Speed,
                    FlagStrings.Retries,
                    FlagStrings.ImagePath,
                    FlagStrings.ImageName,
                    FlagStrings.Overwrite,

                    // Drive Configuration
                    FlagStrings.DriveType,
                    FlagStrings.DriveReadOffset,
                    FlagStrings.DriveC2Shift,
                    FlagStrings.DrivePregapStart,
                    FlagStrings.DriveReadMethod,
                    FlagStrings.DriveSectorOrder,

                    // Drive Specific
                    FlagStrings.PlextorLeadinSkip,
                    FlagStrings.PlextorLeadinRetries,
                    FlagStrings.AsusSkipLeadout,

                    // Offset
                    FlagStrings.ForceOffset,
                    FlagStrings.AudioSilenceThreshold,
                    FlagStrings.CorrectOffsetShift,
                    FlagStrings.OffsetShiftRelocate,

                    // Split
                    FlagStrings.ForceSplit,
                    FlagStrings.LeaveUnchanged,
                    FlagStrings.ForceQTOC,
                    FlagStrings.SkipFill,
                    FlagStrings.ISO9660Trim,

                    // Miscellaneous
                    FlagStrings.LBAStart,
                    FlagStrings.LBAEnd,
                    FlagStrings.RefineSubchannel,
                    FlagStrings.Skip,
                    FlagStrings.DumpReadSize,
                    FlagStrings.OverreadLeadout,
                },
            };
        }

        /// <inheritdoc/>
#if NET48
        public override string GetDefaultExtension(MediaType? mediaType) => Converters.Extension(mediaType);
#else
        public override string? GetDefaultExtension(MediaType? mediaType) => Converters.Extension(mediaType);
#endif

        /// <inheritdoc/>
        public override List<string> GetDeleteableFilePaths(string basePath)
        {
            var deleteableFiles = new List<string>();

            if (File.Exists($"{basePath}.scram"))
                deleteableFiles.Add($"{basePath}.scram");
            if (File.Exists($"{basePath}.scrap"))
                deleteableFiles.Add($"{basePath}.scrap");

            return deleteableFiles;
        }

        /// <inheritdoc/>
        public override List<string> GetLogFilePaths(string basePath)
        {
            var logFiles = new List<string>();

            switch (this.Type)
            {
                case MediaType.CDROM:
                    if (File.Exists($"{basePath}.cdtext"))
                        logFiles.Add($"{basePath}.cdtext");
                    if (File.Exists($"{basePath}.fulltoc"))
                        logFiles.Add($"{basePath}.fulltoc");
                    if (File.Exists($"{basePath}.log"))
                        logFiles.Add($"{basePath}.log");
                    if (File.Exists($"{basePath}.state"))
                        logFiles.Add($"{basePath}.state");
                    if (File.Exists($"{basePath}.subcode"))
                        logFiles.Add($"{basePath}.subcode");
                    if (File.Exists($"{basePath}.toc"))
                        logFiles.Add($"{basePath}.toc");
                    break;

                case MediaType.DVD:
                    if (File.Exists($"{basePath}.log"))
                        logFiles.Add($"{basePath}.log");
                    if (File.Exists($"{basePath}.manufacturer"))
                        logFiles.Add($"{basePath}.manufacturer");
                    if (File.Exists($"{basePath}.1.manufacturer"))
                        logFiles.Add($"{basePath}.1.manufacturer");
                    if (File.Exists($"{basePath}.2.manufacturer"))
                        logFiles.Add($"{basePath}.2.manufacturer");
                    if (File.Exists($"{basePath}.physical"))
                        logFiles.Add($"{basePath}.physical");
                    if (File.Exists($"{basePath}.1.physical"))
                        logFiles.Add($"{basePath}.1.physical");
                    if (File.Exists($"{basePath}.2.physical"))
                        logFiles.Add($"{basePath}.2.physical");
                    if (File.Exists($"{basePath}.state"))
                        logFiles.Add($"{basePath}.state");
                    break;
            }

            return logFiles;
        }

        /// <inheritdoc/>
        public override bool IsDumpingCommand()
        {
            return this.BaseCommand == CommandStrings.NONE
                || this.BaseCommand?.Contains(CommandStrings.CD) == true
                || this.BaseCommand?.Contains(CommandStrings.DVD) == true
                || this.BaseCommand?.Contains(CommandStrings.BluRay) == true
                || this.BaseCommand?.Contains(CommandStrings.SACD) == true
                || this.BaseCommand?.Contains(CommandStrings.Dump) == true;
        }

        /// <inheritdoc/>
        protected override void ResetValues()
        {
            BaseCommand = CommandStrings.NONE;

            flags = new Dictionary<string, bool?>();

            // General
            DriveValue = null;
            SpeedValue = null;
            RetriesValue = null;
            ImagePathValue = null;
            ImageNameValue = null;

            // Drive Configuration
            DriveTypeValue = null;
            DriveReadOffsetValue = null;
            DriveC2ShiftValue = null;
            DrivePregapStartValue = null;
            DriveReadMethodValue = null;
            DriveSectorOrderValue = null;

            // Offset
            ForceOffsetValue = null;
            AudioSilenceThresholdValue = null;

            // Split
            SkipFillValue = null;

            // Miscellaneous
            LBAStartValue = null;
            LBAEndValue = null;
            SkipValue = null;
            DumpReadSizeValue = null;
        }

        /// <inheritdoc/>
#if NET48
        protected override void SetDefaultParameters(string drivePath, string filename, int? driveSpeed, Options options)
#else
        protected override void SetDefaultParameters(string? drivePath, string filename, int? driveSpeed, Options options)
#endif
        {
            // If we don't have a CD or DVD, we can't dump using redumper
            if (this.Type != MediaType.CDROM && this.Type != MediaType.DVD)
                return;

            BaseCommand = CommandStrings.NONE;
            switch (this.Type)
            {
                case MediaType.CDROM:
#if NET48
                    switch (this.System)
                    {
                        case RedumpSystem.SuperAudioCD:
                            ModeValues = new List<string> { CommandStrings.SACD };
                            break;
                        default:
                            ModeValues = new List<string> { CommandStrings.CD };
                            break;
                    }
#else
                    ModeValues = this.System switch
                    {
                        RedumpSystem.SuperAudioCD => new List<string> { CommandStrings.SACD },
                        _ => new List<string> { CommandStrings.CD },
                    };
#endif
                    break;
                case MediaType.DVD:
                    ModeValues = new List<string> { CommandStrings.DVD };
                    break;
                case MediaType.BluRay:
                    ModeValues = new List<string> { CommandStrings.BluRay };
                    break;
                default:
                    BaseCommand = null;
                    return;
            }

            this[FlagStrings.Drive] = true;
            DriveValue = drivePath;

            this[FlagStrings.Speed] = true;
            SpeedValue = driveSpeed;

            // Set user-defined options
            if (options.RedumperEnableVerbose)
                this[FlagStrings.Verbose] = options.RedumperEnableVerbose;
            if (options.RedumperEnableDebug)
                this[FlagStrings.Debug] = options.RedumperEnableDebug;
            if (options.RedumperUseBEReading)
            {
                this[FlagStrings.DriveReadMethod] = true;
                DriveReadMethodValue = "BE_CDDA";
            }
            if (options.RedumperUseGenericDriveType)
            {
                this[FlagStrings.DriveType] = true;
                DriveTypeValue = "Generic";
            }

            // Set the output paths
            if (!string.IsNullOrWhiteSpace(filename))
            {
                var imagePath = Path.GetDirectoryName(filename);
                if (!string.IsNullOrWhiteSpace(imagePath))
                {
                    this[FlagStrings.ImagePath] = true;
                    ImagePathValue = $"\"{imagePath}\"";
                }

                string imageName = Path.GetFileNameWithoutExtension(filename);
                if (!string.IsNullOrWhiteSpace(imageName))
                {
                    this[FlagStrings.ImageName] = true;
                    ImageNameValue = $"\"{imageName}\"";
                }
            }

            this[FlagStrings.Retries] = true;
            RetriesValue = options.RedumperRereadCount;
        }

        /// <inheritdoc/>
#if NET48
        protected override bool ValidateAndSetParameters(string parameters)
#else
        protected override bool ValidateAndSetParameters(string? parameters)
#endif
        {
            BaseCommand = CommandStrings.NONE;

            // The string has to be valid by itself first
            if (string.IsNullOrWhiteSpace(parameters))
                return false;

            // Now split the string into parts for easier validation
            // https://stackoverflow.com/questions/14655023/split-a-string-that-has-white-spaces-unless-they-are-enclosed-within-quotes
            parameters = parameters.Trim();
            List<string> parts = Regex.Matches(parameters, @"([a-zA-Z\-]*=)?[\""].+?[\""]|[^ ]+")
                .Cast<Match>()
                .Select(m => m.Value)
                .ToList();

            // Setup the modes
            ModeValues = new List<string>();

            // All modes should be cached separately
            int index = 0;
            for (; index < parts.Count; index++)
            {
                // Flag to see if we have a flag
                bool isFlag = false;

                string part = parts[index];
                switch (part)
                {
                    case CommandStrings.CD:
                    case CommandStrings.DVD:
                    case CommandStrings.BluRay:
                    case CommandStrings.SACD:
                    case CommandStrings.Dump:
                    case CommandStrings.Protection:
                    case CommandStrings.Refine:
                    case CommandStrings.Split:
                    case CommandStrings.Verify:
                    case CommandStrings.DVDKey:
                    case CommandStrings.DVDIsoKey:
                    case CommandStrings.Info:
                        ModeValues.Add(part);
                        break;

                    // Default is either a flag or an invalid mode
                    default:
                        if (part.StartsWith("-"))
                        {
                            isFlag = true;
                            break;
                        }
                        else
                        {
                            return false;
                        }
                }

                // If we had a flag, break out
                if (isFlag)
                    break;
            }

            // Loop through all auxiliary flags, if necessary
            for (int i = index; i < parts.Count; i++)
            {
                // Flag read-out values
                byte? byteValue = null;
                int? intValue = null;
#if NET48
                string stringValue = null;
#else
                string? stringValue = null;
#endif

                #region General

                // Help
                ProcessFlagParameter(parts, FlagStrings.HelpShort, FlagStrings.HelpLong, ref i);

                // Verbose
                ProcessFlagParameter(parts, FlagStrings.Verbose, ref i);

                // Debug
                ProcessFlagParameter(parts, FlagStrings.Debug, ref i);

                // Drive
                stringValue = ProcessStringParameter(parts, FlagStrings.Drive, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    DriveValue = stringValue;

                // Speed
                intValue = ProcessInt32Parameter(parts, FlagStrings.Speed, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    SpeedValue = intValue;

                // Retries
                intValue = ProcessInt32Parameter(parts, FlagStrings.Retries, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    RetriesValue = intValue;

                // Image Path
                stringValue = ProcessStringParameter(parts, FlagStrings.ImagePath, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    ImagePathValue = $"\"{stringValue.Trim('"')}\"";

                // Image Name
                stringValue = ProcessStringParameter(parts, FlagStrings.ImageName, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    ImageNameValue = $"\"{stringValue.Trim('"')}\"";

                // Overwrite
                ProcessFlagParameter(parts, FlagStrings.Overwrite, ref i);

                #endregion

                #region Drive Configuration

                // Drive Type
                stringValue = ProcessStringParameter(parts, FlagStrings.DriveType, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    DriveTypeValue = stringValue;

                // Drive Read Offset
                intValue = ProcessInt32Parameter(parts, FlagStrings.DriveReadOffset, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    DriveReadOffsetValue = intValue;

                // Drive C2 Shift
                intValue = ProcessInt32Parameter(parts, FlagStrings.DriveC2Shift, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    DriveC2ShiftValue = intValue;

                // Drive Pregap Start
                intValue = ProcessInt32Parameter(parts, FlagStrings.DrivePregapStart, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    DrivePregapStartValue = intValue;

                // Drive Read Method
                stringValue = ProcessStringParameter(parts, FlagStrings.DriveReadMethod, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    DriveReadMethodValue = stringValue;

                // Drive Sector Order
                stringValue = ProcessStringParameter(parts, FlagStrings.DriveSectorOrder, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    DriveSectorOrderValue = stringValue;

                #endregion

                #region Drive Specific

                // Plextor Leadin Skip
                ProcessFlagParameter(parts, FlagStrings.PlextorLeadinSkip, ref i);

                // Plextor Leadin Retries
                intValue = ProcessInt32Parameter(parts, FlagStrings.PlextorLeadinRetries, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    PlextorLeadinRetriesValue = intValue;

                // Asus Skip Leadout
                ProcessFlagParameter(parts, FlagStrings.AsusSkipLeadout, ref i);

                #endregion

                #region Offset

                // Force Offset
                intValue = ProcessInt32Parameter(parts, FlagStrings.ForceOffset, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    ForceOffsetValue = intValue;

                // Audio Silence Threshold
                intValue = ProcessInt32Parameter(parts, FlagStrings.AudioSilenceThreshold, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    AudioSilenceThresholdValue = intValue;

                // Correct Offset Shift
                ProcessFlagParameter(parts, FlagStrings.CorrectOffsetShift, ref i);

                // Correct Shift Relocate
                ProcessFlagParameter(parts, FlagStrings.OffsetShiftRelocate, ref i);

                #endregion

                #region Split

                // Force Split
                ProcessFlagParameter(parts, FlagStrings.ForceSplit, ref i);

                // Leave Unchanged
                ProcessFlagParameter(parts, FlagStrings.LeaveUnchanged, ref i);

                // Force QTOC
                ProcessFlagParameter(parts, FlagStrings.ForceQTOC, ref i);

                // Skip Fill
                byteValue = ProcessUInt8Parameter(parts, FlagStrings.SkipFill, ref i);
                if (byteValue != null && byteValue != Byte.MinValue)
                    SkipFillValue = byteValue;

                // ISO9660 Trim
                ProcessFlagParameter(parts, FlagStrings.ISO9660Trim, ref i);

                #endregion

                #region Miscellaneous

                // LBA Start
                intValue = ProcessInt32Parameter(parts, FlagStrings.LBAStart, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    LBAStartValue = intValue;

                // LBA End
                intValue = ProcessInt32Parameter(parts, FlagStrings.LBAEnd, ref i);
                if (intValue != null && intValue != Int32.MinValue)
                    LBAEndValue = intValue;

                // Refine Subchannel
                ProcessFlagParameter(parts, FlagStrings.RefineSubchannel, ref i);

                // Skip
                stringValue = ProcessStringParameter(parts, FlagStrings.Skip, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    SkipValue = stringValue;

                // Skip
                intValue = ProcessInt32Parameter(parts, FlagStrings.DumpReadSize, ref i);
                if (!string.IsNullOrWhiteSpace(stringValue))
                    DumpReadSizeValue = intValue;

                // Overread Leadout
                ProcessFlagParameter(parts, FlagStrings.OverreadLeadout, ref i);

                #endregion
            }

            // If the image name was not set, set it with a default value
            if (string.IsNullOrWhiteSpace(this.ImageNameValue))
                this.ImageNameValue = "track";

            return true;
        }

        #endregion

        #region Information Extraction Methods

        /// <summary>
        /// Get the cuesheet from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Newline-delimited cuesheet if possible, null on error</returns>
#if NET48
        private static string GetCuesheet(string log)
#else
        private static string? GetCuesheet(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the dat line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("CUE [") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    // Now that we're at the relevant entries, read each line in and concatenate
#if NET48
                    string cueString = string.Empty, line = sr.ReadLine()?.Trim();
#else
                    string? cueString = string.Empty, line = sr.ReadLine()?.Trim();
#endif
                    while (!string.IsNullOrWhiteSpace(line))
                    {
                        cueString += line + "\n";
                        line = sr.ReadLine()?.Trim();
                    }

                    return cueString.TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the datfile from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Newline-delimited datfile if possible, null on error</returns>
#if NET48
        private static string GetDatfile(string log)
#else
        private static string? GetDatfile(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the dat line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("dat:") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    // Now that we're at the relevant entries, read each line in and concatenate
                    var datString = string.Empty;
                    var line = sr.ReadLine()?.Trim();
                    while (line?.StartsWith("<rom") == true)
                    {
                        datString += line + "\n";
                        if (sr.EndOfStream)
                            break;

                        line = sr.ReadLine()?.Trim();
                    }

                    return datString.TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the DVD protection information, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Formatted string representing the DVD protection, null on error</returns>
#if NET48
        private static string GetDVDProtection(string log)
#else
        private static string? GetDVDProtection(string log)
#endif
        {
            // If one of the files doesn't exist, we can't get info from them
            if (!File.Exists(log))
                return null;

            // Setup all of the individual pieces
#if NET48
            string region = null, rceProtection = null, copyrightProtectionSystemType = null, vobKeys = null, decryptedDiscKey = null;
#else
            string? region = null, rceProtection = null, copyrightProtectionSystemType = null, vobKeys = null, decryptedDiscKey = null;
#endif
            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the copyright information
                    while (sr.ReadLine()?.Trim().StartsWith("copyright:") == false) ;

                    // Now read until we hit the manufacturing information
                    var line = sr.ReadLine()?.Trim();
                    while (line != null && !sr.EndOfStream)
                    {
                        if (line.StartsWith("protection system type"))
                        {
#if NET48
                            copyrightProtectionSystemType = line.Substring("protection system type: ".Length);
#else
                            copyrightProtectionSystemType = line["protection system type: ".Length..];
#endif
                            if (copyrightProtectionSystemType == "none" || copyrightProtectionSystemType == "<none>")
                                copyrightProtectionSystemType = "No";
                        }
                        else if (line.StartsWith("region management information:"))
                        {
#if NET48
                            region = line.Substring("region management information: ".Length);
#else
                            region = line["region management information: ".Length..];
#endif
                        }
                        else if (line.StartsWith("disc key"))
                        {
#if NET48
                            decryptedDiscKey = line.Substring("disc key: ".Length).Replace(':', ' ');
#else
                            decryptedDiscKey = line["disc key: ".Length..].Replace(':', ' ');
#endif
                        }
                        else if (line.StartsWith("title keys"))
                        {
                            vobKeys = string.Empty;

                            line = sr.ReadLine()?.Trim();
                            while (!string.IsNullOrWhiteSpace(line))
                            {
                                var match = Regex.Match(line, @"^(.*?): (.*?)$");
                                if (match.Success)
                                {
                                    string normalizedKey = match.Groups[2].Value.Replace(':', ' ');
                                    if (normalizedKey == "none" || normalizedKey == "<none>")
                                        normalizedKey = "No Title Key";
                                    else if (normalizedKey == "<error>")
                                        normalizedKey = "Error Retrieving Title Key";

                                    vobKeys += $"{match.Groups[1].Value} Title Key: {match.Groups[2].Value.Replace(':', ' ')}\n";
                                }
                                else
                                {
                                    break;
                                }

                                line = sr.ReadLine()?.Trim();
                            }
                        }
                        else
                        {
                            break;
                        }

                        line = sr.ReadLine()?.Trim();
                    }
                }
                catch { }
            }

            // Now we format everything we can
            string protection = string.Empty;
            if (!string.IsNullOrEmpty(region))
                protection += $"Region: {region}\n";
            if (!string.IsNullOrEmpty(rceProtection))
                protection += $"RCE Protection: {rceProtection}\n";
            if (!string.IsNullOrEmpty(copyrightProtectionSystemType))
                protection += $"Copyright Protection System Type: {copyrightProtectionSystemType}\n";
            if (!string.IsNullOrEmpty(vobKeys))
                protection += vobKeys;
            if (!string.IsNullOrEmpty(decryptedDiscKey))
                protection += $"Decrypted Disc Key: {decryptedDiscKey}\n";

            return protection;
        }

        /// <summary>
        /// Get the detected error count from the input files, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Error count if possible, -1 on error</returns>
        public static long GetErrorCount(string log)
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return -1;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the errors lines
                    while (!sr.EndOfStream && sr.ReadLine()?.Trim()?.StartsWith("CD-ROM [") == false) ;
                    if (sr.EndOfStream)
                        return 0;

                    // Now that we're at the relevant lines, find the error count
                    while (!sr.EndOfStream)
                    {
                        // Skip forward to the "REDUMP.ORG" line
                        var line = string.Empty;
                        while (!sr.EndOfStream && (line = sr.ReadLine()?.Trim())?.StartsWith("REDUMP.ORG errors") == false) ;
                        if (string.IsNullOrEmpty(line))
                            break;

                        // REDUMP.ORG errors: <error count>
                        string[] parts = line.Split(' ');
                        if (long.TryParse(parts[2], out long redump))
                            return redump;
                        else
                            return -1;
                    }

                    return -1;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return -1;
                }
            }
        }

        /// <summary>
        /// Get the layerbreak from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Layerbreak if possible, null on error</returns>
#if NET48
        private static string GetLayerbreak(string log)
#else
        private static string? GetLayerbreak(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the disc structure lines
                    while (!sr.EndOfStream && sr.ReadLine()?.Trim()?.StartsWith("layer 0") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    // Now that we're at the relevant lines, find the layerbreak
#if NET48
                    string layerbreak = null;
#else
                    string? layerbreak = null;
#endif
                    while (!sr.EndOfStream)
                    {
                        var line = sr.ReadLine()?.Trim();

                        // If we have a null line, just break
                        if (line == null)
                            break;

                        // Single-layer discs have no layerbreak
                        if (line.Contains("layers count: 1"))
                        {
                            return null;
                        }

                        // Dual-layer discs have a regular layerbreak (new)
                        else if (line.StartsWith("layer break:"))
                        {
                            // layer break: <layerbreak>
#if NET48
                            layerbreak = line.Substring("layer break: ".Length).Trim();
#else
                            layerbreak = line["layer break: ".Length..].Trim();
#endif
                        }

                        // Dual-layer discs have a regular layerbreak (old)
                        else if (line.StartsWith("data "))
                        {
                            // data { LBA: <startLBA> .. <endLBA>, length: <length>, hLBA: <startLBA> .. <endLBA> }
                            string[] split = line.Split(' ').Where(s => !string.IsNullOrEmpty(s)).ToArray();
#if NET48
                            layerbreak = layerbreak ?? split[7].TrimEnd(',');
#else
                            layerbreak ??= split[7].TrimEnd(',');
#endif
                        }
                    }

                    // Return the layerbreak, if possible
                    return layerbreak;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get multisession information from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Formatted multisession information, null on error</returns>
#if NET48
        private static string GetMultisessionInformation(string log)
#else
        private static string? GetMultisessionInformation(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the multisession lines
                    while (!sr.EndOfStream && sr.ReadLine()?.Trim()?.StartsWith("multisession:") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    // Now that we're at the relevant lines, find the session info
#if NET48
                    string firstSession = null, secondSession = null;
#else
                    string? firstSession = null, secondSession = null;
#endif
                    while (!sr.EndOfStream)
                    {
                        var line = sr.ReadLine()?.Trim();

                        // If we have a null line, just break
                        if (line == null)
                            break;

                        // Store the first session range
                        if (line.Contains("session 1:"))
#if NET48
                            firstSession = line.Substring("session 1: ".Length).Trim();
#else
                            firstSession = line["session 1: ".Length..].Trim();
#endif

                        // Store the secomd session range
                        else if (line.Contains("session 2:"))
#if NET48
                            secondSession = line.Substring("session 2: ".Length).Trim();
#else
                            secondSession = line["session 2: ".Length..].Trim();
#endif
                    }

                    // If either is blank, we don't have multisession
                    if (string.IsNullOrEmpty(firstSession) || string.IsNullOrEmpty(secondSession))
                        return null;

                    // Create and return the formatted output
                    return $"Session 1: {firstSession}\nSession 2: {secondSession}";
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the existence of an anti-modchip string from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Anti-modchip existence if possible, false on error</returns>
        private static bool? GetPlayStationAntiModchipDetected(string log)
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Check for the anti-modchip strings
                    var line = sr.ReadLine()?.Trim();
                    while (!sr.EndOfStream)
                    {
                        if (line == null)
                            return false;

                        if (line.StartsWith("anti-modchip: no"))
                            return false;
                        else if (line.StartsWith("anti-modchip: yes"))
                            return true;

                        line = sr.ReadLine()?.Trim();
                    }

                    return false;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the detected missing EDC count from the input files, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Status of PS1 EDC, if possible</returns>
        private static bool? GetPlayStationEDCStatus(string log)
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Check for the EDC strings
                    var line = sr.ReadLine()?.Trim();
                    while (!sr.EndOfStream)
                    {
                        if (line == null)
                            return false;

                        if (line.Contains("EDC: no"))
                            return false;
                        else if (line.Contains("EDC: yes"))
                            return true;

                        line = sr.ReadLine()?.Trim();
                    }

                    return false;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the LibCrypt data from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>PS1 LibCrypt data, if possible</returns>
#if NET48
        private static string GetPlayStationLibCryptData(string log)
#else
        private static string? GetPlayStationLibCryptData(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the LibCrypt line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("libcrypt:") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    // Now that we're at the relevant entries, read each line in and concatenate
#if NET48
                    string libCryptString = "", line = sr.ReadLine()?.Trim();
#else
                    string? libCryptString = "", line = sr.ReadLine()?.Trim();
#endif
                    while (line?.StartsWith("MSF:") == true)
                    {
                        libCryptString += line + "\n";
                        line = sr.ReadLine()?.Trim();
                    }

                    return libCryptString.TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the existence of LibCrypt from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Status of PS1 LibCrypt, if possible</returns>
        private static bool? GetPlayStationLibCryptStatus(string log)
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Check for the libcrypt strings
                    var line = sr.ReadLine()?.Trim();
                    while (!sr.EndOfStream)
                    {
                        if (line == null)
                            return false;

                        if (line.StartsWith("libcrypt: no"))
                            return false;
                        else if (line.StartsWith("libcrypt: yes"))
                            return true;

                        line = sr.ReadLine()?.Trim();
                    }

                    return false;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the PVD from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Newline-delimited PVD if possible, null on error</returns>
#if NET48
        private static string GetPVD(string log)
#else
        private static string? GetPVD(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the PVD line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("PVD:") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    // Now that we're at the relevant entries, read each line in and concatenate
#if NET48
                    string pvdString = "", line = sr.ReadLine()?.Trim();
#else
                    string? pvdString = "", line = sr.ReadLine()?.Trim();
#endif
                    while (line?.StartsWith("03") == true)
                    {
                        pvdString += line + "\n";
                        line = sr.ReadLine()?.Trim();
                    }

                    return pvdString.TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the non-zero data start from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Non-zero dta start if possible, null on error</returns>
#if NET48
        private static string GetRingNonZeroDataStart(string log)
#else
        private static string? GetRingNonZeroDataStart(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // If we find the sample range, return the start value only
#if NET48
                    string line;
#else
                    string? line;
#endif
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine()?.TrimStart();
                        if (line?.StartsWith("non-zero data sample range") == true)
#if NET48
                            return line.Substring("non-zero data sample range: [".Length).Trim().Split(' ')[0];
#else
                            return line["non-zero data sample range: [".Length..].Trim().Split(' ')[0];
#endif
                    }

                    // We couldn't detect it then
                    return null;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the build info from a Saturn disc, if possible
        /// </summary>
        /// <<param name="segaHeader">String representing a formatter variant of the Saturn header</param>
        /// <returns>True on successful extraction of info, false otherwise</returns>
        /// TODO: Remove when Redumper gets native reading support
#if NET48
        private static bool GetSaturnBuildInfo(string segaHeader, out string serial, out string version, out string date)
#else
        private static bool GetSaturnBuildInfo(string? segaHeader, out string? serial, out string? version, out string? date)
#endif
        {
            serial = null; version = null; date = null;

            // If the input header is null, we can't do a thing
            if (string.IsNullOrWhiteSpace(segaHeader))
                return false;

            // Now read it in cutting it into lines for easier parsing
            try
            {
                string[] header = segaHeader.Split('\n');
#if NET48
                string serialVersionLine = header[2].Substring(58);
                string dateLine = header[3].Substring(58);
                serial = serialVersionLine.Substring(0, 10).Trim();
                version = serialVersionLine.Substring(10, 6).TrimStart('V', 'v');
                date = dateLine.Substring(0, 8);
#else
                string serialVersionLine = header[2][58..];
                string dateLine = header[3][58..];
                serial = serialVersionLine[..10].Trim();
                version = serialVersionLine.Substring(10, 6).TrimStart('V', 'v');
                date = dateLine[..8];
#endif
                date = $"{date[0]}{date[1]}{date[2]}{date[3]}-{date[4]}{date[5]}-{date[6]}{date[7]}";
                return true;
            }
            catch
            {
                // We don't care what the error is
                return false;
            }
        }

        /// <summary>
        /// Get the header from a Saturn, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Header as a byte array if possible, null on error</returns>
#if NET48
        private static string GetSaturnHeader(string log)
#else
        private static string? GetSaturnHeader(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the SS line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("SS [") == false) ;
                    if (sr.EndOfStream)
                        return null;

#if NET48
                    string line, headerString = "";
#else
                    string? line, headerString = "";
#endif
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine()?.TrimStart();
                        if (line?.StartsWith("header:") == true)
                        {
                            line = sr.ReadLine()?.TrimStart();
                            while (line?.StartsWith("00") == true)
                            {
                                headerString += line + "\n";
                                line = sr.ReadLine()?.Trim();
                            }
                        }
                        else
                        {
                            break;
                        }
                    }

                    return headerString.TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the header from a Saturn, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Header as a byte array if possible, null on error</returns>
#if NET48
        private static string GetSecuROMData(string log)
#else
        private static string? GetSecuROMData(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the SecuROM line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("SecuROM [") == false) ;
                    if (sr.EndOfStream)
                        return null;

                    var lines = new List<string>();
                    while (!sr.EndOfStream)
                    {
                        var line = sr.ReadLine()?.TrimStart();

                        // Skip the "version"/"scheme" line
                        if (line?.StartsWith("version:") == true || line?.StartsWith("scheme:") == true)
                            continue;

                        // Only read until while there are MSF lines
                        if (line?.StartsWith("MSF:") != true)
                            break;

                        lines.Add(line);
                    }

                    return string.Join("\n", lines).TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the header from a Sega CD / Mega CD, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Header as a byte array if possible, null on error</returns>
#if NET48
        private static string GetSegaCDHeader(string log, out string buildDate, out string serial, out string region)
#else
        private static string? GetSegaCDHeader(string log, out string? buildDate, out string? serial, out string? region)
#endif
        {
            // Set the default values
            buildDate = null; serial = null; region = null;

            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the MCD line
                    while (!sr.EndOfStream && sr.ReadLine()?.TrimStart()?.StartsWith("MCD [") == false) ;
                    if (sr.EndOfStream)
                        return null;

#if NET48
                    string line, headerString = string.Empty;
#else
                    string? line, headerString = string.Empty;
#endif
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine()?.TrimStart();
                        if (line == null)
                            break;

                        if (line.StartsWith("build date:"))
                        {
#if NET48
                            buildDate = line.Substring("build date: ".Length).Trim();
#else
                            buildDate = line["build date: ".Length..].Trim();
#endif
                        }
                        else if (line.StartsWith("serial:"))
                        {
#if NET48
                            serial = line.Substring("serial: ".Length).Trim();
#else
                            serial = line["serial: ".Length..].Trim();
#endif
                        }
                        else if (line.StartsWith("region:"))
                        {
#if NET48
                            region = line.Substring("region: ".Length).Trim();
#else
                            region = line["region: ".Length..].Trim();
#endif
                        }
                        else if (line.StartsWith("regions:"))
                        {
#if NET48
                            region = line.Substring("regions: ".Length).Trim();
#else
                            region = line["regions: ".Length..].Trim();
#endif
                        }
                        else if (line.StartsWith("header:"))
                        {
                            line = sr.ReadLine()?.TrimStart();
                            while (line?.StartsWith("01") == true)
                            {
                                headerString += line + "\n";
                                line = sr.ReadLine()?.Trim();
                            }
                        }
                        else
                        {
                            break;
                        }
                    }

                    return headerString.TrimEnd('\n');
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the universal hash from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Universal hash if possible, null on error</returns>
#if NET48
        private static string GetUniversalHash(string log)
#else
        private static string? GetUniversalHash(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // If we find the universal hash line, return the hash only
#if NET48
                    string line;
#else
                    string? line;
#endif
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine()?.TrimStart();
                        if (line?.StartsWith("Universal Hash") == true)
#if NET48
                            return line.Substring("Universal Hash (SHA-1): ".Length).Trim();
#else
                            return line["Universal Hash (SHA-1): ".Length..].Trim();
#endif
                    }

                    // We couldn't detect it then
                    return null;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the write offset from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Sample write offset if possible, null on error</returns>
#if NET48
        private static string GetWriteOffset(string log)
#else
        private static string? GetWriteOffset(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // If we find the disc write offset line, return the offset
#if NET48
                    string line;
#else
                    string? line;
#endif
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine()?.TrimStart();
                        if (line?.StartsWith("disc write offset") == true)
#if NET48
                            return line.Substring("disc write offset: ".Length).Trim();
#else
                            return line["disc write offset: ".Length..].Trim();
#endif
                    }

                    // We couldn't detect it then
                    return null;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get the version. if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>Version if possible, null on error</returns>
#if NET48
        private static string GetVersion(string log)
#else
        private static string? GetVersion(string log)
#endif
        {
            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return null;

            // Samples:
            // redumper v2022.10.28 [Oct 28 2022, 05:41:43] (print usage: --help,-h)
            // redumper v2022.12.22 build_87 [Dec 22 2022, 01:56:26]

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Skip first line (dump date)
                    sr.ReadLine();

                    // Generate regex
                    // Permissive
                    var regex = new Regex(@"^redumper (v.+) \[.+\]");
                    // Strict
                    //var regex = new Regex(@"^redumper (v\d{4}\.\d{2}\.\d{2}(| build_\d+)) \[.+\]");

                    // Extract the version string
                    var match = regex.Match(sr.ReadLine()?.Trim() ?? string.Empty);
                    var version = match.Groups[1].Value;
                    return string.IsNullOrWhiteSpace(version) ? null : version;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return null;
                }
            }
        }

        /// <summary>
        /// Get hardware information from the input file, if possible
        /// </summary>
        /// <param name="log">Log file location</param>
        /// <returns>True if hardware info was set, false otherwise</returns>
#if NET48
        private static bool GetHardwareInfo(string log, out string manufacturer, out string model, out string firmware)
#else
        private static bool GetHardwareInfo(string log, out string? manufacturer, out string? model, out string? firmware)
#endif
        {
            // Set the default values
            manufacturer = null; model = null; firmware = null;

            // If the file doesn't exist, we can't get info from it
            if (!File.Exists(log))
                return false;

            using (var sr = File.OpenText(log))
            {
                try
                {
                    // Fast forward to the drive information line
                    while (!(sr.ReadLine()?.Trim().StartsWith("drive path:") ?? true)) ;

                    // If we find the hardware info line, return each value
                    // drive: <vendor_id> - <product_id> (revision level: <product_revision_level>, vendor specific: <vendor_specific>)
                    var regex = new Regex(@"drive: (.+) - (.+) \(revision level: (.+), vendor specific: (.+)\)");

#if NET48
                    string line;
#else
                    string? line;
#endif
                    while ((line = sr.ReadLine()) != null)
                    {
                        var match = regex.Match(line.Trim());
                        if (match.Success)
                        {
                            manufacturer = match.Groups[1].Value;
                            model = match.Groups[2].Value;
                            firmware = match.Groups[3].Value;
                            firmware += match.Groups[4].Value == "<empty>" ? "" : $" ({match.Groups[4].Value})";
                            return true;
                        }
                    }

                    // We couldn't detect it then
                    return false;
                }
                catch
                {
                    // We don't care what the exception is right now
                    return false;
                }
            }
        }

        #endregion
    }
}
