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

using System;
using System.Linq;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Extents;
using Aaru.CommonTypes.Interfaces;
using Aaru.Core.Logging;
using Aaru.Decoders.SCSI;
using Aaru.Logging;
using Humanizer;

namespace Aaru.Core.Devices.Dumping;

partial class Dump
{
    /// <summary>
    ///     Dumps data when dumping from a SCSI Block Commands compliant device, optical variant (magneto-optical and
    ///     successors)
    /// </summary>
    /// <param name="blocks">Media blocks</param>
    /// <param name="maxBlocksToRead">Maximum number of blocks to read in a single command</param>
    /// <param name="blockSize">Block size in bytes</param>
    /// <param name="currentTry">Resume information</param>
    /// <param name="extents">Correctly dump extents</param>
    /// <param name="currentSpeed">Current speed</param>
    /// <param name="minSpeed">Minimum speed</param>
    /// <param name="maxSpeed">Maximum speed</param>
    /// <param name="totalDuration">Total time spent in commands</param>
    /// <param name="scsiReader">SCSI reader</param>
    /// <param name="mhddLog">MHDD log</param>
    /// <param name="ibgLog">ImgBurn log</param>
    /// <param name="imageWriteDuration">Total time spent writing to image</param>
    /// <param name="newTrim">Set if we need to start a trim</param>
    /// <param name="blankExtents">Blank extents</param>
    void ReadOpticalData(in ulong     blocks, in uint maxBlocksToRead, in uint blockSize, DumpHardware currentTry,
                         ExtentsULong extents, ref double currentSpeed, ref double minSpeed, ref double maxSpeed,
                         ref double   totalDuration, Reader scsiReader, MhddLog mhddLog, IbgLog ibgLog,
                         ref double   imageWriteDuration, ref bool newTrim, ref ExtentsULong blankExtents)
    {
        const uint maxBlocks      = 256;
        var        writtenExtents = new ExtentsULong();
        bool       written;
        uint       c = maxBlocks;
        bool       conditionMet;
        bool       changingCounter;
        bool       changingWritten;
        uint       blocksToRead = maxBlocksToRead;
        bool       sense;
        byte[]     buffer;
        ulong      sectorSpeedStart = 0;
        var        canMediumScan    = true;
        var        outputFormat     = _outputPlugin as IWritableImage;

        InitProgress?.Invoke();

        if(blankExtents is null)
        {
            blankExtents = new ExtentsULong();

            written = _dev.MediumScan(out ReadOnlySpan<byte> senseBuffer,
                                      true,
                                      false,
                                      false,
                                      false,
                                      false,
                                      0,
                                      1,
                                      1,
                                      out _,
                                      out _,
                                      uint.MaxValue,
                                      out _);

            DecodedSense? decodedSense = Sense.Decode(senseBuffer.ToArray());

            if(_dev.LastError != 0 || decodedSense?.SenseKey == SenseKeys.IllegalRequest)
            {
                UpdateStatus?.Invoke(Localization.Core.The_current_environment_doesn_t_support_the_medium_scan_command);

                canMediumScan = false;
                writtenExtents.Add(0, blocks - 1);
            }

            // TODO: Find a place where MEDIUM SCAN works properly
            else if(senseBuffer.IsEmpty) AaruLogging.WriteLine(Localization.Core.MEDIUM_SCAN_github_plead_message);

            changingCounter = false;
            changingWritten = false;

            for(uint b = 0; b < blocks; b += c)
            {
                if(!canMediumScan) break;

                if(_aborted)
                {
                    _resume.BlankExtents = null;
                    UpdateStatus?.Invoke(Localization.Core.Aborted);

                    break;
                }

                if(changingWritten)
                {
                    changingWritten = false;
                    written         = !written;
                    c               = maxBlocks;
                }

                if(changingCounter)
                {
                    b               -= c;
                    changingCounter =  false;
                }

                if(b + c >= blocks) c = (uint)(blocks - b);

                UpdateProgress?.Invoke(written
                                           ? string.Format(Localization.Core
                                                                       .Scanning_for_0_written_blocks_starting_in_block_1,
                                                           c,
                                                           b)
                                           : string.Format(Localization.Core
                                                                       .Scanning_for_0_blank_blocks_starting_in_block_1,
                                                           c,
                                                           b),
                                       b,
                                       (long)blocks);

                conditionMet = _dev.MediumScan(out _,
                                               written,
                                               false,
                                               false,
                                               false,
                                               false,
                                               b,
                                               c,
                                               c,
                                               out _,
                                               out _,
                                               uint.MaxValue,
                                               out _);

                if(conditionMet)
                {
                    if(written)
                        writtenExtents.Add(b, c, true);
                    else
                        blankExtents.Add(b, c, true);

                    if(c < maxBlocks) changingWritten = true;
                }
                else
                {
                    if(c > 64)
                        c /= 2;
                    else
                        c--;

                    changingCounter = true;

                    if(c != 0) continue;

                    written = !written;
                    c       = maxBlocks;
                }
            }

            if(_resume != null && canMediumScan)
                _resume.BlankExtents = ExtentsConverter.ToMetadata(blankExtents).ToArray();

            EndProgress?.Invoke();
        }
        else
        {
            writtenExtents.Add(0, blocks - 1);

            foreach(Tuple<ulong, ulong> blank in blankExtents.ToArray())
                for(ulong b = blank.Item1; b <= blank.Item2; b++)
                    writtenExtents.Remove(b);
        }

        if(writtenExtents.Count == 0)
        {
            UpdateStatus?.Invoke(Localization.Core.Cannot_dump_empty_media);

            return;
        }

        InitProgress?.Invoke();

        Tuple<ulong, ulong>[] extentsToDump = writtenExtents.ToArray();

        foreach(Tuple<ulong, ulong> extent in extentsToDump)
        {
            if(extent.Item2 < _resume.NextBlock) continue; // Skip this extent

            ulong nextBlock = extent.Item1;

            if(extent.Item1 < _resume.NextBlock) nextBlock = (uint)_resume.NextBlock;

            _speedStopwatch.Restart();

            for(ulong i = nextBlock; i <= extent.Item2; i += blocksToRead)
            {
                if(_aborted)
                {
                    currentTry.Extents = ExtentsConverter.ToMetadata(extents);
                    UpdateStatus?.Invoke(Localization.Core.Aborted);

                    break;
                }

                if(extent.Item2 + 1 - i < blocksToRead) blocksToRead = (uint)(extent.Item2 + 1 - i);

                if(currentSpeed > maxSpeed && currentSpeed > 0) maxSpeed = currentSpeed;

                if(currentSpeed < minSpeed && currentSpeed > 0) minSpeed = currentSpeed;

                UpdateProgress?.Invoke(string.Format(Localization.Core.Reading_sector_0_of_1_2,
                                                     i,
                                                     blocks,
                                                     ByteSize.FromMegabytes(currentSpeed).Per(_oneSecond).Humanize()),
                                       (long)i,
                                       (long)blocks);

                sense = scsiReader.ReadBlocks(out buffer, i, blocksToRead, out double cmdDuration, out _, out _);
                totalDuration += cmdDuration;

                if(!sense && !_dev.Error)
                {
                    mhddLog.Write(i, cmdDuration, blocksToRead);
                    ibgLog.Write(i, currentSpeed * 1024);
                    _writeStopwatch.Restart();

                    outputFormat.WriteSectors(buffer,
                                              i,
                                              false,
                                              blocksToRead,
                                              Enumerable.Repeat(SectorStatus.Dumped, (int)blocksToRead).ToArray());

                    imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds;
                    extents.Add(i, blocksToRead, true);
                }
                else
                {
                    // TODO: Reset device after X errors
                    if(_stopOnError) return; // TODO: Return more cleanly

                    if(i + _skip > extent.Item2 + 1) _skip = (uint)(extent.Item2 + 1 - i);

                    // Write empty data
                    _writeStopwatch.Restart();

                    outputFormat.WriteSectors(new byte[blockSize * _skip],
                                              i,
                                              false,
                                              _skip,
                                              Enumerable.Repeat(SectorStatus.NotDumped, (int)_skip).ToArray());

                    imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds;

                    for(ulong b = i; b < i + _skip; b++) _resume.BadBlocks.Add(b);

                    mhddLog.Write(i, cmdDuration < 500 ? 65535 : cmdDuration, _skip);

                    ibgLog.Write(i, 0);
                    AaruLogging.WriteLine(Localization.Core.Skipping_0_blocks_from_errored_block_1, _skip, i);
                    i       += _skip - blocksToRead;
                    newTrim =  true;
                }

                _writeStopwatch.Stop();
                sectorSpeedStart  += blocksToRead;
                _resume.NextBlock =  i + blocksToRead;

                double elapsed = _speedStopwatch.Elapsed.TotalSeconds;

                if(elapsed <= 0) continue;

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

        _speedStopwatch.Stop();
        _resume.BadBlocks = _resume.BadBlocks.Distinct().ToList();

        EndProgress?.Invoke();
    }
}