// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Read.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
//     Reads MAME Compressed Hunks of Data disk images.
//
// --[ License ] --------------------------------------------------------------
//
//     This library is free software; you can redistribute it and/or modify
//     it under the terms of the GNU Lesser General Public License as
//     published by the Free Software Foundation; either version 2.1 of the
//     License, or (at your option) any later version.
//
//     This library 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
//     Lesser General Public License for more details.
//
//     You should have received a copy of the GNU Lesser General Public
//     License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.CommonTypes.Structs.Devices.ATA;
using Aaru.Decoders.CD;
using Aaru.Helpers;
using Aaru.Logging;
using Session = Aaru.CommonTypes.Structs.Session;

namespace Aaru.Images;

public sealed partial class Chd
{
#region IOpticalMediaImage Members

    /// <inheritdoc />
    [SuppressMessage("ReSharper", "UnusedVariable")]
    public ErrorNumber Open(IFilter imageFilter)
    {
        Stream stream = imageFilter.GetDataForkStream();
        stream.Seek(0, SeekOrigin.Begin);
        var magic = new byte[8];
        stream.EnsureRead(magic, 0, 8);

        if(!_chdTag.SequenceEqual(magic)) return ErrorNumber.InvalidArgument;

        // Read length
        var buffer = new byte[4];
        stream.EnsureRead(buffer, 0, 4);
        var length = BitConverter.ToUInt32(buffer.Reverse().ToArray(), 0);
        buffer = new byte[4];
        stream.EnsureRead(buffer, 0, 4);
        var version = BitConverter.ToUInt32(buffer.Reverse().ToArray(), 0);

        buffer = new byte[length];
        stream.Seek(0, SeekOrigin.Begin);
        stream.EnsureRead(buffer, 0, (int)length);

        ulong nextMetaOff      = 0;
        var   hunkMapStopwatch = new Stopwatch();

        switch(version)
        {
            case 1:
            {
                HeaderV1 hdrV1 = Marshal.ByteArrayToStructureBigEndian<HeaderV1>(buffer);

                AaruLogging.Debug(MODULE_NAME, "hdrV1.tag = \"{0}\"", Encoding.ASCII.GetString(hdrV1.tag));

                AaruLogging.Debug(MODULE_NAME, "hdrV1.length = {0} bytes", hdrV1.length);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.version = {0}",      hdrV1.version);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.flags = {0}",        (Flags)hdrV1.flags);

                AaruLogging.Debug(MODULE_NAME, "hdrV1.compression = {0}", (Compression)hdrV1.compression);

                AaruLogging.Debug(MODULE_NAME, "hdrV1.hunksize = {0}",   hdrV1.hunksize);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.totalhunks = {0}", hdrV1.totalhunks);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.cylinders = {0}",  hdrV1.cylinders);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.heads = {0}",      hdrV1.heads);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.sectors = {0}",    hdrV1.sectors);
                AaruLogging.Debug(MODULE_NAME, "hdrV1.md5 = {0}",        ArrayHelpers.ByteArrayToHex(hdrV1.md5));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV1.parentmd5 = {0}",
                                  ArrayHelpers.ArrayIsNullOrEmpty(hdrV1.parentmd5)
                                      ? "null"
                                      : ArrayHelpers.ByteArrayToHex(hdrV1.parentmd5));

                AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);

                hunkMapStopwatch.Restart();
                _hunkTable = new ulong[hdrV1.totalhunks];

                var hunkSectorCount = (uint)Math.Ceiling((double)hdrV1.totalhunks * 8 / 512);

                var hunkSectorBytes = new byte[512];

                for(var i = 0; i < hunkSectorCount; i++)
                {
                    stream.EnsureRead(hunkSectorBytes, 0, 512);

                    // This does the big-endian trick but reverses the order of elements also
                    Array.Reverse(hunkSectorBytes);
                    HunkSector hunkSector = Marshal.ByteArrayToStructureLittleEndian<HunkSector>(hunkSectorBytes);

                    // This restores the order of elements
                    Array.Reverse(hunkSector.hunkEntry);

                    if(_hunkTable.Length >= i * 512 / 8 + 512 / 8)
                        Array.Copy(hunkSector.hunkEntry, 0, _hunkTable, i * 512 / 8, 512 / 8);
                    else
                        Array.Copy(hunkSector.hunkEntry, 0, _hunkTable, i * 512 / 8, _hunkTable.Length - i * 512 / 8);
                }

                hunkMapStopwatch.Stop();

                AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);

                _imageInfo.MediaType         = MediaType.GENERIC_HDD;
                _imageInfo.Sectors           = hdrV1.hunksize * hdrV1.totalhunks;
                _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
                _imageInfo.SectorSize        = 512;
                _imageInfo.Version           = "1";
                _imageInfo.ImageSize         = _imageInfo.SectorSize * hdrV1.hunksize * hdrV1.totalhunks;

                _totalHunks     = hdrV1.totalhunks;
                _sectorsPerHunk = hdrV1.hunksize;
                _hdrCompression = hdrV1.compression;
                _mapVersion     = 1;
                _isHdd          = true;

                _imageInfo.Cylinders       = hdrV1.cylinders;
                _imageInfo.Heads           = hdrV1.heads;
                _imageInfo.SectorsPerTrack = hdrV1.sectors;

                break;
            }

            case 2:
            {
                HeaderV2 hdrV2 = Marshal.ByteArrayToStructureBigEndian<HeaderV2>(buffer);

                AaruLogging.Debug(MODULE_NAME, "hdrV2.tag = \"{0}\"", Encoding.ASCII.GetString(hdrV2.tag));

                AaruLogging.Debug(MODULE_NAME, "hdrV2.length = {0} bytes", hdrV2.length);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.version = {0}",      hdrV2.version);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.flags = {0}",        (Flags)hdrV2.flags);

                AaruLogging.Debug(MODULE_NAME, "hdrV2.compression = {0}", (Compression)hdrV2.compression);

                AaruLogging.Debug(MODULE_NAME, "hdrV2.hunksize = {0}",   hdrV2.hunksize);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.totalhunks = {0}", hdrV2.totalhunks);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.cylinders = {0}",  hdrV2.cylinders);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.heads = {0}",      hdrV2.heads);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.sectors = {0}",    hdrV2.sectors);
                AaruLogging.Debug(MODULE_NAME, "hdrV2.md5 = {0}",        ArrayHelpers.ByteArrayToHex(hdrV2.md5));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV2.parentmd5 = {0}",
                                  ArrayHelpers.ArrayIsNullOrEmpty(hdrV2.parentmd5)
                                      ? "null"
                                      : ArrayHelpers.ByteArrayToHex(hdrV2.parentmd5));

                AaruLogging.Debug(MODULE_NAME, "hdrV2.seclen = {0}", hdrV2.seclen);

                AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);
                hunkMapStopwatch.Restart();

                _hunkTable = new ulong[hdrV2.totalhunks];

                // How many sectors uses the BAT
                var hunkSectorCount = (uint)Math.Ceiling((double)hdrV2.totalhunks * 8 / 512);

                var hunkSectorBytes = new byte[512];

                for(var i = 0; i < hunkSectorCount; i++)
                {
                    stream.EnsureRead(hunkSectorBytes, 0, 512);

                    // This does the big-endian trick but reverses the order of elements also
                    Array.Reverse(hunkSectorBytes);
                    HunkSector hunkSector = Marshal.ByteArrayToStructureLittleEndian<HunkSector>(hunkSectorBytes);

                    // This restores the order of elements
                    Array.Reverse(hunkSector.hunkEntry);

                    if(_hunkTable.Length >= i * 512 / 8 + 512 / 8)
                        Array.Copy(hunkSector.hunkEntry, 0, _hunkTable, i * 512 / 8, 512 / 8);
                    else
                        Array.Copy(hunkSector.hunkEntry, 0, _hunkTable, i * 512 / 8, _hunkTable.Length - i * 512 / 8);
                }

                hunkMapStopwatch.Stop();

                AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);

                _imageInfo.MediaType         = MediaType.GENERIC_HDD;
                _imageInfo.Sectors           = hdrV2.hunksize * hdrV2.totalhunks;
                _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
                _imageInfo.SectorSize        = hdrV2.seclen;
                _imageInfo.Version           = "2";
                _imageInfo.ImageSize         = _imageInfo.SectorSize * hdrV2.hunksize * hdrV2.totalhunks;

                _totalHunks     = hdrV2.totalhunks;
                _sectorsPerHunk = hdrV2.hunksize;
                _hdrCompression = hdrV2.compression;
                _mapVersion     = 1;
                _isHdd          = true;

                _imageInfo.Cylinders       = hdrV2.cylinders;
                _imageInfo.Heads           = hdrV2.heads;
                _imageInfo.SectorsPerTrack = hdrV2.sectors;

                break;
            }

            case 3:
            {
                HeaderV3 hdrV3 = Marshal.ByteArrayToStructureBigEndian<HeaderV3>(buffer);

                AaruLogging.Debug(MODULE_NAME, "hdrV3.tag = \"{0}\"", Encoding.ASCII.GetString(hdrV3.tag));

                AaruLogging.Debug(MODULE_NAME, "hdrV3.length = {0} bytes", hdrV3.length);
                AaruLogging.Debug(MODULE_NAME, "hdrV3.version = {0}",      hdrV3.version);
                AaruLogging.Debug(MODULE_NAME, "hdrV3.flags = {0}",        (Flags)hdrV3.flags);

                AaruLogging.Debug(MODULE_NAME, "hdrV3.compression = {0}", (Compression)hdrV3.compression);

                AaruLogging.Debug(MODULE_NAME, "hdrV3.totalhunks = {0}",   hdrV3.totalhunks);
                AaruLogging.Debug(MODULE_NAME, "hdrV3.logicalbytes = {0}", hdrV3.logicalbytes);
                AaruLogging.Debug(MODULE_NAME, "hdrV3.metaoffset = {0}",   hdrV3.metaoffset);
                AaruLogging.Debug(MODULE_NAME, "hdrV3.md5 = {0}",          ArrayHelpers.ByteArrayToHex(hdrV3.md5));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV3.parentmd5 = {0}",
                                  ArrayHelpers.ArrayIsNullOrEmpty(hdrV3.parentmd5)
                                      ? "null"
                                      : ArrayHelpers.ByteArrayToHex(hdrV3.parentmd5));

                AaruLogging.Debug(MODULE_NAME, "hdrV3.hunkbytes = {0}", hdrV3.hunkbytes);

                AaruLogging.Debug(MODULE_NAME, "hdrV3.sha1 = {0}", ArrayHelpers.ByteArrayToHex(hdrV3.sha1));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV3.parentsha1 = {0}",
                                  ArrayHelpers.ArrayIsNullOrEmpty(hdrV3.parentsha1)
                                      ? "null"
                                      : ArrayHelpers.ByteArrayToHex(hdrV3.parentsha1));

                AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);
                hunkMapStopwatch.Restart();

                _hunkMap = new byte[hdrV3.totalhunks * 16];
                stream.EnsureRead(_hunkMap, 0, _hunkMap.Length);

                hunkMapStopwatch.Stop();

                AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);

                nextMetaOff = hdrV3.metaoffset;

                _imageInfo.ImageSize = hdrV3.logicalbytes;
                _imageInfo.Version   = "3";

                _totalHunks     = hdrV3.totalhunks;
                _bytesPerHunk   = hdrV3.hunkbytes;
                _hdrCompression = hdrV3.compression;
                _mapVersion     = 3;

                break;
            }

            case 4:
            {
                HeaderV4 hdrV4 = Marshal.ByteArrayToStructureBigEndian<HeaderV4>(buffer);

                AaruLogging.Debug(MODULE_NAME, "hdrV4.tag = \"{0}\"", Encoding.ASCII.GetString(hdrV4.tag));

                AaruLogging.Debug(MODULE_NAME, "hdrV4.length = {0} bytes", hdrV4.length);
                AaruLogging.Debug(MODULE_NAME, "hdrV4.version = {0}",      hdrV4.version);
                AaruLogging.Debug(MODULE_NAME, "hdrV4.flags = {0}",        (Flags)hdrV4.flags);

                AaruLogging.Debug(MODULE_NAME, "hdrV4.compression = {0}", (Compression)hdrV4.compression);

                AaruLogging.Debug(MODULE_NAME, "hdrV4.totalhunks = {0}",   hdrV4.totalhunks);
                AaruLogging.Debug(MODULE_NAME, "hdrV4.logicalbytes = {0}", hdrV4.logicalbytes);
                AaruLogging.Debug(MODULE_NAME, "hdrV4.metaoffset = {0}",   hdrV4.metaoffset);
                AaruLogging.Debug(MODULE_NAME, "hdrV4.hunkbytes = {0}",    hdrV4.hunkbytes);

                AaruLogging.Debug(MODULE_NAME, "hdrV4.sha1 = {0}", ArrayHelpers.ByteArrayToHex(hdrV4.sha1));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV4.parentsha1 = {0}",
                                  ArrayHelpers.ArrayIsNullOrEmpty(hdrV4.parentsha1)
                                      ? "null"
                                      : ArrayHelpers.ByteArrayToHex(hdrV4.parentsha1));

                AaruLogging.Debug(MODULE_NAME, "hdrV4.rawsha1 = {0}", ArrayHelpers.ByteArrayToHex(hdrV4.rawsha1));

                AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);
                hunkMapStopwatch.Restart();

                _hunkMap = new byte[hdrV4.totalhunks * 16];
                stream.EnsureRead(_hunkMap, 0, _hunkMap.Length);

                hunkMapStopwatch.Stop();

                AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);

                nextMetaOff = hdrV4.metaoffset;

                _imageInfo.ImageSize = hdrV4.logicalbytes;
                _imageInfo.Version   = "4";

                _totalHunks     = hdrV4.totalhunks;
                _bytesPerHunk   = hdrV4.hunkbytes;
                _hdrCompression = hdrV4.compression;
                _mapVersion     = 3;

                break;
            }

            case 5:
            {
                HeaderV5 hdrV5 = Marshal.ByteArrayToStructureBigEndian<HeaderV5>(buffer);

                AaruLogging.Debug(MODULE_NAME, "hdrV5.tag = \"{0}\"", Encoding.ASCII.GetString(hdrV5.tag));

                AaruLogging.Debug(MODULE_NAME, "hdrV5.length = {0} bytes", hdrV5.length);
                AaruLogging.Debug(MODULE_NAME, "hdrV5.version = {0}",      hdrV5.version);

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV5.compressor0 = \"{0}\"",
                                  Encoding.ASCII.GetString(BigEndianBitConverter.GetBytes(hdrV5.compressor0)));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV5.compressor1 = \"{0}\"",
                                  Encoding.ASCII.GetString(BigEndianBitConverter.GetBytes(hdrV5.compressor1)));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV5.compressor2 = \"{0}\"",
                                  Encoding.ASCII.GetString(BigEndianBitConverter.GetBytes(hdrV5.compressor2)));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV5.compressor3 = \"{0}\"",
                                  Encoding.ASCII.GetString(BigEndianBitConverter.GetBytes(hdrV5.compressor3)));

                AaruLogging.Debug(MODULE_NAME, "hdrV5.logicalbytes = {0}", hdrV5.logicalbytes);
                AaruLogging.Debug(MODULE_NAME, "hdrV5.mapoffset = {0}",    hdrV5.mapoffset);
                AaruLogging.Debug(MODULE_NAME, "hdrV5.metaoffset = {0}",   hdrV5.metaoffset);
                AaruLogging.Debug(MODULE_NAME, "hdrV5.hunkbytes = {0}",    hdrV5.hunkbytes);
                AaruLogging.Debug(MODULE_NAME, "hdrV5.unitbytes = {0}",    hdrV5.unitbytes);

                AaruLogging.Debug(MODULE_NAME, "hdrV5.sha1 = {0}", ArrayHelpers.ByteArrayToHex(hdrV5.sha1));

                AaruLogging.Debug(MODULE_NAME,
                                  "hdrV5.parentsha1 = {0}",
                                  ArrayHelpers.ArrayIsNullOrEmpty(hdrV5.parentsha1)
                                      ? "null"
                                      : ArrayHelpers.ByteArrayToHex(hdrV5.parentsha1));

                AaruLogging.Debug(MODULE_NAME, "hdrV5.rawsha1 = {0}", ArrayHelpers.ByteArrayToHex(hdrV5.rawsha1));

                _totalHunks      = (uint)((hdrV5.logicalbytes + hdrV5.hunkbytes - 1) / hdrV5.hunkbytes);
                _bytesPerHunk    = hdrV5.hunkbytes;
                _unitBytes       = hdrV5.unitbytes;
                _hdrCompression  = hdrV5.compressor0;
                _hdrCompression1 = hdrV5.compressor1;
                _hdrCompression2 = hdrV5.compressor2;
                _hdrCompression3 = hdrV5.compressor3;
                _mapVersion      = 5;

                AaruLogging.Debug(MODULE_NAME, Localization.Reading_Hunk_map);
                hunkMapStopwatch.Restart();

                if(hdrV5.compressor0 == 0)
                {
                    // Uncompressed map: 4 bytes per hunk (big-endian offset / hunkbytes)
                    _hunkTableSmall = new uint[_totalHunks];
                    stream.Seek((long)hdrV5.mapoffset, SeekOrigin.Begin);

                    for(uint i = 0; i < _totalHunks; i++)
                    {
                        var entryBytes = new byte[4];
                        stream.EnsureRead(entryBytes, 0, 4);

                        _hunkTableSmall[i] = BigEndianBitConverter.ToUInt32(entryBytes, 0);
                    }
                }
                else
                {
                    // Compressed map
                    ErrorNumber mapErr = DecompressV5Map(stream,
                                                         hdrV5.mapoffset,
                                                         _totalHunks,
                                                         _bytesPerHunk,
                                                         _unitBytes,
                                                         out _rawMap);

                    if(mapErr != ErrorNumber.NoError) return mapErr;
                }

                hunkMapStopwatch.Stop();

                AaruLogging.Debug(MODULE_NAME, Localization.Took_0_seconds, hunkMapStopwatch.Elapsed.TotalSeconds);

                nextMetaOff = hdrV5.metaoffset;

                _imageInfo.ImageSize = hdrV5.logicalbytes;
                _imageInfo.Version   = "5";

                break;
            }

            default:
                AaruLogging.Error(string.Format(Localization.Unsupported_CHD_version_0, version));

                return ErrorNumber.NotSupported;
        }

        if(_mapVersion >= 3)
        {
            _isCdrom   = false;
            _isHdd     = false;
            _isGdrom   = false;
            _swapAudio = false;
            _tracks    = new Dictionary<uint, Track>();

            AaruLogging.Debug(MODULE_NAME, Localization.Reading_metadata);

            ulong currentSector = 0;
            uint  currentTrack  = 1;

            while(nextMetaOff > 0)
            {
                var hdrBytes = new byte[16];
                stream.Seek((long)nextMetaOff, SeekOrigin.Begin);
                stream.EnsureRead(hdrBytes, 0, hdrBytes.Length);
                MetadataHeader header = Marshal.ByteArrayToStructureBigEndian<MetadataHeader>(hdrBytes);
                var            meta   = new byte[header.flagsAndLength & 0xFFFFFF];
                stream.EnsureRead(meta, 0, meta.Length);

                AaruLogging.Debug(MODULE_NAME,
                                  Localization.Found_metadata_0_,
                                  Encoding.ASCII.GetString(BigEndianBitConverter.GetBytes(header.tag)));

                switch(header.tag)
                {
                    // "GDDD"
                    case HARD_DISK_METADATA:
                        if(_isCdrom || _isGdrom)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_hard_disk_and_a_CGD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        string gddd      = StringHandlers.CToString(meta);
                        Regex  gdddRegEx = MetadataHddRegex();
                        Match  gdddMatch = gdddRegEx.Match(gddd);

                        if(gdddMatch.Success)
                        {
                            _isHdd                     = true;
                            _imageInfo.SectorSize      = uint.Parse(gdddMatch.Groups["bps"].Value);
                            _imageInfo.Cylinders       = uint.Parse(gdddMatch.Groups["cylinders"].Value);
                            _imageInfo.Heads           = uint.Parse(gdddMatch.Groups["heads"].Value);
                            _imageInfo.SectorsPerTrack = uint.Parse(gdddMatch.Groups["sectors"].Value);
                        }

                        break;

                    // "CHCD"
                    case CDROM_OLD_METADATA:
                        if(_isHdd)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_hard_disk_and_a_CD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        if(_isGdrom)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_GD_ROM_and_a_CD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        var chdTracksNumber = BigEndianBitConverter.ToUInt32(meta, 0);

                        // Byteswapped
                        if(chdTracksNumber > 99) chdTracksNumber = BigEndianBitConverter.ToUInt32(meta, 0);

                        currentSector = 0;

                        for(uint i = 0; i < chdTracksNumber; i++)
                        {
                            var chdTrack = new TrackOld
                            {
                                type        = BigEndianBitConverter.ToUInt32(meta, (int)(4 + i * 24 + 0)),
                                subType     = BigEndianBitConverter.ToUInt32(meta, (int)(4 + i * 24 + 4)),
                                dataSize    = BigEndianBitConverter.ToUInt32(meta, (int)(4 + i * 24 + 8)),
                                subSize     = BigEndianBitConverter.ToUInt32(meta, (int)(4 + i * 24 + 12)),
                                frames      = BigEndianBitConverter.ToUInt32(meta, (int)(4 + i * 24 + 16)),
                                extraFrames = BigEndianBitConverter.ToUInt32(meta, (int)(4 + i * 24 + 20))
                            };

                            var aaruTrack = new Track();

                            switch((TrackTypeOld)chdTrack.type)
                            {
                                case TrackTypeOld.Audio:
                                    aaruTrack.BytesPerSector    = 2352;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.Audio;

                                    break;
                                case TrackTypeOld.Mode1:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TrackTypeOld.Mode1Raw:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TrackTypeOld.Mode2:
                                case TrackTypeOld.Mode2FormMix:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2336;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                case TrackTypeOld.Mode2Form1:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode2Form1;

                                    break;
                                case TrackTypeOld.Mode2Form2:
                                    aaruTrack.BytesPerSector    = 2324;
                                    aaruTrack.RawBytesPerSector = 2324;
                                    aaruTrack.Type              = TrackType.CdMode2Form2;

                                    break;
                                case TrackTypeOld.Mode2Raw:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_track_type_0,
                                                                    chdTrack.type));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            switch((SubTypeOld)chdTrack.subType)
                            {
                                case SubTypeOld.Cooked:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.PackedInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                case SubTypeOld.None:
                                    aaruTrack.SubchannelType = TrackSubchannelType.None;

                                    break;
                                case SubTypeOld.Raw:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.RawInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_subchannel_type_0,
                                                                    chdTrack.type));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            aaruTrack.Description = string.Format(Localization.Track_0, i + 1);
                            aaruTrack.EndSector   = currentSector + chdTrack.frames - 1;
                            aaruTrack.File        = imageFilter.Filename;
                            aaruTrack.FileType    = "BINARY";
                            aaruTrack.Filter      = imageFilter;
                            aaruTrack.StartSector = currentSector;
                            aaruTrack.Sequence    = i + 1;
                            aaruTrack.Session     = 1;

                            if(aaruTrack.Sequence == 1) aaruTrack.Indexes.Add(0, -150);

                            aaruTrack.Indexes.Add(1, (int)currentSector);
                            currentSector += chdTrack.frames + chdTrack.extraFrames;
                            _tracks.Add(aaruTrack.Sequence, aaruTrack);
                        }

                        _isCdrom = true;

                        break;

                    // "CHTR"
                    case CDROM_TRACK_METADATA:
                        if(_isHdd)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_hard_disk_and_a_CD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        if(_isGdrom)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_GD_ROM_and_a_CD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        string chtr      = StringHandlers.CToString(meta);
                        Regex  chtrRegEx = MetadataCdromRegex();
                        Match  chtrMatch = chtrRegEx.Match(chtr);

                        if(chtrMatch.Success)
                        {
                            _isCdrom = true;

                            var    trackNo   = uint.Parse(chtrMatch.Groups["track"].Value);
                            var    frames    = uint.Parse(chtrMatch.Groups["frames"].Value);
                            string subtype   = chtrMatch.Groups["sub_type"].Value;
                            string tracktype = chtrMatch.Groups["track_type"].Value;

                            if(trackNo != currentTrack)
                            {
                                AaruLogging.Error(Localization.Unsorted_tracks_cannot_proceed);

                                return ErrorNumber.NotSupported;
                            }

                            var aaruTrack = new Track();

                            switch(tracktype)
                            {
                                case TRACK_TYPE_AUDIO:
                                    aaruTrack.BytesPerSector    = 2352;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.Audio;

                                    break;
                                case TRACK_TYPE_MODE1:
                                case TRACK_TYPE_MODE1_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TRACK_TYPE_MODE1_RAW:
                                case TRACK_TYPE_MODE1_RAW_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TRACK_TYPE_MODE2:
                                case TRACK_TYPE_MODE2_2K:
                                case TRACK_TYPE_MODE2_FM:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2336;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                case TRACK_TYPE_MODE2_F1:
                                case TRACK_TYPE_MODE2_F1_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode2Form1;

                                    break;
                                case TRACK_TYPE_MODE2_F2:
                                case TRACK_TYPE_MODE2_F2_2K:
                                    aaruTrack.BytesPerSector    = 2324;
                                    aaruTrack.RawBytesPerSector = 2324;
                                    aaruTrack.Type              = TrackType.CdMode2Form2;

                                    break;
                                case TRACK_TYPE_MODE2_RAW:
                                case TRACK_TYPE_MODE2_RAW_2K:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_track_type_0, tracktype));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            switch(subtype)
                            {
                                case SUB_TYPE_COOKED:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.PackedInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                case SUB_TYPE_NONE:
                                    aaruTrack.SubchannelType = TrackSubchannelType.None;

                                    break;
                                case SUB_TYPE_RAW:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.RawInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_subchannel_type_0,
                                                                    subtype));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            aaruTrack.Description = string.Format(Localization.Track_0, trackNo);
                            aaruTrack.EndSector   = currentSector + frames - 1;
                            aaruTrack.File        = imageFilter.Filename;
                            aaruTrack.FileType    = "BINARY";
                            aaruTrack.Filter      = imageFilter;
                            aaruTrack.StartSector = currentSector;
                            aaruTrack.Sequence    = trackNo;
                            aaruTrack.Session     = 1;

                            if(aaruTrack.Sequence == 1) aaruTrack.Indexes.Add(0, -150);

                            aaruTrack.Indexes.Add(1, (int)currentSector);
                            currentSector += frames;
                            currentTrack++;
                            _tracks.Add(aaruTrack.Sequence, aaruTrack);
                        }

                        break;

                    // "CHT2"
                    case CDROM_TRACK_METADATA2:
                        if(_isHdd)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_hard_disk_and_a_CD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        if(_isGdrom)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_GD_ROM_and_a_CD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        string cht2      = StringHandlers.CToString(meta);
                        Regex  cht2RegEx = MetadataCdrom2Regex();
                        Match  cht2Match = cht2RegEx.Match(cht2);

                        if(cht2Match.Success)
                        {
                            _isCdrom = true;

                            var    trackNo   = uint.Parse(cht2Match.Groups["track"].Value);
                            var    frames    = uint.Parse(cht2Match.Groups["frames"].Value);
                            string subtype   = cht2Match.Groups["sub_type"].Value;
                            string trackType = cht2Match.Groups["track_type"].Value;

                            var pregap = uint.Parse(cht2Match.Groups["pregap"].Value);

                            // What is this, really? Same as track type?
                            string pregapType = cht2Match.Groups["pgtype"].Value;

                            // Read above, but for subchannel
                            string pregapSubType = cht2Match.Groups["pgsub"].Value;

                            // This is a recommendation (shall) of 150 sectors at the end of the last data track,
                            // or of any data track followed by an audio track, according to Yellow Book.
                            // It is indistinguishable from normal data.
                            // TODO: Does CHD store it, or like CDRWin, ignores it?
                            var postgap = uint.Parse(cht2Match.Groups["postgap"].Value);

                            if(trackNo != currentTrack)
                            {
                                AaruLogging.Error(Localization.Unsorted_tracks_cannot_proceed);

                                return ErrorNumber.NotSupported;
                            }

                            var aaruTrack = new Track();

                            switch(trackType)
                            {
                                case TRACK_TYPE_AUDIO:
                                    aaruTrack.BytesPerSector    = 2352;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.Audio;

                                    break;
                                case TRACK_TYPE_MODE1:
                                case TRACK_TYPE_MODE1_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TRACK_TYPE_MODE1_RAW:
                                case TRACK_TYPE_MODE1_RAW_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TRACK_TYPE_MODE2:
                                case TRACK_TYPE_MODE2_2K:
                                case TRACK_TYPE_MODE2_FM:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2336;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                case TRACK_TYPE_MODE2_F1:
                                case TRACK_TYPE_MODE2_F1_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode2Form1;

                                    break;
                                case TRACK_TYPE_MODE2_F2:
                                case TRACK_TYPE_MODE2_F2_2K:
                                    aaruTrack.BytesPerSector    = 2324;
                                    aaruTrack.RawBytesPerSector = 2324;
                                    aaruTrack.Type              = TrackType.CdMode2Form2;

                                    break;
                                case TRACK_TYPE_MODE2_RAW:
                                case TRACK_TYPE_MODE2_RAW_2K:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_track_type_0, trackType));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            switch(subtype)
                            {
                                case SUB_TYPE_COOKED:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.PackedInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                case SUB_TYPE_NONE:
                                    aaruTrack.SubchannelType = TrackSubchannelType.None;

                                    break;
                                case SUB_TYPE_RAW:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.RawInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_subchannel_type_0,
                                                                    subtype));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            aaruTrack.Description = string.Format(Localization.Track_0, trackNo);
                            aaruTrack.EndSector   = currentSector + frames - 1;
                            aaruTrack.File        = imageFilter.Filename;
                            aaruTrack.FileType    = "BINARY";
                            aaruTrack.Filter      = imageFilter;
                            aaruTrack.StartSector = currentSector;
                            aaruTrack.Sequence    = trackNo;
                            aaruTrack.Session     = 1;

                            if(aaruTrack.Sequence == 1)
                            {
                                if(pregap <= 150)
                                {
                                    aaruTrack.Indexes.Add(0, -150);
                                    aaruTrack.Pregap = 150;
                                }
                                else
                                {
                                    aaruTrack.Indexes.Add(0, -1 * (int)pregap);
                                    aaruTrack.Pregap = pregap;
                                }

                                aaruTrack.Indexes.Add(1, (int)currentSector);
                            }
                            else if(pregap > 0)
                            {
                                aaruTrack.Indexes.Add(0, (int)currentSector);
                                aaruTrack.Pregap = pregap;
                                aaruTrack.Indexes.Add(1, (int)(currentSector + pregap));
                            }
                            else
                                aaruTrack.Indexes.Add(1, (int)currentSector);

                            currentSector += frames;
                            currentTrack++;
                            _tracks.Add(aaruTrack.Sequence, aaruTrack);
                        }

                        break;

                    // "CHGT"
                    case GDROM_OLD_METADATA:
                        _swapAudio = true;
                        goto case GDROM_METADATA;

                    // "CHGD"
                    case GDROM_METADATA:
                        if(_isHdd)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_hard_disk_and_a_GD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        if(_isCdrom)
                        {
                            AaruLogging.Error(Localization
                                                 .Image_cannot_be_a_CD_ROM_and_a_GD_ROM_at_the_same_time_aborting);

                            return ErrorNumber.NotSupported;
                        }

                        string chgd      = StringHandlers.CToString(meta);
                        Regex  chgdRegEx = MetadataGdromRegex();
                        Match  chgdMatch = chgdRegEx.Match(chgd);

                        if(chgdMatch.Success)
                        {
                            _isGdrom = true;

                            var    trackNo   = uint.Parse(chgdMatch.Groups["track"].Value);
                            var    frames    = uint.Parse(chgdMatch.Groups["frames"].Value);
                            string subtype   = chgdMatch.Groups["sub_type"].Value;
                            string trackType = chgdMatch.Groups["track_type"].Value;

                            // TODO: Check pregap, postgap and pad behaviour
                            var    pregap        = uint.Parse(chgdMatch.Groups["pregap"].Value);
                            string pregapType    = chgdMatch.Groups["pgtype"].Value;
                            string pregapSubType = chgdMatch.Groups["pgsub"].Value;
                            var    postgap       = uint.Parse(chgdMatch.Groups["postgap"].Value);
                            var    pad           = uint.Parse(chgdMatch.Groups["pad"].Value);

                            if(trackNo != currentTrack)
                            {
                                AaruLogging.Error(Localization.Unsorted_tracks_cannot_proceed);

                                return ErrorNumber.NotSupported;
                            }

                            var aaruTrack = new Track();

                            switch(trackType)
                            {
                                case TRACK_TYPE_AUDIO:
                                    aaruTrack.BytesPerSector    = 2352;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.Audio;

                                    break;
                                case TRACK_TYPE_MODE1:
                                case TRACK_TYPE_MODE1_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TRACK_TYPE_MODE1_RAW:
                                case TRACK_TYPE_MODE1_RAW_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode1;

                                    break;
                                case TRACK_TYPE_MODE2:
                                case TRACK_TYPE_MODE2_2K:
                                case TRACK_TYPE_MODE2_FM:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2336;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                case TRACK_TYPE_MODE2_F1:
                                case TRACK_TYPE_MODE2_F1_2K:
                                    aaruTrack.BytesPerSector    = 2048;
                                    aaruTrack.RawBytesPerSector = 2048;
                                    aaruTrack.Type              = TrackType.CdMode2Form1;

                                    break;
                                case TRACK_TYPE_MODE2_F2:
                                case TRACK_TYPE_MODE2_F2_2K:
                                    aaruTrack.BytesPerSector    = 2324;
                                    aaruTrack.RawBytesPerSector = 2324;
                                    aaruTrack.Type              = TrackType.CdMode2Form2;

                                    break;
                                case TRACK_TYPE_MODE2_RAW:
                                case TRACK_TYPE_MODE2_RAW_2K:
                                    aaruTrack.BytesPerSector    = 2336;
                                    aaruTrack.RawBytesPerSector = 2352;
                                    aaruTrack.Type              = TrackType.CdMode2Formless;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_track_type_0, trackType));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            switch(subtype)
                            {
                                case SUB_TYPE_COOKED:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.PackedInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                case SUB_TYPE_NONE:
                                    aaruTrack.SubchannelType = TrackSubchannelType.None;

                                    break;
                                case SUB_TYPE_RAW:
                                    aaruTrack.SubchannelFile   = imageFilter.Filename;
                                    aaruTrack.SubchannelType   = TrackSubchannelType.RawInterleaved;
                                    aaruTrack.SubchannelFilter = imageFilter;

                                    break;
                                default:
                                {
                                    AaruLogging.Error(string.Format(Localization.Unsupported_subchannel_type_0,
                                                                    subtype));

                                    return ErrorNumber.NotSupported;
                                }
                            }

                            aaruTrack.Description = string.Format(Localization.Track_0, trackNo);
                            aaruTrack.EndSector   = currentSector + frames - 1;
                            aaruTrack.File        = imageFilter.Filename;
                            aaruTrack.FileType    = "BINARY";
                            aaruTrack.Filter      = imageFilter;
                            aaruTrack.StartSector = currentSector;
                            aaruTrack.Sequence    = trackNo;
                            aaruTrack.Session     = (ushort)(trackNo > 2 ? 2 : 1);

                            if(aaruTrack.Sequence == 1)
                            {
                                if(pregap <= 150)
                                {
                                    aaruTrack.Indexes.Add(0, -150);
                                    aaruTrack.Pregap = 150;
                                }
                                else
                                {
                                    aaruTrack.Indexes.Add(0, -1 * (int)pregap);
                                    aaruTrack.Pregap = pregap;
                                }

                                aaruTrack.Indexes.Add(1, (int)currentSector);
                            }
                            else if(pregap > 0)
                            {
                                aaruTrack.Indexes.Add(0, (int)currentSector);
                                aaruTrack.Pregap = pregap;
                                aaruTrack.Indexes.Add(1, (int)(currentSector + pregap));
                            }
                            else
                                aaruTrack.Indexes.Add(1, (int)currentSector);

                            currentSector += frames;
                            currentTrack++;
                            _tracks.Add(aaruTrack.Sequence, aaruTrack);
                        }

                        break;

                    // "IDNT"
                    case HARD_DISK_IDENT_METADATA:
                        Identify.IdentifyDevice? idnt = CommonTypes.Structs.Devices.ATA.Identify.Decode(meta);

                        if(idnt.HasValue)
                        {
                            _imageInfo.MediaManufacturer     = idnt.Value.MediaManufacturer;
                            _imageInfo.MediaSerialNumber     = idnt.Value.MediaSerial;
                            _imageInfo.DriveModel            = idnt.Value.Model;
                            _imageInfo.DriveSerialNumber     = idnt.Value.SerialNumber;
                            _imageInfo.DriveFirmwareRevision = idnt.Value.FirmwareRevision;

                            if(idnt is { CurrentCylinders: > 0, CurrentHeads: > 0, CurrentSectorsPerTrack: > 0 })
                            {
                                _imageInfo.Cylinders       = idnt.Value.CurrentCylinders;
                                _imageInfo.Heads           = idnt.Value.CurrentHeads;
                                _imageInfo.SectorsPerTrack = idnt.Value.CurrentSectorsPerTrack;
                            }
                            else
                            {
                                _imageInfo.Cylinders       = idnt.Value.Cylinders;
                                _imageInfo.Heads           = idnt.Value.Heads;
                                _imageInfo.SectorsPerTrack = idnt.Value.SectorsPerTrack;
                            }
                        }

                        _identify = meta;

                        if(!_imageInfo.ReadableMediaTags.Contains(MediaTagType.ATA_IDENTIFY))
                            _imageInfo.ReadableMediaTags.Add(MediaTagType.ATA_IDENTIFY);

                        break;
                    case PCMCIA_CIS_METADATA:
                        _cis = meta;

                        if(!_imageInfo.ReadableMediaTags.Contains(MediaTagType.PCMCIA_CIS))
                            _imageInfo.ReadableMediaTags.Add(MediaTagType.PCMCIA_CIS);

                        break;
                }

                nextMetaOff = header.next;
            }

            if(_isHdd)
            {
                _sectorsPerHunk              = _bytesPerHunk        / _imageInfo.SectorSize;
                _imageInfo.Sectors           = _imageInfo.ImageSize / _imageInfo.SectorSize;
                _imageInfo.MediaType         = MediaType.GENERIC_HDD;
                _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;
            }
            else if(_isCdrom)
            {
                _sectorsPerHunk              = _bytesPerHunk / CD_FRAME_SIZE;
                _imageInfo.MediaType         = MediaType.CDROM;
                _imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc;

                foreach(Track aaruTrack in _tracks.Values)
                    _imageInfo.Sectors += aaruTrack.EndSector - aaruTrack.StartSector + 1;
            }
            else if(_isGdrom)
            {
                _sectorsPerHunk              = _bytesPerHunk / CD_FRAME_SIZE;
                _imageInfo.MediaType         = MediaType.GDROM;
                _imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc;

                foreach(Track aaruTrack in _tracks.Values)
                    _imageInfo.Sectors += aaruTrack.EndSector - aaruTrack.StartSector + 1;
            }
            else
            {
                AaruLogging.Error(Localization.Image_does_not_represent_a_known_media_aborting);

                return ErrorNumber.NotSupported;
            }
        }

        if(_isCdrom || _isGdrom)
        {
            _offsetmap  = new Dictionary<ulong, uint>();
            _partitions = [];
            ulong partPos = 0;

            foreach(Track aaruTrack in _tracks.Values)
            {
                var partition = new Partition
                {
                    Description = aaruTrack.Description,
                    Size = (aaruTrack.EndSector - (ulong)aaruTrack.Indexes[1] + 1) * (ulong)aaruTrack.RawBytesPerSector,
                    Length = aaruTrack.EndSector - (ulong)aaruTrack.Indexes[1] + 1,
                    Sequence = aaruTrack.Sequence,
                    Offset = partPos,
                    Start = (ulong)aaruTrack.Indexes[1],
                    Type = aaruTrack.Type.ToString()
                };

                partPos += partition.Length;
                _offsetmap.Add(aaruTrack.StartSector, aaruTrack.Sequence);

                if(aaruTrack.SubchannelType != TrackSubchannelType.None)
                {
                    if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSubchannel))
                        _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSubchannel);
                }

                switch(aaruTrack.Type)
                {
                    case TrackType.CdMode1:
                    case TrackType.CdMode2Form1:
                        if(aaruTrack.RawBytesPerSector == 2352)
                        {
                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSync))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSync);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorHeader))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorHeader);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSubHeader))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSubHeader);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorEcc))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorEcc);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorEccP))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorEccP);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorEccQ))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorEccQ);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorEdc))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorEdc);
                        }

                        break;
                    case TrackType.CdMode2Form2:
                        if(aaruTrack.RawBytesPerSector == 2352)
                        {
                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSync))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSync);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorHeader))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorHeader);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSubHeader))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSubHeader);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorEdc))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorEdc);
                        }

                        break;
                    case TrackType.CdMode2Formless:
                        if(aaruTrack.RawBytesPerSector == 2352)
                        {
                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSync))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSync);

                            if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorHeader))
                                _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorHeader);
                        }

                        break;
                }

                if(aaruTrack.BytesPerSector > _imageInfo.SectorSize)
                    _imageInfo.SectorSize = (uint)aaruTrack.BytesPerSector;

                _partitions.Add(partition);
            }

            _imageInfo.HasPartitions = true;
            _imageInfo.HasSessions   = true;
        }

        _maxBlockCache  = (int)(MAX_CACHE_SIZE / (_imageInfo.SectorSize * _sectorsPerHunk));
        _maxSectorCache = (int)(MAX_CACHE_SIZE / _imageInfo.SectorSize);

        _imageStream = stream;

        _sectorCache = new Dictionary<ulong, byte[]>();
        _hunkCache   = new Dictionary<ulong, byte[]>();

        // TODO: Detect CompactFlash
        // TODO: Get manufacturer and drive name from CIS if applicable
        if(_cis != null) _imageInfo.MediaType = MediaType.PCCardTypeI;

        _sectorBuilder = new SectorBuilder();

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus)
    {
        buffer       = null;
        sectorStatus = SectorStatus.NotDumped;

        if(negative) return ErrorNumber.NotSupported;

        if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;

        Track track = _isHdd ? new Track() : GetTrack(sectorAddress);
        uint  sectorSize;

        if(!_sectorCache.TryGetValue(sectorAddress, out byte[] sector))
        {
            if(_isHdd)
                sectorSize = _imageInfo.SectorSize;
            else
            {
                sectorSize = (uint)track.RawBytesPerSector +
                             (track.SubchannelType != TrackSubchannelType.None ? 96u : 0u);
            }

            ulong hunkNo = sectorAddress                   / _sectorsPerHunk;
            ulong secOff = sectorAddress % _sectorsPerHunk * sectorSize;

            ErrorNumber errno = GetHunk(hunkNo, out byte[] hunk);

            if(errno != ErrorNumber.NoError) return errno;

            sector = new byte[sectorSize];
            Array.Copy(hunk, (int)secOff, sector, 0, sector.Length);

            if(_sectorCache.Count >= _maxSectorCache) _sectorCache.Clear();

            _sectorCache.Add(sectorAddress, sector);
        }

        if(_isHdd)
        {
            buffer = sector;

            sectorStatus = SectorStatus.Dumped;

            return ErrorNumber.NoError;
        }

        uint sectorOffset;
        var  mode2 = false;

        switch(track.Type)
        {
            case TrackType.CdMode1:
            {
                if(track.RawBytesPerSector == 2352)
                {
                    sectorOffset = 16;
                    sectorSize   = 2048;
                }
                else
                {
                    sectorOffset = 0;
                    sectorSize   = 2048;
                }

                break;
            }
            case TrackType.CdMode2Form1:
            {
                if(track.RawBytesPerSector == 2352)
                {
                    sectorOffset = 0;
                    sectorSize   = 2352;
                    mode2        = true;
                }
                else
                {
                    sectorOffset = 0;
                    sectorSize   = 2048;
                }

                break;
            }

            case TrackType.CdMode2Form2:
            {
                if(track.RawBytesPerSector == 2352)
                {
                    sectorOffset = 0;
                    sectorSize   = 2352;
                    mode2        = true;
                }
                else
                {
                    sectorOffset = 0;
                    sectorSize   = 2324;
                }

                break;
            }

            case TrackType.CdMode2Formless:
            {
                sectorOffset = 0;
                sectorSize   = (uint)track.RawBytesPerSector;
                mode2        = true;

                break;
            }

            case TrackType.Audio:
            {
                sectorOffset = 0;
                sectorSize   = 2352;

                break;
            }

            default:
                return ErrorNumber.NotSupported;
        }

        sectorStatus = SectorStatus.Dumped;
        buffer       = new byte[sectorSize];

        if(mode2)
            buffer = Sector.GetUserDataFromMode2(sector);
        else if(track.Type == TrackType.Audio && _swapAudio)
        {
            for(var i = 0; i < 2352; i += 2)
            {
                buffer[i + 1] = sector[i];
                buffer[i]     = sector[i + 1];
            }
        }
        else
            Array.Copy(sector, sectorOffset, buffer, 0, sectorSize);

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer)
    {
        buffer = null;

        if(negative) return ErrorNumber.NotSupported;

        if(_isHdd) return ErrorNumber.NotSupported;

        if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;

        Track track = GetTrack(sectorAddress);

        uint sectorSize;

        if(!_sectorCache.TryGetValue(sectorAddress, out byte[] sector))
        {
            sectorSize = (uint)track.RawBytesPerSector + (track.SubchannelType != TrackSubchannelType.None ? 96u : 0u);

            ulong hunkNo = sectorAddress                   / _sectorsPerHunk;
            ulong secOff = sectorAddress % _sectorsPerHunk * sectorSize;

            ErrorNumber errno = GetHunk(hunkNo, out byte[] hunk);

            if(errno != ErrorNumber.NoError) return errno;

            sector = new byte[sectorSize];
            Array.Copy(hunk, (int)secOff, sector, 0, sector.Length);

            if(_sectorCache.Count >= _maxSectorCache) _sectorCache.Clear();

            _sectorCache.Add(sectorAddress, sector);
        }

        if(_isHdd)
        {
            buffer = sector;

            return ErrorNumber.NoError;
        }

        uint sectorOffset;

        if(tag == SectorTagType.CdSectorSubchannel)
        {
            switch(track.SubchannelType)
            {
                case TrackSubchannelType.None:
                    return ErrorNumber.NoData;
                case TrackSubchannelType.RawInterleaved:
                    sectorOffset = (uint)track.RawBytesPerSector;
                    sectorSize   = 96;

                    break;
                default:
                    return ErrorNumber.NotSupported;
            }
        }
        else
        {
            switch(track.Type)
            {
                case TrackType.CdMode1:
                case TrackType.CdMode2Form1:
                {
                    if(track.RawBytesPerSector == 2352)
                    {
                        switch(tag)
                        {
                            case SectorTagType.CdSectorSync:
                            {
                                sectorOffset = 0;
                                sectorSize   = 12;

                                break;
                            }

                            case SectorTagType.CdSectorHeader:
                            {
                                sectorOffset = 12;
                                sectorSize   = 4;

                                break;
                            }

                            case SectorTagType.CdSectorSubHeader:
                                return ErrorNumber.NotSupported;
                            case SectorTagType.CdSectorEcc:
                            {
                                sectorOffset = 2076;
                                sectorSize   = 276;

                                break;
                            }

                            case SectorTagType.CdSectorEccP:
                            {
                                sectorOffset = 2076;
                                sectorSize   = 172;

                                break;
                            }

                            case SectorTagType.CdSectorEccQ:
                            {
                                sectorOffset = 2248;
                                sectorSize   = 104;

                                break;
                            }

                            case SectorTagType.CdSectorEdc:
                            {
                                sectorOffset = 2064;
                                sectorSize   = 4;

                                break;
                            }

                            default:
                                return ErrorNumber.NotSupported;
                        }
                    }
                    else
                        return ErrorNumber.NoData;

                    break;
                }

                case TrackType.CdMode2Form2:
                {
                    if(track.RawBytesPerSector == 2352)
                    {
                        switch(tag)
                        {
                            case SectorTagType.CdSectorSync:
                            {
                                sectorOffset = 0;
                                sectorSize   = 12;

                                break;
                            }

                            case SectorTagType.CdSectorHeader:
                            {
                                sectorOffset = 12;
                                sectorSize   = 4;

                                break;
                            }

                            case SectorTagType.CdSectorSubHeader:
                            {
                                sectorOffset = 16;
                                sectorSize   = 8;

                                break;
                            }

                            case SectorTagType.CdSectorEdc:
                            {
                                sectorOffset = 2348;
                                sectorSize   = 4;

                                break;
                            }

                            default:
                                return ErrorNumber.NotSupported;
                        }
                    }
                    else
                    {
                        switch(tag)
                        {
                            case SectorTagType.CdSectorSync:
                            case SectorTagType.CdSectorHeader:
                            case SectorTagType.CdSectorEcc:
                            case SectorTagType.CdSectorEccP:
                            case SectorTagType.CdSectorEccQ:
                                return ErrorNumber.NotSupported;
                            case SectorTagType.CdSectorSubHeader:
                            {
                                sectorOffset = 0;
                                sectorSize   = 8;

                                break;
                            }

                            case SectorTagType.CdSectorEdc:
                            {
                                sectorOffset = 2332;
                                sectorSize   = 4;

                                break;
                            }

                            default:
                                return ErrorNumber.NotSupported;
                        }
                    }

                    break;
                }

                case TrackType.CdMode2Formless:
                {
                    if(track.RawBytesPerSector == 2352)
                    {
                        switch(tag)
                        {
                            case SectorTagType.CdSectorSync:
                            case SectorTagType.CdSectorHeader:
                            case SectorTagType.CdSectorEcc:
                            case SectorTagType.CdSectorEccP:
                            case SectorTagType.CdSectorEccQ:
                                return ErrorNumber.NotSupported;
                            case SectorTagType.CdSectorSubHeader:
                            {
                                sectorOffset = 0;
                                sectorSize   = 8;

                                break;
                            }

                            case SectorTagType.CdSectorEdc:
                            {
                                sectorOffset = 2332;
                                sectorSize   = 4;

                                break;
                            }

                            default:
                                return ErrorNumber.NotSupported;
                        }
                    }
                    else
                        return ErrorNumber.NoData;

                    break;
                }

                case TrackType.Audio:
                    return ErrorNumber.NoData;
                default:
                    return ErrorNumber.NotImplemented;
            }
        }

        buffer = new byte[sectorSize];

        if(track.Type == TrackType.Audio && _swapAudio)
        {
            for(var i = 0; i < 2352; i += 2)
            {
                buffer[i + 1] = sector[i];
                buffer[i]     = sector[i + 1];
            }
        }
        else
            Array.Copy(sector, sectorOffset, buffer, 0, sectorSize);

        if(track.Type == TrackType.Audio && _swapAudio)
        {
            for(var i = 0; i < 2352; i += 2)
            {
                buffer[i + 1] = sector[i];
                buffer[i]     = sector[i + 1];
            }
        }
        else
            Array.Copy(sector, sectorOffset, buffer, 0, sectorSize);

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectors(ulong              sectorAddress, bool negative, uint length, out byte[] buffer,
                                   out SectorStatus[] sectorStatus)
    {
        buffer       = null;
        sectorStatus = null;

        if(negative) return ErrorNumber.NotSupported;

        if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;

        if(sectorAddress + length > _imageInfo.Sectors) return ErrorNumber.OutOfRange;

        var ms = new MemoryStream();
        sectorStatus = new SectorStatus[length];

        for(uint i = 0; i < length; i++)
        {
            ErrorNumber errno = ReadSector(sectorAddress + i, false, out byte[] sector, out SectorStatus status);
            sectorStatus[i] = status;

            if(errno != ErrorNumber.NoError) return errno;

            ms.Write(sector, 0, sector.Length);
        }

        buffer = ms.ToArray();

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorsTag(ulong      sectorAddress, bool negative, uint length, SectorTagType tag,
                                      out byte[] buffer)
    {
        buffer = null;

        if(negative) return ErrorNumber.NotSupported;

        if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;

        if(sectorAddress + length > _imageInfo.Sectors) return ErrorNumber.OutOfRange;

        var ms = new MemoryStream();

        for(uint i = 0; i < length; i++)
        {
            ErrorNumber errno = ReadSectorTag(sectorAddress + i, false, tag, out byte[] sector);

            if(errno != ErrorNumber.NoError) return errno;

            ms.Write(sector, 0, sector.Length);
        }

        buffer = ms.ToArray();

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorLong(ulong            sectorAddress, bool negative, out byte[] buffer,
                                      out SectorStatus sectorStatus)
    {
        buffer       = null;
        sectorStatus = SectorStatus.NotDumped;

        if(negative) return ErrorNumber.NotSupported;

        if(_isHdd) return ReadSector(sectorAddress, false, out buffer, out sectorStatus);

        if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;

        Track track = GetTrack(sectorAddress);

        if(!_sectorCache.TryGetValue(sectorAddress, out byte[] sector))
        {
            uint sectorSize = (uint)track.RawBytesPerSector +
                              (track.SubchannelType != TrackSubchannelType.None ? 96u : 0u);

            ulong hunkNo = sectorAddress                   / _sectorsPerHunk;
            ulong secOff = sectorAddress % _sectorsPerHunk * sectorSize;

            ErrorNumber errno = GetHunk(hunkNo, out byte[] hunk);

            if(errno != ErrorNumber.NoError) return errno;

            sectorStatus = SectorStatus.Dumped;
            sector       = new byte[sectorSize];
            Array.Copy(hunk, (int)secOff, sector, 0, sector.Length);

            if(_sectorCache.Count >= _maxSectorCache) _sectorCache.Clear();

            _sectorCache.Add(sectorAddress, sector);
        }

        sectorStatus = SectorStatus.Dumped;
        buffer       = new byte[track.RawBytesPerSector];

        if(track.Type == TrackType.Audio && _swapAudio)
        {
            for(var i = 0; i < 2352; i += 2)
            {
                buffer[i + 1] = sector[i];
                buffer[i]     = sector[i + 1];
            }
        }
        else
            Array.Copy(sector, 0, buffer, 0, track.RawBytesPerSector);

        switch(track.Type)
        {
            case TrackType.CdMode1 when track.RawBytesPerSector == 2048:
            {
                var fullSector = new byte[2352];

                Array.Copy(buffer, 0, fullSector, 16, 2048);
                _sectorBuilder.ReconstructPrefix(ref fullSector, TrackType.CdMode1, (long)sectorAddress);
                _sectorBuilder.ReconstructEcc(ref fullSector, TrackType.CdMode1);

                buffer = fullSector;

                break;
            }
            case TrackType.CdMode2Form1 when track.RawBytesPerSector == 2048:
            {
                var fullSector = new byte[2352];

                Array.Copy(buffer, 0, fullSector, 24, 2048);
                _sectorBuilder.ReconstructPrefix(ref fullSector, TrackType.CdMode2Form1, (long)sectorAddress);
                _sectorBuilder.ReconstructEcc(ref fullSector, TrackType.CdMode2Form1);

                buffer = fullSector;

                break;
            }
            case TrackType.CdMode2Form1 when track.RawBytesPerSector == 2324:
            {
                var fullSector = new byte[2352];

                Array.Copy(buffer, 0, fullSector, 24, 2324);
                _sectorBuilder.ReconstructPrefix(ref fullSector, TrackType.CdMode2Form2, (long)sectorAddress);
                _sectorBuilder.ReconstructEcc(ref fullSector, TrackType.CdMode2Form2);

                buffer = fullSector;

                break;
            }
            case TrackType.CdMode2Formless when track.RawBytesPerSector == 2336:
            {
                var fullSector = new byte[2352];

                _sectorBuilder.ReconstructPrefix(ref fullSector, TrackType.CdMode2Formless, (long)sectorAddress);
                Array.Copy(buffer, 0, fullSector, 16, 2336);

                buffer = fullSector;

                break;
            }
        }

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorsLong(ulong              sectorAddress, bool negative, uint length, out byte[] buffer,
                                       out SectorStatus[] sectorStatus)
    {
        buffer       = null;
        sectorStatus = null;

        if(negative) return ErrorNumber.NotSupported;

        if(sectorAddress > _imageInfo.Sectors - 1) return ErrorNumber.OutOfRange;

        if(sectorAddress + length > _imageInfo.Sectors) return ErrorNumber.OutOfRange;

        var ms = new MemoryStream();
        sectorStatus = new SectorStatus[length];

        for(uint i = 0; i < length; i++)
        {
            ErrorNumber errno = ReadSectorLong(sectorAddress + i, false, out byte[] sector, out SectorStatus status);
            sectorStatus[i] = status;

            if(errno != ErrorNumber.NoError) return errno;

            ms.Write(sector, 0, sector.Length);
        }

        buffer = ms.ToArray();

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer)
    {
        buffer = null;

        switch(tag)
        {
            case MediaTagType.ATA_IDENTIFY:
                if(_imageInfo.ReadableMediaTags.Contains(MediaTagType.ATA_IDENTIFY))
                    buffer = _identify?.Clone() as byte[];

                return buffer == null ? ErrorNumber.NoData : ErrorNumber.NoError;
            case MediaTagType.PCMCIA_CIS:
                if(_imageInfo.ReadableMediaTags.Contains(MediaTagType.PCMCIA_CIS)) buffer = _cis?.Clone() as byte[];

                return buffer == null ? ErrorNumber.NoData : ErrorNumber.NoError;
            default:
                return ErrorNumber.NotSupported;
        }
    }

    /// <inheritdoc />
    public List<Track> GetSessionTracks(Session session) => _isHdd ? null : GetSessionTracks(session.Sequence);

    /// <inheritdoc />
    public List<Track> GetSessionTracks(ushort session) =>
        _isHdd ? null : _tracks.Values.Where(track => track.Session == session).ToList();

    /// <inheritdoc />
    public ErrorNumber ReadSector(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus)
    {
        buffer       = null;
        sectorStatus = SectorStatus.NotDumped;

        return _isHdd
                   ? ErrorNumber.NotSupported
                   : ReadSector(GetAbsoluteSector(sectorAddress, track), false, out buffer, out sectorStatus);
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag, out byte[] buffer)
    {
        buffer = null;

        return _isHdd
                   ? ErrorNumber.NotSupported
                   : ReadSectorTag(GetAbsoluteSector(sectorAddress, track), false, tag, out buffer);
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectors(ulong              sectorAddress, uint length, uint track, out byte[] buffer,
                                   out SectorStatus[] sectorStatus)
    {
        buffer       = null;
        sectorStatus = null;

        return _isHdd
                   ? ErrorNumber.NotSupported
                   : ReadSectors(GetAbsoluteSector(sectorAddress, track), false, length, out buffer, out sectorStatus);
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorsTag(ulong      sectorAddress, uint length, uint track, SectorTagType tag,
                                      out byte[] buffer)
    {
        buffer = null;

        return _isHdd
                   ? ErrorNumber.NotSupported
                   : ReadSectorsTag(GetAbsoluteSector(sectorAddress, track), false, length, tag, out buffer);
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorLong(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus)
    {
        buffer       = null;
        sectorStatus = SectorStatus.NotDumped;

        return _isHdd
                   ? ErrorNumber.NotSupported
                   : ReadSectorLong(GetAbsoluteSector(sectorAddress, track), false, out buffer, out sectorStatus);
    }

    /// <inheritdoc />
    public ErrorNumber ReadSectorsLong(ulong              sectorAddress, uint length, uint track, out byte[] buffer,
                                       out SectorStatus[] sectorStatus)
    {
        buffer       = null;
        sectorStatus = null;

        return _isHdd
                   ? ErrorNumber.NotSupported
                   : ReadSectorsLong(GetAbsoluteSector(sectorAddress, track),
                                     false,
                                     length,
                                     out buffer,
                                     out sectorStatus);
    }

#endregion
}