using System;
using System.IO;
#if NET35_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using SabreTools.Hashing;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Models.Logiqx;
using SabreTools.Models.PIC;
using SabreTools.RedumpLib.Data;

namespace MPF.Processors
{
    /// <summary>
    /// Includes processing-specific utility functionality
    /// </summary>
    public static class ProcessingTool
    {
        #region Constants

        /// <summary>
        /// Shift-JIS encoding for detection and conversion
        /// </summary>
        private static readonly Encoding ShiftJIS = Encoding.GetEncoding(932);

        #endregion

        #region Information Extraction

        /// <summary>
        /// Generate the proper datfile from the input Datafile, if possible
        /// </summary>
        /// <param name="datafile">.dat file location</param>
        /// <returns>Relevant pieces of the datfile, null on error</returns>
        public static string? GenerateDatfile(Datafile? datafile)
        {
            // If we don't have a valid datafile, we can't do anything
            if (datafile?.Game == null || datafile.Game.Length == 0)
                return null;

            var roms = datafile.Game[0].Rom;
            if (roms == null || roms.Length == 0)
                return null;

            // Otherwise, reconstruct the hash data with only the required info
            try
            {
                string datString = string.Empty;
                for (int i = 0; i < roms.Length; i++)
                {
                    var rom = roms[i];
                    datString += $"<rom name=\"{rom.Name}\" size=\"{rom.Size}\" crc=\"{rom.CRC}\" md5=\"{rom.MD5}\" sha1=\"{rom.SHA1}\" />\n";
                }

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

        /// <summary>
        /// Get the Base64 representation of a string
        /// </summary>
        /// <param name="content">String content to encode</param>
        /// <returns>Base64-encoded contents, if possible</returns>
        public static string? GetBase64(string? content)
        {
            if (string.IsNullOrEmpty(content))
                return null;

            byte[] temp = Encoding.UTF8.GetBytes(content);
            return Convert.ToBase64String(temp);
        }

        /// <summary>
        /// Get Datafile from a standard DAT
        /// </summary>
        /// <param name="dat">Path to the DAT file to parse</param>
        /// <returns>Filled Datafile on success, null on error</returns>
        public static Datafile? GetDatafile(string? dat)
        {
            // If the file doesn't exist, we can't read it
            if (string.IsNullOrEmpty(dat))
                return null;
            if (!File.Exists(dat))
                return null;

            try
            {
                // Open and read in the XML file
                XmlReader xtr = XmlReader.Create(dat, new XmlReaderSettings
                {
                    CheckCharacters = false,
#if NET40_OR_GREATER || NETCOREAPP
                    DtdProcessing = DtdProcessing.Ignore,
#endif
                    IgnoreComments = true,
                    IgnoreWhitespace = true,
                    ValidationFlags = XmlSchemaValidationFlags.None,
                    ValidationType = ValidationType.None,
                });

                // If the reader is null for some reason, we can't do anything
                if (xtr == null)
                    return null;

                var serializer = new XmlSerializer(typeof(Datafile));
                return serializer.Deserialize(xtr) as Datafile;
            }
            catch
            {
                // We don't care what the exception is right now
                return null;
            }
        }

        /// <summary>
        /// Gets disc information from a PIC file
        /// </summary>
        /// <param name="pic">Path to a PIC.bin file</param>
        /// <returns>Filled DiscInformation on success, null on error</returns>
        /// <remarks>This omits the emergency brake information, if it exists</remarks>
        public static DiscInformation? GetDiscInformation(string pic)
        {
            try
            {
                return SabreTools.Serialization.Deserializers.PIC.DeserializeFile(pic);
            }
            catch
            {
                // We don't care what the error was
                return null;
            }
        }

        /// <summary>
        /// Get the last modified date from a file path, if possible
        /// </summary>
        /// <param name="filename">Path to the input file</param>
        /// <returns>Filled DateTime on success, null on failure</returns>
        public static DateTime? GetFileModifiedDate(string? filename, bool fallback = false)
        {
            if (string.IsNullOrEmpty(filename))
                return fallback ? DateTime.UtcNow : null;
            if (!File.Exists(filename))
                return fallback ? DateTime.UtcNow : null;

            var fi = new FileInfo(filename);
            return fi.LastWriteTimeUtc;
        }

        /// <summary>
        /// Get the full lines from the input file, if possible
        /// </summary>
        /// <param name="filename">file location</param>
        /// <param name="binary">True if should read as binary, false otherwise (default)</param>
        /// <returns>Full text of the file, null on error</returns>
        public static string? GetFullFile(string filename, bool binary = false)
        {
            // If the file doesn't exist, we can't get info from it
            if (string.IsNullOrEmpty(filename))
                return null;
            if (!File.Exists(filename))
                return null;

            // Read the entire file as bytes
            byte[] bytes = File.ReadAllBytes(filename);

            // If we're reading as binary
            if (binary)
                return BitConverter.ToString(bytes).Replace("-", string.Empty);

            // If we're reading as text
            return NormalizeShiftJIS(bytes);
        }

        /// <summary>
        /// Get the split values for ISO-based media
        /// </summary>
        /// <param name="datafile">Datafile represenging the hash data</param>
        /// <returns>True if extraction was successful, false otherwise</returns>
        public static bool GetISOHashValues(Datafile? datafile, out long size, out string? crc32, out string? md5, out string? sha1)
        {
            size = -1; crc32 = null; md5 = null; sha1 = null;

            if (datafile?.Game == null || datafile.Game.Length == 0)
                return false;

            var roms = datafile.Game[0].Rom;
            if (roms == null || roms.Length == 0)
                return false;

            var rom = roms[0];

            _ = long.TryParse(rom.Size, out size);
            crc32 = rom.CRC;
            md5 = rom.MD5;
            sha1 = rom.SHA1;

            return true;
        }

        /// <summary>
        /// Get the split values for ISO-based media
        /// </summary>
        /// <param name="hashData">String representing the combined hash data</param>
        /// <returns>True if extraction was successful, false otherwise</returns>
        public static bool GetISOHashValues(string? hashData, out long size, out string? crc32, out string? md5, out string? sha1)
        {
            size = -1; crc32 = null; md5 = null; sha1 = null;

            if (string.IsNullOrEmpty(hashData))
                return false;

            var hashreg = new Regex(@"<rom name="".*?"" size=""(.*?)"" crc=""(.*?)"" md5=""(.*?)"" sha1=""(.*?)""", RegexOptions.Compiled);
            Match m = hashreg.Match(hashData);
            if (m.Success)
            {
                _ = long.TryParse(m.Groups[1].Value, out size);
                crc32 = m.Groups[2].Value;
                md5 = m.Groups[3].Value;
                sha1 = m.Groups[4].Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Get the layerbreak info associated from the disc information
        /// </summary>
        /// <param name="di">Disc information containing unformatted data</param>
        /// <returns>True if layerbreak info was set, false otherwise</returns>
        public static bool GetLayerbreaks(DiscInformation? di, out long? layerbreak1, out long? layerbreak2, out long? layerbreak3)
        {
            // Set the default values
            layerbreak1 = null; layerbreak2 = null; layerbreak3 = null;

            // If we don't have valid disc information, we can't do anything
            if (di?.Units == null || di.Units.Length <= 1)
                return false;

            // Wrap big-endian reading
            static int ReadFromArrayBigEndian(byte[]? bytes, int offset)
            {
                if (bytes == null)
                    return default;

                return bytes.ReadInt32BigEndian(ref offset);
            }

            // Layerbreak 1 (2+ layers)
            if (di.Units.Length >= 2)
            {
                long offset = ReadFromArrayBigEndian(di.Units[0]?.Body?.FormatDependentContents, 0x0C);
                long value = ReadFromArrayBigEndian(di.Units[0]?.Body?.FormatDependentContents, 0x10);
                layerbreak1 = value - offset + 2;
            }

            // Layerbreak 2 (3+ layers)
            if (di.Units.Length >= 3)
            {
                long offset = ReadFromArrayBigEndian(di.Units[1]?.Body?.FormatDependentContents, 0x0C);
                long value = ReadFromArrayBigEndian(di.Units[1]?.Body?.FormatDependentContents, 0x10);
                layerbreak2 = layerbreak1 + value - offset + 2;
            }

            // Layerbreak 3 (4 layers)
            if (di.Units.Length >= 4)
            {
                long offset = ReadFromArrayBigEndian(di.Units[2]?.Body?.FormatDependentContents, 0x0C);
                long value = ReadFromArrayBigEndian(di.Units[2]?.Body?.FormatDependentContents, 0x10);
                layerbreak3 = layerbreak2 + value - offset + 2;
            }

            return true;
        }

        /// <summary>
        /// Get the PIC identifier from the first disc information unit, if possible
        /// </summary>
        /// <param name="di">Disc information containing the data</param>
        /// <returns>String representing the PIC identifier, null on error</returns>
        public static string? GetPICIdentifier(DiscInformation? di)
        {
            // If we don't have valid disc information, we can't do anything
            if (di?.Units == null || di.Units.Length < 1)
                return null;

            // We assume the identifier is consistent across all units
            return di.Units[0]?.Body?.DiscTypeIdentifier;
        }

        /// <summary>
        /// Normalize a byte array that may contain Shift-JIS characters
        /// </summary>
        /// <param name="contents">String as a byte array to normalize</param>
        /// <returns>Normalized version of a string</returns>
        public static string NormalizeShiftJIS(byte[]? contents)
        {
            // Invalid arrays are passed as-is
            if (contents == null || contents.Length == 0)
                return string.Empty;

#if NET462_OR_GREATER || NETCOREAPP
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
#endif

            // If the line contains Shift-JIS characters
            if (BytesContainsShiftJIS(contents))
                return ShiftJIS.GetString(contents);

            return Encoding.UTF8.GetString(contents);
        }

        /// <summary>
        /// Determine if a byte array contains Shift-JIS encoded characters
        /// </summary>
        /// <param name="line">Byte array to check for Shift-JIS encoding</param>
        /// <returns>True if the byte array contains Shift-JIS characters, false otherwise</returns>
        /// <see href="https://www.lemoda.net/c/detect-shift-jis/"/> 
        internal static bool BytesContainsShiftJIS(byte[] bytes)
        {
            // Invalid arrays do not count
            if (bytes == null || bytes.Length == 0)
                return false;

            // Loop through and check each pair of bytes
            for (int i = 0; i < bytes.Length - 1; i++)
            {
                byte first = bytes[i];
                byte second = bytes[i + 1];

                if ((first >= 0x81 && first <= 0x84) ||
                    (first >= 0x87 && first <= 0x9F))
                {
                    if (second >= 0x40 && second <= 0x9E)
                        return true;
                    else if (second >= 0x9F && second <= 0xFC)
                        return true;
                }
                else if (first >= 0xE0 && first <= 0xEF)
                {
                    if (second >= 0x40 && second <= 0x9E)
                        return true;
                    else if (second >= 0x9F && second <= 0xFC)
                        return true;
                }
            }

            return false;
        }

        #endregion

        #region Category Extraction

        /// <summary>
        /// Determine the category based on the UMDImageCreator string
        /// </summary>
        /// <param name="region">String representing the category</param>
        /// <returns>Category, if possible</returns>
        public static DiscCategory? GetUMDCategory(string? category)
        {
            return category?.ToLowerInvariant() switch
            {
                "game" => DiscCategory.Games,
                "video" => DiscCategory.Video,
                "audio" => DiscCategory.Audio,
                _ => null,
            };
        }

        #endregion

        #region Region Extraction

        /// <summary>
        /// Determine the region based on the PlayStation serial code
        /// </summary>
        /// <param name="serial">PlayStation serial code</param>
        /// <returns>Region mapped from name, if possible</returns>
        public static Region? GetPlayStationRegion(string? serial)
        {
            // If we have a fully invalid serial
            if (string.IsNullOrEmpty(serial))
                return null;

            // Standardized "S" serials
            if (serial!.StartsWith("S"))
            {
                // string publisher = serial[0] + serial[1];
                // char secondRegion = serial[3];
                switch (serial[2])
                {
                    case 'A': return Region.Asia;
                    case 'C': return Region.China;
                    case 'E': return Region.Europe;
                    case 'K': return Region.SouthKorea;
                    case 'U': return Region.UnitedStatesOfAmerica;
                    case 'P':
                        // Region of S_P_ serials may be Japan, Asia, or SouthKorea
                        return serial[3] switch
                        {
                            // Check first two digits of S_PS serial
                            'S' => (Region?)(serial.Substring(5, 2) switch
                            {
                                "46" => Region.SouthKorea,
                                "51" => Region.Asia,
                                "56" => Region.SouthKorea,
                                "55" => Region.Asia,
                                _ => Region.Japan,
                            }),

                            // Check first three digits of S_PM serial
                            'M' => (Region?)(serial.Substring(5, 3) switch
                            {
                                "645" => Region.SouthKorea,
                                "675" => Region.SouthKorea,
                                "885" => Region.SouthKorea,
                                _ => Region.Japan, // Remaining S_PM serials may be Japan or Asia
                            }),
                            _ => (Region?)Region.Japan,
                        };
                }
            }

            // Japan-only special serial
            else if (serial.StartsWith("PAPX"))
                return Region.Japan;

            // Region appears entirely random
            else if (serial.StartsWith("PABX"))
                return null;

            // Region appears entirely random
            else if (serial.StartsWith("PBPX"))
                return null;

            // Japan-only special serial
            else if (serial.StartsWith("PCBX"))
                return Region.Japan;

            // Japan-only special serial
            else if (serial.StartsWith("PCXC"))
                return Region.Japan;

            // Single disc known, Japan
            else if (serial.StartsWith("PDBX"))
                return Region.Japan;

            // Single disc known, Europe
            else if (serial.StartsWith("PEBX"))
                return Region.Europe;

            // Single disc known, USA
            else if (serial.StartsWith("PUBX"))
                return Region.UnitedStatesOfAmerica;

            return null;
        }

        /// <summary>
        /// Determine the region based on the XGD serial character
        /// </summary>
        /// <param name="region">Character denoting the region</param>
        /// <returns>Region, if possible</returns>
        public static Region? GetXGDRegion(char? region)
        {
            return region switch
            {
                'W' => (Region?)Region.World,
                'A' => (Region?)Region.UnitedStatesOfAmerica,
                'J' => (Region?)Region.JapanAsia,
                'E' => (Region?)Region.Europe,
                'K' => (Region?)Region.USAJapan,
                'L' => (Region?)Region.USAEurope,
                'H' => (Region?)Region.JapanEurope,
                _ => null,
            };
        }

        #endregion

        #region PlayStation specific tools

        /// <summary>
        /// Get if LibCrypt data is detected in the subchannel file, if possible
        /// </summary>
        /// <param name="subPath">Path to the subchannel file</param>
        /// <returns>True if LibCrypt was detected, false otherwise</returns>
        public static bool DetectLibCrypt(string? subPath)
        {
            if (string.IsNullOrEmpty(subPath))
                return false;

            // Create conversion delegates
            byte _btoi(byte b) => (byte)(b / 16 * 10 + b % 16);
            byte _itob(byte i) => (byte)(i / 10 * 16 + i % 10);

            try
            {
                if (!File.Exists(subPath))
                    return false;

                // Open the subfile
                var subFile = File.OpenRead(subPath);

                // Check the size
                long size = subFile.Length;
                if (size % 96 != 0)
                    return false;

                // Setup loop variables
                byte[] expected = new byte[12];
                uint time = 0;

                // Process sector data
                for (uint sector = 150; sector < ((size / 96) + 150); sector++)
                {
                    // Read the sector header
                    subFile.Seek(12, SeekOrigin.Current);
                    byte[] actual = subFile.ReadBytes(12);

                    // Skip the rest of the data for the sector
                    subFile.Seek(72, SeekOrigin.Current);

                    // New track
                    if ((_btoi(actual[1]) == (_btoi(expected[1]) + 1)) && (actual[2] == 0 || actual[2] == 1))
                    {
                        Array.Copy(actual, expected, 6);
                        time = (uint)((_btoi((byte)(actual[3] * 60)) + _btoi(actual[4])) * 75) + _btoi(actual[5]);
                    }

                    // New index
                    else if (_btoi(actual[2]) == (_btoi(expected[2]) + 1) && actual[1] == expected[1])
                    {
                        Array.Copy(actual, 2, expected, 2, 4);
                        time = (uint)((_btoi((byte)(actual[3] * 60)) + _btoi(actual[4])) * 75) + _btoi(actual[5]);
                    }

                    // MSF1 [3-5]
                    else
                    {
                        if (expected[2] == 0)
                            time--;
                        else
                            time++;

                        expected[3] = _itob((byte)(time / 60 / 75));
                        expected[4] = _itob((byte)((time / 75) % 60));
                        expected[5] = _itob((byte)(time % 75));
                    }

                    // Force skip [6]
                    actual[6] = expected[6] = 0;

                    // MSF2 [7-9]
                    expected[7] = _itob((byte)(sector / 60 / 75));
                    expected[8] = _itob((byte)((sector / 75) % 60));
                    expected[9] = _itob((byte)(sector % 75));

                    // CRC-16 [10-11] -- TODO: Ensure byte order is correct
                    var crcWrapper = new HashWrapper(HashType.CRC16);
                    crcWrapper.Process(expected, 0, 10);
                    byte[] crc = crcWrapper.CurrentHashBytes!;
                    expected[10] = crc[0];
                    expected[11] = crc[1];

                    // If a subchannel mismatch is detected
                    if (!actual.EqualsExactly(expected))
                        return true;
                }
            }
            catch
            {
                // We are not concerned with the error
                return false;
            }

            return false;
        }

        #endregion

        #region PlayStation 3 specific tools

        /// <summary>
        /// Validates a getkey log to check for presence of valid PS3 key
        /// </summary>
        /// <param name="logPath">Path to getkey log file</param>
        /// <param name="key">Output key string, null if not valid</param>
        /// <param name="id">Output disc ID string, null if not valid</param>
        /// <param name="pic">Output PIC string, null if not valid</param>
        /// <returns>True if path to log file contains valid key, false otherwise</returns>
        public static bool ParseGetKeyLog(string? logPath, out string? key, out string? id, out string? pic)
        {
            key = id = pic = null;

            if (string.IsNullOrEmpty(logPath))
                return false;

            try
            {
                if (!File.Exists(logPath))
                    return false;

                // Protect from attempting to read from really long files
                FileInfo logFile = new(logPath);
                if (logFile.Length > 65536)
                    return false;

                // Read from .getkey.log file
                using StreamReader sr = File.OpenText(logPath);

                // Determine whether GetKey was successful
                string? line;
                while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("get_dec_key succeeded!") == false) ;
                if (line == null)
                    return false;

                // Look for Disc Key in log
                while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("disc_key = ") == false) ;

                // If end of file reached, no key found
                if (line == null)
                    return false;

                // Get Disc Key from log
                string discKeyStr = line.Substring("disc_key = ".Length);

                // Validate Disc Key from log
                if (discKeyStr.Length != 32)
                    return false;

                // Convert Disc Key to byte array
                key = discKeyStr;
                if (key == null)
                    return false;

                // Read Disc ID
                while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("disc_id = ") == false) ;

                // If end of file reached, no ID found
                if (line == null)
                    return false;

                // Get Disc ID from log
                string discIDStr = line.Substring("disc_id = ".Length);

                // Validate Disc ID from log
                if (discIDStr.Length != 32)
                    return false;

                // Replace X's in Disc ID with 00000001
                discIDStr = discIDStr.Substring(0, 24) + "00000001";

                // Convert Disc ID to byte array
                id = discIDStr;
                if (id == null)
                    return false;

                // Look for PIC in log
                while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("PIC:") == false) ;

                // If end of file reached, no PIC found
                if (line == null)
                    return false;

                // Get PIC from log
                string discPICStr = "";
                for (int i = 0; i < 9; i++)
                    discPICStr += sr.ReadLine();
                if (discPICStr == null)
                    return false;

                // Validate PIC from log
                if (discPICStr.Length != 264)
                    return false;

                // Convert PIC to byte array
                pic = discPICStr;
                if (pic == null)
                    return false;

                // Double check for warnings in .getkey.log
                while ((line = sr.ReadLine()) != null)
                {
                    string t = line.Trim();
                    if (t.StartsWith("WARNING"))
                        return false;
                    else if (t.StartsWith("SUCCESS"))
                        return true;
                }
            }
            catch
            {
                // We are not concerned with the error
                return false;
            }

            return true;
        }

        /// <summary>
        /// Validates a getkey log to check for presence of valid PS3 key
        /// </summary>
        /// <param name="logPath">Path to getkey log file</param>
        /// <param name="key">Output 16 byte disc key, null if not valid</param>
        /// <param name="id">Output 16 byte disc ID, null if not valid</param>
        /// <param name="pic">Output 230 byte PIC, null if not valid</param>
        /// <returns>True if path to log file contains valid key, false otherwise</returns>
        public static bool ParseGetKeyLog(string? logPath, out byte[]? key, out byte[]? id, out byte[]? pic)
        {
            key = id = pic = null;
            if (ParseGetKeyLog(logPath, out string? keyString, out string? idString, out string? picString))
            {
                if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(idString) || string.IsNullOrEmpty(picString) || picString!.Length < 230)
                    return false;

                key = keyString.FromHexString();
                id = idString.FromHexString();
                pic = picString.Substring(0, 230).FromHexString();
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Validates a hexadecimal disc ID
        /// </summary>
        /// <param name="discID">String representing hexadecimal disc ID</param>
        /// <returns>True if string is a valid disc ID, false otherwise</returns>
        public static byte[]? ParseDiscID(string? discID)
        {
            if (string.IsNullOrEmpty(discID))
                return null;

            string cleandiscID = discID!.Trim().Replace("\n", string.Empty);

            if (discID!.Length != 32)
                return null;

            // Censor last 4 bytes by replacing with 0x00000001
            cleandiscID = cleandiscID.Substring(0, 24) + "00000001";

            // Convert to byte array, null if invalid hex string
            return cleandiscID.FromHexString();
        }

        /// <summary>
        /// Validates a key file to check for presence of valid PS3 key
        /// </summary>
        /// <param name="keyPath">Path to key file</param>
        /// <returns>Output 16 byte key, null if not valid</returns>
        public static byte[]? ParseKeyFile(string? keyPath)
        {
            if (string.IsNullOrEmpty(keyPath))
                return null;

            // Try read from key file
            try
            {
                if (!File.Exists(keyPath))
                    return null;

                // Key file must be exactly 16 bytes long
                FileInfo keyFile = new(keyPath);
                if (keyFile.Length != 16)
                    return null;
                byte[] key = new byte[16];

                // Read 16 bytes from Key file
                using FileStream fs = new(keyPath, FileMode.Open, FileAccess.Read);
                using BinaryReader reader = new(fs);
                int numBytes = reader.Read(key, 0, 16);
                if (numBytes != 16)
                    return null;

                return key;
            }
            catch
            {
                // Not concerned with error
                return null;
            }
        }

        /// <summary>
        /// Validates a hexadecimal key
        /// </summary>
        /// <param name="hexKey">String representing hexadecimal key</param>
        /// <returns>Output 16 byte key, null if not valid</returns>
        public static byte[]? ParseHexKey(string? hexKey)
        {
            if (string.IsNullOrEmpty(hexKey))
                return null;

            string cleanHexKey = hexKey!.Trim().Replace("\n", string.Empty);

            if (cleanHexKey.Length != 32)
                return null;

            // Convert to byte array, null if invalid hex string
            return cleanHexKey.FromHexString();
        }

        /// <summary>
        /// Validates a PIC file path
        /// </summary>
        /// <param name="picPath">Path to PIC file</param>
        /// <returns>Output PIC byte array, null if not valid</returns>
        public static byte[]? ParsePICFile(string? picPath)
        {
            if (string.IsNullOrEmpty(picPath))
                return null;

            // Try read from PIC file
            try
            {
                if (!File.Exists(picPath))
                    return null;

                // PIC file must be at least 115 bytes long
                FileInfo picFile = new(picPath);
                if (picFile.Length < 115)
                    return null;
                byte[] pic = new byte[115];

                // Read 115 bytes from PIC file
                using FileStream fs = new(picPath, FileMode.Open, FileAccess.Read);
                using BinaryReader reader = new(fs);
                int numBytes = reader.Read(pic, 0, 115);
                if (numBytes != 115)
                    return null;

                // Validate that a PIC was read by checking first 6 bytes
                if (pic[0] != 0x10 ||
                    pic[1] != 0x02 ||
                    pic[2] != 0x00 ||
                    pic[3] != 0x00 ||
                    pic[4] != 0x44 ||
                    pic[5] != 0x49)
                    return null;

                return pic;
            }
            catch
            {
                // Not concerned with error
                return null;
            }
        }

        /// <summary>
        /// Validates a PIC
        /// </summary>
        /// <param name="inputPIC">String representing PIC</param>
        /// <returns>Output PIC byte array, null if not valid</returns>
        public static byte[]? ParsePIC(string? inputPIC)
        {
            if (string.IsNullOrEmpty(inputPIC))
                return null;

            string cleanPIC = inputPIC!.Trim().Replace("\n", string.Empty).Replace("\r", string.Empty);

            if (cleanPIC.Length < 230)
                return null;

            // Convert to byte array, null if invalid hex string
            return cleanPIC.Substring(0, 230).FromHexString();
        }

        /// <summary>
        /// Validates a string representing a layerbreak value (in sectors)
        /// </summary>
        /// <param name="inputLayerbreak">String representing layerbreak value</param>
        /// <param name="layerbreak">Output layerbreak value, null if not valid</param>
        /// <returns>True if layerbreak is valid, false otherwise</returns>
        public static long? ParseLayerbreak(string? inputLayerbreak)
        {
            if (string.IsNullOrEmpty(inputLayerbreak))
                return null;

            if (!long.TryParse(inputLayerbreak, out long layerbreak))
                return null;

            return ParseLayerbreak(layerbreak);
        }

        /// <summary>
        /// Validates a layerbreak value (in sectors)
        /// </summary>
        /// <param name="inputLayerbreak">Number representing layerbreak value</param>
        /// <param name="layerbreak">Output layerbreak value, null if not valid</param>
        /// <returns>True if layerbreak is valid, false otherwise</returns>
        public static long? ParseLayerbreak(long? layerbreak)
        {
            // Check that layerbreak is positive number and smaller than largest disc size (in sectors)
            if (layerbreak <= 0 || layerbreak > 24438784)
                return null;

            return layerbreak;
        }

        /// <summary>
        /// Converts a CRC32 hash hex string into uint32 representation
        /// </summary>
        /// <param name="inputLayerbreak">Hex string representing CRC32 hash</param>
        /// <param name="layerbreak">Output CRC32 value, null if not valid</param>
        /// <returns>True if CRC32 hash string is valid, false otherwise</returns>
        public static uint? ParseCRC32(string? inputCRC32)
        {
            if (string.IsNullOrEmpty(inputCRC32))
                return null;

            byte[]? crc32 = inputCRC32.FromHexString();
            if (crc32 == null || crc32.Length != 4)
                return null;

            return (uint)(0x01000000 * crc32[0] + 0x00010000 * crc32[1] + 0x00000100 * crc32[2] + 0x00000001 * crc32[3]);
        }

        #endregion

        #region Xbox and Xbox 360

        /// <summary>
        /// Get the XGD1 Master ID (XMID) information
        /// </summary>
        /// <param name="dmi">DMI.bin file location</param>
        /// <returns>String representation of the XGD1 DMI information, empty string on error</returns>
        public static string GetXMID(string dmi)
        {
            if (!File.Exists(dmi))
                return string.Empty;

            try
            {
                using var br = new BinaryReader(File.OpenRead(dmi));
                br.BaseStream.Seek(8, SeekOrigin.Begin);
                return new string(br.ReadChars(8));
            }
            catch
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// Get the XGD2/3 Master ID (XeMID) information
        /// </summary>
        /// <param name="dmi">DMI.bin file location</param>
        /// <returns>String representation of the XGD2/3 DMI information, empty string on error</returns>
        public static string GetXeMID(string dmi)
        {
            if (!File.Exists(dmi))
                return string.Empty;

            try
            {
                using var br = new BinaryReader(File.OpenRead(dmi));
                br.BaseStream.Seek(64, SeekOrigin.Begin);
                return new string(br.ReadChars(14));
            }
            catch
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// Get XGD type from SS.bin file
        /// </summary>
        /// <param name="ss"></param>
        /// <param name="xgdType"></param>
        /// <returns></returns>
        public static bool GetXGDType(string? ss, out int xgdType)
        {
            xgdType = 0;

            if (string.IsNullOrEmpty(ss))
                return false;

            if (!File.Exists(ss))
                return false;

            using FileStream fs = File.OpenRead(ss);
            byte[] buf = new byte[3];
            int numBytes = fs.Read(buf, 13, 16);

            if (numBytes != 3)
                return false;

            return GetXGDType(buf, out xgdType);
        }

        /// <summary>
        /// Get XGD type from SS.bin sector
        /// </summary>
        /// <param name="ss">Byte array of SS.bin sector</param>
        /// <param name="xgdType">XGD type</param>
        /// <returns>True if successful, false otherwise</returns>
        public static bool GetXGDType(byte[] ss, out int xgdType)
        {
            xgdType = 0;

            // Concatenate the last three values
            long lastThree = (((ss[13] << 8) | ss[14]) << 8) | ss[15];

            // Return XGD type based on value
            switch (lastThree)
            {
                case 0x2033AF:
                    xgdType = 1;
                    return true;
                case 0x20339F:
                    xgdType = 2;
                    return true;
                case 0x238E0F:
                    xgdType = 3;
                    return true;
                default:
                    return false;
            }
        }

        /// <summary>
        /// Determine if a given SS.bin is valid (Known XGD type and SSv2 if XGD3)
        /// </summary>
        /// <param name="ssPath">Path to the SS file to check</param>
        /// <returns>True if valid, false otherwise</returns>
        public static bool IsValidSS(string ssPath)
        {
            if (!File.Exists(ssPath))
                return false;

            byte[] ss = File.ReadAllBytes(ssPath);
            if (ss.Length != 2048)
                return false;

            return IsValidSS(ss);
        }

        /// <summary>
        /// Determine if a given SS is valid (2048 bytes, known XGD type, and SSv2 if XGD3)
        /// </summary>
        /// <param name="ss">Byte array of SS sector</param>
        /// <returns>True if SS is valid, false otherwise</returns>
        public static bool IsValidSS(byte[] ss)
        {
            // Check 1 sector long
            if (ss.Length != 2048)
                return false;

            // Must be a valid XGD type
            if (!GetXGDType(ss, out int xgdType))
                return false;

            // Only continue to check SSv2 for XGD3
            if (xgdType != 3)
                return true;
            
            // Determine if XGD3 SS.bin is SSv1 (Original Kreon) or SSv2 (0800 / Repaired Kreon)
#if NET20
            var checkArr = new byte[72];
            Array.Copy(ss, 32, checkArr, 0, 72);
            return Array.Exists(checkArr, x => x != 0);
#else
            return ss.Skip(32).Take(72).Any(x => x != 0);
#endif
        }

        /// <summary>
        /// Determine if a given SS has already been cleaned
        /// </summary>
        /// <param name="ss">Byte array of SS sector</param>
        /// <returns>True if SS is clean, false otherwise</returns>
        public static bool IsCleanSS(byte[] ss)
        {
            if (ss.Length != 2048)
                return false;

            if (!GetXGDType(ss, out int xgdType))
                return false;

#if NET20
            var checkArr = new byte[72];
            Array.Copy(ss, 32, checkArr, 0, 72);
            if (xgdType == 3 && Array.Exists(checkArr, x => x != 0))
#else
            if (xgdType == 3 && ss.Skip(32).Take(72).Any(x => x != 0))
#endif
            {
                // Check for a cleaned SSv2

                int rtOffset = 0x24;

                if (ss[rtOffset + 36] != 0x01)
                    return false;
                if (ss[rtOffset + 37] != 0x00)
                    return false;
                if (ss[rtOffset + 39] != 0x01)
                    return false;
                if (ss[rtOffset + 40] != 0x00)
                    return false;
                if (ss[rtOffset + 45] != 0x5B)
                    return false;
                if (ss[rtOffset + 46] != 0x00)
                    return false;
                if (ss[rtOffset + 48] != 0x5B)
                    return false;
                if (ss[rtOffset + 49] != 0x00)
                    return false;
                if (ss[rtOffset + 54] != 0xB5)
                    return false;
                if (ss[rtOffset + 55] != 0x00)
                    return false;
                if (ss[rtOffset + 57] != 0xB5)
                    return false;
                if (ss[rtOffset + 58] != 0x00)
                    return false;
                if (ss[rtOffset + 63] != 0x0F)
                    return false;
                if (ss[rtOffset + 64] != 0x01)
                    return false;
                if (ss[rtOffset + 66] != 0x0F)
                    return false;
                if (ss[rtOffset + 67] != 0x01)
                    return false;
            }
            else
            {
                // Check for a cleaned SSv1

                int rtOffset = 0x204;

                if (ss[rtOffset + 36] != 0x01)
                    return false;
                if (ss[rtOffset + 37] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 39] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 40] != 0x00)
                    return false;
                if (ss[rtOffset + 45] != 0x5B)
                    return false;
                if (ss[rtOffset + 46] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 48] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 49] != 0x00)
                    return false;
                if (ss[rtOffset + 54] != 0xB5)
                    return false;
                if (ss[rtOffset + 55] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 57] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 58] != 0x00)
                    return false;
                if (ss[rtOffset + 63] != 0x0F)
                    return false;
                if (ss[rtOffset + 64] != 0x01)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 66] != 0x00)
                    return false;
                if (xgdType == 2 && ss[rtOffset + 67] != 0x00)
                    return false;
            }

            // All angles are as expected, it is clean
            return true;
        }

        /// <summary>
        /// Clean a rawSS.bin file and write it to a file
        /// </summary>
        /// <param name="rawSS">Path to the raw SS file to read from</param>
        /// <param name="cleanSS">Path to the clean SS file to write to</param>
        /// <returns>True if successful, false otherwise</returns>
        public static bool CleanSS(string rawSS, string cleanSS)
        {
            if (!File.Exists(rawSS))
                return false;

            byte[] ss = File.ReadAllBytes(rawSS);
            if (ss.Length != 2048)
                return false;

            if (!CleanSS(ss))
                return false;

            File.WriteAllBytes(cleanSS, ss);
            return true;
        }

        /// <summary>
        /// Fix a SS sector to its predictable clean form.
        /// With help from ss_sector_range
        /// </summary>
        /// <param name="ss">Byte array of raw SS sector</param>
        /// <returns>True if successful, false otherwise</returns>
        public static bool CleanSS(byte[] ss)
        {
            // Must be entire sector
            if (ss.Length != 2048)
                return false;

            // Determine XGD type
            if (!GetXGDType(ss, out int xgdType))
                return false;
            
            // Determine if XGD3 SS.bin is SSv1 (Original Kreon) or SSv2 (0800 / Repaired Kreon)
#if NET20
            var checkArr = new byte[72];
            Array.Copy(ss, 32, checkArr, 0, 72);
            bool ssv2 = Array.Exists(checkArr, x => x != 0);
#else
            bool ssv2 = ss.Skip(32).Take(72).Any(x => x != 0);
#endif

            // Do not produce an SS hash for bad SS (SSv1 XGD3 / Unrepaired Kreon SS)
            if(xgdType == 3 && !ssv2)
                return false;

            switch (xgdType)
            {
                case 1:
                    // Leave Original Xbox SS.bin unchanged
                    return true;

                case 2:
                    // Fix standard SSv1 ss.bin
                    ss[552] = 1;   // 0x01
                    ss[553] = 0;   // 0x00
                    ss[555] = 0;   // 0x00
                    ss[556] = 0;   // 0x00

                    ss[561] = 91;  // 0x5B
                    ss[562] = 0;   // 0x00
                    ss[564] = 0;   // 0x00
                    ss[565] = 0;   // 0x00

                    ss[570] = 181; // 0xB5
                    ss[571] = 0;   // 0x00
                    ss[573] = 0;   // 0x00
                    ss[574] = 0;   // 0x00

                    ss[579] = 15;  // 0x0F
                    ss[580] = 1;   // 0x01
                    ss[582] = 0;   // 0x00
                    ss[583] = 0;   // 0x00
                    return true;

                case 3:
                    if (ssv2)
                    {
                        ss[72] = 1;   // 0x01
                        ss[73] = 0;   // 0x00
                        ss[75] = 1;   // 0x01
                        ss[76] = 0;   // 0x00

                        ss[81] = 91;  // 0x5B
                        ss[82] = 0;   // 0x00
                        ss[84] = 91;  // 0x5B
                        ss[85] = 0;   // 0x00

                        ss[90] = 181; // 0xB5
                        ss[91] = 0;   // 0x00
                        ss[93] = 181; // 0xB5
                        ss[94] = 0;   // 0x00

                        ss[99] = 15;  // 0x0F
                        ss[100] = 1;   // 0x01
                        ss[102] = 15;  // 0x0F
                        ss[103] = 1;   // 0x01
                    }
                    else
                    {
                        ss[552] = 1;   // 0x01
                        ss[553] = 0;   // 0x00

                        ss[561] = 91;  // 0x5B
                        ss[562] = 0;   // 0x00

                        ss[570] = 181; // 0xB5
                        ss[571] = 0;   // 0x00

                        ss[579] = 15;  // 0x0F
                        ss[580] = 1;   // 0x01
                    }

                    return true;

                default:
                    // Unknown XGD type
                    return false;
            }
        }

        /// <summary>
        /// Get Security Sector ranges from SS.bin
        /// </summary>
        /// <param name="ssBin">Path to SS.bin file</param>
        /// <returns>Sector ranges if found, null otherwise</returns>
        public static string? GetSSRanges(string ssBin)
        {
            if (!File.Exists(ssBin))
                return null;

            byte[] ss = File.ReadAllBytes(ssBin);
            if (ss.Length != 2048)
                return null;

            return GetSSRanges(ss);
        }

        /// <summary>
        /// Get Security Sector ranges from SS sector.
        /// With help from ss_sector_range
        /// </summary>
        /// <param name="ss">Byte array of SS sector</param>
        /// <returns>Sector ranges if found, null otherwise</returns>
        public static string? GetSSRanges(byte[] ss)
        {
            if (ss.Length != 2048)
                return null;

            if (!GetXGDType(ss, out int xgdType))
                return null;

            //uint numRanges = ss[1632];
            uint numRanges;
            if (xgdType == 1)
                numRanges = 16;
            else
                numRanges = 4;


            uint[] startLBA = new uint[numRanges];
            uint[] endLBA = new uint[numRanges];
            for (uint i = 0; i < numRanges; i++)
            {
                // Determine range Physical Sector Number
                uint startPSN = (uint)((((ss[i * 9 + 1636] << 8) | ss[i * 9 + 1637]) << 8) | ss[i * 9 + 1638]);
                uint endPSN = (uint)((((ss[i * 9 + 1639] << 8) | ss[i * 9 + 1640]) << 8) | ss[i * 9 + 1641]);

                // Determine range Logical Sector Number
                if (xgdType == 1 && startPSN >= (1913776 + 0x030000))
                {
                    // Layer 1 of XGD1
                    startLBA[i] = (1913776 + 0x030000) * 2 - (startPSN ^ 0xFFFFFF) - 0x030000 - 1;
                    endLBA[i] = (1913776 + 0x030000) * 2 - (endPSN ^ 0xFFFFFF) - 0x030000 - 1;
                }
                else if (xgdType == 2 && startPSN >= (1913760 + 0x030000))
                {
                    // Layer 1 of XGD2
                    startLBA[i] = (1913760 + 0x030000) * 2 - (startPSN ^ 0xFFFFFF) - 0x030000 - 1;
                    endLBA[i] = (1913760 + 0x030000) * 2 - (endPSN ^ 0xFFFFFF) - 0x030000 - 1;
                }
                else if (xgdType == 3 && startPSN >= (2133520 + 0x030000))
                {
                    // Layer 1 of XGD3
                    startLBA[i] = (2133520 + 0x030000) * 2 - (startPSN ^ 0xFFFFFF) - 0x030000 - 1;
                    endLBA[i] = (2133520 + 0x030000) * 2 - (endPSN ^ 0xFFFFFF) - 0x030000 - 1;
                }
                else
                {
                    // Layer 0
                    startLBA[i] = startPSN - 0x030000;
                    endLBA[i] = endPSN - 0x030000;
                }
            }

            // Sort ranges for XGD1
            if (xgdType == 1)
                Array.Sort(startLBA, endLBA);

            // Represent ranges as string
            string? ranges = null;
            if (xgdType == 1)
            {
                for (int i = 0; i < 16; i++)
                {
                    ranges += $"{startLBA[i]}-{endLBA[i]}";
                    if (i != numRanges - 1)
                        ranges += "\r\n";
                }
            }
            else
            {
                ranges = $"{startLBA[0]}-{endLBA[0]}\r\n{startLBA[3]}-{endLBA[3]}";
            }

            return ranges;
        }

        #endregion
    }
}