// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : SecureDigital.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Core algorithms.
//
// --[ Description ] ----------------------------------------------------------
//
//     Scans SecureDigital and MultiMediaCard devices.
//
// --[ 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

using System;
using Aaru.Core.Logging;
using Aaru.Decoders.MMC;
using Aaru.Decoders.SecureDigital;
using Humanizer;
using CSD = Aaru.Decoders.MMC.CSD;
using DeviceType = Aaru.CommonTypes.Enums.DeviceType;

namespace Aaru.Core.Devices.Scanning;

/// <summary>Implements scanning a SecureDigital or MultiMediaCard flash card</summary>
public sealed partial class MediaScan
{
    ScanResults SecureDigital()
    {
        var    results = new ScanResults();
        byte[] cmdBuf;
        bool   sense;
        results.Blocks = 0;
        const uint   timeout       = 5;
        const ushort sdProfile     = 0x0001;
        ushort       blocksToRead  = 128;
        uint         blockSize     = 512;
        var          byteAddressed = true;
        var          supportsCmd23 = false;

        switch(_dev.Type)
        {
            case DeviceType.MMC:
            {
                sense = _dev.ReadCsd(out cmdBuf, out _, timeout, out _);

                if(!sense)
                {
                    CSD csd = Decoders.MMC.Decoders.DecodeCSD(cmdBuf);
                    results.Blocks = (ulong)((csd.Size + 1) * Math.Pow(2, csd.SizeMultiplier + 2));
                    blockSize      = (uint)Math.Pow(2, csd.ReadBlockLength);

                    // Found at least since MMC System Specification 3.31
                    supportsCmd23 = csd.Version >= 3;

                    if(csd.Size == 0xFFF)
                    {
                        sense = _dev.ReadExtendedCsd(out cmdBuf, out _, timeout, out _);

                        if(!sense)
                        {
                            ExtendedCSD ecsd = Decoders.MMC.Decoders.DecodeExtendedCSD(cmdBuf);
                            results.Blocks = ecsd.SectorCount;
                            blockSize      = (uint)(ecsd.SectorSize == 1 ? 4096 : 512);

                            blocksToRead = (ushort)(ecsd.OptimalReadSize * 4096 / blockSize);

                            if(blocksToRead == 0) blocksToRead = 128;

                            // Supposing it's high-capacity MMC if it has Extended CSD...
                            byteAddressed = false;
                        }
                    }
                }

                break;
            }

            case DeviceType.SecureDigital:
            {
                sense = _dev.ReadCsd(out cmdBuf, out _, timeout, out _);

                if(!sense)
                {
                    Decoders.SecureDigital.CSD csd = Decoders.SecureDigital.Decoders.DecodeCSD(cmdBuf);

                    results.Blocks = (ulong)(csd.Structure == 0
                                                 ? (csd.Size + 1) * Math.Pow(2, csd.SizeMultiplier + 2)
                                                 : (csd.Size + 1) * 1024);

                    blockSize = (uint)Math.Pow(2, csd.ReadBlockLength);

                    // Structure >=1 for SDHC/SDXC, so that's block addressed
                    byteAddressed = csd.Structure == 0;

                    if(blockSize != 512)
                    {
                        uint ratio = blockSize / 512;
                        results.Blocks *= ratio;
                        blockSize      =  512;
                    }

                    sense = _dev.ReadScr(out cmdBuf, out _, timeout, out _);

                    if(!sense)
                    {
                        supportsCmd23 = Decoders.SecureDigital.Decoders.DecodeSCR(cmdBuf)
                                               ?.CommandSupport.HasFlag(CommandSupport.SetBlockCount) ??
                                        false;
                    }
                }

                break;
            }
        }

        if(results.Blocks == 0)
        {
            StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_get_device_size);

            return results;
        }

        if(supportsCmd23)
        {
            sense = _dev.ReadWithBlockCount(out cmdBuf, out _, 0, blockSize, 1, byteAddressed, timeout, out double _);

            if(sense || _dev.Error)
            {
                UpdateStatus?.Invoke(Localization.Core
                                                 .Environment_does_not_support_setting_block_count_downgrading_to_OS_reading);

                supportsCmd23 = false;
            }

            // Need to restart device, otherwise is it just busy streaming data with no one listening
            sense = _dev.ReOpen();

            if(sense)
            {
                StoppingErrorMessage?.Invoke(string.Format(Localization.Core.Error_0_reopening_device, _dev.LastError));

                return results;
            }
        }

        if(supportsCmd23)
        {
            while(true)
            {
                sense = _dev.ReadWithBlockCount(out cmdBuf,
                                                out _,
                                                0,
                                                blockSize,
                                                blocksToRead,
                                                byteAddressed,
                                                timeout,
                                                out double _);

                if(sense) blocksToRead /= 2;

                if(!sense || blocksToRead == 1) break;
            }

            if(sense)
            {
                StoppingErrorMessage?.Invoke(string.Format(Localization.Core
                                                                       .Device_error_0_trying_to_guess_ideal_transfer_length,
                                                           _dev.LastError));

                return results;
            }
        }

        results.A              = 0; // <3ms
        results.B              = 0; // >=3ms, <10ms
        results.C              = 0; // >=10ms, <50ms
        results.D              = 0; // >=50ms, <150ms
        results.E              = 0; // >=150ms, <500ms
        results.F              = 0; // >=500ms
        results.Errored        = 0;
        results.ProcessingTime = 0;
        double currentSpeed = 0;
        results.MaxSpeed          = double.MinValue;
        results.MinSpeed          = double.MaxValue;
        results.UnreadableSectors = [];
        results.SeekMax           = double.MinValue;
        results.SeekMin           = double.MaxValue;
        results.SeekTotal         = 0;
        const int seekTimes = 100;

        var rnd = new Random();

        if(supportsCmd23 || blocksToRead == 1)
            UpdateStatus?.Invoke(string.Format(Localization.Core.Reading_0_sectors_at_a_time, blocksToRead));
        else if(_useBufferedReads)
        {
            UpdateStatus?.Invoke(string.Format(Localization.Core.Reading_0_sectors_at_a_time_using_OS_buffered_reads,
                                               blocksToRead));
        }
        else
        {
            UpdateStatus?.Invoke(string.Format(Localization.Core.Reading_0_sectors_using_sequential_single_commands,
                                               blocksToRead));
        }

        InitBlockMap?.Invoke(results.Blocks, blockSize, blocksToRead, sdProfile);
        var mhddLog = new MhddLog(_mhddLogPath, _dev, results.Blocks, blockSize, blocksToRead, false);
        var ibgLog  = new IbgLog(_ibgLogPath, sdProfile);

        _scanStopwatch.Restart();
        InitProgress?.Invoke();
        uint   accumulatedSpeedSectors = 0;
        double accumulatedSpeedMs      = 0;

        for(ulong i = 0; i < results.Blocks; i += blocksToRead)
        {
            if(_aborted) break;

            if(results.Blocks - i < blocksToRead) blocksToRead = (byte)(results.Blocks - i);

            if(currentSpeed > results.MaxSpeed && currentSpeed > 0) results.MaxSpeed = currentSpeed;

            if(currentSpeed < results.MinSpeed && currentSpeed > 0) results.MinSpeed = currentSpeed;

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

            bool error;

            _speedStopwatch.Restart();

            if(blocksToRead == 1)
            {
                error = _dev.ReadSingleBlock(out cmdBuf,
                                             out _,
                                             (uint)i,
                                             blockSize,
                                             byteAddressed,
                                             timeout,
                                             out double _);
            }
            else if(supportsCmd23)
            {
                error = _dev.ReadWithBlockCount(out cmdBuf,
                                                out _,
                                                (uint)i,
                                                blockSize,
                                                blocksToRead,
                                                byteAddressed,
                                                timeout,
                                                out double _);
            }
            else if(_useBufferedReads)
                error = _dev.BufferedOsRead(out cmdBuf, (long)(i * blockSize), blockSize * blocksToRead, out double _);
            else
            {
                error = _dev.ReadMultipleUsingSingle(out cmdBuf,
                                                     out _,
                                                     (uint)i,
                                                     blockSize,
                                                     blocksToRead,
                                                     byteAddressed,
                                                     timeout,
                                                     out double _);
            }

            _speedStopwatch.Stop();
            accumulatedSpeedMs      += _speedStopwatch.Elapsed.TotalMilliseconds;
            accumulatedSpeedSectors += blocksToRead;
            results.ProcessingTime  += _speedStopwatch.Elapsed.TotalMilliseconds;

            if(!error)
            {
                switch(_speedStopwatch.Elapsed.TotalMilliseconds)
                {
                    case >= 500:
                        results.F += blocksToRead;

                        break;
                    case >= 150:
                        results.E += blocksToRead;

                        break;
                    case >= 50:
                        results.D += blocksToRead;

                        break;
                    case >= 10:
                        results.C += blocksToRead;

                        break;
                    case >= 3:
                        results.B += blocksToRead;

                        break;
                    default:
                        results.A += blocksToRead;

                        break;
                }

                ScanTime?.Invoke(i, _speedStopwatch.Elapsed.TotalMilliseconds);
                mhddLog.Write(i, _speedStopwatch.Elapsed.TotalMilliseconds);
                ibgLog.Write(i, currentSpeed * 1024);
            }
            else
            {
                ScanUnreadable?.Invoke(i);
                results.Errored += blocksToRead;

                for(ulong b = i; b < i + blocksToRead; b++) results.UnreadableSectors.Add(b);

                mhddLog.Write(i,
                              _speedStopwatch.Elapsed.TotalMilliseconds < 500
                                  ? 65535
                                  : _speedStopwatch.Elapsed.TotalMilliseconds);

                ibgLog.Write(i, 0);
            }

            if(accumulatedSpeedMs < 100) continue;

            currentSpeed = accumulatedSpeedSectors * blockSize / (1048576 * (accumulatedSpeedMs / 1000.0));
            ScanSpeed?.Invoke(i, currentSpeed                             * 1024);
            accumulatedSpeedMs      = 0;
            accumulatedSpeedSectors = 0;
        }

        _scanStopwatch.Stop();
        EndProgress?.Invoke();
        mhddLog.Close();

        currentSpeed = accumulatedSpeedSectors * blockSize / (1048576 * (accumulatedSpeedMs / 1000.0));

        ibgLog.Close(_dev,
                     results.Blocks,
                     blockSize,
                     _scanStopwatch.Elapsed.TotalSeconds,
                     currentSpeed                             * 1024,
                     blockSize * (double)(results.Blocks + 1) / 1024 / (results.ProcessingTime / 1000),
                     _devicePath);

        InitProgress?.Invoke();

        for(var i = 0; i < seekTimes; i++)
        {
            if(_aborted || !_seekTest) break;

            var seekPos = (uint)rnd.Next((int)results.Blocks);

            PulseProgress?.Invoke(string.Format(Localization.Core.Seeking_to_sector_0, seekPos));

            _dev.ReadSingleBlock(out cmdBuf, out _, seekPos, blockSize, byteAddressed, timeout, out double seekCur);

            if(seekCur > results.SeekMax && seekCur > 0) results.SeekMax = seekCur;

            if(seekCur < results.SeekMin && seekCur > 0) results.SeekMin = seekCur;

            results.SeekTotal += seekCur;
            GC.Collect();
        }

        EndProgress?.Invoke();

        results.ProcessingTime /= 1000;
        results.TotalTime      =  _scanStopwatch.Elapsed.TotalSeconds;
        results.AvgSpeed       =  blockSize * (double)(results.Blocks + 1) / 1048576 / results.ProcessingTime;
        results.SeekTimes      =  seekTimes;

        return results;
    }
}