// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : MMC.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Core.
//
// --[ Description ] ----------------------------------------------------------
//
//     Detects media types in MultiMediaCommand devices
//
// --[ License ] --------------------------------------------------------------
//
//     This program is free software: you can redistribute it and/or modify
//     it under the terms of the GNU General Public License as
//     published by the Free Software Foundation, either version 3 of the
//     License, or (at your option) any later version.
//
//     This program is distributed in the hope that it will be useful,
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//     GNU General Public License for more details.
//
//     You should have received a copy of the GNU General Public License
//     along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2023 Natalia Portillo
// ****************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.Checksums;
using Aaru.CommonTypes;
using Aaru.Console;
using Aaru.Decoders.Bluray;
using Aaru.Decoders.CD;
using Aaru.Decoders.DVD;
using Aaru.Decoders.SCSI.MMC;
using Aaru.Decoders.Sega;
using Aaru.Devices;
using Aaru.Helpers;
using DMI = Aaru.Decoders.Xbox.DMI;

// ReSharper disable JoinDeclarationAndInitializer

namespace Aaru.Core.Media.Detection
{
    /// <summary>Detects media type for MMC class devices</summary>
    public static class MMC
    {
        /// <summary>SHA256 of PlayStation 2 boot sectors, seen in PAL discs</summary>
        const string PS2_PAL_HASH = "5d04ff236613e1d8adcf9c201874acd6f6deed1e04306558b86f91cfb626f39d";

        /// <summary>SHA256 of PlayStation 2 boot sectors, seen in Japanese, American, Malaysian and Korean discs</summary>
        const string PS2_NTSC_HASH = "0bada1426e2c0351b872ef2a9ad2e5a0ac3918f4c53aa53329cb2911a8e16c23";

        /// <summary>SHA256 of PlayStation 2 boot sectors, seen in Japanese discs</summary>
        const string PS2_JAPANESE_HASH = "b82bffb809070d61fe050b7e1545df53d8f3cc648257cdff7502bc0ba6b38870";

        static readonly byte[] _ps3Id =
        {
            0x50, 0x6C, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x33, 0x00, 0x00, 0x00, 0x00
        };

        static readonly byte[] _ps4Id =
        {
            0x50, 0x6C, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x34, 0x00, 0x00, 0x00, 0x00
        };

        static readonly byte[] _ps5Id =
        {
            0x50, 0x6C, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x35, 0x00, 0x00, 0x00, 0x00
        };

        static readonly byte[] _operaId =
        {
            0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01
        };

        // Only present on bootable CDs, but those make more than 99% of all available
        static readonly byte[] _fmTownsBootId =
        {
            0x49, 0x50, 0x4C, 0x34, 0xEB, 0x55, 0x06
        };

        /// <summary>Present on first two seconds of second track, says "COPYRIGHT BANDAI"</summary>
        static readonly byte[] _playdiaCopyright =
        {
            0x43, 0x4F, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, 0x42, 0x41, 0x4E, 0x44, 0x41, 0x49
        };

        static readonly byte[] _pcEngineSignature =
        {
            0x50, 0x43, 0x20, 0x45, 0x6E, 0x67, 0x69, 0x6E, 0x65, 0x20, 0x43, 0x44, 0x2D, 0x52, 0x4F, 0x4D, 0x20, 0x53,
            0x59, 0x53, 0x54, 0x45, 0x4D
        };

        static readonly byte[] _pcFxSignature =
        {
            0x50, 0x43, 0x2D, 0x46, 0x58, 0x3A, 0x48, 0x75, 0x5F, 0x43, 0x44, 0x2D, 0x52, 0x4F, 0x4D
        };

        static readonly byte[] _atariSignature =
        {
            0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41,
            0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52,
            0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41,
            0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x49, 0x52, 0x54, 0x41, 0x52, 0x41, 0x20, 0x49, 0x50, 0x41,
            0x52, 0x50, 0x56, 0x4F, 0x44, 0x45, 0x44, 0x20, 0x54, 0x41, 0x20, 0x41, 0x45, 0x48, 0x44, 0x41, 0x52, 0x45,
            0x41, 0x20, 0x52, 0x54
        };

        /// <summary>This is some kind of header. Every 10 bytes there's an audio byte.</summary>
        static readonly byte[] _videoNowColorFrameMarker =
        {
            0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3,
            0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81,
            0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7,
            0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3,
            0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00,
            0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3,
            0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81,
            0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7,
            0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3,
            0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00,
            0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3,
            0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81,
            0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7,
            0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x00, 0x00, 0x02, 0x01, 0x04, 0x02, 0x06, 0x03, 0xFF, 0x00, 0x08, 0x04,
            0x0A, 0x05, 0x0C, 0x06, 0x0E, 0x07, 0xFF, 0x00, 0x11, 0x08, 0x13, 0x09, 0x15, 0x0A, 0x17, 0x0B, 0xFF, 0x00,
            0x19, 0x0C, 0x1B, 0x0D, 0x1D, 0x0E, 0x1F, 0x0F, 0xFF, 0x00, 0x00, 0x28, 0x02, 0x29, 0x04, 0x2A, 0x06, 0x2B,
            0xFF, 0x00, 0x08, 0x2C, 0x0A, 0x2D, 0x0C, 0x2E, 0x0E, 0x2F, 0xFF, 0x00, 0x11, 0x30, 0x13, 0x31, 0x15, 0x32,
            0x17, 0x33, 0xFF, 0x00, 0x19, 0x34, 0x1B, 0x35, 0x1D, 0x36, 0x1F, 0x37, 0xFF, 0x00, 0x00, 0x38, 0x02, 0x39,
            0x04, 0x3A, 0x06, 0x3B, 0xFF, 0x00, 0x08, 0x3C, 0x0A, 0x3D, 0x0C, 0x3E, 0x0E, 0x3F, 0xFF, 0x00, 0x11, 0x40,
            0x13, 0x41, 0x15, 0x42, 0x17, 0x43, 0xFF, 0x00, 0x19, 0x44, 0x1B, 0x45, 0x1D, 0x46, 0x1F, 0x47, 0xFF, 0x00,
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
            0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
            0xFF, 0xFF, 0xFF, 0x00
        };

        static bool IsData(byte[] sector)
        {
            if(sector?.Length != 2352)
                return false;

            byte[] syncMark =
            {
                0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00
            };

            byte[] testMark = new byte[12];
            Array.Copy(sector, 0, testMark, 0, 12);

            return syncMark.SequenceEqual(testMark) && (sector[0xF] == 0 || sector[0xF] == 1 || sector[0xF] == 2);
        }

        static bool IsScrambledData(byte[] sector, int wantedLba, out int offset)
        {
            offset = 0;

            if(sector?.Length != 2352)
                return false;

            byte[] syncMark =
            {
                0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00
            };

            byte[] testMark = new byte[12];

            for(int i = 0; i <= 2336; i++)
            {
                Array.Copy(sector, i, testMark, 0, 12);

                if(!syncMark.SequenceEqual(testMark) ||
                   (sector[i + 0xF] != 0x60 && sector[i + 0xF] != 0x61 && sector[i + 0xF] != 0x62))
                    continue;

                // De-scramble M and S
                int minute = sector[i + 12] ^ 0x01;
                int second = sector[i + 13] ^ 0x80;
                int frame  = sector[i + 14];

                // Convert to binary
                minute = (minute / 16 * 10) + (minute & 0x0F);
                second = (second / 16 * 10) + (second & 0x0F);
                frame  = (frame  / 16 * 10) + (frame  & 0x0F);

                // Calculate the first found LBA
                int lba = (minute * 60 * 75) + (second * 75) + frame - 150;

                // Calculate the difference between the found LBA and the requested one
                int diff = wantedLba - lba;

                offset = i + (2352 * diff);

                return true;
            }

            return false;
        }

        static byte[] DescrambleAndFixOffset(byte[] sector, int offsetBytes, int sectorsForOffset)
        {
            byte[] descrambled = new byte[2352];

            int offsetFix = offsetBytes < 0 ? (2352 * sectorsForOffset) + offsetBytes : offsetBytes;

            Array.Copy(sector, offsetFix, descrambled, 0, 2352);

            return Sector.Scramble(descrambled);
        }

        /// <summary>Checks if the media corresponds to CD-i.</summary>
        /// <param name="sector0">Contents of LBA 0, with all headers.</param>
        /// <param name="sector16">Contents of LBA 0, with all headers.</param>
        /// <returns><c>true</c> if it corresponds to a CD-i, <c>false</c>otherwise.</returns>
        static bool IsCdi(byte[] sector0, byte[] sector16)
        {
            if(sector16?.Length != 2352)
                return false;

            byte[] cdiMark =
            {
                0x01, 0x43, 0x44, 0x2D
            };

            bool isData = IsData(sector0);

            if(!isData ||
               (sector0[0xF] != 2 && sector0[0xF] != 1))
                return false;

            byte[] testMark = new byte[4];
            Array.Copy(sector16, 24, testMark, 0, 4);

            return cdiMark.SequenceEqual(testMark);
        }

        static bool IsVideoNowColor(byte[] videoFrame)
        {
            if(videoFrame is null ||
               videoFrame.Length < _videoNowColorFrameMarker.Length)
                return false;

            byte[] buffer = new byte[_videoNowColorFrameMarker.Length];

            for(int framePosition = 0; framePosition + buffer.Length < videoFrame.Length; framePosition++)
            {
                Array.Copy(videoFrame, framePosition, buffer, 0, buffer.Length);

                for(int ab = 9; ab < buffer.Length; ab += 10)
                    buffer[ab] = 0;

                if(!_videoNowColorFrameMarker.SequenceEqual(buffer))
                    continue;

                return true;
            }

            return false;
        }

        internal static int GetVideoNowColorOffset(byte[] data)
        {
            byte[] buffer = new byte[_videoNowColorFrameMarker.Length];

            for(int framePosition = 0; framePosition + buffer.Length < data.Length; framePosition++)
            {
                Array.Copy(data, framePosition, buffer, 0, buffer.Length);

                for(int ab = 9; ab < buffer.Length; ab += 10)
                    buffer[ab] = 0;

                if(!_videoNowColorFrameMarker.SequenceEqual(buffer))
                    continue;

                return 18032 - framePosition;
            }

            return 0;
        }

        internal static void DetectDiscType(ref MediaType mediaType, int sessions, FullTOC.CDFullTOC? decodedToc,
                                            Device dev, out bool hiddenTrack, out bool hiddenData,
                                            int firstTrackLastSession, ulong blocks)
        {
            uint                startOfFirstDataTrack = uint.MaxValue;
            DI.DiscInformation? blurayDi              = null;
            byte[]              cmdBuf;
            bool                sense;
            byte                secondSessionFirstTrack = 0;
            byte[]              sector0;
            byte[]              sector1                      = null;
            byte[]              ps2BootSectors               = null;
            byte[]              playdia1                     = null;
            byte[]              playdia2                     = null;
            byte[]              firstDataSectorNotZero       = null;
            byte[]              secondDataSectorNotZero      = null;
            byte[]              firstTrackSecondSession      = null;
            byte[]              firstTrackSecondSessionAudio = null;
            byte[]              videoNowColorFrame;
            hiddenTrack = false;
            hiddenData  = false;

            sense = dev.GetConfiguration(out cmdBuf, out _, 0, MmcGetConfigurationRt.Current, dev.Timeout, out _);

            if(!sense)
            {
                Features.SeparatedFeatures ftr = Features.Separate(cmdBuf);

                AaruConsole.DebugWriteLine("Media-Info command", "GET CONFIGURATION current profile is {0:X4}h",
                                           ftr.CurrentProfile);

                switch(ftr.CurrentProfile)
                {
                    case 0x0001:
                        mediaType = MediaType.GENERIC_HDD;

                        break;
                    case 0x0005:
                        mediaType = MediaType.CDMO;

                        break;
                    case 0x0008:
                        mediaType = MediaType.CD;

                        break;
                    case 0x0009:
                        mediaType = MediaType.CDR;

                        break;
                    case 0x000A:
                        mediaType = MediaType.CDRW;

                        break;
                    case 0x0010:
                        mediaType = MediaType.DVDROM;

                        break;
                    case 0x0011:
                        mediaType = MediaType.DVDR;

                        break;
                    case 0x0012:
                        mediaType = MediaType.DVDRAM;

                        break;
                    case 0x0013:
                    case 0x0014:
                        mediaType = MediaType.DVDRW;

                        break;
                    case 0x0015:
                    case 0x0016:
                        mediaType = MediaType.DVDRDL;

                        break;
                    case 0x0017:
                        mediaType = MediaType.DVDRWDL;

                        break;
                    case 0x0018:
                        mediaType = MediaType.DVDDownload;

                        break;
                    case 0x001A:
                        mediaType = MediaType.DVDPRW;

                        break;
                    case 0x001B:
                        mediaType = MediaType.DVDPR;

                        break;
                    case 0x0020:
                        mediaType = MediaType.DDCD;

                        break;
                    case 0x0021:
                        mediaType = MediaType.DDCDR;

                        break;
                    case 0x0022:
                        mediaType = MediaType.DDCDRW;

                        break;
                    case 0x002A:
                        mediaType = MediaType.DVDPRWDL;

                        break;
                    case 0x002B:
                        mediaType = MediaType.DVDPRDL;

                        break;
                    case 0x0040:
                        mediaType = MediaType.BDROM;

                        break;
                    case 0x0041:
                    case 0x0042:
                        mediaType = MediaType.BDR;

                        break;
                    case 0x0043:
                        mediaType = MediaType.BDRE;

                        break;
                    case 0x0050:
                        mediaType = MediaType.HDDVDROM;

                        break;
                    case 0x0051:
                        mediaType = MediaType.HDDVDR;

                        break;
                    case 0x0052:
                        mediaType = MediaType.HDDVDRAM;

                        break;
                    case 0x0053:
                        mediaType = MediaType.HDDVDRW;

                        break;
                    case 0x0058:
                        mediaType = MediaType.HDDVDRDL;

                        break;
                    case 0x005A:
                        mediaType = MediaType.HDDVDRWDL;

                        break;
                }
            }

            if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == 2) == true)
                secondSessionFirstTrack = decodedToc.Value.TrackDescriptors.Where(t => t.SessionNumber == 2).
                                                     Min(t => t.POINT);

            if(mediaType == MediaType.CD      ||
               mediaType == MediaType.CDROMXA ||
               mediaType == MediaType.CDI)
            {
                sense = dev.ReadAtip(out cmdBuf, out _, dev.Timeout, out _);

                if(!sense)
                {
                    ATIP.CDATIP atip = ATIP.Decode(cmdBuf);

                    if(atip != null)

                        // Only CD-R and CD-RW have ATIP
                        mediaType = atip.DiscType ? MediaType.CDRW : MediaType.CDR;
                }
            }

            if(mediaType == MediaType.CD ||
               mediaType == MediaType.CDROMXA)
            {
                bool hasDataTrack                  = false;
                bool hasAudioTrack                 = false;
                bool allFirstSessionTracksAreAudio = true;
                bool hasVideoTrack                 = false;

                if(decodedToc.HasValue)
                {
                    FullTOC.TrackDataDescriptor a0Track =
                        decodedToc.Value.TrackDescriptors.FirstOrDefault(t => t.POINT == 0xA0 && t.ADR == 1);

                    if(a0Track.POINT == 0xA0)
                        switch(a0Track.PSEC)
                        {
                            case 0x10:
                                AaruConsole.DebugWriteLine("Media detection", "TOC says disc type is CD-i.");
                                mediaType = MediaType.CDI;

                                break;
                            case 0x20:
                                AaruConsole.DebugWriteLine("Media detection", "TOC says disc type is CD-ROM XA.");
                                mediaType = MediaType.CDROMXA;

                                break;
                        }

                    foreach(FullTOC.TrackDataDescriptor track in
                        decodedToc.Value.TrackDescriptors.Where(t => t.POINT > 0 && t.POINT <= 0x99))
                    {
                        if(track.TNO == 1 &&
                           ((TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrack ||
                            (TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrackIncremental))
                            allFirstSessionTracksAreAudio &= firstTrackLastSession != 1;

                        if((TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrack ||
                           (TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrackIncremental)
                        {
                            uint startAddress = (uint)((track.PHOUR * 3600 * 75) + (track.PMIN * 60 * 75) +
                                                       (track.PSEC  * 75)        + track.PFRAME - 150);

                            if(startAddress < startOfFirstDataTrack)
                                startOfFirstDataTrack = startAddress;

                            hasDataTrack                  =  true;
                            allFirstSessionTracksAreAudio &= track.POINT >= firstTrackLastSession;
                        }
                        else
                        {
                            hasAudioTrack = true;
                        }

                        hasVideoTrack |= track.ADR == 4;
                    }
                }

                if(mediaType != MediaType.CDI)
                {
                    if(hasDataTrack                  &&
                       hasAudioTrack                 &&
                       allFirstSessionTracksAreAudio &&
                       sessions == 2)
                    {
                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Disc has audio and data tracks, two sessions, and all data tracks are in second session, setting as CD+.");

                        mediaType = MediaType.CDPLUS;
                    }

                    if(!hasDataTrack &&
                       hasAudioTrack &&
                       sessions == 1)
                    {
                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Disc has only audio tracks in a single session, setting as CD Digital Audio.");

                        mediaType = MediaType.CDDA;
                    }

                    if(hasDataTrack   &&
                       !hasAudioTrack &&
                       sessions == 1)
                    {
                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Disc has only data tracks in a single session, setting as CD-ROM.");

                        mediaType = MediaType.CDROM;
                    }

                    if(hasVideoTrack &&
                       !hasDataTrack &&
                       sessions == 1)
                    {
                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Disc has video tracks in a single session, setting as CD Video.");

                        mediaType = MediaType.CDV;
                    }
                }

                if((mediaType == MediaType.CD || mediaType == MediaType.CDROM) && hasDataTrack)
                {
                    foreach(uint startAddress in decodedToc.Value.TrackDescriptors.
                                                            Where(t => t.POINT > 0 && t.POINT <= 0x99 &&
                                                                       ((TocControl)(t.CONTROL & 0x0D) ==
                                                                        TocControl.DataTrack ||
                                                                        (TocControl)(t.CONTROL & 0x0D) ==
                                                                        TocControl.DataTrackIncremental)).
                                                            Select(track => (uint)((track.PHOUR     * 3600 * 75) +
                                                                                        (track.PMIN * 60   * 75) +
                                                                                        (track.PSEC * 75)        +
                                                                                        track.PFRAME - 150) + 16))
                    {
                        sense = dev.ReadCd(out cmdBuf, out _, startAddress, 2352, 1, MmcSectorTypes.AllTypes, false,
                                           false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                           MmcSubchannel.None, dev.Timeout, out _);

                        if(sense || dev.Error)
                            continue;

                        if(cmdBuf[0]  != 0x00 ||
                           cmdBuf[1]  != 0xFF ||
                           cmdBuf[2]  != 0xFF ||
                           cmdBuf[3]  != 0xFF ||
                           cmdBuf[4]  != 0xFF ||
                           cmdBuf[5]  != 0xFF ||
                           cmdBuf[6]  != 0xFF ||
                           cmdBuf[7]  != 0xFF ||
                           cmdBuf[8]  != 0xFF ||
                           cmdBuf[9]  != 0xFF ||
                           cmdBuf[10] != 0xFF ||
                           cmdBuf[11] != 0x00 ||
                           cmdBuf[15] != 0x02)
                            continue;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Disc has a mode 2 data track, setting as CD-ROM XA.");

                        mediaType = MediaType.CDROMXA;

                        break;
                    }
                }
            }

            if(secondSessionFirstTrack                                                   != 0 &&
               decodedToc?.TrackDescriptors.Any(t => t.POINT == secondSessionFirstTrack) == true)
            {
                FullTOC.TrackDataDescriptor secondSessionFirstTrackTrack =
                    decodedToc.Value.TrackDescriptors.First(t => t.POINT == secondSessionFirstTrack);

                uint firstSectorSecondSessionFirstTrack =
                    (uint)((secondSessionFirstTrackTrack.PHOUR * 3600 * 75) +
                           (secondSessionFirstTrackTrack.PMIN  * 60   * 75) + (secondSessionFirstTrackTrack.PSEC * 75) +
                           secondSessionFirstTrackTrack.PFRAME - 150);

                sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack, 2352, 1,
                                   MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true,
                                   MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                if(!sense &&
                   !dev.Error)
                {
                    firstTrackSecondSession = cmdBuf;
                }
                else
                {
                    sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack, 2352, 1,
                                       MmcSectorTypes.Cdda, false, false, true, MmcHeaderCodes.AllHeaders, true, true,
                                       MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                    if(!sense &&
                       !dev.Error)
                        firstTrackSecondSession = cmdBuf;
                }

                sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack - 1, 2352, 3,
                                   MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true,
                                   MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                if(!sense &&
                   !dev.Error)
                {
                    firstTrackSecondSessionAudio = cmdBuf;
                }
                else
                {
                    sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack - 1, 2352, 3,
                                       MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false,
                                       MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                    if(!sense &&
                       !dev.Error)
                        firstTrackSecondSessionAudio = cmdBuf;
                }
            }

            videoNowColorFrame = new byte[9 * 2352];

            for(int i = 0; i < 9; i++)
            {
                sense = dev.ReadCd(out cmdBuf, out _, (uint)i, 2352, 1, MmcSectorTypes.AllTypes, false, false, true,
                                   MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None,
                                   dev.Timeout, out _);

                if(sense || dev.Error)
                {
                    sense = dev.ReadCd(out cmdBuf, out _, (uint)i, 2352, 1, MmcSectorTypes.Cdda, false, false, false,
                                       MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None,
                                       dev.Timeout, out _);

                    if(sense || !dev.Error)
                    {
                        videoNowColorFrame = null;

                        break;
                    }
                }

                Array.Copy(cmdBuf, 0, videoNowColorFrame, i * 2352, 2352);
            }

            FullTOC.TrackDataDescriptor? firstTrack = decodedToc?.TrackDescriptors.FirstOrDefault(t => t.POINT == 1);

            if(firstTrack?.POINT == 1)
            {
                uint firstTrackSector = (uint)((firstTrack.Value.PHOUR * 3600 * 75) +
                                               (firstTrack.Value.PMIN  * 60   * 75) + (firstTrack.Value.PSEC * 75) +
                                               firstTrack.Value.PFRAME - 150);

                // Check for hidden data before start of track 1
                if(firstTrackSector > 0)
                {
                    sense = dev.ReadCd(out sector0, out _, 0, 2352, 1, MmcSectorTypes.AllTypes, false, false, true,
                                       MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None,
                                       dev.Timeout, out _);

                    if(!dev.Error &&
                       !sense)
                    {
                        hiddenTrack = true;

                        hiddenData = IsData(sector0);

                        if(hiddenData)
                        {
                            sense = dev.ReadCd(out byte[] sector16, out _, 16, 2352, 1, MmcSectorTypes.AllTypes, false,
                                               false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                               MmcSubchannel.None, dev.Timeout, out _);

                            if(!sense &&
                               IsCdi(sector0, sector16))
                            {
                                mediaType = MediaType.CDIREADY;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Disc has a hidden CD-i track in track 1's pregap, setting as CD-i Ready.");

                                return;
                            }
                        }
                        else
                        {
                            hiddenData = IsScrambledData(sector0, 0, out int combinedOffset);

                            if(hiddenData)
                            {
                                int sectorsForOffset = combinedOffset / 2352;

                                if(sectorsForOffset < 0)
                                    sectorsForOffset *= -1;

                                if(combinedOffset % 2352 != 0)
                                    sectorsForOffset++;

                                int lba0  = 0;
                                int lba16 = 16;

                                if(combinedOffset < 0)
                                {
                                    lba0  -= sectorsForOffset;
                                    lba16 -= sectorsForOffset;
                                }

                                sense = dev.ReadCd(out sector0, out _, (uint)lba0, 2352, (uint)sectorsForOffset + 1,
                                                   MmcSectorTypes.AllTypes, false, false, true,
                                                   MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                                   MmcSubchannel.None, dev.Timeout, out _);

                                // Drive does not support reading negative sectors?
                                if(sense && lba0 < 0)
                                {
                                    dev.ReadCd(out sector0, out _, 0, 2352, 2, MmcSectorTypes.AllTypes, false, false,
                                               true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                               MmcSubchannel.None, dev.Timeout, out _);

                                    sector0 = DescrambleAndFixOffset(sector0, combinedOffset, sectorsForOffset);
                                }
                                else
                                    sector0 = DescrambleAndFixOffset(sector0, combinedOffset, sectorsForOffset);

                                dev.ReadCd(out byte[] sector16, out _, (uint)lba16, 2352, (uint)sectorsForOffset + 1,
                                           MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true,
                                           true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                                sector16 = DescrambleAndFixOffset(sector16, combinedOffset, sectorsForOffset);

                                if(IsCdi(sector0, sector16))
                                {
                                    mediaType = MediaType.CDIREADY;

                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Disc has a hidden CD-i track in track 1's pregap, setting as CD-i Ready.");

                                    return;
                                }
                            }
                        }
                    }
                }
            }

            sector0 = null;

            switch(mediaType)
            {
                case MediaType.CD:
                case MediaType.CDDA:
                case MediaType.CDPLUS:
                case MediaType.CDROM:
                case MediaType.CDROMXA:
                {
                    sense = dev.ReadCd(out cmdBuf, out _, 0, 2352, 1, MmcSectorTypes.AllTypes, false, false, true,
                                       MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None,
                                       dev.Timeout, out _);

                    if(!sense &&
                       !dev.Error)
                    {
                        sector0 = new byte[2048];
                        Array.Copy(cmdBuf, 16, sector0, 0, 2048);

                        sense = dev.ReadCd(out cmdBuf, out _, 1, 2352, 1, MmcSectorTypes.AllTypes, false, false, true,
                                           MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                           MmcSubchannel.None, dev.Timeout, out _);

                        if(!sense &&
                           !dev.Error)
                        {
                            sector1 = new byte[2048];
                            Array.Copy(cmdBuf, 16, sector1, 0, 2048);
                        }

                        sense = dev.ReadCd(out cmdBuf, out _, 4200, 2352, 1, MmcSectorTypes.AllTypes, false, false,
                                           true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                           MmcSubchannel.None, dev.Timeout, out _);

                        if(!sense &&
                           !dev.Error)
                        {
                            playdia1 = new byte[2048];
                            Array.Copy(cmdBuf, 24, playdia1, 0, 2048);
                        }

                        sense = dev.ReadCd(out cmdBuf, out _, 4201, 2352, 1, MmcSectorTypes.AllTypes, false, false,
                                           true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                           MmcSubchannel.None, dev.Timeout, out _);

                        if(!sense &&
                           !dev.Error)
                        {
                            playdia2 = new byte[2048];
                            Array.Copy(cmdBuf, 24, playdia2, 0, 2048);
                        }

                        if(startOfFirstDataTrack != uint.MaxValue)
                        {
                            sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack, 2352, 1,
                                               MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders,
                                               true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                firstDataSectorNotZero = new byte[2048];
                                Array.Copy(cmdBuf, 16, firstDataSectorNotZero, 0, 2048);
                            }

                            sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack + 1, 2352, 1,
                                               MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders,
                                               true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                secondDataSectorNotZero = new byte[2048];
                                Array.Copy(cmdBuf, 16, secondDataSectorNotZero, 0, 2048);
                            }
                        }

                        var ps2Ms = new MemoryStream();

                        for(uint p = 0; p < 12; p++)
                        {
                            sense = dev.ReadCd(out cmdBuf, out _, p, 2352, 1, MmcSectorTypes.AllTypes, false, false,
                                               true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                               MmcSubchannel.None, dev.Timeout, out _);

                            if(sense || dev.Error)
                                break;

                            ps2Ms.Write(cmdBuf, cmdBuf[0x0F] == 0x02 ? 24 : 16, 2048);
                        }

                        if(ps2Ms.Length == 0x6000)
                            ps2BootSectors = ps2Ms.ToArray();
                    }
                    else
                    {
                        sense = dev.ReadCd(out cmdBuf, out _, 0, 2324, 1, MmcSectorTypes.Mode2, false, false, false,
                                           MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None,
                                           dev.Timeout, out _);

                        if(!sense &&
                           !dev.Error)
                        {
                            sector0 = new byte[2048];
                            Array.Copy(cmdBuf, 0, sector0, 0, 2048);

                            sense = dev.ReadCd(out cmdBuf, out _, 1, 2324, 1, MmcSectorTypes.Mode2, false, false, false,
                                               MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None,
                                               dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                sector1 = new byte[2048];
                                Array.Copy(cmdBuf, 1, sector0, 0, 2048);
                            }

                            sense = dev.ReadCd(out cmdBuf, out _, 4200, 2324, 1, MmcSectorTypes.Mode2, false, false,
                                               false, MmcHeaderCodes.None, true, false, MmcErrorField.None,
                                               MmcSubchannel.None, dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                playdia1 = new byte[2048];
                                Array.Copy(cmdBuf, 0, playdia1, 0, 2048);
                            }

                            sense = dev.ReadCd(out cmdBuf, out _, 4201, 2324, 1, MmcSectorTypes.Mode2, false, false,
                                               false, MmcHeaderCodes.None, true, false, MmcErrorField.None,
                                               MmcSubchannel.None, dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                playdia2 = new byte[2048];
                                Array.Copy(cmdBuf, 0, playdia2, 0, 2048);
                            }

                            if(startOfFirstDataTrack != uint.MaxValue)
                            {
                                sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack, 2324, 1,
                                                   MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true,
                                                   false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                                if(!sense &&
                                   !dev.Error)
                                {
                                    firstDataSectorNotZero = new byte[2048];
                                    Array.Copy(cmdBuf, 0, firstDataSectorNotZero, 0, 2048);
                                }

                                sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack + 1, 2324, 1,
                                                   MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true,
                                                   false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _);

                                if(!sense &&
                                   !dev.Error)
                                {
                                    secondDataSectorNotZero = new byte[2048];
                                    Array.Copy(cmdBuf, 0, secondDataSectorNotZero, 0, 2048);
                                }
                            }

                            var ps2Ms = new MemoryStream();

                            for(uint p = 0; p < 12; p++)
                            {
                                sense = dev.ReadCd(out cmdBuf, out _, p, 2324, 1, MmcSectorTypes.Mode2, false, false,
                                                   false, MmcHeaderCodes.None, true, false, MmcErrorField.None,
                                                   MmcSubchannel.None, dev.Timeout, out _);

                                if(sense || dev.Error)
                                    break;

                                ps2Ms.Write(cmdBuf, 0, 2048);
                            }

                            if(ps2Ms.Length == 0x6000)
                                ps2BootSectors = ps2Ms.ToArray();
                        }
                        else
                        {
                            sense = dev.ReadCd(out cmdBuf, out _, 0, 2048, 1, MmcSectorTypes.Mode1, false, false, false,
                                               MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None,
                                               dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                sector0 = cmdBuf;

                                sense = dev.ReadCd(out cmdBuf, out _, 0, 2048, 1, MmcSectorTypes.Mode1, false, false,
                                                   false, MmcHeaderCodes.None, true, false, MmcErrorField.None,
                                                   MmcSubchannel.None, dev.Timeout, out _);

                                if(!sense &&
                                   !dev.Error)
                                    sector1 = cmdBuf;

                                sense = dev.ReadCd(out cmdBuf, out _, 0, 2048, 12, MmcSectorTypes.Mode1, false, false,
                                                   false, MmcHeaderCodes.None, true, false, MmcErrorField.None,
                                                   MmcSubchannel.None, dev.Timeout, out _);

                                if(!sense &&
                                   !dev.Error)
                                    ps2BootSectors = cmdBuf;

                                if(startOfFirstDataTrack != uint.MaxValue)
                                {
                                    sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack, 2048, 1,
                                                       MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None,
                                                       true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout,
                                                       out _);

                                    if(!sense &&
                                       !dev.Error)
                                        firstDataSectorNotZero = cmdBuf;

                                    sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack + 1, 2048, 1,
                                                       MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None,
                                                       true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout,
                                                       out _);

                                    if(!sense &&
                                       !dev.Error)
                                        secondDataSectorNotZero = cmdBuf;
                                }
                            }
                            else
                            {
                                goto case MediaType.DVDROM;
                            }
                        }
                    }

                    break;
                }

                // TODO: Check for CD-i Ready
                case MediaType.CDI: break;
                case MediaType.DVDROM:
                case MediaType.HDDVDROM:
                case MediaType.BDROM:
                case MediaType.UHDBD:
                case MediaType.Unknown:
                    if(mediaType == MediaType.BDROM ||
                       mediaType == MediaType.UHDBD)
                    {
                        sense = dev.ReadDiscStructure(out cmdBuf, out _, MmcDiscStructureMediaType.Bd, 0, 0,
                                                      MmcDiscStructureFormat.DiscInformation, 0, dev.Timeout, out _);

                        if(!sense)
                            blurayDi = DI.Decode(cmdBuf);
                    }

                    sense = dev.Read16(out cmdBuf, out _, 0, false, false, false, 0, 2048, 0, 1, false, dev.Timeout,
                                       out _);

                    if(!sense &&
                       !dev.Error)
                    {
                        sector0 = cmdBuf;

                        sense = dev.Read16(out cmdBuf, out _, 0, false, false, false, 1, 2048, 0, 1, false, dev.Timeout,
                                           out _);

                        if(!sense &&
                           !dev.Error)
                            sector1 = cmdBuf;

                        sense = dev.Read16(out cmdBuf, out _, 0, false, false, false, 0, 2048, 0, 12, false,
                                           dev.Timeout, out _);

                        if(!sense     &&
                           !dev.Error &&
                           cmdBuf.Length == 0x6000)
                            ps2BootSectors = cmdBuf;
                    }
                    else
                    {
                        sense = dev.Read12(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 1, false,
                                           dev.Timeout, out _);

                        if(!sense &&
                           !dev.Error)
                        {
                            sector0 = cmdBuf;

                            sense = dev.Read12(out cmdBuf, out _, 0, false, false, false, false, 1, 2048, 0, 1, false,
                                               dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                                sector1 = cmdBuf;

                            sense = dev.Read12(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 12, false,
                                               dev.Timeout, out _);

                            if(!sense     &&
                               !dev.Error &&
                               cmdBuf.Length == 0x6000)
                                ps2BootSectors = cmdBuf;
                        }
                        else
                        {
                            sense = dev.Read10(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 1,
                                               dev.Timeout, out _);

                            if(!sense &&
                               !dev.Error)
                            {
                                sector0 = cmdBuf;

                                sense = dev.Read10(out cmdBuf, out _, 0, false, false, false, false, 1, 2048, 0, 1,
                                                   dev.Timeout, out _);

                                if(!sense &&
                                   !dev.Error)
                                    sector1 = cmdBuf;

                                sense = dev.Read10(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 12,
                                                   dev.Timeout, out _);

                                if(!sense     &&
                                   !dev.Error &&
                                   cmdBuf.Length == 0x6000)
                                    ps2BootSectors = cmdBuf;
                            }
                            else
                            {
                                sense = dev.Read6(out cmdBuf, out _, 0, 2048, 1, dev.Timeout, out _);

                                if(!sense &&
                                   !dev.Error)
                                {
                                    sector0 = cmdBuf;

                                    sense = dev.Read6(out cmdBuf, out _, 1, 2048, 1, dev.Timeout, out _);

                                    if(!sense &&
                                       !dev.Error)
                                        sector1 = cmdBuf;

                                    sense = dev.Read6(out cmdBuf, out _, 0, 2048, 12, dev.Timeout, out _);

                                    if(!sense     &&
                                       !dev.Error &&
                                       cmdBuf.Length == 0x6000)
                                        ps2BootSectors = cmdBuf;
                                }
                            }
                        }
                    }

                    if(mediaType == MediaType.DVDROM)
                    {
                        sense = dev.ReadDiscStructure(out cmdBuf, out _, MmcDiscStructureMediaType.Dvd, 0, 0,
                                                      MmcDiscStructureFormat.PhysicalInformation, 0, dev.Timeout,
                                                      out _);

                        if(!sense)
                        {
                            PFI.PhysicalFormatInformation? pfi = PFI.Decode(cmdBuf, mediaType);

                            if(pfi != null)
                            {
                                switch(pfi.Value.DiskCategory)
                                {
                                    case DiskCategory.DVDPR:
                                        mediaType = MediaType.DVDPR;

                                        break;
                                    case DiskCategory.DVDPRDL:
                                        mediaType = MediaType.DVDPRDL;

                                        break;
                                    case DiskCategory.DVDPRW:
                                        mediaType = MediaType.DVDPRW;

                                        break;
                                    case DiskCategory.DVDPRWDL:
                                        mediaType = MediaType.DVDPRWDL;

                                        break;
                                    case DiskCategory.DVDR:
                                        mediaType = pfi.Value.PartVersion >= 6 ? MediaType.DVDRDL : MediaType.DVDR;

                                        break;
                                    case DiskCategory.DVDRAM:
                                        mediaType = MediaType.DVDRAM;

                                        break;
                                    case DiskCategory.DVDRW:
                                        mediaType = pfi.Value.PartVersion >= 15 ? MediaType.DVDRWDL : MediaType.DVDRW;

                                        break;
                                    case DiskCategory.HDDVDR:
                                        mediaType = MediaType.HDDVDR;

                                        break;
                                    case DiskCategory.HDDVDRAM:
                                        mediaType = MediaType.HDDVDRAM;

                                        break;
                                    case DiskCategory.HDDVDROM:
                                        mediaType = MediaType.HDDVDROM;

                                        break;
                                    case DiskCategory.HDDVDRW:
                                        mediaType = MediaType.HDDVDRW;

                                        break;
                                    case DiskCategory.Nintendo:
                                        mediaType = pfi.Value.DiscSize == DVDSize.Eighty ? MediaType.GOD
                                                        : MediaType.WOD;

                                        break;
                                    case DiskCategory.UMD:
                                        mediaType = MediaType.UMD;

                                        break;
                                }
                            }
                        }

                        sense = dev.ReadDiscStructure(out cmdBuf, out _, MmcDiscStructureMediaType.Dvd, 0, 0,
                                                      MmcDiscStructureFormat.DiscManufacturingInformation, 0,
                                                      dev.Timeout, out _);

                        if(!sense)
                        {
                            if(DMI.IsXbox(cmdBuf))
                            {
                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found Xbox DMI, setting disc type to Xbox Game Disc (XGD).");

                                mediaType = MediaType.XGD;

                                return;
                            }

                            if(DMI.IsXbox360(cmdBuf))
                            {
                                // All XGD3 all have the same number of blocks
                                if(blocks == 25063   || // Locked (or non compatible drive)
                                   blocks == 4229664 || // Xtreme unlock
                                   blocks == 4246304)   // Wxripper unlock
                                {
                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found Xbox 360 DMI with {0} blocks, setting disc type to Xbox 360 Game Disc 3 (XGD3).");

                                    mediaType = MediaType.XGD3;

                                    return;
                                }

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found Xbox 360 DMI with {0} blocks, setting disc type to Xbox 360 Game Disc 2 (XGD2).");

                                mediaType = MediaType.XGD2;

                                return;
                            }
                        }
                    }

                    break;

                // Recordables will be checked for PhotoCD only
                case MediaType.CDR:
                    // Check if ISO9660
                    sense = dev.Read12(out byte[] isoSector, out _, 0, false, false, false, false, 16, 2048, 0, 1,
                                       false, dev.Timeout, out _);

                    // Sector 16 reads, and contains "CD001" magic?
                    if(sense                ||
                       isoSector[1] != 0x43 ||
                       isoSector[2] != 0x44 ||
                       isoSector[3] != 0x30 ||
                       isoSector[4] != 0x30 ||
                       isoSector[5] != 0x31)
                        return;

                    // From sectors 16 to 31
                    uint isoSectorPosition = 16;

                    while(isoSectorPosition < 32)
                    {
                        sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, isoSectorPosition, 2048,
                                           0, 1, false, dev.Timeout, out _);

                        // If sector cannot be read, break here
                        if(sense)
                            break;

                        // If sector does not contain "CD001" magic, break
                        if(isoSector[1] != 0x43 ||
                           isoSector[2] != 0x44 ||
                           isoSector[3] != 0x30 ||
                           isoSector[4] != 0x30 ||
                           isoSector[5] != 0x31)
                            break;

                        // If it is PVD or end of descriptor chain, break
                        if(isoSector[0] == 1 ||
                           isoSector[0] == 255)
                            break;

                        isoSectorPosition++;
                    }

                    // If it's not an ISO9660 PVD, return
                    if(isoSector[0] != 1    ||
                       isoSector[1] != 0x43 ||
                       isoSector[2] != 0x44 ||
                       isoSector[3] != 0x30 ||
                       isoSector[4] != 0x30 ||
                       isoSector[5] != 0x31)
                        return;

                    uint rootStart  = BitConverter.ToUInt32(isoSector, 158);
                    uint rootLength = BitConverter.ToUInt32(isoSector, 166);

                    if(rootStart  == 0 ||
                       rootLength == 0)
                        return;

                    rootLength /= 2048;

                    try
                    {
                        using var rootMs = new MemoryStream();

                        for(uint i = 0; i < rootLength; i++)
                        {
                            sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, rootStart + i, 2048,
                                               0, 1, false, dev.Timeout, out _);

                            if(sense)
                                break;

                            rootMs.Write(isoSector, 0, 2048);
                        }

                        isoSector = rootMs.ToArray();
                    }
                    catch
                    {
                        return;
                    }

                    if(isoSector.Length < 2048)
                        return;

                    int  rootPos   = 0;
                    uint pcdStart  = 0;
                    uint pcdLength = 0;

                    while(isoSector[rootPos]           > 0                &&
                          rootPos                      < isoSector.Length &&
                          rootPos + isoSector[rootPos] <= isoSector.Length)
                    {
                        int    nameLen = isoSector[rootPos + 32];
                        byte[] tmpName = new byte[nameLen];
                        Array.Copy(isoSector, rootPos + 33, tmpName, 0, nameLen);
                        string name = StringHandlers.CToString(tmpName).ToUpperInvariant();

                        if(name.EndsWith(";1", StringComparison.InvariantCulture))
                            name = name.Substring(0, name.Length - 2);

                        if(name                             == "PHOTO_CD" &&
                           (isoSector[rootPos + 25] & 0x02) == 0x02)
                        {
                            pcdStart  = BitConverter.ToUInt32(isoSector, rootPos + 2);
                            pcdLength = BitConverter.ToUInt32(isoSector, rootPos + 10) / 2048;
                        }

                        rootPos += isoSector[rootPos];
                    }

                    if(pcdLength > 0)
                    {
                        try
                        {
                            using var pcdMs = new MemoryStream();

                            for(uint i = 0; i < pcdLength; i++)
                            {
                                sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, pcdStart + i,
                                                   2048, 0, 1, false, dev.Timeout, out _);

                                if(sense)
                                    break;

                                pcdMs.Write(isoSector, 0, 2048);
                            }

                            isoSector = pcdMs.ToArray();
                        }
                        catch
                        {
                            return;
                        }

                        if(isoSector.Length < 2048)
                            return;

                        for(int pi = 0; pi < pcdLength; pi++)
                        {
                            int  pcdPos  = pi * 2048;
                            uint infoPos = 0;

                            while(isoSector[pcdPos]          > 0                &&
                                  pcdPos                     < isoSector.Length &&
                                  pcdPos + isoSector[pcdPos] <= isoSector.Length)
                            {
                                int    nameLen = isoSector[pcdPos + 32];
                                byte[] tmpName = new byte[nameLen];
                                Array.Copy(isoSector, pcdPos + 33, tmpName, 0, nameLen);
                                string name = StringHandlers.CToString(tmpName).ToUpperInvariant();

                                if(name.EndsWith(";1", StringComparison.InvariantCulture))
                                    name = name.Substring(0, name.Length - 2);

                                if(name == "INFO.PCD")
                                {
                                    infoPos = BitConverter.ToUInt32(isoSector, pcdPos + 2);

                                    break;
                                }

                                pcdPos += isoSector[pcdPos];
                            }

                            if(infoPos > 0)
                            {
                                sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, infoPos, 2048,
                                                   0, 1, false, dev.Timeout, out _);

                                if(sense)
                                    break;

                                byte[] systemId = new byte[8];
                                Array.Copy(isoSector, 0, systemId, 0, 8);

                                string id = StringHandlers.CToString(systemId).TrimEnd();

                                switch(id)
                                {
                                    case "PHOTO_CD":
                                        mediaType = MediaType.PCD;

                                        AaruConsole.DebugWriteLine("Media detection",
                                                                   "Found Photo CD description file, setting disc type to Photo CD.");

                                        return;
                                }
                            }
                        }
                    }

                    break;

                // Other recordables will not be checked
                case MediaType.CDRW:
                case MediaType.CDMRW:
                case MediaType.DDCDR:
                case MediaType.DDCDRW:
                case MediaType.DVDR:
                case MediaType.DVDRW:
                case MediaType.DVDPR:
                case MediaType.DVDPRW:
                case MediaType.DVDPRWDL:
                case MediaType.DVDRDL:
                case MediaType.DVDPRDL:
                case MediaType.DVDRAM:
                case MediaType.DVDRWDL:
                case MediaType.DVDDownload:
                case MediaType.HDDVDRAM:
                case MediaType.HDDVDR:
                case MediaType.HDDVDRW:
                case MediaType.HDDVDRDL:
                case MediaType.HDDVDRWDL:
                case MediaType.BDR:
                case MediaType.BDRE:
                case MediaType.BDRXL:
                case MediaType.BDREXL: return;
            }

            if(sector0 == null)
                return;

            switch(mediaType)
            {
                case MediaType.CD:
                case MediaType.CDDA:
                case MediaType.CDPLUS:
                case MediaType.CDROM:
                case MediaType.CDROMXA:
                    // TODO: Pippin requires interpreting Apple Partition Map, reading HFS and checking for Pippin signatures
                {
                    if(CD.DecodeIPBin(sector0).HasValue)
                    {
                        mediaType = MediaType.MEGACD;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Found Mega/Sega CD IP.BIN, setting disc type to Mega CD.");

                        return;
                    }

                    if(Saturn.DecodeIPBin(sector0).HasValue)
                    {
                        mediaType = MediaType.SATURNCD;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Found Sega Saturn IP.BIN, setting disc type to Saturn CD.");

                        return;
                    }

                    // Are GDR detectable ???
                    if(Dreamcast.DecodeIPBin(sector0).HasValue)
                    {
                        mediaType = MediaType.GDROM;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Found Sega Dreamcast IP.BIN, setting disc type to GD-ROM.");

                        return;
                    }

                    if(ps2BootSectors is { Length: 0x6000 })
                    {
                        // The decryption key is applied as XOR. As first byte is originally always NULL, it gives us the key :)
                        byte decryptByte = ps2BootSectors[0];

                        for(int i = 0; i < 0x6000; i++)
                            ps2BootSectors[i] ^= decryptByte;

                        string ps2BootSectorsHash = Sha256Context.Data(ps2BootSectors, out _);

                        AaruConsole.DebugWriteLine("Media-info Command", "PlayStation 2 boot sectors SHA256: {0}",
                                                   ps2BootSectorsHash);

                        if(ps2BootSectorsHash == PS2_PAL_HASH  ||
                           ps2BootSectorsHash == PS2_NTSC_HASH ||
                           ps2BootSectorsHash == PS2_JAPANESE_HASH)
                        {
                            mediaType = MediaType.PS2CD;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Sony PlayStation 2 boot sectors, setting disc type to PS2 CD.");

                            goto hasPs2CdBoot;
                        }
                    }

                    if(sector0 != null)
                    {
                        byte[] syncBytes = new byte[7];
                        Array.Copy(sector0, 0, syncBytes, 0, 7);

                        if(_operaId.SequenceEqual(syncBytes))
                        {
                            mediaType = MediaType.ThreeDO;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Opera filesystem, setting disc type to 3DO.");

                            return;
                        }

                        if(_fmTownsBootId.SequenceEqual(syncBytes))
                        {
                            mediaType = MediaType.FMTOWNS;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found FM-Towns boot, setting disc type to FM-Towns.");

                            return;
                        }
                    }

                    if(playdia1 != null &&
                       playdia2 != null)
                    {
                        byte[] pd1 = new byte[_playdiaCopyright.Length];
                        byte[] pd2 = new byte[_playdiaCopyright.Length];

                        Array.Copy(playdia1, 38, pd1, 0, pd1.Length);
                        Array.Copy(playdia2, 0, pd2, 0, pd1.Length);

                        if(_playdiaCopyright.SequenceEqual(pd1) &&
                           _playdiaCopyright.SequenceEqual(pd2))
                        {
                            mediaType = MediaType.Playdia;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Playdia copyright, setting disc type to Playdia.");

                            return;
                        }
                    }

                    if(secondDataSectorNotZero != null)
                    {
                        byte[] pce = new byte[_pcEngineSignature.Length];
                        Array.Copy(secondDataSectorNotZero, 32, pce, 0, pce.Length);

                        if(_pcEngineSignature.SequenceEqual(pce))
                        {
                            mediaType = MediaType.SuperCDROM2;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found PC-Engine CD signature, setting disc type to Super CD-ROM².");

                            return;
                        }
                    }

                    if(firstDataSectorNotZero != null)
                    {
                        byte[] pcfx = new byte[_pcFxSignature.Length];
                        Array.Copy(firstDataSectorNotZero, 0, pcfx, 0, pcfx.Length);

                        if(_pcFxSignature.SequenceEqual(pcfx))
                        {
                            mediaType = MediaType.PCFX;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found PC-FX copyright, setting disc type to PC-FX.");

                            return;
                        }
                    }

                    if(firstTrackSecondSessionAudio != null)
                    {
                        byte[] jaguar = new byte[_atariSignature.Length];

                        for(int i = 0; i + jaguar.Length <= firstTrackSecondSessionAudio.Length; i += 2)
                        {
                            Array.Copy(firstTrackSecondSessionAudio, i, jaguar, 0, jaguar.Length);

                            if(!_atariSignature.SequenceEqual(jaguar))
                                continue;

                            mediaType = MediaType.JaguarCD;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Atari signature, setting disc type to Jaguar CD.");

                            break;
                        }
                    }

                    if(firstTrackSecondSession?.Length >= 2336)
                    {
                        byte[] milcd = new byte[2048];
                        Array.Copy(firstTrackSecondSession, 24, milcd, 0, 2048);

                        if(Dreamcast.DecodeIPBin(milcd).HasValue)
                        {
                            mediaType = MediaType.MilCD;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Sega Dreamcast IP.BIN on second session, setting disc type to MilCD.");

                            return;
                        }
                    }

                    // TODO: Detect black and white VideoNow
                    // TODO: Detect VideoNow XP
                    if(IsVideoNowColor(videoNowColorFrame))
                    {
                        mediaType = MediaType.VideoNowColor;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Found VideoNow! Color frame, setting disc type to VideoNow Color.");

                        return;
                    }

                    // Check CD+G, CD+EG and CD+MIDI
                    if(mediaType == MediaType.CDDA)
                    {
                        sense = dev.ReadCd(out byte[] subBuf, out _, 150, 96, 8, MmcSectorTypes.Cdda, false, false,
                                           false, MmcHeaderCodes.None, false, false, MmcErrorField.None,
                                           MmcSubchannel.Raw, dev.Timeout, out _);

                        if(!sense)
                        {
                            bool cdg    = false;
                            bool cdeg   = false;
                            bool cdmidi = false;

                            for(int i = 0; i < 8; i++)
                            {
                                byte[] tmpSub = new byte[96];
                                Array.Copy(subBuf, i * 96, tmpSub, 0, 96);
                                DetectRwPackets(tmpSub, out bool cdgPacket, out bool cdegPacket, out bool cdmidiPacket);

                                if(cdgPacket)
                                    cdg = true;

                                if(cdegPacket)
                                    cdeg = true;

                                if(cdmidiPacket)
                                    cdmidi = true;
                            }

                            if(cdeg)
                            {
                                mediaType = MediaType.CDEG;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found enhanced graphics RW packet, setting disc type to CD+EG.");

                                return;
                            }

                            if(cdg)
                            {
                                mediaType = MediaType.CDG;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found graphics RW packet, setting disc type to CD+G.");

                                return;
                            }

                            if(cdmidi)
                            {
                                mediaType = MediaType.CDMIDI;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found MIDI RW packet, setting disc type to CD+MIDI.");

                                return;
                            }
                        }
                    }

                    // If it has a PS2 boot area it can still be PS1, so check for SYSTEM.CNF below
                    hasPs2CdBoot:

                    // Check if ISO9660
                    sense = dev.Read12(out byte[] isoSector, out _, 0, false, false, false, false, 16, 2048, 0, 1,
                                       false, dev.Timeout, out _);

                    // Sector 16 reads, and contains "CD001" magic?
                    if(sense                ||
                       isoSector[1] != 0x43 ||
                       isoSector[2] != 0x44 ||
                       isoSector[3] != 0x30 ||
                       isoSector[4] != 0x30 ||
                       isoSector[5] != 0x31)
                        return;

                    // From sectors 16 to 31
                    uint isoSectorPosition = 16;

                    while(isoSectorPosition < 32)
                    {
                        sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, isoSectorPosition, 2048,
                                           0, 1, false, dev.Timeout, out _);

                        // If sector cannot be read, break here
                        if(sense)
                            break;

                        // If sector does not contain "CD001" magic, break
                        if(isoSector[1] != 0x43 ||
                           isoSector[2] != 0x44 ||
                           isoSector[3] != 0x30 ||
                           isoSector[4] != 0x30 ||
                           isoSector[5] != 0x31)
                            break;

                        // If it is PVD or end of descriptor chain, break
                        if(isoSector[0] == 1 ||
                           isoSector[0] == 255)
                            break;

                        isoSectorPosition++;
                    }

                    // If it's not an ISO9660 PVD, return
                    if(isoSector[0] != 1    ||
                       isoSector[1] != 0x43 ||
                       isoSector[2] != 0x44 ||
                       isoSector[3] != 0x30 ||
                       isoSector[4] != 0x30 ||
                       isoSector[5] != 0x31)
                        return;

                    uint rootStart  = BitConverter.ToUInt32(isoSector, 158);
                    uint rootLength = BitConverter.ToUInt32(isoSector, 166);

                    if(rootStart  == 0 ||
                       rootLength == 0)
                        return;

                    rootLength /= 2048;

                    try
                    {
                        using var rootMs = new MemoryStream();

                        for(uint i = 0; i < rootLength; i++)
                        {
                            sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, rootStart + i, 2048,
                                               0, 1, false, dev.Timeout, out _);

                            if(sense)
                                break;

                            rootMs.Write(isoSector, 0, 2048);
                        }

                        isoSector = rootMs.ToArray();
                    }
                    catch
                    {
                        return;
                    }

                    if(isoSector.Length < 2048)
                        return;

                    List<string> rootEntries   = new List<string>();
                    uint         ngcdIplStart  = 0;
                    uint         ngcdIplLength = 0;
                    uint         vcdStart      = 0;
                    uint         vcdLength     = 0;
                    uint         pcdStart      = 0;
                    uint         pcdLength     = 0;
                    uint         ps1Start      = 0;
                    uint         ps1Length     = 0;

                    for(int ri = 0; ri < rootLength; ri++)
                    {
                        int rootPos = ri * 2048;

                        while(rootPos                      < isoSector.Length &&
                              isoSector[rootPos]           > 0                &&
                              rootPos + isoSector[rootPos] <= isoSector.Length)
                        {
                            int    nameLen = isoSector[rootPos + 32];
                            byte[] tmpName = new byte[nameLen];
                            Array.Copy(isoSector, rootPos + 33, tmpName, 0, nameLen);
                            string name = StringHandlers.CToString(tmpName).ToUpperInvariant();

                            if(name.EndsWith(";1", StringComparison.InvariantCulture))
                                name = name.Substring(0, name.Length - 2);

                            rootEntries.Add(name);

                            switch(name)
                            {
                                case "IPL.TXT":
                                    ngcdIplStart  = BitConverter.ToUInt32(isoSector, rootPos + 2);
                                    ngcdIplLength = BitConverter.ToUInt32(isoSector, rootPos + 10);

                                    break;

                                case "VCD" when (isoSector[rootPos   + 25] & 0x02) == 0x02:
                                case "SVCD" when (isoSector[rootPos  + 25] & 0x02) == 0x02:
                                case "HQVCD" when (isoSector[rootPos + 25] & 0x02) == 0x02:
                                    vcdStart  = BitConverter.ToUInt32(isoSector, rootPos + 2);
                                    vcdLength = BitConverter.ToUInt32(isoSector, rootPos + 10) / 2048;

                                    break;
                                case "PHOTO_CD" when (isoSector[rootPos + 25] & 0x02) == 0x02:
                                    pcdStart  = BitConverter.ToUInt32(isoSector, rootPos + 2);
                                    pcdLength = BitConverter.ToUInt32(isoSector, rootPos + 10) / 2048;

                                    break;
                                case "SYSTEM.CNF":
                                    ps1Start  = BitConverter.ToUInt32(isoSector, rootPos + 2);
                                    ps1Length = BitConverter.ToUInt32(isoSector, rootPos + 10);

                                    break;
                            }

                            rootPos += isoSector[rootPos];
                        }
                    }

                    if(rootEntries.Count == 0)
                        return;

                    if(rootEntries.Contains("CD32.TM"))
                    {
                        mediaType = MediaType.CD32;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Found CD32.TM file in root, setting disc type to Amiga CD32.");

                        return;
                    }

                    if(rootEntries.Contains("CDTV.TM"))
                    {
                        mediaType = MediaType.CDTV;

                        AaruConsole.DebugWriteLine("Media detection",
                                                   "Found CDTV.TM file in root, setting disc type to Commodore CDTV.");

                        return;
                    }

                    // "IPL.TXT" length
                    if(ngcdIplLength > 0)
                    {
                        uint ngcdSectors = ngcdIplLength / 2048;

                        if(ngcdIplLength % 2048 > 0)
                            ngcdSectors++;

                        string iplTxt;

                        // Read "IPL.TXT"
                        try
                        {
                            using var ngcdMs = new MemoryStream();

                            for(uint i = 0; i < ngcdSectors; i++)
                            {
                                sense = dev.Read12(out isoSector, out _, 0, false, false, false, false,
                                                   ngcdIplStart + i, 2048, 0, 1, false, dev.Timeout, out _);

                                if(sense)
                                    break;

                                ngcdMs.Write(isoSector, 0, 2048);
                            }

                            isoSector = ngcdMs.ToArray();
                            iplTxt    = Encoding.ASCII.GetString(isoSector);
                        }
                        catch
                        {
                            iplTxt = null;
                        }

                        // Check "IPL.TXT" lines
                        if(iplTxt != null)
                        {
                            using var sr = new StringReader(iplTxt);

                            bool correctNeoGeoCd = true;
                            int  lineNumber      = 0;

                            while(sr.Peek() > 0)
                            {
                                string line = sr.ReadLine();

                                // End of file
                                if(line is null ||
                                   line.Length == 0)
                                {
                                    if(lineNumber == 0)
                                        correctNeoGeoCd = false;

                                    break;
                                }

                                // Split line by comma
                                string[] split = line.Split(',');

                                // Empty line
                                if(split.Length == 0)
                                    continue;

                                // More than 3 entries
                                if(split.Length != 3)
                                {
                                    if(line[0] < 0x20)
                                        break;

                                    correctNeoGeoCd = false;

                                    break;
                                }

                                // Split filename
                                string[] split2 = split[0].Split('.');

                                // Filename must have two parts, name and extension
                                if(split2.Length != 2)
                                {
                                    correctNeoGeoCd = false;

                                    break;
                                }

                                // Name must be smaller or equal to 8 characters
                                if(split2[0].Length > 8)
                                {
                                    correctNeoGeoCd = false;

                                    break;
                                }

                                // Extension must be smaller or equal to 8 characters
                                if(split2[1].Length > 3)
                                {
                                    correctNeoGeoCd = false;

                                    break;
                                }

                                // Second part must be a single digit
                                if(split[1].Length != 1 ||
                                   !byte.TryParse(split[1], out _))
                                {
                                    correctNeoGeoCd = false;

                                    break;
                                }

                                // Third part must be bigger or equal to 1 and smaller or equal to 8
                                if(split[2].Length < 1 ||
                                   split[2].Length > 8)
                                {
                                    correctNeoGeoCd = false;

                                    break;
                                }

                                try
                                {
                                    _ = Convert.ToUInt32(split[2], 16);
                                }
                                catch
                                {
                                    correctNeoGeoCd = false;

                                    break;
                                }

                                lineNumber++;
                            }

                            if(correctNeoGeoCd)
                            {
                                mediaType = MediaType.NeoGeoCD;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found correct IPL.TXT file in root, setting disc type to Neo Geo CD.");

                                return;
                            }
                        }
                    }

                    if(vcdLength > 0)
                    {
                        try
                        {
                            using var vcdMs = new MemoryStream();

                            for(uint i = 0; i < vcdLength; i++)
                            {
                                sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, vcdStart + i,
                                                   2048, 0, 1, false, dev.Timeout, out _);

                                if(sense)
                                    break;

                                vcdMs.Write(isoSector, 0, 2048);
                            }

                            isoSector = vcdMs.ToArray();
                        }
                        catch
                        {
                            return;
                        }

                        if(isoSector.Length < 2048)
                            return;

                        uint infoPos = 0;

                        for(int vi = 0; vi < vcdLength; vi++)
                        {
                            int vcdPos = vi * 2048;

                            while(vcdPos                     < isoSector.Length &&
                                  isoSector[vcdPos]          > 0                &&
                                  vcdPos + isoSector[vcdPos] <= isoSector.Length)
                            {
                                int    nameLen = isoSector[vcdPos + 32];
                                byte[] tmpName = new byte[nameLen];
                                Array.Copy(isoSector, vcdPos + 33, tmpName, 0, nameLen);
                                string name = StringHandlers.CToString(tmpName).ToUpperInvariant();

                                if(name.EndsWith(";1", StringComparison.InvariantCulture))
                                    name = name.Substring(0, name.Length - 2);

                                if(name == "INFO.VCD" ||
                                   name == "INFO.SVD")
                                {
                                    infoPos = BitConverter.ToUInt32(isoSector, vcdPos + 2);

                                    break;
                                }

                                vcdPos += isoSector[vcdPos];
                            }
                        }

                        if(infoPos > 0)
                        {
                            sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, infoPos, 2048, 0, 1,
                                               false, dev.Timeout, out _);

                            if(sense)
                                break;

                            byte[] systemId = new byte[8];
                            Array.Copy(isoSector, 0, systemId, 0, 8);

                            string id = StringHandlers.CToString(systemId).TrimEnd();

                            switch(id)
                            {
                                case "VIDEO_CD":
                                    mediaType = MediaType.VCD;

                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found Video CD description file, setting disc type to Video CD.");

                                    return;
                                case "SUPERVCD":
                                    mediaType = MediaType.SVCD;

                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found Super Video CD description file, setting disc type to Super Video CD.");

                                    break;
                                case "HQ-VCD":
                                    mediaType = MediaType.CVD;

                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found China Video Disc description file, setting disc type to China Video Disc.");

                                    break;
                            }
                        }
                    }

                    if(pcdLength > 0)
                    {
                        try
                        {
                            using var pcdMs = new MemoryStream();

                            for(uint i = 0; i < pcdLength; i++)
                            {
                                sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, pcdStart + i,
                                                   2048, 0, 1, false, dev.Timeout, out _);

                                if(sense)
                                    break;

                                pcdMs.Write(isoSector, 0, 2048);
                            }

                            isoSector = pcdMs.ToArray();
                        }
                        catch
                        {
                            return;
                        }

                        if(isoSector.Length < 2048)
                            return;

                        uint infoPos = 0;

                        for(int pi = 0; pi < pcdLength; pi++)
                        {
                            int pcdPos = pi * 2048;

                            while(pcdPos                     < isoSector.Length &&
                                  isoSector[pcdPos]          > 0                &&
                                  pcdPos + isoSector[pcdPos] <= isoSector.Length)
                            {
                                int    nameLen = isoSector[pcdPos + 32];
                                byte[] tmpName = new byte[nameLen];
                                Array.Copy(isoSector, pcdPos + 33, tmpName, 0, nameLen);
                                string name = StringHandlers.CToString(tmpName).ToUpperInvariant();

                                if(name.EndsWith(";1", StringComparison.InvariantCulture))
                                    name = name.Substring(0, name.Length - 2);

                                if(name == "INFO.PCD")
                                {
                                    infoPos = BitConverter.ToUInt32(isoSector, pcdPos + 2);

                                    break;
                                }

                                pcdPos += isoSector[pcdPos];
                            }
                        }

                        if(infoPos > 0)
                        {
                            sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, infoPos, 2048, 0, 1,
                                               false, dev.Timeout, out _);

                            if(sense)
                                break;

                            byte[] systemId = new byte[8];
                            Array.Copy(isoSector, 0, systemId, 0, 8);

                            string id = StringHandlers.CToString(systemId).TrimEnd();

                            switch(id)
                            {
                                case "PHOTO_CD":
                                    mediaType = MediaType.PCD;

                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found Photo CD description file, setting disc type to Photo CD.");

                                    return;
                            }
                        }
                    }

                    // "SYSTEM.CNF" length
                    if(ps1Length > 0)
                    {
                        uint ps1Sectors = ps1Length / 2048;

                        if(ps1Length % 2048 > 0)
                            ps1Sectors++;

                        string ps1Txt;

                        // Read "SYSTEM.CNF"
                        try
                        {
                            using var ps1Ms = new MemoryStream();

                            for(uint i = 0; i < ps1Sectors; i++)
                            {
                                sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, ps1Start + i,
                                                   2048, 0, 1, false, dev.Timeout, out _);

                                if(sense)
                                    break;

                                ps1Ms.Write(isoSector, 0, 2048);
                            }

                            isoSector = ps1Ms.ToArray();
                            ps1Txt    = Encoding.ASCII.GetString(isoSector);
                        }
                        catch
                        {
                            ps1Txt = null;
                        }

                        // Check "SYSTEM.CNF" lines
                        if(ps1Txt != null)
                        {
                            using var sr = new StringReader(ps1Txt);

                            string ps1BootFile = null;
                            string ps2BootFile = null;

                            while(sr.Peek() > 0)
                            {
                                string line = sr.ReadLine();

                                // End of file
                                if(line is null ||
                                   line.Length == 0)
                                    break;

                                line = line.Replace(" ", "");

                                if(line.StartsWith("BOOT=cdrom:", StringComparison.InvariantCultureIgnoreCase))
                                {
                                    ps1BootFile = line.Substring(11);

                                    if(ps1BootFile.StartsWith('\\'))
                                        ps1BootFile = ps1BootFile.Substring(1);

                                    if(ps1BootFile.EndsWith(";1", StringComparison.InvariantCultureIgnoreCase))
                                        ps1BootFile = ps1BootFile.Substring(0, ps1BootFile.Length - 2);

                                    break;
                                }

                                if(line.StartsWith("BOOT2=cdrom0:", StringComparison.InvariantCultureIgnoreCase))
                                {
                                    ps2BootFile = line.Substring(13);

                                    if(ps2BootFile.StartsWith('\\'))
                                        ps2BootFile = ps2BootFile.Substring(1);

                                    if(ps2BootFile.EndsWith(";1", StringComparison.InvariantCultureIgnoreCase))
                                        ps2BootFile = ps2BootFile.Substring(0, ps2BootFile.Length - 2);

                                    break;
                                }
                            }

                            if(ps1BootFile != null &&
                               rootEntries.Contains(ps1BootFile.ToUpperInvariant()))
                            {
                                mediaType = MediaType.PS1CD;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found correct SYSTEM.CNF file in root pointing to existing file in root, setting disc type to PlayStation CD.");
                            }

                            if(ps2BootFile != null &&
                               rootEntries.Contains(ps2BootFile.ToUpperInvariant()))
                            {
                                mediaType = MediaType.PS2CD;

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Found correct SYSTEM.CNF file in root pointing to existing file in root, setting disc type to PlayStation 2 CD.");
                            }
                        }
                    }

                    break;
                }

                // TODO: Check for CD-i Ready
                case MediaType.CDI: break;
                case MediaType.DVDROM:
                case MediaType.HDDVDROM:
                case MediaType.BDROM:
                case MediaType.UHDBD:
                case MediaType.Unknown:
                    // TODO: Nuon requires reading the filesystem, searching for a file called "/NUON/NUON.RUN"
                    if(ps2BootSectors is { Length: 0x6000 })
                    {
                        // The decryption key is applied as XOR. As first byte is originally always NULL, it gives us the key :)
                        byte decryptByte = ps2BootSectors[0];

                        for(int i = 0; i < 0x6000; i++)
                            ps2BootSectors[i] ^= decryptByte;

                        string ps2BootSectorsHash = Sha256Context.Data(ps2BootSectors, out _);

                        AaruConsole.DebugWriteLine("Media-info Command", "PlayStation 2 boot sectors SHA256: {0}",
                                                   ps2BootSectorsHash);

                        if(ps2BootSectorsHash == PS2_PAL_HASH  ||
                           ps2BootSectorsHash == PS2_NTSC_HASH ||
                           ps2BootSectorsHash == PS2_JAPANESE_HASH)
                        {
                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Sony PlayStation 2 boot sectors, setting disc type to PS2 DVD.");

                            mediaType = MediaType.PS2DVD;
                        }
                    }

                    if(sector1 != null)
                    {
                        byte[] tmp = new byte[_ps3Id.Length];
                        Array.Copy(sector1, 0, tmp, 0, tmp.Length);

                        if(tmp.SequenceEqual(_ps3Id))
                            switch(mediaType)
                            {
                                case MediaType.BDROM:
                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found Sony PlayStation 3 boot sectors, setting disc type to PS3 Blu-ray.");

                                    mediaType = MediaType.PS3BD;

                                    break;
                                case MediaType.DVDROM:
                                    AaruConsole.DebugWriteLine("Media detection",
                                                               "Found Sony PlayStation 3 boot sectors, setting disc type to PS3 DVD.");

                                    mediaType = MediaType.PS3DVD;

                                    break;
                            }

                        tmp = new byte[_ps4Id.Length];
                        Array.Copy(sector1, 512, tmp, 0, tmp.Length);

                        if(tmp.SequenceEqual(_ps4Id) &&
                           mediaType == MediaType.BDROM)
                        {
                            mediaType = MediaType.PS4BD;

                            AaruConsole.DebugWriteLine("Media detection",
                                                       "Found Sony PlayStation 4 boot sectors, setting disc type to PS4 Blu-ray.");
                        }
                    }

                    if(blurayDi                              != null &&
                       blurayDi?.Units?.Length               > 0     &&
                       blurayDi?.Units[0].DiscTypeIdentifier != null)
                    {
                        string blurayType = StringHandlers.CToString(blurayDi?.Units[0].DiscTypeIdentifier);

                        switch(blurayType)
                        {
                            case "XG4":
                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Blu-ray type set to \"XG4\", setting disc type to Xbox One Disc (XGD4).");

                                mediaType = MediaType.XGD4;

                                break;

                            // TODO: PS5
                            case "BDU":
                                if(sector1 != null)
                                {
                                    byte[] tmp = new byte[_ps5Id.Length];
                                    Array.Copy(sector1, 1024, tmp, 0, tmp.Length);

                                    if(tmp.SequenceEqual(_ps5Id))
                                    {
                                        mediaType = MediaType.PS5BD;

                                        AaruConsole.DebugWriteLine("Media detection",
                                                                   "Found Sony PlayStation 5 boot sectors, setting disc type to PS5 Ultra HD Blu-ray.");

                                        break;
                                    }
                                }

                                AaruConsole.DebugWriteLine("Media detection",
                                                           "Blu-ray type set to \"BDU\", setting disc type to Ultra HD Blu-ray.");

                                mediaType = MediaType.UHDBD;

                                break;
                        }
                    }

                    break;
            }
        }

        static void DetectRwPackets(byte[] subchannel, out bool cdgPacket, out bool cdegPacket, out bool cdmidiPacket)
        {
            cdgPacket    = false;
            cdegPacket   = false;
            cdmidiPacket = false;

            byte[] cdSubRwPack1 = new byte[24];
            byte[] cdSubRwPack2 = new byte[24];
            byte[] cdSubRwPack3 = new byte[24];
            byte[] cdSubRwPack4 = new byte[24];

            int i = 0;

            for(int j = 0; j < 24; j++)
                cdSubRwPack1[j] = (byte)(subchannel[i++] & 0x3F);

            for(int j = 0; j < 24; j++)
                cdSubRwPack2[j] = (byte)(subchannel[i++] & 0x3F);

            for(int j = 0; j < 24; j++)
                cdSubRwPack3[j] = (byte)(subchannel[i++] & 0x3F);

            for(int j = 0; j < 24; j++)
                cdSubRwPack4[j] = (byte)(subchannel[i++] & 0x3F);

            switch(cdSubRwPack1[0])
            {
                case 0x08:
                case 0x09:
                    cdgPacket = true;

                    break;
                case 0x0A:
                    cdegPacket = true;

                    break;
                case 0x38:
                    cdmidiPacket = true;

                    break;
            }

            switch(cdSubRwPack2[0])
            {
                case 0x08:
                case 0x09:
                    cdgPacket = true;

                    break;
                case 0x0A:
                    cdegPacket = true;

                    break;
                case 0x38:
                    cdmidiPacket = true;

                    break;
            }

            switch(cdSubRwPack3[0])
            {
                case 0x08:
                case 0x09:
                    cdgPacket = true;

                    break;
                case 0x0A:
                    cdegPacket = true;

                    break;
                case 0x38:
                    cdmidiPacket = true;

                    break;
            }

            switch(cdSubRwPack4[0])
            {
                case 0x08:
                case 0x09:
                    cdgPacket = true;

                    break;
                case 0x0A:
                    cdegPacket = true;

                    break;
                case 0x38:
                    cdmidiPacket = true;

                    break;
            }
        }
    }
}