// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Read.cs
// Author(s)      : Rebecca Wallander <sakcheen@gmail.com>
//
// Component      : Disk image plugins.
//
// --[ Description ] ----------------------------------------------------------
//
//     Reads A2R flux 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 Rebecca Wallander
// ****************************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Helpers;
using Aaru.Logging;

namespace Aaru.Images;

[SuppressMessage("ReSharper", "UnusedType.Global")]
public sealed partial class A2R
{
#region IWritableFluxImage Members

    /// <inheritdoc />
    public ErrorNumber Open(IFilter imageFilter)
    {
        _a2RStream = imageFilter.GetDataForkStream();
        _a2RStream.Seek(0, SeekOrigin.Begin);

        _a2RFilter = imageFilter;
        _mediaTags = [];

        // Per A2R spec: Read 8-byte header
        // Bytes 0-2: Signature ("A2R")
        // Byte 3: Version (0x32 = 2.x, 0x33 = 3.x)
        // Byte 4: High bit test (0xFF)
        // Bytes 5-7: Line test (0x0A 0x0D 0x0A)
        var hdr = new byte[Marshal.SizeOf<A2RHeader>()];
        _a2RStream.EnsureRead(hdr, 0, Marshal.SizeOf<A2RHeader>());

        _header = Marshal.ByteArrayToStructureLittleEndian<A2RHeader>(hdr);

        AaruLogging.Debug(MODULE_NAME, "header.signature = \"{0}\"", StringHandlers.CToString(_header.signature));

        AaruLogging.Debug(MODULE_NAME, "header.version = {0}",        _header.version);
        AaruLogging.Debug(MODULE_NAME, "header.highBitTest = {0:X2}", _header.highBitTest);

        // TODO: ensure high bit test is 0xFF
        // TODO: ensure line test is 0x0A 0x0D 0x0A

        AaruLogging.Debug(MODULE_NAME,
                          "header.lineTest = {0:X2} {1:X2} {2:X2}",
                          _header.lineTest[0],
                          _header.lineTest[1],
                          _header.lineTest[2]);

        var infoMagic = new byte[4];
        _a2RStream.EnsureRead(infoMagic, 0, 4);

        // Per A2R spec: INFO chunk must be the first chunk after the 8-byte header (at byte 8)
        if(!_infoChunkSignature.SequenceEqual(infoMagic)) return ErrorNumber.UnrecognizedFormat;

        _a2RStream.Seek(-4, SeekOrigin.Current);

        switch(_header.version)
        {
            case 0x32:
            {
                var infoChnk = new byte[Marshal.SizeOf<InfoChunkV2>()];
                _a2RStream.EnsureRead(infoChnk, 0, Marshal.SizeOf<InfoChunkV2>());
                _infoChunkV2 = Marshal.ByteArrayToStructureLittleEndian<InfoChunkV2>(infoChnk);

                AaruLogging.Debug(MODULE_NAME,
                                  "infoChunk.header.chunkId = \"{0}\"",
                                  StringHandlers.CToString(_infoChunkV2.header.chunkId));

                AaruLogging.Debug(MODULE_NAME, "infoChunk.header.chunkSize = {0}", _infoChunkV2.header.chunkSize);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.version = {0}", _infoChunkV2.version);

                AaruLogging.Debug(MODULE_NAME,
                                  "infoChunk.creator = \"{0}\"",
                                  StringHandlers.CToString(_infoChunkV2.creator).TrimEnd());

                AaruLogging.Debug(MODULE_NAME, "infoChunk.diskType = {0}", _infoChunkV2.diskType);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.writeProtected = {0}", _infoChunkV2.writeProtected);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.synchronized = {0}", _infoChunkV2.synchronized);

                _imageInfo.Creator = Encoding.ASCII.GetString(_infoChunkV2.creator).TrimEnd();

                // Store write protection status as media tag
                // Boolean value: 1 byte where 0 = false (not write protected), 1 = true (write protected)
                _mediaTags[MediaTagType.Floppy_WriteProtection] = _infoChunkV2.writeProtected == 1 ? [1] : [0];
                _imageInfo.ReadableMediaTags.Add(MediaTagType.Floppy_WriteProtection);

                // The disk type actually only denotes the form factor of the media, and not the media type.
                switch(_infoChunkV2.diskType)
                {
                    case A2RDiskType._35:
                        _imageInfo.Heads           = 2;
                        _imageInfo.Cylinders       = 80;
                        _imageInfo.MediaType       = MediaType.AppleSonyDS;
                        _imageInfo.SectorsPerTrack = 10;

                        break;
                    case A2RDiskType._525:
                        _imageInfo.Heads     = 1;
                        _imageInfo.Cylinders = 40;
                        _imageInfo.MediaType = MediaType.Apple32SS;

                        break;
                    default:
                        return ErrorNumber.OutOfRange;
                }

                // A2R images always represent block media (floppy disks)
                _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;

                break;
            }
            case 0x33:
            {
                var infoChk = new byte[Marshal.SizeOf<InfoChunkV3>()];
                _a2RStream.EnsureRead(infoChk, 0, Marshal.SizeOf<InfoChunkV3>());
                _infoChunkV3 = Marshal.ByteArrayToStructureLittleEndian<InfoChunkV3>(infoChk);

                AaruLogging.Debug(MODULE_NAME,
                                  "infoChunk.header.chunkId = \"{0}\"",
                                  StringHandlers.CToString(_infoChunkV3.header.chunkId));

                AaruLogging.Debug(MODULE_NAME, "infoChunk.header.chunkSize = {0}", _infoChunkV3.header.chunkSize);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.version = {0}", _infoChunkV3.version);

                AaruLogging.Debug(MODULE_NAME,
                                  "infoChunk.creator = \"{0}\"",
                                  StringHandlers.CToString(_infoChunkV3.creator).TrimEnd());

                AaruLogging.Debug(MODULE_NAME, "infoChunk.driveType = {0}", _infoChunkV3.driveType);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.writeProtected = {0}", _infoChunkV3.writeProtected);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.synchronized = {0}", _infoChunkV3.synchronized);

                AaruLogging.Debug(MODULE_NAME, "infoChunk.hardSectorCount = {0}", _infoChunkV3.hardSectorCount);

                _imageInfo.Creator = Encoding.ASCII.GetString(_infoChunkV3.creator).TrimEnd();

                // Drive type is actually only used to denote the drive that was used to create the image, 
                // and not the media type. I.e if an 80-track drive is being used to image a double-sided 40-track disk, 
                // but the user is only reading every other cylinder, the Drive Type should still be 3 and the Location 
                // fields in the capture should be using the actual cylinder number that the head is physically on.
                switch(_infoChunkV3.driveType)
                {
                    case A2rDriveType.SS_525_40trk_quarterStep:
                        _imageInfo.Heads     = 1;
                        _imageInfo.Cylinders = 40;
                        _imageInfo.MediaType = MediaType.Apple32SS;

                        break;
                    case A2rDriveType.DS_35_80trk_appleCLV:
                        _imageInfo.Heads           = 2;
                        _imageInfo.Cylinders       = 80;
                        _imageInfo.MediaType       = MediaType.AppleSonyDS;
                        _imageInfo.SectorsPerTrack = 10;

                        break;
                    case A2rDriveType.DS_525_80trk:
                        _imageInfo.Heads     = 2;
                        _imageInfo.Cylinders = 80;
                        _imageInfo.MediaType = MediaType.DOS_525_HD;

                        break;
                    case A2rDriveType.DS_525_40trk:
                        _imageInfo.Heads           = 2;
                        _imageInfo.Cylinders       = 40;
                        _imageInfo.MediaType       = MediaType.DOS_525_DS_DD_9;
                        _imageInfo.SectorsPerTrack = 9;

                        break;
                    case A2rDriveType.DS_35_80trk:
                        _imageInfo.Heads           = 2;
                        _imageInfo.Cylinders       = 80;
                        _imageInfo.MediaType       = MediaType.DOS_35_HD;
                        _imageInfo.SectorsPerTrack = 18;

                        break;
                    case A2rDriveType.DS_8:
                        _imageInfo.Heads     = 2;
                        _imageInfo.Cylinders = 40;

                        break;
                    case A2rDriveType.DS_3_80trk:
                        _imageInfo.Heads     = 2;
                        _imageInfo.Cylinders = 80;

                        break;
                    case A2rDriveType.DS_3_40trk:
                        _imageInfo.Heads     = 2;
                        _imageInfo.Cylinders = 40;

                        break;
                    default:
                        return ErrorNumber.OutOfRange;
                }

                // A2R images always represent block media (floppy disks)
                _imageInfo.MetadataMediaType = MetadataMediaType.BlockMedia;

                break;
            }
        }

        _a2RCaptures = [];

        // Per A2R spec: Process chunks sequentially. Chunk structure: Chunk ID (4 bytes) + Chunk Size (4 bytes) + Chunk Data
        // Unknown chunks are safely skipped by reading chunk size and seeking past them
        while(_a2RStream.Position < _a2RStream.Length)
        {
            var chunkHdr = new byte[Marshal.SizeOf<ChunkHeader>()];
            _a2RStream.EnsureRead(chunkHdr, 0, Marshal.SizeOf<ChunkHeader>());
            ChunkHeader chunkHeader = Marshal.ByteArrayToStructureLittleEndian<ChunkHeader>(chunkHdr);
            _a2RStream.Seek(-Marshal.SizeOf<ChunkHeader>(), SeekOrigin.Current);

            // Per A2R spec: Process known chunks (INFO, RWCP, SLVD, META for 3.x; INFO, STRM, META for 2.x)
            // Unknown chunks are skipped
            switch(chunkHeader.chunkId)
            {
                case var rwcp when rwcp.SequenceEqual(_rwcpChunkSignature):
                    // Per A2R 3.x spec: RWCP (Raw Captures) chunk contains raw flux data streams
                    // Chunk header: ChunkHeader (8 bytes) + RWCP Version (1 byte) + Resolution (4 bytes) + Reserved (11 bytes)
                    // Each capture: Mark (0x43) + Capture Type (1 byte) + Location (2 bytes) + Index Signals + Flux Data
                    var rwcpBuffer = new byte[Marshal.SizeOf<RwcpChunkHeader>()];
                    _a2RStream.EnsureRead(rwcpBuffer, 0, Marshal.SizeOf<RwcpChunkHeader>());
                    RwcpChunkHeader rwcpChunk = Marshal.ByteArrayToStructureLittleEndian<RwcpChunkHeader>(rwcpBuffer);

                    // Per A2R 3.x spec: RWCP captures start with mark 0x43
                    // Chunk ends with mark 0x58
                    while(_a2RStream.ReadByte() == 0x43)
                    {
                        var capture = new StreamCapture
                        {
                            mark        = 0x43,
                            captureType = (byte)_a2RStream.ReadByte()
                        };

                        var location = new byte[2];
                        _a2RStream.EnsureRead(location, 0, 2);
                        capture.location = BitConverter.ToUInt16(location);

                        A2RLocationToHeadTrackSub(capture.location,
                                                  _imageInfo.MediaType,
                                                  out capture.head,
                                                  out capture.track,
                                                  out capture.subTrack);

                        if(capture.head + 1 > _imageInfo.Heads) _imageInfo.Heads = capture.head + 1;

                        if(capture.track + 1 > _imageInfo.Cylinders) _imageInfo.Cylinders = (uint)(capture.track + 1);

                        capture.numberOfIndexSignals = (byte)_a2RStream.ReadByte();
                        capture.indexSignals         = new uint[capture.numberOfIndexSignals];

                        for(var i = 0; capture.numberOfIndexSignals > i; i++)
                        {
                            var index = new byte[4];
                            _a2RStream.EnsureRead(index, 0, 4);
                            capture.indexSignals[i] = BitConverter.ToUInt32(index);
                            // TODO: Soft sectored disks are not required to have more than one index signal, 
                            // even for captures that were multiple rotations. If we encounter a capture with only 
                            // one index signal but multiple rotations, we should repeat index signals.
                        }

                        // Per A2R 3.x spec: If synchronized == 1, capture started at index signal
                        // The internal representation expects a 0 tick index signal if capture starts at index
                        // (The A2R format excludes this 0 signal per spec, but we add it back for internal use)
                        if(_infoChunkV3.synchronized == 1)
                        {
                            var indexSignalsWithZero = new uint[capture.numberOfIndexSignals + 1];
                            indexSignalsWithZero[0] = 0;

                            for(var i = 0; i < capture.numberOfIndexSignals; i++)
                                indexSignalsWithZero[i + 1] = capture.indexSignals[i];

                            capture.indexSignals         = indexSignalsWithZero;
                            capture.numberOfIndexSignals = (byte)(capture.numberOfIndexSignals + 1);
                        }

                        var dataSize = new byte[4];
                        _a2RStream.EnsureRead(dataSize, 0, 4);
                        capture.captureDataSize = BitConverter.ToUInt32(dataSize);

                        capture.dataOffset = _a2RStream.Position;

                        capture.resolution = rwcpChunk.resolution;

                        _a2RCaptures.Add(capture);

                        _a2RStream.Seek(capture.captureDataSize, SeekOrigin.Current);
                    }

                    break;
                case var meta when meta.SequenceEqual(_metaChunkSignature):
                    // Per A2R spec: META chunk contains tab-delimited UTF-8 metadata
                    // Format: key\tvalue\n for each entry, final \n at end
                    // Used in both 2.x and 3.x formats
                    _meta = new Dictionary<string, string>();

                    _a2RStream.Seek(Marshal.SizeOf<ChunkHeader>(), SeekOrigin.Current);

                    var metadataBuffer = new byte[chunkHeader.chunkSize];
                    _a2RStream.EnsureRead(metadataBuffer, 0, (int)chunkHeader.chunkSize);

                    string metaData = Encoding.UTF8.GetString(metadataBuffer);

                    string[] metaFields = metaData.Split('\n');

                    foreach(string[] keyValue in metaFields.Select(static field => field.Split('\t'))
                                                           .Where(static keyValue => keyValue.Length == 2))
                        _meta.Add(keyValue[0], keyValue[1]);

                    if(_meta.TryGetValue("image_date", out string imageDate))
                        _imageInfo.CreationTime = DateTime.Parse(imageDate);

                    if(_meta.TryGetValue("title", out string title)) _imageInfo.MediaTitle = title;

                    break;
                case var slvd when slvd.SequenceEqual(_slvdChunkSignature):
                    // Per A2R 3.x spec: SLVD (Solved) chunks contain sector-level flux data.
                    // SLVD chunks are not currently supported in Aaru. Skip the chunk and continue processing.
                    AaruLogging.Debug(MODULE_NAME,
                                      "SLVD chunk encountered but not yet supported. Only RWCP chunk data will be processed.");

                    _a2RStream.Seek(Marshal.SizeOf<ChunkHeader>(), SeekOrigin.Current);
                    _a2RStream.Seek(chunkHeader.chunkSize, SeekOrigin.Current);

                    break;
                case var strm when strm.SequenceEqual(_strmChunkSignature):
                    // Per A2R 2.x spec: STRM (Stream) chunk contains raw flux data streams.
                    // Structure per capture: Location (1 byte) + Capture Type (1 byte) + Data Length (4 bytes) +
                    // Estimated Loop Point (4 bytes) + Data (variable length)
                    // Chunk ends when Location byte is 0xFF
                    // A2R 2.x uses fixed resolution of 125 nanoseconds (125,000 picoseconds) per tick
                    var strmBuffer = new byte[Marshal.SizeOf<ChunkHeader>()];
                    _a2RStream.EnsureRead(strmBuffer, 0, Marshal.SizeOf<ChunkHeader>());
                    ChunkHeader strmChunk = Marshal.ByteArrayToStructureLittleEndian<ChunkHeader>(strmBuffer);

                    // Per A2R 2.x spec: STRM chunk ends when Location byte is 0xFF (not position-based)
                    while(true)
                    {
                        byte locationByte = (byte)_a2RStream.ReadByte();

                        // Check for end marker (0xFF Location value signals end of STRM chunk)
                        if(locationByte == 0xFF) break;

                        var capture = new StreamCapture
                        {
                            location    = locationByte,
                            captureType = (byte)_a2RStream.ReadByte(),
                            resolution  = 125000 // Per A2R 2.x spec: fixed 125 nanoseconds per tick
                        };

                        // Per A2R 2.x spec: Capture Type 1 = timing, 2 = bits (legacy), 3 = xtiming
                        if(capture.captureType == 2)
                        {
                            // Per A2R 2.x spec: bits capture type contains 16384-byte bitstream (not flux transitions)
                            // TODO: Add bitstream support when Aaru supports bitstream reading/writing
                            AaruLogging.Debug(MODULE_NAME,
                                              "Bits capture type (2) encountered at location {0}. Bitstream support not yet available in Aaru. Skipping capture.",
                                              capture.location);

                            var dataSize = new byte[4];
                            _a2RStream.EnsureRead(dataSize, 0, 4);
                            uint dataLength = BitConverter.ToUInt32(dataSize);

                            // Skip Estimated Loop Point (4 bytes)
                            _a2RStream.Seek(4, SeekOrigin.Current);

                            // Skip bitstream data (16384 bytes for bits type)
                            _a2RStream.Seek(dataLength, SeekOrigin.Current);

                            continue;
                        }

                        // Read Data Length (4 bytes)
                        var dataSizeBytes = new byte[4];
                        _a2RStream.EnsureRead(dataSizeBytes, 0, 4);
                        capture.captureDataSize = BitConverter.ToUInt32(dataSizeBytes);

                        // Per A2R 2.x spec: Estimated Loop Point is a hint for track length
                        // It indicates duration until sync sensor was triggered at loop point
                        // While not a true index signal, it's good enough to use as an index signal duration
                        var estimatedLoopPointBytes = new byte[4];
                        _a2RStream.EnsureRead(estimatedLoopPointBytes, 0, 4);
                        uint estimatedLoopPoint = BitConverter.ToUInt32(estimatedLoopPointBytes);

                        A2RLocationToHeadTrackSub(capture.location,
                                                  _imageInfo.MediaType,
                                                  out capture.head,
                                                  out capture.track,
                                                  out capture.subTrack);

                        if(capture.head + 1 > _imageInfo.Heads) _imageInfo.Heads = capture.head + 1;

                        if(capture.track + 1 > _imageInfo.Cylinders) _imageInfo.Cylinders = (uint)(capture.track + 1);

                        // Use Estimated Loop Point as index signal duration (absolute timing from start of track)
                        // A2R 2.x format doesn't have true index signals, but Estimated Loop Point serves the same purpose
                        // Per A2R 2.x spec: If synchronized == 1, capture started at index signal
                        // The internal representation expects a 0 tick index signal if capture starts at index
                        if(_infoChunkV2.synchronized == 1)
                        {
                            capture.numberOfIndexSignals = 2;
                            capture.indexSignals         = [0, estimatedLoopPoint];
                        }
                        else
                        {
                            capture.numberOfIndexSignals = 1;
                            capture.indexSignals         = [estimatedLoopPoint];
                        }

                        capture.dataOffset = _a2RStream.Position;

                        _a2RCaptures.Add(capture);

                        _a2RStream.Seek(capture.captureDataSize, SeekOrigin.Current);
                    }

                    break;
            }
        }

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber CapturesLength(uint head, ushort track, byte subTrack, out uint length)
    {
        long index = HeadTrackSubToA2RLocation(head, track, subTrack, _imageInfo.MediaType);

        length = (uint)_a2RCaptures.FindAll(capture => index == capture.location).Count;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFluxIndexResolution(uint      head, ushort track, byte subTrack, uint captureIndex,
                                               out ulong resolution)
    {
        resolution = StreamCaptureAtIndex(head, track, subTrack, captureIndex).resolution;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFluxDataResolution(uint      head, ushort track, byte subTrack, uint captureIndex,
                                              out ulong resolution)
    {
        resolution = StreamCaptureAtIndex(head, track, subTrack, captureIndex).resolution;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFluxResolution(uint      head,            ushort    track, byte subTrack, uint captureIndex,
                                          out ulong indexResolution, out ulong dataResolution)
    {
        indexResolution = dataResolution = StreamCaptureAtIndex(head, track, subTrack, captureIndex).resolution;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFluxCapture(uint       head,            ushort    track, byte subTrack, uint captureIndex,
                                       out ulong  indexResolution, out ulong dataResolution, out byte[] indexBuffer,
                                       out byte[] dataBuffer)
    {
        dataBuffer = indexBuffer = null;

        ErrorNumber error =
            ReadFluxResolution(head, track, subTrack, captureIndex, out indexResolution, out dataResolution);

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

        error = ReadFluxDataCapture(head, track, subTrack, captureIndex, out dataBuffer);

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

        error = ReadFluxIndexCapture(head, track, subTrack, captureIndex, out indexBuffer);

        return error;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFluxIndexCapture(uint       head, ushort track, byte subTrack, uint captureIndex,
                                            out byte[] buffer)
    {
        buffer = null;

        List<byte> tmpBuffer = [0];

        StreamCapture capture = StreamCaptureAtIndex(head, track, subTrack, captureIndex);

        uint previousTicks = 0;

        for(var i = 0; i < capture.numberOfIndexSignals; i++)
        {
            uint ticks = capture.indexSignals[i] - previousTicks;
            tmpBuffer.AddRange(UInt32ToFluxRepresentation(ticks));

            previousTicks = capture.indexSignals[i];
        }

        buffer = tmpBuffer.ToArray();

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFluxDataCapture(uint head, ushort track, byte subTrack, uint captureIndex, out byte[] buffer)
    {
        buffer = null;

        StreamCapture capture = StreamCaptureAtIndex(head, track, subTrack, captureIndex);

        // Per A2R 2.x spec: Capture type 2 = bits (16384-byte bitstream, not flux transitions)
        // TODO: Add bitstream support when Aaru supports bitstream reading/writing
        if(capture.captureType == 2) return ErrorNumber.NotImplemented;

        Stream stream = _a2RFilter.GetDataForkStream();
        var    br     = new BinaryReader(stream);

        br.BaseStream.Seek(capture.dataOffset, SeekOrigin.Begin);
        buffer = br.ReadBytes((int)capture.captureDataSize);

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber SubTrackLength(uint head, ushort track, out byte length)
    {
        length = 0;

        List<StreamCapture> captures = _a2RCaptures.FindAll(c => c.head == head && c.track == track);

        if(captures.Count <= 0) return ErrorNumber.OutOfRange;

        length = (byte)(captures.Max(static c => c.subTrack) + 1);

        return ErrorNumber.NoError;
    }

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

        if(!_mediaTags.TryGetValue(tag, out byte[] data)) return ErrorNumber.NoData;

        buffer = data?.Clone() as byte[];

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

    /// <inheritdoc />
    public ErrorNumber
        ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus) =>
        throw new NotImplementedException();

    /// <inheritdoc />
    public ErrorNumber ReadSectorLong(ulong            sectorAddress, bool negative, out byte[] buffer,
                                      out SectorStatus sectorStatus) => throw new NotImplementedException();

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

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

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

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

#endregion

    StreamCapture StreamCaptureAtIndex(uint head, ushort track, byte subTrack, uint captureIndex)
    {
        long index = HeadTrackSubToA2RLocation(head, track, subTrack, _imageInfo.MediaType);

        return _a2RCaptures.FindAll(capture => index == capture.location)[(int)captureIndex];
    }

    /// <inheritdoc />
    public ErrorNumber GetAllFluxCaptures(out List<FluxCapture> captures)
    {
        captures = [];

        if(_a2RCaptures is { Count: > 0 })
        {
            // Group captures by head/track/subtrack to assign capture indices
            var grouped = _a2RCaptures.GroupBy(c => new { c.head, c.track, c.subTrack })
                                      .ToList();

            foreach(var group in grouped)
            {
                uint captureIndex = 0;

                foreach(StreamCapture streamCapture in group)
                {
                    captures.Add(new FluxCapture
                    {
                        Head            = streamCapture.head,
                        Track           = streamCapture.track,
                        SubTrack        = streamCapture.subTrack,
                        CaptureIndex    = captureIndex++,
                        IndexResolution = streamCapture.resolution,
                        DataResolution  = streamCapture.resolution
                    });
                }
            }
        }

        return ErrorNumber.NoError;
    }
}