// /***************************************************************************
// 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-2026 Natalia Portillo
// ****************************************************************************/

// ReSharper disable JoinDeclarationAndInitializer

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.Checksums;
using Aaru.CommonTypes;
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 Aaru.Logging;
using Sentry;
using DMI = Aaru.Decoders.Xbox.DMI;
using Sector = Aaru.Decoders.CD.Sector;

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";
    const string MODULE_NAME       = "Media detection";

    static readonly byte[] _ps3Id = "PlayStation3\0\0\0\0"u8.ToArray();

    static readonly byte[] _ps4Id = "PlayStation4\0\0\0\0"u8.ToArray();

    static readonly byte[] _ps5Id = "PlayStation5\0\0\0\0"u8.ToArray();

    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  = "COPYRIGHT BANDAI"u8.ToArray();
    static readonly byte[] _pcEngineSignature = "PC Engine CD-ROM SYSTEM"u8.ToArray();
    static readonly byte[] _pcFxSignature     = "PC-FX:Hu_CD-ROM"u8.ToArray();
    static readonly byte[] _atariSignature =
        "TAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTARA IPARPVODED TA AEHDAREA RT"u8.ToArray();

    /// <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];

        var 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];

        var testMark = new byte[12];

        for(var 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)
    {
        var 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;

        var 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;

        var buffer = new byte[_videoNowColorFrameMarker.Length];

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

            for(var 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)
    {
        var buffer = new byte[_videoNowColorFrameMarker.Length];

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

            for(var 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;
        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 byte[] cmdBuf, out _, 0, MmcGetConfigurationRt.Current, dev.Timeout, out _);

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

            AaruLogging.Debug(MODULE_NAME,
                              Localization.Core.GET_CONFIGURATION_current_profile_is_0,
                              ftr.CurrentProfile);

            mediaType = ftr.CurrentProfile switch
                        {
                            0x0001           => MediaType.GENERIC_HDD,
                            0x0005           => MediaType.CDMO,
                            0x0008           => MediaType.CD,
                            0x0009           => MediaType.CDR,
                            0x000A           => MediaType.CDRW,
                            0x0010           => MediaType.DVDROM,
                            0x0011           => MediaType.DVDR,
                            0x0012           => MediaType.DVDRAM,
                            0x0013 or 0x0014 => MediaType.DVDRW,
                            0x0015 or 0x0016 => MediaType.DVDRDL,
                            0x0017           => MediaType.DVDRWDL,
                            0x0018           => MediaType.DVDDownload,
                            0x001A           => MediaType.DVDPRW,
                            0x001B           => MediaType.DVDPR,
                            0x0020           => MediaType.DDCD,
                            0x0021           => MediaType.DDCDR,
                            0x0022           => MediaType.DDCDRW,
                            0x002A           => MediaType.DVDPRWDL,
                            0x002B           => MediaType.DVDPRDL,
                            0x0040           => MediaType.BDROM,
                            0x0041 or 0x0042 => MediaType.BDR,
                            0x0043           => MediaType.BDRE,
                            0x0050           => MediaType.HDDVDROM,
                            0x0051           => MediaType.HDDVDR,
                            0x0052           => MediaType.HDDVDRAM,
                            0x0053           => MediaType.HDDVDRW,
                            0x0058           => MediaType.HDDVDRDL,
                            0x005A           => MediaType.HDDVDRWDL,
                            _                => mediaType
                        };
        }

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

        if(mediaType is MediaType.CD or MediaType.CDROMXA or 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 is MediaType.CD or MediaType.CDROMXA)
        {
            var hasDataTrack                  = false;
            var hasAudioTrack                 = false;
            var allFirstSessionTracksAreAudio = true;
            var hasVideoTrack                 = false;

            if(decodedToc.HasValue)
            {
                FullTOC.TrackDataDescriptor a0Track =
                    decodedToc.Value.TrackDescriptors.FirstOrDefault(static t => t is { POINT: 0xA0, ADR: 1 });

                if(a0Track.POINT == 0xA0)
                {
                    switch(a0Track.PSEC)
                    {
                        case 0x10:
                            AaruLogging.Debug(MODULE_NAME, Localization.Core.TOC_says_disc_type_is_CD_i);
                            mediaType = MediaType.CDI;

                            break;
                        case 0x20:
                            AaruLogging.Debug(MODULE_NAME, Localization.Core.TOC_says_disc_type_is_CD_ROM_XA);

                            mediaType = MediaType.CDROMXA;

                            break;
                    }
                }

                foreach(FullTOC.TrackDataDescriptor track in
                        decodedToc.Value.TrackDescriptors.Where(static t => t.POINT is > 0 and <= 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)
                    {
                        var 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)
            {
                switch(hasDataTrack)
                {
                    case true when hasAudioTrack && allFirstSessionTracksAreAudio && sessions == 2:
                        AaruLogging.Debug(MODULE_NAME,
                                          Localization.Core
                                                      .Disc_has_audio_and_data_two_sessions_all_data_second_CD_Plus);

                        mediaType = MediaType.CDPLUS;

                        break;
                    case false when hasAudioTrack && sessions == 1:
                        AaruLogging.Debug(MODULE_NAME,
                                          Localization.Core.Disc_has_only_audio_in_a_session_CD_Digital_Audio);

                        mediaType = MediaType.CDDA;

                        break;
                }

                if(hasDataTrack && !hasAudioTrack && sessions == 1)
                {
                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_only_data_in_a_session_CD_ROM);

                    mediaType = MediaType.CDROM;
                }

                if(hasVideoTrack && !hasDataTrack && sessions == 1)
                {
                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_video_tracks_CD_Video);

                    mediaType = MediaType.CDV;
                }
            }

            if(mediaType is MediaType.CD or MediaType.CDROM && hasDataTrack)
            {
                foreach(uint startAddress in decodedToc.Value.TrackDescriptors
                                                       .Where(static t => t.POINT is > 0 and <= 0x99 &&
                                                                          ((TocControl)(t.CONTROL & 0x0D) ==
                                                                           TocControl.DataTrack ||
                                                                           (TocControl)(t.CONTROL & 0x0D) ==
                                                                           TocControl.DataTrackIncremental))
                                                       .Select(static 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;

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_a_mode_two_data_track_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);

            var 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(var 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 ==
                                                             decodedToc.Value.TrackDescriptors
                                                                       .Min(static m => m.POINT));

        if(firstTrack?.POINT is >= 1 and < 0xA0)
        {
            var 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;

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_hidden_CD_i_pregap_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++;

                            var lba0  = 0;
                            var 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;

                                AaruLogging.Debug(MODULE_NAME,
                                                  Localization.Core.Disc_has_hidden_CD_i_pregap_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 is MediaType.BDROM or 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)
                        {
                            mediaType = pfi.Value.DiskCategory switch
                                        {
                                            DiskCategory.DVDPR    => MediaType.DVDPR,
                                            DiskCategory.DVDPRDL  => MediaType.DVDPRDL,
                                            DiskCategory.DVDPRW   => MediaType.DVDPRW,
                                            DiskCategory.DVDPRWDL => MediaType.DVDPRWDL,
                                            DiskCategory.DVDR => pfi.Value.PartVersion >= 6
                                                                     ? MediaType.DVDRDL
                                                                     : MediaType.DVDR,
                                            DiskCategory.DVDRAM => MediaType.DVDRAM,
                                            DiskCategory.DVDRW => pfi.Value.PartVersion >= 15
                                                                      ? MediaType.DVDRWDL
                                                                      : MediaType.DVDRW,
                                            DiskCategory.HDDVDR   => MediaType.HDDVDR,
                                            DiskCategory.HDDVDRAM => MediaType.HDDVDRAM,
                                            DiskCategory.HDDVDROM => MediaType.HDDVDROM,
                                            DiskCategory.HDDVDRW  => MediaType.HDDVDRW,
                                            DiskCategory.Nintendo => pfi.Value.DiscSize == DVDSize.Eighty
                                                                         ? MediaType.GOD
                                                                         : MediaType.WOD,
                                            DiskCategory.UMD => MediaType.UMD,
                                            _                => mediaType
                                        };
                        }
                    }

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

                    if(!sense)
                    {
                        if(DMI.IsXbox(cmdBuf))
                        {
                            AaruLogging.Debug(MODULE_NAME,
                                              "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 is 25063 or 4229664 or 4246304 or 4267015) // Wxripper unlock
                            {
                                AaruLogging.Debug(MODULE_NAME,
                                                  "Found Xbox 360 DMI with {0} blocks, setting disc type to Xbox 360 Game Disc 3 (XGD3).",
                                                  blocks);

                                mediaType = MediaType.XGD3;

                                return;
                            }

                            AaruLogging.Debug(MODULE_NAME,
                                              "Found Xbox 360 DMI with {0} blocks, setting disc type to Xbox 360 Game Disc 2 (XGD2).",
                                              blocks);

                            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;

                var rootStart  = BitConverter.ToUInt32(isoSector, 158);
                var 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(Exception ex)
                {
                    SentrySdk.CaptureException(ex);

                    return;
                }

                if(isoSector.Length < 2048) return;

                var  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];
                    var 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[..^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(Exception ex)
                    {
                        SentrySdk.CaptureException(ex);

                        return;
                    }

                    if(isoSector.Length < 2048) return;

                    for(var 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];
                            var 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[..^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;

                            var 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;

                                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Photo_CD_description_file);

                                    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;

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Mega_Sega_CD_IP_BIN);

                    return;
                }

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

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sega_Saturn_IP_BIN);

                    return;
                }

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

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sega_Dreamcast_IP_BIN);

                    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(var i = 0; i < 0x6000; i++) ps2BootSectors[i] ^= decryptByte;

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

                    AaruLogging.Debug(MODULE_NAME,
                                      Localization.Core.PlayStation_2_boot_sectors_SHA256_0,
                                      ps2BootSectorsHash);

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

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_2_boot_sectors);

                        goto hasPs2CdBoot;
                    }
                }

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

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

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Opera_filesystem);

                        return;
                    }

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

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_FM_Towns_boot);

                        return;
                    }
                }

                if(playdia1 != null && playdia2 != null)
                {
                    var pd1 = new byte[_playdiaCopyright.Length];
                    var 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;

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Playdia_copyright);

                        return;
                    }
                }

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

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

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_PC_Engine_CD_signature);

                        return;
                    }
                }

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

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

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_PC_FX_copyright);

                        return;
                    }
                }

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

                    for(var 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;

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Atari_signature);

                        break;
                    }
                }

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

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

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sega_Dreamcast_IP_BIN_on_second_session);

                        return;
                    }
                }

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

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_VideoNow_Color_frame);

                    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)
                    {
                        var cdg    = false;
                        var cdeg   = false;
                        var cdmidi = false;

                        for(var i = 0; i < 8; i++)
                        {
                            var 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;

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_enhanced_graphics_RW_packet);

                            return;
                        }

                        if(cdg)
                        {
                            mediaType = MediaType.CDG;

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_graphics_RW_packet);

                            return;
                        }

                        if(cdmidi)
                        {
                            mediaType = MediaType.CDMIDI;

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_MIDI_RW_packet);

                            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;

                var rootStart  = BitConverter.ToUInt32(isoSector, 158);
                var 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(Exception ex)
                {
                    SentrySdk.CaptureException(ex);

                    return;
                }

                if(isoSector.Length < 2048) return;

                List<string> rootEntries   = [];
                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(var 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];
                        var 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[..^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;

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_CD32_TM_file_in_root);

                    return;
                }

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

                    AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_CDTV_TM_file_in_root);

                    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(Exception ex)
                    {
                        SentrySdk.CaptureException(ex);

                        iplTxt = null;
                    }

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

                        var correctNeoGeoCd = true;
                        var 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 is < 1 or > 8)
                            {
                                correctNeoGeoCd = false;

                                break;
                            }

                            try
                            {
                                _ = Convert.ToUInt32(split[2], 16);
                            }
                            catch(Exception ex)
                            {
                                SentrySdk.CaptureException(ex);

                                correctNeoGeoCd = false;

                                break;
                            }

                            lineNumber++;
                        }

                        if(correctNeoGeoCd)
                        {
                            mediaType = MediaType.NeoGeoCD;

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_correct_IPL_TXT_file_in_root);

                            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(Exception ex)
                    {
                        SentrySdk.CaptureException(ex);

                        return;
                    }

                    if(isoSector.Length < 2048) return;

                    uint infoPos = 0;

                    for(var 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];
                            var 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[..^2];

                            if(name is "INFO.VCD" or "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;

                        var 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;

                                AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Video_CD_description_file);

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

                                AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Super_Video_CD_description_file);

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

                                AaruLogging.Debug(MODULE_NAME,
                                                  Localization.Core.Found_China_Video_Disc_description_file);

                                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(Exception ex)
                    {
                        SentrySdk.CaptureException(ex);

                        return;
                    }

                    if(isoSector.Length < 2048) return;

                    uint infoPos = 0;

                    for(var 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];
                            var 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[..^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;

                        var 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;

                                AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Photo_CD_description_file);

                                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(Exception ex)
                    {
                        SentrySdk.CaptureException(ex);

                        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[11..];

                                if(ps1BootFile.StartsWith('\\')) ps1BootFile = ps1BootFile[1..];

                                if(ps1BootFile.EndsWith(";1", StringComparison.InvariantCultureIgnoreCase))
                                    ps1BootFile = ps1BootFile[..^2];

                                break;
                            }

                            if(!line.StartsWith("BOOT2=cdrom0:", StringComparison.InvariantCultureIgnoreCase)) continue;

                            ps2BootFile = line[13..];

                            if(ps2BootFile.StartsWith('\\')) ps2BootFile = ps2BootFile[1..];

                            if(ps2BootFile.EndsWith(";1", StringComparison.InvariantCultureIgnoreCase))
                                ps2BootFile = ps2BootFile[..^2];

                            break;
                        }

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

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_correct_SYSTEM_CNF_file_in_root_PS1);
                        }

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

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_correct_SYSTEM_CNF_file_in_root_PS2);
                        }
                    }
                }

                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(var i = 0; i < 0x6000; i++) ps2BootSectors[i] ^= decryptByte;

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

                    AaruLogging.Debug(MODULE_NAME,
                                      Localization.Core.PlayStation_2_boot_sectors_SHA256_0,
                                      ps2BootSectorsHash);

                    if(ps2BootSectorsHash is PS2_PAL_HASH or PS2_NTSC_HASH or PS2_JAPANESE_HASH)
                    {
                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_2_boot_sectors_DVD);

                        mediaType = MediaType.PS2DVD;
                    }
                }

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

                    if(tmp.SequenceEqual(_ps3Id))
                    {
                        switch(mediaType)
                        {
                            case MediaType.BDROM:
                                AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_3_boot_sectors);

                                mediaType = MediaType.PS3BD;

                                break;
                            case MediaType.DVDROM:
                                AaruLogging.Debug(MODULE_NAME,
                                                  Localization.Core.Found_Sony_PlayStation_3_boot_sectors_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;

                        AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_4_boot_sectors);
                    }
                }

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

                    switch(blurayType)
                    {
                        case "BDR":
                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Blu_ray_type_BDR_setting_disc_type_BDR);

                            mediaType = MediaType.BDR;

                            break;
                        case "XG4":
                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Blu_ray_type_XG4_setting_disc_type_XGD4);

                            mediaType = MediaType.XGD4;

                            break;

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

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

                                    AaruLogging.Debug(MODULE_NAME,
                                                      Localization.Core.Found_Sony_PlayStation_5_boot_sectors);

                                    break;
                                }
                            }

                            AaruLogging.Debug(MODULE_NAME, Localization.Core.Blu_ray_type_BDU_setting_disc_type_UHD);

                            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;

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

        var i = 0;

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

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

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

        for(var 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;
        }
    }
}