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

// ReSharper disable JoinDeclarationAndInitializer
// ReSharper disable InlineOutVariableDeclaration
// ReSharper disable TooWideLocalVariableScope

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Aaru.Checksums;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Core.Graphics;
using Aaru.Core.Logging;
using Aaru.Decoders.CD;
using Aaru.Devices;
using Aaru.Logging;
using Humanizer;
using SkiaSharp;

namespace Aaru.Core.Devices.Dumping;

partial class Dump
{
    /// <summary>Reads the first track pregap from a CompactDisc</summary>
    /// <param name="blockSize">Block size in bytes</param>
    /// <param name="currentSpeed">Current speed</param>
    /// <param name="mediaTags">List of media tags</param>
    /// <param name="supportedSubchannel">Subchannel the drive can read</param>
    /// <param name="totalDuration">Total time spent sending commands to a drive</param>
    /// <param name="outputOptical">Output optical image</param>
    /// <param name="subLog">Subchannel log</param>
    /// <param name="offsetBytes">Drive read offset in bytes</param>
    /// <param name="sectorsForOffset">Number of sectors needed to cover the offset</param>
    /// <param name="desiredSubchannel">Subchannel type the user wants written to the image</param>
    /// <param name="subchannelExtents">Set of sector LBAs that still need subchannel data</param>
    void ReadCdFirstTrackPregap(uint blockSize, ref double currentSpeed, Dictionary<MediaTagType, byte[]> mediaTags,
                                MmcSubchannel supportedSubchannel, ref double totalDuration,
                                IWritableOpticalImage outputOptical, SubchannelLog subLog, int offsetBytes,
                                int sectorsForOffset, MmcSubchannel desiredSubchannel, HashSet<int> subchannelExtents)
    {
        bool   sense;                           // Sense indicator
        byte[] cmdBuf;                          // Data buffer
        double cmdDuration;                     // Command execution time
        ulong  sectorSpeedStart            = 0; // Used to calculate correct speed
        var    firstTrackPregapSectorsGood = 0;
        var    firstTrackPregapMs          = new MemoryStream();

        uint subSize = supportedSubchannel switch
                       {
                           MmcSubchannel.None => 0,
                           MmcSubchannel.Raw  => 96,
                           _                  => 16
                       };

        bool needsOffsetCorrection = _fixOffset && offsetBytes != 0;

        // Expand read range to include extra sectors needed for offset correction.
        // For positive offset: we must read a few sectors past LBA -1 (into LBA 0, 1, …).
        // For negative offset: we must read a few sectors before LBA -150 (deeper lead-in).
        int startLba           = needsOffsetCorrection && offsetBytes < 0 ? -150             - sectorsForOffset : -150;
        int endLba             = needsOffsetCorrection && offsetBytes > 0 ? sectorsForOffset - 1 : -1;
        int totalSectorsToRead = endLba                                                      - startLba + 1;

        var allSectorsBuffer = new byte[totalSectorsToRead * blockSize];
        var readSuccess      = new bool[totalSectorsToRead];
        var sectorIsAudio    = new bool[totalSectorsToRead];

        UpdateStatus?.Invoke(Localization.Core.Reading_first_track_pregap);
        InitProgress?.Invoke();
        _speedStopwatch.Restart();

        for(int firstTrackPregapBlock = startLba;
            firstTrackPregapBlock <= endLba && _resume.NextBlock == 0;
            firstTrackPregapBlock++)
        {
            if(_aborted)
            {
                UpdateStatus?.Invoke(Localization.Core.Aborted);

                break;
            }

            int bufferIndex = firstTrackPregapBlock - startLba;

            if(firstTrackPregapBlock >= -150)
            {
                PulseProgress?.Invoke(string.Format(Localization.Core.Trying_to_read_first_track_pregap_sector_0_1,
                                                    firstTrackPregapBlock,
                                                    ByteSize.FromMegabytes(currentSpeed).Per(_oneSecond).Humanize()));
            }

            // ReSharper disable IntVariableOverflowInUncheckedContext
            sense = _omnidrive
                        ? _dev.OmniDriveReadCd(out cmdBuf,
                                               out _,
                                               (uint)firstTrackPregapBlock,
                                               1,
                                               _dev.Timeout,
                                               out cmdDuration)
                        : _dev.ReadCd(out cmdBuf,
                                      out _,
                                      (uint)firstTrackPregapBlock,
                                      blockSize,
                                      1,
                                      MmcSectorTypes.AllTypes,
                                      false,
                                      false,
                                      true,
                                      MmcHeaderCodes.AllHeaders,
                                      true,
                                      true,
                                      MmcErrorField.None,
                                      supportedSubchannel,
                                      _dev.Timeout,
                                      out cmdDuration);

            // ReSharper restore IntVariableOverflowInUncheckedContext

            if(!sense && !_dev.Error)
            {
                Array.Copy(cmdBuf, 0, allSectorsBuffer, bufferIndex * (int)blockSize, (int)blockSize);

                // Determine if this sector is audio by checking for the CD sync mark
                var sectorData = new byte[2352];
                Array.Copy(cmdBuf, 0, sectorData, 0, 2352);
                sectorIsAudio[bufferIndex] = !IsData(sectorData);
                readSuccess[bufferIndex]   = true;

                if(firstTrackPregapBlock >= -150)
                {
                    firstTrackPregapSectorsGood++;
                    totalDuration += cmdDuration;
                    (_mediaGraph as Spiral)?.PaintCdLeadInSector(firstTrackPregapBlock, SKColors.Green);
                }
            }
            else
            {
                if(firstTrackPregapBlock >= -150)
                    (_mediaGraph as Spiral)?.PaintCdLeadInSector(firstTrackPregapBlock, SKColors.Red);
            }

            if(firstTrackPregapBlock < -150) continue;

            sectorSpeedStart++;

            double elapsed = _speedStopwatch.Elapsed.TotalSeconds;

            if(elapsed <= 0) continue;

            currentSpeed     = sectorSpeedStart * blockSize / (1048576 * elapsed);
            sectorSpeedStart = 0;
            _speedStopwatch.Restart();
        }

        _speedStopwatch.Stop();

        uint blocksToRead = 150;

        // Apply offset correction if any audio sector is present in the pregap range
        if(needsOffsetCorrection || _omnidrive && offsetBytes != 0)
        {
            var hasAudioSectors = false;

            for(var i = 0; i < totalSectorsToRead && !hasAudioSectors; i++)
            {
                int lba = startLba + i;

                if(lba is >= -150 and <= -1 && readSuccess[i] && sectorIsAudio[i]) hasAudioSectors = true;
            }

            if(hasAudioSectors || _omnidrive)
            {
                blocksToRead = (uint)totalSectorsToRead;

                FixOffsetData(offsetBytes,
                              2352,
                              sectorsForOffset,
                              supportedSubchannel,
                              ref blocksToRead,
                              subSize,
                              ref allSectorsBuffer,
                              blockSize,
                              false);
            }
        }

        // Write sectors LBA -150 to -1 from the (possibly offset-corrected) buffer.
        // After FixOffsetData the buffer always contains the corrected 150 pregap sectors
        // starting at index 0, regardless of whether the offset was positive or negative.
        var firstSectorWritten = false;

        for(var firstTrackPregapBlock = (int)-blocksToRead; firstTrackPregapBlock <= -1; firstTrackPregapBlock++)
        {
            var bufferIndex = (int)(firstTrackPregapBlock + blocksToRead); // 0 = LBA -150, 149 = LBA -1

            // Map back to the pre-correction read index to check whether the read succeeded.
            // For negative offset the extra sectors were prepended, so index shifts by sectorsForOffset.
            int originalIndex = needsOffsetCorrection && offsetBytes < 0 ? bufferIndex + sectorsForOffset : bufferIndex;

            bool success = originalIndex < readSuccess.Length && readSuccess[originalIndex];

            if(outputOptical.OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreNegativeSectors))
            {
                var data = new byte[2352];
                Array.Copy(allSectorsBuffer, bufferIndex * (int)blockSize, data, 0, 2352);

                if(_omnidrive)
                {
                    if(IsScrambledData(data, firstTrackPregapBlock, out _))
                    {
                        data = Sector.Scramble(data);
                        SectorFixResult eccFixed = CdChecksums.FixSector(data);

                        success = eccFixed switch
                                  {
                                      SectorFixResult.Correct => true,
                                      SectorFixResult.Fixed => true,
                                      SectorFixResult.CouldNotFix => false,
                                      _ => (data[0x00F] & 0x03) == 2 && (data[0x012] & 0x20) == 0x20
                                  };
                    }
                }

                outputOptical.WriteSectorLong(data,
                                              (ulong)-firstTrackPregapBlock,
                                              true,
                                              success ? SectorStatus.Dumped : SectorStatus.Errored);

                if(supportedSubchannel != MmcSubchannel.None && desiredSubchannel != MmcSubchannel.None)
                {
                    var sub = new byte[subSize];
                    Array.Copy(allSectorsBuffer, bufferIndex * (int)blockSize + 2352, sub, 0, (int)subSize);

                    if(supportedSubchannel == MmcSubchannel.Q16) sub = Subchannel.ConvertQToRaw(sub);

                    subLog?.WriteEntry(sub,
                                       supportedSubchannel == MmcSubchannel.Raw,
                                       firstTrackPregapBlock,
                                       1,
                                       false,
                                       false);

                    if(_fixSubchannelPosition)
                    {
                        // Write subchannel to the sector address its Q channel indicates,
                        // which may differ from the read position (positive or negative LBA).
                        byte[] deSub = Subchannel.Deinterleave(sub);
                        var    q     = new byte[12];
                        Array.Copy(deSub, 12, q, 0, 12);

                        CRC16CcittContext.Data(q, 10, out byte[] crc);
                        bool crcOk = crc[0] == q[10] && crc[1] == q[11];

                        if(crcOk && (q[0] & 0x3) == 1)
                        {
                            var amin   = (byte)(q[7] / 16 * 10 + (q[7] & 0x0F));
                            var asec   = (byte)(q[8] / 16 * 10 + (q[8] & 0x0F));
                            var aframe = (byte)(q[9] / 16 * 10 + (q[9] & 0x0F));
                            int aPos   = amin * 60 * 75 + asec * 75 + aframe - 150;

                            if(aPos < 0)
                            {
                                outputOptical.WriteSectorTag(sub, (ulong)-aPos, true, SectorTagType.CdSectorSubchannel);

                                subchannelExtents.Remove(aPos);
                            }
                            else
                            {
                                outputOptical.WriteSectorTag(sub, (ulong)aPos, false, SectorTagType.CdSectorSubchannel);

                                subchannelExtents.Remove(aPos);
                            }
                        }
                        else
                        {
                            // Q CRC bad or not a position packet: fall back to read address
                            outputOptical.WriteSectorTag(sub,
                                                         (ulong)-firstTrackPregapBlock,
                                                         true,
                                                         SectorTagType.CdSectorSubchannel);
                        }
                    }
                    else
                    {
                        outputOptical.WriteSectorTag(sub,
                                                     (ulong)-firstTrackPregapBlock,
                                                     true,
                                                     SectorTagType.CdSectorSubchannel);
                    }
                }
            }
            else
            {
                if(success)
                {
                    firstSectorWritten = true;
                    firstTrackPregapMs.Write(allSectorsBuffer, bufferIndex * (int)blockSize, (int)blockSize);
                }
                else if(firstSectorWritten) firstTrackPregapMs.Write(new byte[blockSize], 0, (int)blockSize);

                // Log subchannel even when we cannot store it individually in the image
                if(supportedSubchannel != MmcSubchannel.None && success)
                {
                    var sub = new byte[subSize];
                    Array.Copy(allSectorsBuffer, bufferIndex * (int)blockSize + 2352, sub, 0, (int)subSize);

                    if(supportedSubchannel == MmcSubchannel.Q16) sub = Subchannel.ConvertQToRaw(sub);

                    subLog?.WriteEntry(sub,
                                       supportedSubchannel == MmcSubchannel.Raw,
                                       firstTrackPregapBlock,
                                       1,
                                       false,
                                       false);
                }
            }
        }

        if(firstTrackPregapSectorsGood > 0 &&
           !outputOptical.OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreNegativeSectors))
            mediaTags.Add(MediaTagType.CD_FirstTrackPregap, firstTrackPregapMs.ToArray());

        EndProgress?.Invoke();

        UpdateStatus?.Invoke(string.Format(Localization.Core.Got_0_first_track_pregap_sectors,
                                           firstTrackPregapSectorsGood));

        firstTrackPregapMs.Close();
    }

    /// <summary>Calculate track pregaps</summary>
    /// <param name="dev">Device</param>
    /// <param name="dumpLog">Dumping log</param>
    /// <param name="updateStatus">Progress update callback</param>
    /// <param name="tracks">List of tracks</param>
    /// <param name="supportsPqSubchannel">Set if drive supports reading PQ subchannel</param>
    /// <param name="supportsRwSubchannel">Set if drive supports reading RW subchannel</param>
    /// <param name="dbDev">Database entry for device</param>
    /// <param name="inexactPositioning">Set if we found the drive does not return the exact subchannel we requested</param>
    /// <param name="dumping">Set if dumping, otherwise media info</param>
    public static void SolveTrackPregaps(Device                 dev, UpdateStatusHandler updateStatus, Track[] tracks,
                                         bool                   supportsPqSubchannel, bool supportsRwSubchannel,
                                         Database.Models.Device dbDev, out bool inexactPositioning, bool dumping)
    {
        var                   sense  = true; // Sense indicator
        byte[]                subBuf = null;
        int                   posQ;
        uint                  retries;
        bool?                 bcd = null;
        byte[]                crc;
        Dictionary<uint, int> pregaps = new();
        inexactPositioning = false;

        if(!supportsPqSubchannel && !supportsRwSubchannel) return;

        // Check if subchannel is BCD
        for(retries = 0; retries < 10; retries++)
        {
            sense = supportsRwSubchannel
                        ? GetSectorForPregapRaw(dev, 11, dbDev, out subBuf, false)
                        : GetSectorForPregapQ16(dev, 11, out subBuf, false);

            if(sense) continue;

            bcd = (subBuf[9] & 0x10) > 0;

            break;
        }

        AaruLogging.Debug(PREGAP_MODULE_NAME,
                          bcd switch
                          {
                              true  => Localization.Core.Subchannel_is_BCD,
                              false => Localization.Core.Subchannel_is_not_BCD,
                              _     => Localization.Core.Could_not_detect_drive_subchannel_BCD
                          });

        if(bcd is null)
        {
            updateStatus?.Invoke(Localization.Core
                                             .Could_not_detect_if_drive_subchannel_is_BCD_or_not_pregaps_could_not_be_calculated_dump_may_be_incorrect);

            return;
        }

        // Initialize the dictionary
        foreach(Track t in tracks) pregaps[t.Sequence] = 0;

        for(var t = 0; t < tracks.Length; t++)
        {
            Track track        = tracks[t];
            var   trackRetries = 0;

            // First track of each session has at least 150 sectors of pregap and is not always readable
            if(tracks.Where(trk => trk.Session == track.Session).MinBy(static trk => trk.Sequence).Sequence ==
               track.Sequence)
            {
                AaruLogging.Debug(PREGAP_MODULE_NAME, Localization.Core.Skipping_track_0, track.Sequence);

                if(track.Sequence > 1) pregaps[track.Sequence] = 150;

                continue;
            }

            if(t > 0 && tracks[t - 1].Type == tracks[t].Type && dumping)
            {
                AaruLogging.Debug(PREGAP_MODULE_NAME, Localization.Core.Skipping_track_0, track.Sequence);

                continue;
            }

            if(dumping && dev.Manufacturer.ToLowerInvariant().StartsWith("plextor", StringComparison.Ordinal))
            {
                AaruLogging.Debug(PREGAP_MODULE_NAME,
                                  Localization.Core.Skipping_track_0_due_to_Plextor_firmware_bug,
                                  track.Sequence);

                continue;
            }

            AaruLogging.Debug(PREGAP_MODULE_NAME, Localization.Core.Track_0, track.Sequence);

            int   lba           = (int)track.StartSector - 1;
            var   pregapFound   = false;
            Track previousTrack = tracks.FirstOrDefault(trk => trk.Sequence == track.Sequence - 1);

            var goneBack                      = false;
            var goFront                       = false;
            var forward                       = false;
            var crcOk                         = false;
            var previousPregapIsPreviousTrack = false;

            // Check if pregap is 0
            for(retries = 0; retries < 10 && !pregapFound; retries++)
            {
                sense = supportsRwSubchannel
                            ? GetSectorForPregapRaw(dev, (uint)lba, dbDev, out subBuf, track.Type == TrackType.Audio)
                            : GetSectorForPregapQ16(dev, (uint)lba, out subBuf, track.Type        == TrackType.Audio);

                if(sense)
                {
                    AaruLogging.Debug(PREGAP_MODULE_NAME,
                                      Localization.Core.LBA_0_Try_1_Sense_2,
                                      lba,
                                      retries + 1,
                                      sense);

                    continue;
                }

                if(bcd == false) BinaryToBcdQ(subBuf);

                CRC16CcittContext.Data(subBuf, 10, out crc);

                AaruLogging.Debug(PREGAP_MODULE_NAME,
                                  Localization.Core
                                              .LBA_0_Try_1_Sense_2_Q_3_4_5_6_7_8_9_10_11_12_CRC_13_14_Calculated_CRC_15_16,
                                  lba,
                                  retries + 1,
                                  sense,
                                  subBuf[0],
                                  subBuf[1],
                                  subBuf[2],
                                  subBuf[3],
                                  subBuf[4],
                                  subBuf[5],
                                  subBuf[6],
                                  subBuf[7],
                                  subBuf[8],
                                  subBuf[9],
                                  subBuf[10],
                                  subBuf[11],
                                  crc[0],
                                  crc[1]);

                crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11];

                // Try to do a simple correction
                if(!crcOk)
                {
                    // Data track cannot have 11xxb in CONTROL
                    if((subBuf[0] & 0x40) > 0) subBuf[0] &= 0x7F;

                    // ADR only uses two bits
                    subBuf[0] &= 0xF3;

                    // Don't care about other Q modes
                    if((subBuf[0] & 0xF) == 1)
                    {
                        // ZERO only used in DDCD
                        subBuf[6] = 0;

                        // Fix BCD numbering
                        for(var i = 1; i < 10; i++)
                        {
                            if((subBuf[i] & 0xF0) > 0xA0) subBuf[i] &= 0x7F;

                            if((subBuf[i] & 0x0F) > 0x0A) subBuf[i] &= 0xF7;
                        }
                    }

                    CRC16CcittContext.Data(subBuf, 10, out crc);

                    crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11];

                    if(crcOk)
                    {
                        AaruLogging.Debug(PREGAP_MODULE_NAME,
                                          Localization.Core
                                                      .LBA_0_Try_1_Sense_2_Q_FIXED_3_4_5_6_7_8_9_10_11_12_CRC_13_14_Calculated_CRC_15_16,
                                          lba,
                                          retries + 1,
                                          sense,
                                          subBuf[0],
                                          subBuf[1],
                                          subBuf[2],
                                          subBuf[3],
                                          subBuf[4],
                                          subBuf[5],
                                          subBuf[6],
                                          subBuf[7],
                                          subBuf[8],
                                          subBuf[9],
                                          subBuf[10],
                                          subBuf[11],
                                          crc[0],
                                          crc[1]);
                    }
                    else
                        continue;
                }

                BcdToBinaryQ(subBuf);

                // Q position
                if((subBuf[0] & 0xF) != 1) continue;

                posQ = subBuf[7] * 60 * 75 + subBuf[8] * 75 + subBuf[9] - 150;

                if(subBuf[1] != track.Sequence - 1 || subBuf[2] == 0 || posQ != lba) break;

                pregaps[track.Sequence] = 0;

                pregapFound = true;
            }

            if(pregapFound) continue;

            // Calculate pregap
            lba = (int)track.StartSector - 150;

            while(lba > (int)previousTrack.StartSector && lba <= (int)track.StartSector)
            {
                // Some drives crash if you try to read just before the previous read, so seek away first
                if(!forward)
                {
                    sense = supportsRwSubchannel
                                ? GetSectorForPregapRaw(dev,
                                                        (uint)lba - 10,
                                                        dbDev,
                                                        out subBuf,
                                                        track.Type == TrackType.Audio)
                                : GetSectorForPregapQ16(dev, (uint)lba - 10, out subBuf, track.Type == TrackType.Audio);
                }

                for(retries = 0; retries < 10; retries++)
                {
                    sense = supportsRwSubchannel
                                ? GetSectorForPregapRaw(dev,
                                                        (uint)lba,
                                                        dbDev,
                                                        out subBuf,
                                                        track.Type == TrackType.Audio)
                                : GetSectorForPregapQ16(dev, (uint)lba, out subBuf, track.Type == TrackType.Audio);

                    if(sense) continue;

                    if(bcd == false) BinaryToBcdQ(subBuf);

                    CRC16CcittContext.Data(subBuf, 10, out crc);

                    AaruLogging.Debug(PREGAP_MODULE_NAME,
                                      Localization.Core
                                                  .LBA_0_Try_1_Sense_2_Q_3_4_5_6_7_8_9_10_11_12_CRC_13_14_Calculated_CRC_15_16,
                                      lba,
                                      retries + 1,
                                      sense,
                                      subBuf[0],
                                      subBuf[1],
                                      subBuf[2],
                                      subBuf[3],
                                      subBuf[4],
                                      subBuf[5],
                                      subBuf[6],
                                      subBuf[7],
                                      subBuf[8],
                                      subBuf[9],
                                      subBuf[10],
                                      subBuf[11],
                                      crc[0],
                                      crc[1]);

                    crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11];

                    // Try to do a simple correction
                    if(!crcOk)
                    {
                        // Data track cannot have 11xxb in CONTROL
                        if((subBuf[0] & 0x40) > 0) subBuf[0] &= 0x7F;

                        // ADR only uses two bits
                        subBuf[0] &= 0xF3;

                        // Don't care about other Q modes
                        if((subBuf[0] & 0xF) == 1)
                        {
                            // ZERO only used in DDCD
                            subBuf[6] = 0;

                            // Fix BCD numbering
                            for(var i = 1; i < 10; i++)
                            {
                                if((subBuf[i] & 0xF0) > 0xA0) subBuf[i] &= 0x7F;

                                if((subBuf[i] & 0x0F) > 0x0A) subBuf[i] &= 0xF7;
                            }
                        }

                        CRC16CcittContext.Data(subBuf, 10, out crc);

                        crcOk = crc[0] == subBuf[10] && crc[1] == subBuf[11];

                        if(crcOk)
                        {
                            AaruLogging.Debug(PREGAP_MODULE_NAME,
                                              Localization.Core
                                                          .LBA_0_Try_1_Sense_2_Q_FIXED_3_4_5_6_7_8_9_10_11_12_CRC_13_14_Calculated_CRC_15_16,
                                              lba,
                                              retries + 1,
                                              sense,
                                              subBuf[0],
                                              subBuf[1],
                                              subBuf[2],
                                              subBuf[3],
                                              subBuf[4],
                                              subBuf[5],
                                              subBuf[6],
                                              subBuf[7],
                                              subBuf[8],
                                              subBuf[9],
                                              subBuf[10],
                                              subBuf[11],
                                              crc[0],
                                              crc[1]);

                            break;
                        }
                    }

                    if(crcOk) break;
                }

                if(retries == 10)
                {
                    if(sense)
                    {
                        trackRetries++;

                        if(trackRetries >= 10)
                        {
                            if(pregaps[track.Sequence] == 0)
                            {
                                if(previousTrack.Type == TrackType.Audio && track.Type != TrackType.Audio ||
                                   previousTrack.Type != TrackType.Audio && track.Type == TrackType.Audio)
                                {
                                    updateStatus?.Invoke(Localization.Core
                                                                     .Could_not_read_subchannel_for_this_track_supposing_hundred_fifty_sectors);
                                }
                                else
                                {
                                    updateStatus?.Invoke(Localization.Core
                                                                     .Could_not_read_subchannel_for_this_track_supposing_zero_sectors);
                                }
                            }
                            else
                            {
                                updateStatus?.Invoke(string.Format(Localization.Core
                                                                      .Could_not_read_subchannel_for_this_track_supposing_0_sectors,
                                                                   pregaps[track.Sequence]));
                            }

                            break;
                        }

                        updateStatus?.Invoke(string.Format(Localization.Core.Could_not_read_subchannel_for_sector_0,
                                                           lba));

                        lba++;
                        forward = true;

                        continue;
                    }

                    updateStatus?.Invoke(string.Format(Localization.Core.Could_not_get_correct_subchannel_for_sector_0,
                                                       lba));
                }

                if(subBuf.All(static b => b == 0))
                {
                    inexactPositioning = true;

                    AaruLogging.Debug(PREGAP_MODULE_NAME, Localization.Core.All_Q_empty_for_LBA_0, lba);

                    break;
                }

                BcdToBinaryQ(subBuf);

                // If it's not Q position
                if((subBuf[0] & 0xF) != 1)
                {
                    // This means we already searched back, so search forward
                    if(goFront)
                    {
                        lba++;
                        forward = true;

                        if(lba == (int)previousTrack.StartSector) break;

                        continue;
                    }

                    // Search back
                    goneBack = true;
                    lba--;
                    forward = false;

                    continue;
                }

                // Previous track
                if(subBuf[1] < track.Sequence)
                {
                    lba++;
                    forward                       = true;
                    previousPregapIsPreviousTrack = true;

                    // Already gone back, so go forward
                    if(goneBack) goFront = true;

                    continue;
                }

                // Same track, but not pregap
                if(subBuf[1] == track.Sequence && subBuf[2] > 0)
                {
                    lba--;
                    forward = false;

                    if(previousPregapIsPreviousTrack) break;

                    continue;
                }

                previousPregapIsPreviousTrack = false;

                // Pregap according to Q position
                posQ = subBuf[7] * 60 * 75 + subBuf[8] * 75 + subBuf[9] - 150;
                int diff    = posQ                   - lba;
                int pregapQ = (int)track.StartSector - lba;

                if(diff != 0)
                {
                    AaruLogging.Debug(PREGAP_MODULE_NAME,
                                      Localization.Core.Invalid_Q_position_for_LBA_0_got_1,
                                      lba,
                                      posQ);

                    inexactPositioning = true;
                }

                // Received a Q post the LBA we wanted, just go back. If we are already going forward, break
                if(posQ > lba)
                {
                    if(forward) break;

                    lba--;

                    continue;
                }

                // Bigger than known change, otherwise we found it
                if(pregapQ > pregaps[track.Sequence])
                {
                    // If CRC is not OK, only accept pregaps less than 10 sectors longer than previously now
                    if(crcOk || pregapQ - pregaps[track.Sequence] < 10)
                    {
                        AaruLogging.Debug(PREGAP_MODULE_NAME,
                                          Localization.Core.Pregap_for_track_0_1,
                                          track.Sequence,
                                          pregapQ);

                        pregaps[track.Sequence] = pregapQ;
                    }

                    // We are going forward, so we have already been in the previous track, so add 1 to pregap and get out of here
                    else if(forward)
                    {
                        pregaps[track.Sequence]++;

                        break;
                    }
                }
                else if(pregapQ == pregaps[track.Sequence]) break;

                lba--;
                forward = false;
            }
        }

        foreach(Track trk in tracks)
        {
            trk.Pregap = (ulong)pregaps[trk.Sequence];

            // Do not reduce pregap, or starting position of session's first track
            if(tracks.Where(t => t.Session == trk.Session).MinBy(static t => t.Sequence).Sequence == trk.Sequence)
                continue;

            if(dumping)
            {
                // Minus five, to ensure dumping will fix if there is a pregap LBA 0
                var red = 5;

                while(trk.Pregap > 0 && red > 0)
                {
                    trk.Pregap--;
                    red--;
                }
            }

            trk.StartSector -= trk.Pregap;

#if DEBUG
            updateStatus?.Invoke(string.Format(Localization.Core.Track_0_pregap_is_1_sectors,
                                               trk.Sequence,
                                               trk.Pregap));
#endif
        }
    }

    /// <summary>Reads a RAW subchannel sector for pregap calculation</summary>
    /// <param name="dev">Device</param>
    /// <param name="lba">LBA</param>
    /// <param name="dbDev">Database entry for device</param>
    /// <param name="subBuf">Read subchannel</param>
    /// <param name="audioTrack">Set if it is an audio track</param>
    /// <returns><c>true</c> if read correctly, <c>false</c> otherwise</returns>
    static bool GetSectorForPregapRaw(Device dev, uint lba, Database.Models.Device dbDev, out byte[] subBuf,
                                      bool   audioTrack)
    {
        byte[] cmdBuf;
        bool   sense;
        subBuf = null;

        if(audioTrack)
        {
            sense = dev.ReadCd(out cmdBuf,
                               out _,
                               lba,
                               2448,
                               1,
                               MmcSectorTypes.Cdda,
                               false,
                               false,
                               false,
                               MmcHeaderCodes.None,
                               true,
                               false,
                               MmcErrorField.None,
                               MmcSubchannel.Raw,
                               dev.Timeout,
                               out _);

            if(sense)
            {
                sense = dev.ReadCd(out cmdBuf,
                                   out _,
                                   lba,
                                   2448,
                                   1,
                                   MmcSectorTypes.AllTypes,
                                   false,
                                   false,
                                   true,
                                   MmcHeaderCodes.AllHeaders,
                                   true,
                                   true,
                                   MmcErrorField.None,
                                   MmcSubchannel.Raw,
                                   dev.Timeout,
                                   out _);
            }
        }
        else
        {
            sense = dev.ReadCd(out cmdBuf,
                               out _,
                               lba,
                               2448,
                               1,
                               MmcSectorTypes.AllTypes,
                               false,
                               false,
                               true,
                               MmcHeaderCodes.AllHeaders,
                               true,
                               true,
                               MmcErrorField.None,
                               MmcSubchannel.Raw,
                               dev.Timeout,
                               out _);

            if(sense)
            {
                sense = dev.ReadCd(out cmdBuf,
                                   out _,
                                   lba,
                                   2448,
                                   1,
                                   MmcSectorTypes.Cdda,
                                   false,
                                   false,
                                   false,
                                   MmcHeaderCodes.None,
                                   true,
                                   false,
                                   MmcErrorField.None,
                                   MmcSubchannel.Raw,
                                   dev.Timeout,
                                   out _);
            }
        }

        if(!sense)
        {
            var tmpBuf = new byte[96];
            Array.Copy(cmdBuf, 2352, tmpBuf, 0, 96);
            subBuf = DeinterleaveQ(tmpBuf);
        }
        else
        {
            sense = dev.ReadCd(out cmdBuf,
                               out _,
                               lba,
                               96,
                               1,
                               MmcSectorTypes.AllTypes,
                               false,
                               false,
                               false,
                               MmcHeaderCodes.None,
                               false,
                               false,
                               MmcErrorField.None,
                               MmcSubchannel.Raw,
                               dev.Timeout,
                               out _);

            if(sense)
            {
                sense = dev.ReadCd(out cmdBuf,
                                   out _,
                                   lba,
                                   96,
                                   1,
                                   MmcSectorTypes.Cdda,
                                   false,
                                   false,
                                   false,
                                   MmcHeaderCodes.None,
                                   false,
                                   false,
                                   MmcErrorField.None,
                                   MmcSubchannel.Raw,
                                   dev.Timeout,
                                   out _);
            }

            if(!sense)
                subBuf = DeinterleaveQ(cmdBuf);
            else if(dbDev?.ATAPI?.RemovableMedias?.Any(static d => d.SupportsPlextorReadCDDA == true) == true ||
                    dbDev?.SCSI?.RemovableMedias?.Any(static d => d.SupportsPlextorReadCDDA  == true) == true ||
                    dev.Manufacturer.Equals("plextor", StringComparison.InvariantCultureIgnoreCase))
                sense = dev.PlextorReadCdDa(out cmdBuf, out _, lba, 96, 1, PlextorSubchannel.All, dev.Timeout, out _);

            {
                if(!sense) subBuf = DeinterleaveQ(cmdBuf);
            }
        }

        return sense;
    }

    /// <summary>Reads a Q16 subchannel sector for pregap calculation</summary>
    /// <param name="dev">Device</param>
    /// <param name="lba">LBA</param>
    /// <param name="subBuf">Read subchannel</param>
    /// <param name="audioTrack">Set if it is an audio track</param>
    /// <returns><c>true</c> if read correctly, <c>false</c> otherwise</returns>
    static bool GetSectorForPregapQ16(Device dev, uint lba, out byte[] subBuf, bool audioTrack)
    {
        byte[] cmdBuf;
        bool   sense;
        subBuf = null;

        if(audioTrack)
        {
            sense = dev.ReadCd(out cmdBuf,
                               out _,
                               lba,
                               2368,
                               1,
                               MmcSectorTypes.Cdda,
                               false,
                               false,
                               false,
                               MmcHeaderCodes.None,
                               true,
                               false,
                               MmcErrorField.None,
                               MmcSubchannel.Q16,
                               dev.Timeout,
                               out _);

            if(sense)
            {
                sense = dev.ReadCd(out cmdBuf,
                                   out _,
                                   lba,
                                   2368,
                                   1,
                                   MmcSectorTypes.AllTypes,
                                   false,
                                   false,
                                   true,
                                   MmcHeaderCodes.AllHeaders,
                                   true,
                                   true,
                                   MmcErrorField.None,
                                   MmcSubchannel.Q16,
                                   dev.Timeout,
                                   out _);
            }
        }
        else
        {
            sense = dev.ReadCd(out cmdBuf,
                               out _,
                               lba,
                               2368,
                               1,
                               MmcSectorTypes.AllTypes,
                               false,
                               false,
                               true,
                               MmcHeaderCodes.AllHeaders,
                               true,
                               true,
                               MmcErrorField.None,
                               MmcSubchannel.Q16,
                               dev.Timeout,
                               out _);

            if(sense)
            {
                sense = dev.ReadCd(out cmdBuf,
                                   out _,
                                   lba,
                                   2368,
                                   1,
                                   MmcSectorTypes.Cdda,
                                   false,
                                   false,
                                   false,
                                   MmcHeaderCodes.None,
                                   true,
                                   false,
                                   MmcErrorField.None,
                                   MmcSubchannel.Q16,
                                   dev.Timeout,
                                   out _);
            }
        }

        if(!sense)
        {
            subBuf = new byte[16];
            Array.Copy(cmdBuf, 2352, subBuf, 0, 16);
        }
        else
        {
            sense = dev.ReadCd(out cmdBuf,
                               out _,
                               lba,
                               16,
                               1,
                               MmcSectorTypes.AllTypes,
                               false,
                               false,
                               false,
                               MmcHeaderCodes.None,
                               false,
                               false,
                               MmcErrorField.None,
                               MmcSubchannel.Q16,
                               dev.Timeout,
                               out _);

            if(sense)
            {
                sense = dev.ReadCd(out cmdBuf,
                                   out _,
                                   lba,
                                   16,
                                   1,
                                   MmcSectorTypes.Cdda,
                                   false,
                                   false,
                                   false,
                                   MmcHeaderCodes.None,
                                   false,
                                   false,
                                   MmcErrorField.None,
                                   MmcSubchannel.Q16,
                                   dev.Timeout,
                                   out _);
            }

            if(!sense) subBuf = cmdBuf;
        }

        return sense;
    }

    /// <summary>De-interleaves Q subchannel</summary>
    /// <param name="subchannel">Interleaved subchannel</param>
    /// <returns>De-interleaved Q subchannel</returns>
    static byte[] DeinterleaveQ(byte[] subchannel)
    {
        var q = new int[subchannel.Length / 8];

        // De-interlace Q subchannel
        for(var iq = 0; iq < subchannel.Length; iq += 8)
        {
            q[iq / 8] =  (subchannel[iq] & 0x40) << 1;
            q[iq / 8] += subchannel[iq + 1] & 0x40;
            q[iq / 8] += (subchannel[iq + 2] & 0x40) >> 1;
            q[iq / 8] += (subchannel[iq + 3] & 0x40) >> 2;
            q[iq / 8] += (subchannel[iq + 4] & 0x40) >> 3;
            q[iq / 8] += (subchannel[iq + 5] & 0x40) >> 4;
            q[iq / 8] += (subchannel[iq + 6] & 0x40) >> 5;
            q[iq / 8] += (subchannel[iq + 7] & 0x40) >> 6;
        }

        var deQ = new byte[q.Length];

        for(var iq = 0; iq < q.Length; iq++) deQ[iq] = (byte)q[iq];

        return deQ;
    }

    /// <summary>In place converts Q subchannel from binary to BCD numbering</summary>
    /// <param name="q">Q subchannel</param>
    static void BinaryToBcdQ(byte[] q)
    {
        q[1] = (byte)((q[1] / 10 << 4) + q[1] % 10);
        q[2] = (byte)((q[2] / 10 << 4) + q[2] % 10);
        q[3] = (byte)((q[3] / 10 << 4) + q[3] % 10);
        q[4] = (byte)((q[4] / 10 << 4) + q[4] % 10);
        q[5] = (byte)((q[5] / 10 << 4) + q[5] % 10);
        q[6] = (byte)((q[6] / 10 << 4) + q[6] % 10);
        q[7] = (byte)((q[7] / 10 << 4) + q[7] % 10);
        q[8] = (byte)((q[8] / 10 << 4) + q[8] % 10);
        q[9] = (byte)((q[9] / 10 << 4) + q[9] % 10);
    }

    /// <summary>In place converts Q subchannel from BCD to binary numbering</summary>
    /// <param name="q">Q subchannel</param>
    static void BcdToBinaryQ(byte[] q)
    {
        q[1] = (byte)(q[1] / 16 * 10 + (q[1] & 0x0F));
        q[2] = (byte)(q[2] / 16 * 10 + (q[2] & 0x0F));
        q[3] = (byte)(q[3] / 16 * 10 + (q[3] & 0x0F));
        q[4] = (byte)(q[4] / 16 * 10 + (q[4] & 0x0F));
        q[5] = (byte)(q[5] / 16 * 10 + (q[5] & 0x0F));
        q[6] = (byte)(q[6] / 16 * 10 + (q[6] & 0x0F));
        q[7] = (byte)(q[7] / 16 * 10 + (q[7] & 0x0F));
        q[8] = (byte)(q[8] / 16 * 10 + (q[8] & 0x0F));
        q[9] = (byte)(q[9] / 16 * 10 + (q[9] & 0x0F));
    }
}