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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Extents;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Console;
using Aaru.Core.Logging;
using Aaru.Core.Media.Detection;
using Aaru.Database.Models;
using Aaru.Decoders.CD;
using Aaru.Devices;
using Schemas;
using PlatformID = Aaru.CommonTypes.Interop.PlatformID;
using TrackType = Aaru.CommonTypes.Enums.TrackType;
using Version = Aaru.CommonTypes.Interop.Version;

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

namespace Aaru.Core.Devices.Dumping
{
    /// <summary>Implement dumping Compact Discs</summary>

    // TODO: Barcode
    sealed partial class Dump
    {
        /// <summary>Dumps a compact disc</summary>
        void CompactDisc()
        {
            ExtentsULong             audioExtents; // Extents with audio sectors
            ulong                    blocks; // Total number of positive sectors
            uint                     blockSize; // Size of the read sector in bytes
            CdOffset                 cdOffset; // Read offset from database
            byte[]                   cmdBuf; // Data buffer
            DumpHardwareType         currentTry   = null; // Current dump hardware try
            double                   currentSpeed = 0; // Current read speed
            int?                     discOffset   = null; // Disc write offset
            DateTime                 dumpStart    = DateTime.UtcNow; // Time of dump start
            DateTime                 end; // Time of operation end
            ExtentsULong             extents = null; // Extents
            bool                     hiddenData; // Hidden track is data
            IbgLog                   ibgLog; // IMGBurn log
            double                   imageWriteDuration = 0; // Duration of image write
            long                     lastSector; // Last sector number
            var                      leadOutExtents = new ExtentsULong(); // Lead-out extents
            Dictionary<int, long>    leadOutStarts  = new Dictionary<int, long>(); // Lead-out starts
            double                   maxSpeed       = double.MinValue; // Maximum speed
            MhddLog                  mhddLog; // MHDD log
            double                   minSpeed = double.MaxValue; // Minimum speed
            bool                     newTrim; // Is trim a new one?
            int                      offsetBytes = 0; // Read offset
            bool                     read6       = false; // Device supports READ(6)
            bool                     read10      = false; // Device supports READ(10)
            bool                     read12      = false; // Device supports READ(12)
            bool                     read16      = false; // Device supports READ(16)
            bool                     readcd; // Device supports READ CD
            bool                     ret; // Image writing return status
            const uint               sectorSize       = 2352; // Full sector size
            int                      sectorsForOffset = 0; // Sectors needed to fix offset
            bool                     sense            = true; // Sense indicator
            int                      sessions; // Number of sessions in disc
            DateTime                 start; // Start of operation
            SubchannelLog            subLog = null; // Subchannel log
            uint                     subSize; // Subchannel size in bytes
            TrackSubchannelType      subType; // Track subchannel type
            bool                     supportsLongSectors = true; // Supports reading EDC and ECC
            bool                     supportsPqSubchannel; // Supports reading PQ subchannel
            bool                     supportsRwSubchannel; // Supports reading RW subchannel
            byte[]                   tmpBuf; // Temporary buffer
            FullTOC.CDFullTOC?       toc; // Full CD TOC
            double                   totalDuration = 0; // Total commands duration
            Dictionary<byte, byte>   trackFlags    = new Dictionary<byte, byte>(); // Track flags
            Track[]                  tracks; // Tracks in disc
            int                      firstTrackLastSession; // Number of first track in last session
            bool                     hiddenTrack; // Disc has a hidden track before track 1
            MmcSubchannel            supportedSubchannel; // Drive's maximum supported subchannel
            MmcSubchannel            desiredSubchannel; // User requested subchannel
            bool                     bcdSubchannel       = false; // Subchannel positioning is in BCD
            Dictionary<byte, string> isrcs               = new Dictionary<byte, string>();
            string                   mcn                 = null;
            HashSet<int>             subchannelExtents   = new HashSet<int>();
            bool                     cdiReadyReadAsAudio = false;
            uint                     firstLba;

            Dictionary<MediaTagType, byte[]> mediaTags = new Dictionary<MediaTagType, byte[]>(); // Media tags
            Dictionary<byte, int>            smallestPregapLbaPerTrack = new Dictionary<byte, int>();

            MediaType dskType = MediaType.CD;

            if(_dumpRaw)
            {
                _dumpLog.WriteLine("Raw CD dumping not yet implemented");
                StoppingErrorMessage?.Invoke("Raw CD dumping not yet implemented");

                return;
            }

            tracks = GetCdTracks(_dev, _dumpLog, _force, out lastSector, leadOutStarts, mediaTags, StoppingErrorMessage,
                                 out toc, trackFlags, UpdateStatus);

            if(tracks is null)
            {
                _dumpLog.WriteLine("Could not get tracks!");
                StoppingErrorMessage?.Invoke("Could not get tracks!");

                return;
            }

            firstLba = (uint)tracks.Min(t => t.TrackStartSector);

            // Check subchannels support
            supportsPqSubchannel = SupportsPqSubchannel(_dev, _dumpLog, UpdateStatus, firstLba);
            supportsRwSubchannel = SupportsRwSubchannel(_dev, _dumpLog, UpdateStatus, firstLba);

            if(supportsRwSubchannel)
                supportedSubchannel = MmcSubchannel.Raw;
            else if(supportsPqSubchannel)
                supportedSubchannel = MmcSubchannel.Q16;
            else
                supportedSubchannel = MmcSubchannel.None;

            switch(_subchannel)
            {
                case DumpSubchannel.Any:
                    if(supportsRwSubchannel)
                        desiredSubchannel = MmcSubchannel.Raw;
                    else if(supportsPqSubchannel)
                        desiredSubchannel = MmcSubchannel.Q16;
                    else
                        desiredSubchannel = MmcSubchannel.None;

                    break;
                case DumpSubchannel.Rw:
                    if(supportsRwSubchannel)
                        desiredSubchannel = MmcSubchannel.Raw;
                    else
                    {
                        _dumpLog.WriteLine("Drive does not support the requested subchannel format, not continuing...");

                        StoppingErrorMessage?.
                            Invoke("Drive does not support the requested subchannel format, not continuing...");

                        return;
                    }

                    break;
                case DumpSubchannel.RwOrPq:
                    if(supportsRwSubchannel)
                        desiredSubchannel = MmcSubchannel.Raw;
                    else if(supportsPqSubchannel)
                        desiredSubchannel = MmcSubchannel.Q16;
                    else
                    {
                        _dumpLog.WriteLine("Drive does not support the requested subchannel format, not continuing...");

                        StoppingErrorMessage?.
                            Invoke("Drive does not support the requested subchannel format, not continuing...");

                        return;
                    }

                    break;
                case DumpSubchannel.Pq:
                    if(supportsPqSubchannel)
                        desiredSubchannel = MmcSubchannel.Q16;
                    else
                    {
                        _dumpLog.WriteLine("Drive does not support the requested subchannel format, not continuing...");

                        StoppingErrorMessage?.
                            Invoke("Drive does not support the requested subchannel format, not continuing...");

                        return;
                    }

                    break;
                case DumpSubchannel.None:
                    desiredSubchannel = MmcSubchannel.None;

                    break;
                default: throw new ArgumentOutOfRangeException();
            }

            if(desiredSubchannel == MmcSubchannel.Q16 && supportsPqSubchannel)
                supportedSubchannel = MmcSubchannel.Q16;

            // Check if output format supports subchannels
            if(!_outputPlugin.SupportedSectorTags.Contains(SectorTagType.CdSectorSubchannel) &&
               desiredSubchannel != MmcSubchannel.None)
            {
                if(_force || _subchannel == DumpSubchannel.None)
                {
                    _dumpLog.WriteLine("Output format does not support subchannels, continuing...");
                    UpdateStatus?.Invoke("Output format does not support subchannels, continuing...");
                }
                else
                {
                    _dumpLog.WriteLine("Output format does not support subchannels, not continuing...");
                    StoppingErrorMessage?.Invoke("Output format does not support subchannels, not continuing...");

                    return;
                }

                desiredSubchannel = MmcSubchannel.None;
            }

            switch(supportedSubchannel)
            {
                case MmcSubchannel.None:
                    _dumpLog.WriteLine("Checking if drive supports reading without subchannel...");
                    UpdateStatus?.Invoke("Checking if drive supports reading without subchannel...");

                    readcd = !_dev.ReadCd(out cmdBuf, out _, firstLba, sectorSize, 1, MmcSectorTypes.AllTypes, false,
                                          false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None,
                                          supportedSubchannel, _dev.Timeout, out _);

                    if(!readcd)
                    {
                        _dumpLog.WriteLine("Drive does not support READ CD, trying SCSI READ commands...");
                        ErrorMessage?.Invoke("Drive does not support READ CD, trying SCSI READ commands...");

                        _dumpLog.WriteLine("Checking if drive supports READ(6)...");
                        UpdateStatus?.Invoke("Checking if drive supports READ(6)...");
                        read6 = !_dev.Read6(out cmdBuf, out _, firstLba, 2048, 1, _dev.Timeout, out _);
                        _dumpLog.WriteLine("Checking if drive supports READ(10)...");
                        UpdateStatus?.Invoke("Checking if drive supports READ(10)...");

                        read10 = !_dev.Read10(out cmdBuf, out _, 0, false, true, false, false, firstLba, 2048, 0, 1,
                                              _dev.Timeout, out _);

                        _dumpLog.WriteLine("Checking if drive supports READ(12)...");
                        UpdateStatus?.Invoke("Checking if drive supports READ(12)...");

                        read12 = !_dev.Read12(out cmdBuf, out _, 0, false, true, false, false, firstLba, 2048, 0, 1,
                                              false, _dev.Timeout, out _);

                        _dumpLog.WriteLine("Checking if drive supports READ(16)...");
                        UpdateStatus?.Invoke("Checking if drive supports READ(16)...");

                        read16 = !_dev.Read16(out cmdBuf, out _, 0, false, true, false, firstLba, 2048, 0, 1, false,
                                              _dev.Timeout, out _);

                        if(!read6  &&
                           !read10 &&
                           !read12 &&
                           !read16)
                        {
                            _dumpLog.WriteLine("Cannot read from disc, not continuing...");
                            StoppingErrorMessage?.Invoke("Cannot read from disc, not continuing...");

                            return;
                        }

                        if(read6)
                        {
                            _dumpLog.WriteLine("Drive supports READ(6)...");
                            UpdateStatus?.Invoke("Drive supports READ(6)...");
                        }

                        if(read10)
                        {
                            _dumpLog.WriteLine("Drive supports READ(10)...");
                            UpdateStatus?.Invoke("Drive supports READ(10)...");
                        }

                        if(read12)
                        {
                            _dumpLog.WriteLine("Drive supports READ(12)...");
                            UpdateStatus?.Invoke("Drive supports READ(12)...");
                        }

                        if(read16)
                        {
                            _dumpLog.WriteLine("Drive supports READ(16)...");
                            UpdateStatus?.Invoke("Drive supports READ(16)...");
                        }
                    }

                    _dumpLog.WriteLine("Drive can read without subchannel...");
                    UpdateStatus?.Invoke("Drive can read without subchannel...");

                    subSize = 0;
                    subType = TrackSubchannelType.None;

                    break;
                case MmcSubchannel.Raw:
                    _dumpLog.WriteLine("Full raw subchannel reading supported...");
                    UpdateStatus?.Invoke("Full raw subchannel reading supported...");
                    subType = TrackSubchannelType.Raw;
                    subSize = 96;
                    readcd  = true;

                    break;
                case MmcSubchannel.Q16:
                    _dumpLog.WriteLine("PQ subchannel reading supported...");
                    _dumpLog.WriteLine("WARNING: If disc says CD+G, CD+EG, CD-MIDI, CD Graphics or CD Enhanced Graphics, dump will be incorrect!");
                    UpdateStatus?.Invoke("PQ subchannel reading supported...");

                    UpdateStatus?.
                        Invoke("WARNING: If disc says CD+G, CD+EG, CD-MIDI, CD Graphics or CD Enhanced Graphics, dump will be incorrect!");

                    subType = TrackSubchannelType.Q16;
                    subSize = 16;
                    readcd  = true;

                    break;
                default:
                    _dumpLog.WriteLine("Handling subchannel type {0} not supported, exiting...", supportedSubchannel);

                    StoppingErrorMessage?.
                        Invoke($"Handling subchannel type {supportedSubchannel} not supported, exiting...");

                    return;
            }

            switch(desiredSubchannel)
            {
                case MmcSubchannel.None:
                    subType = TrackSubchannelType.None;

                    break;
                case MmcSubchannel.Raw:
                case MmcSubchannel.Q16:
                    subType = TrackSubchannelType.Raw;

                    break;
            }

            blockSize = sectorSize + subSize;

            // Check if subchannel is BCD
            if(supportedSubchannel != MmcSubchannel.None)
            {
                sense = _dev.ReadCd(out cmdBuf, out _, (((firstLba / 75) + 1) * 75) + 35, blockSize, 1,
                                    MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true,
                                    MmcErrorField.None, supportedSubchannel, _dev.Timeout, out _);

                if(!sense)
                {
                    tmpBuf = new byte[subSize];
                    Array.Copy(cmdBuf, sectorSize, tmpBuf, 0, subSize);

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

                    tmpBuf = Subchannel.Deinterleave(tmpBuf);

                    // 9th Q subchannel is always FRAME when in user data area
                    // LBA 35 => MSF 00:02:35 => FRAME 35 (in hexadecimal 0x23)
                    // Sometimes drive returns a pregap here but MSF 00:02:3x => FRAME 3x (hexadecimal 0x20 to 0x27)
                    bcdSubchannel = (tmpBuf[21] & 0x30) > 0;

                    if(bcdSubchannel)
                    {
                        _dumpLog.WriteLine("Drive returns subchannel in BCD...");
                        UpdateStatus?.Invoke("Drive returns subchannel in BCD...");
                    }
                    else
                    {
                        _dumpLog.WriteLine("Drive does not returns subchannel in BCD...");
                        UpdateStatus?.Invoke("Drive does not returns subchannel in BCD...");
                    }
                }
            }

            foreach(Track trk in tracks)
                trk.TrackSubchannelType = subType;

            _dumpLog.WriteLine("Calculating pregaps, can take some time...");
            UpdateStatus?.Invoke("Calculating pregaps, can take some time...");

            SolveTrackPregaps(_dev, _dumpLog, UpdateStatus, tracks, supportsPqSubchannel, supportsRwSubchannel, _dbDev,
                              out bool inexactPositioning, true);

            if(inexactPositioning)
            {
                _dumpLog.WriteLine("WARNING: The drive has returned incorrect Q positioning when calculating pregaps. A best effort has been tried but they may be incorrect.");

                UpdateStatus?.
                    Invoke("WARNING: The drive has returned incorrect Q positioning when calculating pregaps. A best effort has been tried but they may be incorrect.");
            }

            if(!(_outputPlugin as IWritableOpticalImage).OpticalCapabilities.HasFlag(OpticalImageCapabilities.
                   CanStoreRawData))
            {
                if(!_force)
                {
                    _dumpLog.WriteLine("Output format does not support storing raw data, this may end in a loss of data, not continuing...");

                    StoppingErrorMessage?.
                        Invoke("Output format does not support storing raw data, this may end in a loss of data, not continuing...");

                    return;
                }

                _dumpLog.WriteLine("Output format does not support storing raw data, this may end in a loss of data, continuing...");

                ErrorMessage?.
                    Invoke("Output format does not support storing raw data, this may end in a loss of data, continuing...");
            }

            if(!(_outputPlugin as IWritableOpticalImage).OpticalCapabilities.HasFlag(OpticalImageCapabilities.
                   CanStoreAudioTracks) &&
               tracks.Any(track => track.TrackType == TrackType.Audio))
            {
                _dumpLog.WriteLine("Output format does not support audio tracks, cannot continue...");

                StoppingErrorMessage?.Invoke("Output format does not support audio tracks, cannot continue...");

                return;
            }

            if(!(_outputPlugin as IWritableOpticalImage).OpticalCapabilities.HasFlag(OpticalImageCapabilities.
                   CanStorePregaps) &&
               tracks.Where(track => track.TrackSequence !=
                                     tracks.First(t => t.TrackSession == track.TrackSession).TrackSequence).
                      Any(track => track.TrackPregap > 0))
            {
                if(!_force)
                {
                    _dumpLog.WriteLine("Output format does not support pregaps, this may end in a loss of data, not continuing...");

                    StoppingErrorMessage?.
                        Invoke("Output format does not support pregaps, this may end in a loss of data, not continuing...");

                    return;
                }

                _dumpLog.WriteLine("Output format does not support pregaps, this may end in a loss of data, continuing...");

                ErrorMessage?.
                    Invoke("Output format does not support pregaps, this may end in a loss of data, continuing...");
            }

            for(int t = 1; t < tracks.Length; t++)
                tracks[t - 1].TrackEndSector = tracks[t].TrackStartSector - 1;

            tracks[^1].TrackEndSector = (ulong)lastSector;
            blocks                    = (ulong)(lastSector + 1);

            if(blocks == 0)
            {
                StoppingErrorMessage?.Invoke("Cannot dump blank media.");

                return;
            }

            ResumeSupport.Process(true, true, blocks, _dev.Manufacturer, _dev.Model, _dev.Serial, _dev.PlatformId,
                                  ref _resume, ref currentTry, ref extents, _dev.FirmwareRevision, _private, _force);

            if(currentTry == null ||
               extents    == null)
            {
                StoppingErrorMessage?.Invoke("Could not process resume file, not continuing...");

                return;
            }

            // Read media tags
            ReadCdTags(ref dskType, mediaTags, out sessions, out firstTrackLastSession);

            if(!(_outputPlugin as IWritableOpticalImage).OpticalCapabilities.HasFlag(OpticalImageCapabilities.
                   CanStoreSessions) &&
               sessions > 1)
            {
                // TODO: Disabled until 6.0
                /*if(!_force)
                {*/
                _dumpLog.WriteLine("Output format does not support sessions, this will end in a loss of data, not continuing...");

                StoppingErrorMessage?.
                    Invoke("Output format does not support sessions, this will end in a loss of data, not continuing...");

                return;
                /*}

                _dumpLog.WriteLine("Output format does not support sessions, this will end in a loss of data, continuing...");

                ErrorMessage?.
                    Invoke("Output format does not support sessions, this will end in a loss of data, continuing...");*/
            }

            // Check if output format supports all disc tags we have retrieved so far
            foreach(MediaTagType tag in mediaTags.Keys.Where(tag => !_outputPlugin.SupportedMediaTags.Contains(tag)))
            {
                if(_force)
                {
                    _dumpLog.WriteLine("Output format does not support {0}, continuing...", tag);
                    ErrorMessage?.Invoke($"Output format does not support {tag}, continuing...");
                }
                else
                {
                    _dumpLog.WriteLine("Output format does not support {0}, not continuing...", tag);
                    StoppingErrorMessage?.Invoke($"Output format does not support {tag}, not continuing...");

                    return;
                }
            }

            if(leadOutStarts.Any())
            {
                UpdateStatus?.Invoke("Solving lead-outs...");

                foreach(KeyValuePair<int, long> leadOuts in leadOutStarts)
                    foreach(Track trk in tracks.Where(trk => trk.TrackSession   == leadOuts.Key).
                                                Where(trk => trk.TrackEndSector >= (ulong)leadOuts.Value))
                        trk.TrackEndSector = (ulong)leadOuts.Value - 1;

                var dataExtents = new ExtentsULong();

                foreach(Track trk in tracks)
                    dataExtents.Add(trk.TrackStartSector, trk.TrackEndSector);

                Tuple<ulong, ulong>[] dataExtentsArray = dataExtents.ToArray();

                for(int i = 0; i < dataExtentsArray.Length - 1; i++)
                    leadOutExtents.Add(dataExtentsArray[i].Item2 + 1, dataExtentsArray[i + 1].Item1 - 1);
            }

            _dumpLog.WriteLine("Detecting disc type...");
            UpdateStatus?.Invoke("Detecting disc type...");

            MMC.DetectDiscType(ref dskType, sessions, toc, _dev, out hiddenTrack, out hiddenData, firstTrackLastSession,
                               blocks);

            if(hiddenTrack || firstLba > 0)
            {
                _dumpLog.WriteLine("Disc contains a hidden track...");
                UpdateStatus?.Invoke("Disc contains a hidden track...");

                if(!(_outputPlugin as IWritableOpticalImage).OpticalCapabilities.HasFlag(OpticalImageCapabilities.CanStoreHiddenTracks))
                {
                    StoppingErrorMessage?.Invoke("Output format does not properly support storing hidden tracks, this will end in a loss of data, not continuing...");
                    _dumpLog.WriteLine("Output format does not properly support storing hidden tracks, this will end in a loss of data, not continuing...");

                    return;
                }

                List<Track> trkList = new List<Track>
                {
                    new Track
                    {
                        TrackSequence          = 0,
                        TrackSession           = 1,
                        TrackType              = hiddenData ? TrackType.Data : TrackType.Audio,
                        TrackStartSector       = 0,
                        TrackBytesPerSector    = (int)sectorSize,
                        TrackRawBytesPerSector = (int)sectorSize,
                        TrackSubchannelType    = subType,
                        TrackEndSector         = tracks.First(t => t.TrackSequence == 1).TrackStartSector - 1
                    }
                };

                trkList.AddRange(tracks);
                tracks = trkList.ToArray();
            }

            if(tracks.Any(t => t.TrackType == TrackType.Audio) &&
               desiredSubchannel != MmcSubchannel.Raw)
            {
                _dumpLog.WriteLine("WARNING: If disc says CD+G, CD+EG, CD-MIDI, CD Graphics or CD Enhanced Graphics, dump will be incorrect!");

                UpdateStatus?.
                    Invoke("WARNING: If disc says CD+G, CD+EG, CD-MIDI, CD Graphics or CD Enhanced Graphics, dump will be incorrect!");
            }

            // Check mode for tracks
            foreach(Track trk in tracks.Where(t => t.TrackType != TrackType.Audio))
            {
                if(!readcd)
                {
                    trk.TrackType = TrackType.CdMode1;

                    continue;
                }

                _dumpLog.WriteLine("Checking mode for track {0}...", trk.TrackSequence);
                UpdateStatus?.Invoke($"Checking mode for track {trk.TrackSequence}...");

                sense = _dev.ReadCd(out cmdBuf, out _, (uint)(trk.TrackStartSector + trk.TrackPregap), blockSize, 1,
                                    MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true,
                                    MmcErrorField.None, supportedSubchannel, _dev.Timeout, out _);

                if(sense)
                {
                    _dumpLog.WriteLine("Unable to guess mode for track {0}, continuing...", trk.TrackSequence);

                    UpdateStatus?.Invoke($"Unable to guess mode for track {trk.TrackSequence}, continuing...");

                    continue;
                }

                int bufOffset = 0;

                while(cmdBuf[0  + bufOffset] != 0x00 ||
                      cmdBuf[1  + bufOffset] != 0xFF ||
                      cmdBuf[2  + bufOffset] != 0xFF ||
                      cmdBuf[3  + bufOffset] != 0xFF ||
                      cmdBuf[4  + bufOffset] != 0xFF ||
                      cmdBuf[5  + bufOffset] != 0xFF ||
                      cmdBuf[6  + bufOffset] != 0xFF ||
                      cmdBuf[7  + bufOffset] != 0xFF ||
                      cmdBuf[8  + bufOffset] != 0xFF ||
                      cmdBuf[9  + bufOffset] != 0xFF ||
                      cmdBuf[10 + bufOffset] != 0xFF ||
                      cmdBuf[11 + bufOffset] != 0x00)
                {
                    if(bufOffset + 12 >= cmdBuf.Length)
                        break;

                    bufOffset++;
                }

                switch(cmdBuf[15 + bufOffset])
                {
                    case 1:
                    case 0x61: // Scrambled
                        UpdateStatus?.Invoke($"Track {trk.TrackSequence} is MODE1");
                        _dumpLog.WriteLine("Track {0} is MODE1", trk.TrackSequence);
                        trk.TrackType = TrackType.CdMode1;

                        break;
                    case 2:
                    case 0x62: // Scrambled
                        if(dskType == MediaType.CDI ||
                           dskType == MediaType.CDIREADY)
                        {
                            UpdateStatus?.Invoke($"Track {trk.TrackSequence} is MODE2");
                            _dumpLog.WriteLine("Track {0} is MODE2", trk.TrackSequence);
                            trk.TrackType = TrackType.CdMode2Formless;

                            break;
                        }

                        if((cmdBuf[0x012] & 0x20) == 0x20) // mode 2 form 2
                        {
                            UpdateStatus?.Invoke($"Track {trk.TrackSequence} is MODE2 FORM 2");
                            _dumpLog.WriteLine("Track {0} is MODE2 FORM 2", trk.TrackSequence);
                            trk.TrackType = TrackType.CdMode2Form2;

                            break;
                        }

                        UpdateStatus?.Invoke($"Track {trk.TrackSequence} is MODE2 FORM 1");
                        _dumpLog.WriteLine("Track {0} is MODE2 FORM 1", trk.TrackSequence);
                        trk.TrackType = TrackType.CdMode2Form1;

                        // These media type specifications do not legally allow mode 2 tracks to be present
                        if(dskType == MediaType.CDROM  ||
                           dskType == MediaType.CDPLUS ||
                           dskType == MediaType.CDV)
                            dskType = MediaType.CD;

                        break;
                    default:
                        UpdateStatus?.Invoke($"Track {trk.TrackSequence} is unknown mode {cmdBuf[15]}");
                        _dumpLog.WriteLine("Track {0} is unknown mode {1}", trk.TrackSequence, cmdBuf[15]);

                        break;
                }
            }

            if(_outputPlugin.Id == new Guid("12345678-AAAA-BBBB-CCCC-123456789000"))
            {
                if(tracks.Length > 1)
                {
                    StoppingErrorMessage?.Invoke("Output format does not support more than 1 track, not continuing...");
                    _dumpLog.WriteLine("Output format does not support more than 1 track, not continuing...");

                    return;
                }

                if(tracks.Any(t => t.TrackType == TrackType.Audio))
                {
                    StoppingErrorMessage?.Invoke("Output format does not support audio tracks, not continuing...");
                    _dumpLog.WriteLine("Output format does not support audio tracks, not continuing...");

                    return;
                }

                if(tracks.Any(t => t.TrackType != TrackType.CdMode1))
                {
                    StoppingErrorMessage?.Invoke("Output format only supports MODE 1 tracks, not continuing...");
                    _dumpLog.WriteLine("Output format only supports MODE 1 tracks, not continuing...");

                    return;
                }

                supportsLongSectors = false;
            }

            // Check if something prevents from dumping the first track pregap
            if(_dumpFirstTrackPregap && readcd)
            {
                if(_dev.PlatformId == PlatformID.FreeBSD &&
                   !_dev.IsRemote)
                {
                    if(_force)
                    {
                        _dumpLog.
                            WriteLine("FreeBSD panics when reading CD first track pregap, see upstream bug #224253. continuing");

                        ErrorMessage?.
                            Invoke("FreeBSD panics when reading CD first track pregap, see upstream bug #224253. continuing");
                    }
                    else
                    {
                        _dumpLog.
                            WriteLine("FreeBSD panics when reading CD first track pregap, see upstream bug #224253. Not continuing");

                        StoppingErrorMessage?.
                            Invoke("FreeBSD panics when reading CD first track pregap, see upstream bug #224253. Not continuing");

                        return;
                    }

                    _dumpFirstTrackPregap = false;
                }

                if(!_outputPlugin.SupportedMediaTags.Contains(MediaTagType.CD_FirstTrackPregap))
                {
                    if(_force)
                    {
                        _dumpLog.WriteLine("Output format does not support CD first track pregap, continuing...");
                        ErrorMessage?.Invoke("Output format does not support CD first track pregap, continuing...");
                    }
                    else
                    {
                        _dumpLog.WriteLine("Output format does not support CD first track pregap, not continuing...");

                        StoppingErrorMessage?.
                            Invoke("Output format does not support CD first track pregap, not continuing...");

                        return;
                    }

                    _dumpFirstTrackPregap = false;
                }
            }

            // Try how many blocks are readable at once
            while(true)
            {
                if(readcd)
                {
                    sense = _dev.ReadCd(out cmdBuf, out _, firstLba, blockSize, _maximumReadable,
                                        MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true,
                                        true, MmcErrorField.None, supportedSubchannel, _dev.Timeout, out _);

                    if(_dev.Error || sense)
                        _maximumReadable /= 2;
                }
                else if(read16)
                {
                    sense = _dev.Read16(out cmdBuf, out _, 0, false, true, false, firstLba, blockSize, 0,
                                        _maximumReadable, false, _dev.Timeout, out _);

                    if(_dev.Error || sense)
                        _maximumReadable /= 2;
                }
                else if(read12)
                {
                    sense = _dev.Read12(out cmdBuf, out _, 0, false, true, false, false, firstLba, blockSize, 0,
                                        _maximumReadable, false, _dev.Timeout, out _);

                    if(_dev.Error || sense)
                        _maximumReadable /= 2;
                }
                else if(read10)
                {
                    sense = _dev.Read10(out cmdBuf, out _, 0, false, true, false, false, firstLba, blockSize, 0,
                                        (ushort)_maximumReadable, _dev.Timeout, out _);

                    if(_dev.Error || sense)
                        _maximumReadable /= 2;
                }
                else if(read6)
                {
                    sense = _dev.Read6(out cmdBuf, out _, firstLba, blockSize, (byte)_maximumReadable, _dev.Timeout,
                                       out _);

                    if(_dev.Error || sense)
                        _maximumReadable /= 2;
                }

                if(!_dev.Error ||
                   _maximumReadable == 1)
                    break;
            }

            if(_dev.Error || sense)
            {
                _dumpLog.WriteLine("Device error {0} trying to guess ideal transfer length.", _dev.LastError);
                StoppingErrorMessage?.Invoke($"Device error {_dev.LastError} trying to guess ideal transfer length.");
            }

            // Try to read the first track pregap
            if(_dumpFirstTrackPregap && readcd)
                ReadCdFirstTrackPregap(blockSize, ref currentSpeed, mediaTags, supportedSubchannel, ref totalDuration);

            _dumpLog.WriteLine("Reading {0} sectors at a time.", _maximumReadable);
            _dumpLog.WriteLine("Device reports {0} blocks ({1} bytes).", blocks, blocks * blockSize);
            _dumpLog.WriteLine("Device can read {0} blocks at a time.", _maximumReadable);
            _dumpLog.WriteLine("Device reports {0} bytes per logical block.", blockSize);
            _dumpLog.WriteLine("SCSI device type: {0}.", _dev.ScsiType);
            _dumpLog.WriteLine("Media identified as {0}.", dskType);

            UpdateStatus?.Invoke($"Reading {_maximumReadable} sectors at a time.");
            UpdateStatus?.Invoke($"Device reports {blocks} blocks ({blocks * blockSize} bytes).");
            UpdateStatus?.Invoke($"Device can read {_maximumReadable} blocks at a time.");
            UpdateStatus?.Invoke($"Device reports {blockSize} bytes per logical block.");
            UpdateStatus?.Invoke($"SCSI device type: {_dev.ScsiType}.");
            UpdateStatus?.Invoke($"Media identified as {dskType}.");

            ret = _outputPlugin.Create(_outputPath, dskType, _formatOptions, blocks,
                                       supportsLongSectors ? blockSize : 2048);

            // Cannot create image
            if(!ret)
            {
                _dumpLog.WriteLine("Error creating output image, not continuing.");
                _dumpLog.WriteLine(_outputPlugin.ErrorMessage);

                StoppingErrorMessage?.Invoke("Error creating output image, not continuing." + Environment.NewLine +
                                             _outputPlugin.ErrorMessage);

                return;
            }

            try
            {
                byte[] mcnBytes = _outputPlugin.ReadDiskTag(MediaTagType.CD_MCN);

                if(mcnBytes != null)
                    mcn = Encoding.ASCII.GetString(mcnBytes);
            }
            catch(Exception)
            {
                // TODO: Replace for error number
            }

            if((_outputPlugin as IWritableOpticalImage).Tracks != null)
                foreach(Track imgTrack in (_outputPlugin as IWritableOpticalImage).Tracks)
                {
                    try
                    {
                        byte[] isrcBytes =
                            (_outputPlugin as IWritableOpticalImage).ReadSectorTag(imgTrack.TrackSequence,
                                SectorTagType.CdTrackIsrc);

                        if(isrcBytes != null)
                            isrcs[(byte)imgTrack.TrackSequence] = Encoding.ASCII.GetString(isrcBytes);
                    }
                    catch(Exception)
                    {
                        // TODO: Replace for error number
                    }

                    Track trk = tracks.FirstOrDefault(t => t.TrackSequence == imgTrack.TrackSequence);

                    if(trk != null)
                    {
                        trk.TrackPregap      = imgTrack.TrackPregap;
                        trk.TrackStartSector = imgTrack.TrackStartSector;
                        trk.TrackEndSector   = imgTrack.TrackEndSector;

                        foreach(KeyValuePair<ushort, int> imgIdx in imgTrack.Indexes)
                            trk.Indexes[imgIdx.Key] = imgIdx.Value;
                    }
                }

            // Send track list to output plugin. This may fail if subchannel is set but unsupported.
            ret = (_outputPlugin as IWritableOpticalImage).SetTracks(tracks.ToList());

            if(!ret &&
               desiredSubchannel == MmcSubchannel.None)
            {
                _dumpLog.WriteLine("Error sending tracks to output image, not continuing.");
                _dumpLog.WriteLine(_outputPlugin.ErrorMessage);

                StoppingErrorMessage?.Invoke("Error sending tracks to output image, not continuing." +
                                             Environment.NewLine + _outputPlugin.ErrorMessage);

                return;
            }

            // If a subchannel is supported, check if output plugin allows us to write it.
            if(desiredSubchannel != MmcSubchannel.None &&
               !(_outputPlugin as IWritableOpticalImage).OpticalCapabilities.HasFlag(OpticalImageCapabilities.
                   CanStoreSubchannelRw))
            {
                _dumpLog.WriteLine("Output image does not support subchannels, {0}continuing...", _force ? "" : "not ");

                if(_force)
                    ErrorMessage?.Invoke("Output image does not support subchannels, continuing...");
                else
                {
                    StoppingErrorMessage?.Invoke("Output image does not support subchannels, not continuing...");

                    return;
                }
            }

            if(supportedSubchannel != MmcSubchannel.None)
            {
                _dumpLog.WriteLine($"Creating subchannel log in {_outputPrefix + ".sub.log"}");
                subLog = new SubchannelLog(_outputPrefix + ".sub.log", bcdSubchannel);
            }

            // Set track flags
            foreach(KeyValuePair<byte, byte> kvp in trackFlags)
            {
                Track track = tracks.FirstOrDefault(t => t.TrackSequence == kvp.Key);

                if(track is null)
                    continue;

                _dumpLog.WriteLine("Setting flags for track {0}...", track.TrackSequence);
                UpdateStatus?.Invoke($"Setting flags for track {track.TrackSequence}...");

                _outputPlugin.WriteSectorTag(new[]
                {
                    kvp.Value
                }, kvp.Key, SectorTagType.CdTrackFlags);
            }

            // Set MCN
            if(supportedSubchannel == MmcSubchannel.None)
            {
                sense = _dev.ReadMcn(out mcn, out _, out _, _dev.Timeout, out _);

                if(!sense      &&
                   mcn != null &&
                   mcn != "0000000000000")
                {
                    UpdateStatus?.Invoke($"Found Media Catalogue Number: {mcn}");
                    _dumpLog.WriteLine("Found Media Catalogue Number: {0}", mcn);
                }
                else
                    mcn = null;
            }

            // Set ISRCs
            if(supportedSubchannel == MmcSubchannel.None)
                foreach(Track trk in tracks)
                {
                    sense = _dev.ReadIsrc((byte)trk.TrackSequence, out string isrc, out _, out _, _dev.Timeout, out _);

                    if(sense        ||
                       isrc == null ||
                       isrc == "000000000000")
                        continue;

                    isrcs[(byte)trk.TrackSequence] = isrc;

                    UpdateStatus?.Invoke($"Found ISRC for track {trk.TrackSequence}: {isrc}");
                    _dumpLog.WriteLine($"Found ISRC for track {trk.TrackSequence}: {isrc}");
                }

            if(supportedSubchannel != MmcSubchannel.None &&
               desiredSubchannel   != MmcSubchannel.None)
            {
                subchannelExtents = new HashSet<int>();

                _resume.BadSubchannels ??= new List<int>();

                foreach(int sub in _resume.BadSubchannels)
                    subchannelExtents.Add(sub);

                if(_resume.NextBlock < blocks)
                    for(ulong i = _resume.NextBlock; i < blocks; i++)
                        subchannelExtents.Add((int)i);
            }

            if(_resume.NextBlock > 0)
            {
                UpdateStatus?.Invoke($"Resuming from block {_resume.NextBlock}.");
                _dumpLog.WriteLine("Resuming from block {0}.", _resume.NextBlock);
            }

            if(_skip < _maximumReadable)
                _skip = _maximumReadable;

        #if DEBUG
            foreach(Track trk in tracks)
                UpdateStatus?.
                    Invoke($"Track {trk.TrackSequence} starts at LBA {trk.TrackStartSector} and ends at LBA {trk.TrackEndSector}");
        #endif

            // Check offset
            if(_fixOffset)
            {
                if(tracks.All(t => t.TrackType != TrackType.Audio))
                {
                    // No audio tracks so no need to fix offset
                    _dumpLog.WriteLine("No audio tracks, disabling offset fix.");
                    UpdateStatus.Invoke("No audio tracks, disabling offset fix.");

                    _fixOffset = false;
                }

                if(!readcd)
                {
                    _dumpLog.WriteLine("READ CD command is not supported, disabling offset fix. Dump may not be correct.");

                    UpdateStatus?.
                        Invoke("READ CD command is not supported, disabling offset fix. Dump may not be correct.");

                    _fixOffset = false;
                }
            }
            else if(tracks.Any(t => t.TrackType == TrackType.Audio))
            {
                _dumpLog.WriteLine("There are audio tracks and offset fixing is disabled, dump may not be correct.");
                UpdateStatus?.Invoke("There are audio tracks and offset fixing is disabled, dump may not be correct.");
            }

            // Search for read offset in main database
            cdOffset =
                _ctx.CdOffsets.FirstOrDefault(d => (d.Manufacturer == _dev.Manufacturer ||
                                                    d.Manufacturer == _dev.Manufacturer.Replace('/', '-')) &&
                                                   (d.Model == _dev.Model || d.Model == _dev.Model.Replace('/', '-')));

            Media.Info.CompactDisc.GetOffset(cdOffset, _dbDev, _debug, _dev, dskType, _dumpLog, tracks, UpdateStatus,
                                             out int? driveOffset, out int? combinedOffset, out _supportsPlextorD8);

            if(combinedOffset is null)
            {
                if(driveOffset is null)
                {
                    _dumpLog.WriteLine("Drive reading offset not found in database.");
                    UpdateStatus?.Invoke("Drive reading offset not found in database.");
                    _dumpLog.WriteLine("Disc offset cannot be calculated.");
                    UpdateStatus?.Invoke("Disc offset cannot be calculated.");

                    if(tracks.Any(t => t.TrackType == TrackType.Audio))
                    {
                        _dumpLog.WriteLine("Dump may not be correct.");

                        UpdateStatus?.Invoke("Dump may not be correct.");
                    }

                    if(_fixOffset)
                        _fixOffset = false;
                }
                else
                {
                    _dumpLog.WriteLine($"Drive reading offset is {driveOffset} bytes ({driveOffset   / 4} samples).");
                    UpdateStatus?.Invoke($"Drive reading offset is {driveOffset} bytes ({driveOffset / 4} samples).");

                    _dumpLog.WriteLine("Disc write offset is unknown, dump may not be correct.");
                    UpdateStatus?.Invoke("Disc write offset is unknown, dump may not be correct.");

                    offsetBytes = driveOffset.Value;

                    sectorsForOffset = offsetBytes / (int)sectorSize;

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

                    if(offsetBytes % sectorSize != 0)
                        sectorsForOffset++;
                }
            }
            else
            {
                offsetBytes      = combinedOffset.Value;
                sectorsForOffset = offsetBytes / (int)sectorSize;

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

                if(offsetBytes % sectorSize != 0)
                    sectorsForOffset++;

                if(driveOffset is null)
                {
                    _dumpLog.WriteLine("Drive reading offset not found in database.");
                    UpdateStatus?.Invoke("Drive reading offset not found in database.");
                    _dumpLog.WriteLine($"Combined disc and drive offsets are {offsetBytes} bytes ({offsetBytes / 4} samples).");

                    UpdateStatus?.
                        Invoke($"Combined disc and drive offsets are {offsetBytes} bytes ({offsetBytes / 4} samples).");
                }
                else
                {
                    _dumpLog.WriteLine($"Drive reading offset is {driveOffset} bytes ({driveOffset   / 4} samples).");
                    UpdateStatus?.Invoke($"Drive reading offset is {driveOffset} bytes ({driveOffset / 4} samples).");

                    discOffset = offsetBytes - driveOffset;

                    _dumpLog.WriteLine($"Disc offsets is {discOffset} bytes ({discOffset / 4} samples)");

                    UpdateStatus?.Invoke($"Disc offsets is {discOffset} bytes ({discOffset / 4} samples)");
                }
            }

            if(!_fixOffset ||
               tracks.All(t => t.TrackType != TrackType.Audio))
            {
                offsetBytes      = 0;
                sectorsForOffset = 0;
            }

            mhddLog = new MhddLog(_outputPrefix + ".mhddlog.bin", _dev, blocks, blockSize, _maximumReadable, _private);
            ibgLog  = new IbgLog(_outputPrefix  + ".ibg", 0x0008);

            audioExtents = new ExtentsULong();

            foreach(Track audioTrack in tracks.Where(t => t.TrackType == TrackType.Audio))
            {
                audioExtents.Add(audioTrack.TrackStartSector, audioTrack.TrackEndSector);
            }

            // Set speed
            if(_speedMultiplier >= 0)
            {
                _dumpLog.WriteLine($"Setting speed to {(_speed   == 0 ? "MAX for data reading" : $"{_speed}x")}.");
                UpdateStatus?.Invoke($"Setting speed to {(_speed == 0 ? "MAX for data reading" : $"{_speed}x")}.");

                _speed *= _speedMultiplier;

                if(_speed == 0 ||
                   _speed > 0xFFFF)
                    _speed = 0xFFFF;

                _dev.SetCdSpeed(out _, RotationalControl.ClvAndImpureCav, (ushort)_speed, 0, _dev.Timeout, out _);
            }

            // Start reading
            start = DateTime.UtcNow;

            if(dskType == MediaType.CDIREADY)
            {
                Track track0 = tracks.FirstOrDefault(t => t.TrackSequence == 0);

                track0.TrackType = TrackType.CdMode2Formless;

                if(!supportsLongSectors)
                {
                    _dumpLog.WriteLine("Dumping CD-i Ready requires the output image format to support long sectors.");

                    StoppingErrorMessage?.
                        Invoke("Dumping CD-i Ready requires the output image format to support long sectors.");

                    return;
                }

                if(!readcd)
                {
                    _dumpLog.WriteLine("Dumping CD-i Ready requires the drive to support the READ CD command.");

                    StoppingErrorMessage?.
                        Invoke("Dumping CD-i Ready requires the drive to support the READ CD command.");

                    return;
                }

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

                hiddenData = IsData(cmdBuf);

                if(!hiddenData)
                {
                    cdiReadyReadAsAudio = IsScrambledData(cmdBuf, 0, out combinedOffset);

                    if(cdiReadyReadAsAudio)
                    {
                        offsetBytes      = combinedOffset.Value;
                        sectorsForOffset = offsetBytes / (int)sectorSize;

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

                        if(offsetBytes % sectorSize != 0)
                            sectorsForOffset++;

                        _dumpLog.WriteLine("Enabling skipping CD-i Ready hole because drive returns data as audio.");

                        UpdateStatus?.Invoke("Enabling skipping CD-i Ready hole because drive returns data as audio.");

                        _skipCdireadyHole = true;

                        if(driveOffset is null)
                        {
                            _dumpLog.WriteLine("Drive reading offset not found in database.");
                            UpdateStatus?.Invoke("Drive reading offset not found in database.");

                            _dumpLog.
                                WriteLine($"Combined disc and drive offsets are {offsetBytes} bytes ({offsetBytes / 4} samples).");

                            UpdateStatus?.
                                Invoke($"Combined disc and drive offsets are {offsetBytes} bytes ({offsetBytes / 4} samples).");
                        }
                        else
                        {
                            _dumpLog.
                                WriteLine($"Drive reading offset is {driveOffset} bytes ({driveOffset / 4} samples).");

                            UpdateStatus?.
                                Invoke($"Drive reading offset is {driveOffset} bytes ({driveOffset / 4} samples).");

                            discOffset = offsetBytes - driveOffset;

                            _dumpLog.WriteLine($"Disc offsets is {discOffset} bytes ({discOffset / 4} samples)");

                            UpdateStatus?.Invoke($"Disc offsets is {discOffset} bytes ({discOffset / 4} samples)");
                        }
                    }
                }

                if(!_skipCdireadyHole)
                {
                    _dumpLog.WriteLine("There will be thousand of errors between track 0 and track 1, that is normal and you can ignore them.");

                    UpdateStatus?.
                        Invoke("There will be thousand of errors between track 0 and track 1, that is normal and you can ignore them.");
                }

                if(_skipCdireadyHole)
                    ReadCdiReady(blockSize, ref currentSpeed, currentTry, extents, ibgLog, ref imageWriteDuration,
                                 leadOutExtents, ref maxSpeed, mhddLog, ref minSpeed, subSize, supportedSubchannel,
                                 ref totalDuration, tracks, subLog, desiredSubchannel, isrcs, ref mcn,
                                 subchannelExtents, blocks, cdiReadyReadAsAudio, offsetBytes, sectorsForOffset,
                                 smallestPregapLbaPerTrack);
            }

            ReadCdData(audioExtents, blocks, blockSize, ref currentSpeed, currentTry, extents, ibgLog,
                       ref imageWriteDuration, lastSector, leadOutExtents, ref maxSpeed, mhddLog, ref minSpeed,
                       out newTrim, tracks[0].TrackType != TrackType.Audio, offsetBytes, read6, read10, read12, read16,
                       readcd, sectorsForOffset, subSize, supportedSubchannel, supportsLongSectors, ref totalDuration,
                       tracks, subLog, desiredSubchannel, isrcs, ref mcn, subchannelExtents, smallestPregapLbaPerTrack);

            // TODO: Enable when underlying images support lead-outs
            /*
            DumpCdLeadOuts(blocks, blockSize, ref currentSpeed, currentTry, extents, ibgLog, ref imageWriteDuration,
                           leadOutExtents, ref maxSpeed, mhddLog, ref minSpeed, read6, read10, read12, read16, readcd,
                           supportedSubchannel, subSize, ref totalDuration, subLog, desiredSubchannel, isrcs, ref mcn, tracks,
                           smallestPregapLbaPerTrack);
            */

            end = DateTime.UtcNow;
            mhddLog.Close();

            ibgLog.Close(_dev, blocks, blockSize, (end - start).TotalSeconds, currentSpeed * 1024,
                         blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000), _devicePath);

            UpdateStatus?.Invoke($"Dump finished in {(end - start).TotalSeconds} seconds.");

            UpdateStatus?.
                Invoke($"Average dump speed {blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000):F3} KiB/sec.");

            UpdateStatus?.
                Invoke($"Average write speed {blockSize * (double)(blocks + 1) / 1024 / imageWriteDuration:F3} KiB/sec.");

            _dumpLog.WriteLine("Dump finished in {0} seconds.", (end - start).TotalSeconds);

            _dumpLog.WriteLine("Average dump speed {0:F3} KiB/sec.",
                               blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000));

            _dumpLog.WriteLine("Average write speed {0:F3} KiB/sec.",
                               blockSize * (double)(blocks + 1) / 1024 / imageWriteDuration);

            TrimCdUserData(audioExtents, blockSize, currentTry, extents, newTrim, offsetBytes, read6, read10, read12,
                           read16, readcd, sectorsForOffset, subSize, supportedSubchannel, supportsLongSectors,
                           ref totalDuration, subLog, desiredSubchannel, tracks, isrcs, ref mcn, subchannelExtents,
                           smallestPregapLbaPerTrack);

            RetryCdUserData(audioExtents, blockSize, currentTry, extents, offsetBytes, readcd, sectorsForOffset,
                            subSize, supportedSubchannel, ref totalDuration, subLog, desiredSubchannel, tracks, isrcs,
                            ref mcn, subchannelExtents, smallestPregapLbaPerTrack, supportsLongSectors);

            foreach(Tuple<ulong, ulong> leadoutExtent in leadOutExtents.ToArray())
            {
                for(ulong e = leadoutExtent.Item1; e <= leadoutExtent.Item2; e++)
                    subchannelExtents.Remove((int)e);
            }

            if(subchannelExtents.Count > 0 &&
               _retryPasses            > 0 &&
               _retrySubchannel)
                RetrySubchannel(readcd, subSize, supportedSubchannel, ref totalDuration, subLog, desiredSubchannel,
                                tracks, isrcs, ref mcn, subchannelExtents, smallestPregapLbaPerTrack);

            // Write media tags to image
            if(!_aborted)
                foreach(KeyValuePair<MediaTagType, byte[]> tag in mediaTags)
                {
                    if(tag.Value is null)
                    {
                        AaruConsole.ErrorWriteLine("Error: Tag type {0} is null, skipping...", tag.Key);

                        continue;
                    }

                    ret = _outputPlugin.WriteMediaTag(tag.Value, tag.Key);

                    if(ret || _force)
                        continue;

                    // Cannot write tag to image
                    _dumpLog.WriteLine($"Cannot write tag {tag.Key}.");
                    StoppingErrorMessage?.Invoke(_outputPlugin.ErrorMessage);

                    return;
                }

            _resume.BadBlocks.Sort();

            foreach(ulong bad in _resume.BadBlocks)
                _dumpLog.WriteLine("Sector {0} could not be read.", bad);

            currentTry.Extents = ExtentsConverter.ToMetadata(extents);

            _resume.BadSubchannels = new List<int>();
            _resume.BadSubchannels.AddRange(subchannelExtents);
            _resume.BadSubchannels.Sort();

            if(_generateSubchannels                                                         &&
               _outputPlugin.SupportedSectorTags.Contains(SectorTagType.CdSectorSubchannel) &&
               !_aborted)
                Media.CompactDisc.GenerateSubchannels(subchannelExtents, tracks, trackFlags, blocks, subLog, _dumpLog,
                                                      InitProgress, UpdateProgress, EndProgress, _outputPlugin);

            // TODO: Disc ID
            var metadata = new CommonTypes.Structs.ImageInfo
            {
                Application        = "Aaru",
                ApplicationVersion = Version.GetVersion()
            };

            if(!_outputPlugin.SetMetadata(metadata))
                ErrorMessage?.Invoke("Error {0} setting metadata, continuing..." + Environment.NewLine +
                                     _outputPlugin.ErrorMessage);

            _outputPlugin.SetDumpHardware(_resume.Tries);

            if(_preSidecar != null)
                _outputPlugin.SetCicmMetadata(_preSidecar);

            foreach(KeyValuePair<byte, string> isrc in isrcs)
            {
                // TODO: Track tags
                if(!_outputPlugin.WriteSectorTag(Encoding.ASCII.GetBytes(isrc.Value), isrc.Key,
                                                 SectorTagType.CdTrackIsrc))
                    continue;

                UpdateStatus?.Invoke($"Setting ISRC for track {isrc.Key} to {isrc.Value}");
                _dumpLog.WriteLine("Setting ISRC for track {0} to {1}", isrc.Key, isrc.Value);
            }

            if(mcn != null &&
               _outputPlugin.WriteMediaTag(Encoding.ASCII.GetBytes(mcn), MediaTagType.CD_MCN))
            {
                UpdateStatus?.Invoke($"Setting disc Media Catalogue Number to {mcn}");
                _dumpLog.WriteLine("Setting disc Media Catalogue Number to {0}", mcn);
            }

            foreach(Track trk in tracks)
            {
                // Fix track starts in each session's first track
                if(tracks.Where(t => t.TrackSession == trk.TrackSession).OrderBy(t => t.TrackSequence).FirstOrDefault().
                          TrackSequence == trk.TrackSequence)
                {
                    if(trk.TrackSequence == 1)
                        continue;

                    trk.TrackStartSector -= trk.TrackPregap;
                    trk.Indexes[0]       =  (int)trk.TrackStartSector;

                    continue;
                }

                if(trk.Indexes.TryGetValue(0, out int idx0) &&
                   trk.Indexes.TryGetValue(1, out int idx1) &&
                   idx0 == idx1)
                    trk.Indexes.Remove(0);
            }

            (_outputPlugin as IWritableOpticalImage).SetTracks(tracks.ToList());

            _dumpLog.WriteLine("Closing output file.");
            UpdateStatus?.Invoke("Closing output file.");
            DateTime closeStart = DateTime.Now;
            _outputPlugin.Close();
            DateTime closeEnd = DateTime.Now;
            UpdateStatus?.Invoke($"Closed in {(closeEnd - closeStart).TotalSeconds} seconds.");

            subLog?.Close();

            if(_aborted)
            {
                _dumpLog.WriteLine("Aborted!");

                return;
            }

            double totalChkDuration = 0;

            if(_metadata)
                WriteOpticalSidecar(blockSize, blocks, dskType, null, mediaTags, sessions, out totalChkDuration,
                                    discOffset);

            end = DateTime.UtcNow;
            UpdateStatus?.Invoke("");

            UpdateStatus?.
                Invoke($"Took a total of {(end - dumpStart).TotalSeconds:F3} seconds ({totalDuration / 1000:F3} processing commands, {totalChkDuration / 1000:F3} checksumming, {imageWriteDuration:F3} writing, {(closeEnd - closeStart).TotalSeconds:F3} closing).");

            UpdateStatus?.
                Invoke($"Average speed: {blockSize * (double)(blocks + 1) / 1048576 / (totalDuration / 1000):F3} MiB/sec.");

            if(maxSpeed > 0)
                UpdateStatus?.Invoke($"Fastest speed burst: {maxSpeed:F3} MiB/sec.");

            if(minSpeed > 0 &&
               minSpeed < double.MaxValue)
                UpdateStatus?.Invoke($"Slowest speed burst: {minSpeed:F3} MiB/sec.");

            UpdateStatus?.Invoke($"{_resume.BadBlocks.Count} sectors could not be read.");
            UpdateStatus?.Invoke($"{_resume.BadSubchannels.Count} subchannels could not be read.");
            UpdateStatus?.Invoke("");

            Statistics.AddMedia(dskType, true);
        }
    }
}