// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : ReaderATA.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Core algorithms.
//
// --[ Description ] ----------------------------------------------------------
//
//     Contains common code for reading ATA 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
// ****************************************************************************/

using System;
using Aaru.CommonTypes.Enums;
using Aaru.Decoders.ATA;
using Aaru.Logging;
using Identify = Aaru.CommonTypes.Structs.Devices.ATA.Identify;

namespace Aaru.Core.Devices;

sealed partial class Reader
{
    Identify.IdentifyDevice _ataId;
    bool                    _ataRead;
    bool                    _ataReadDma;
    bool                    _ataReadDmaLba;
    bool                    _ataReadDmaLba48;
    bool                    _ataReadDmaRetry;
    bool                    _ataReadDmaRetryLba;
    bool                    _ataReadLba;
    bool                    _ataReadLba48;
    bool                    _ataReadRetry;
    bool                    _ataReadRetryLba;
    bool                    _ataSeek;
    bool                    _ataSeekLba;

    internal bool   IsLba     { get; private set; }
    internal ushort Cylinders { get; private set; }
    internal byte   Heads     { get; private set; }
    internal byte   Sectors   { get; private set; }

    void GetDeviceChs()
    {
        if(_dev.Type != DeviceType.ATA) return;

        if(_ataId.CurrentCylinders > 0 && _ataId is { CurrentHeads: > 0, CurrentSectorsPerTrack: > 0 })
        {
            Cylinders = _ataId.CurrentCylinders;
            Heads     = (byte)_ataId.CurrentHeads;
            Sectors   = (byte)_ataId.CurrentSectorsPerTrack;
            Blocks    = (ulong)(Cylinders * Heads * Sectors);
        }

        if(_ataId.CurrentCylinders != 0 && _ataId.CurrentHeads != 0 && _ataId.CurrentSectorsPerTrack != 0 ||
           _ataId.Cylinders       <= 0                                                                    ||
           _ataId.Heads           <= 0                                                                    ||
           _ataId.SectorsPerTrack <= 0)
            return;

        Cylinders = _ataId.Cylinders;
        Heads     = (byte)_ataId.Heads;
        Sectors   = (byte)_ataId.SectorsPerTrack;
        Blocks    = (ulong)(Cylinders * Heads * Sectors);
    }

    ulong AtaGetBlocks()
    {
        GetDeviceChs();

        if(_ataId.Capabilities.HasFlag(Identify.CapabilitiesBit.LBASupport))
        {
            Blocks = _ataId.LBASectors;
            IsLba  = true;
        }

        if(!_ataId.CommandSet2.HasFlag(Identify.CommandSetBit2.LBA48)) return Blocks;

        Blocks = _ataId.LBA48Sectors;
        IsLba  = true;

        return Blocks;
    }

    bool AtaFindReadCommand()
    {
        if(Blocks == 0) GetDeviceBlocks();

        bool                   sense;
        var                    tries  = 0;
        uint                   lba    = 0;
        ushort                 cyl    = 0;
        byte                   head   = 0;
        byte                   sector = 1;
        AtaErrorRegistersChs   errorChs;
        AtaErrorRegistersLba28 errorLba;
        var                    rnd = new Random();

        while(tries < 10)
        {
            sense            = _dev.Read(out byte[] cmdBuf, out errorChs, false, cyl, head, sector, 1, _timeout, out _);
            _ataRead         = !sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && cmdBuf.Length > 0;
            sense            = _dev.Read(out cmdBuf, out errorChs, true, cyl, head, sector, 1, _timeout, out _);
            _ataReadRetry    = !sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && cmdBuf.Length > 0;
            sense            = _dev.ReadDma(out cmdBuf, out errorChs, false, cyl, head, sector, 1, _timeout, out _);
            _ataReadDma      = !sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && cmdBuf.Length > 0;
            sense            = _dev.ReadDma(out cmdBuf, out errorChs, true, cyl, head, sector, 1, _timeout, out _);
            _ataReadDmaRetry = !sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && cmdBuf.Length > 0;

            sense            = _dev.Read(out cmdBuf, out errorLba, false, lba, 1, _timeout, out _);
            _ataReadLba      = !sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0;
            sense            = _dev.Read(out cmdBuf, out errorLba, true, lba, 1, _timeout, out _);
            _ataReadRetryLba = !sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0;
            sense            = _dev.ReadDma(out cmdBuf, out errorLba, false, lba, 1, _timeout, out _);
            _ataReadDmaLba   = !sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0;
            sense            = _dev.ReadDma(out cmdBuf, out errorLba, true, lba, 1, _timeout, out _);

            _ataReadDmaRetryLba = !sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0;

            sense         = _dev.Read(out cmdBuf, out AtaErrorRegistersLba48 errorLba48, lba, 1, _timeout, out _);
            _ataReadLba48 = !sense && (errorLba48.Status & 0x27) == 0 && errorLba48.Error == 0 && cmdBuf.Length > 0;
            sense         = _dev.ReadDma(out cmdBuf, out errorLba48, lba, 1, _timeout, out _);

            _ataReadDmaLba48 = !sense && (errorLba48.Status & 0x27) == 0 && errorLba48.Error == 0 && cmdBuf.Length > 0;

            if(_ataRead            ||
               _ataReadRetry       ||
               _ataReadDma         ||
               _ataReadDmaRetry    ||
               _ataReadLba         ||
               _ataReadRetryLba    ||
               _ataReadDmaLba      ||
               _ataReadDmaRetryLba ||
               _ataReadLba48       ||
               _ataReadDmaLba48)
                break;

            lba    = (uint)rnd.Next(1,   (int)Blocks);
            cyl    = (ushort)rnd.Next(0, Cylinders);
            head   = (byte)rnd.Next(0,   Heads);
            sector = (byte)rnd.Next(1,   Sectors);
            tries++;
        }

        sense       = _dev.Seek(out errorChs, 0, 0, 1, _timeout, out _);
        _ataSeek    = !sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0;
        sense       = _dev.Seek(out errorLba, 0, _timeout, out _);
        _ataSeekLba = !sense && (errorLba.Status & 0x27) == 0 && errorChs.Error == 0;

        if(IsLba)
        {
            if(Blocks > 0xFFFFFFF && !_ataReadLba48 && !_ataReadDmaLba48)
            {
                ErrorMessage = Localization.Core.Device_needs_48_bit_LBA_commands_but_I_cant_issue_them_Aborting;

                return true;
            }

            if(!_ataReadLba && !_ataReadRetryLba && !_ataReadDmaLba && !_ataReadDmaRetryLba)
            {
                ErrorMessage = Localization.Core.Device_needs_28_bit_LBA_commands_but_I_cant_issue_them_Aborting;

                return true;
            }
        }
        else
        {
            if(!_ataRead && !_ataReadRetry && !_ataReadDma && !_ataReadDmaRetry)
            {
                ErrorMessage = Localization.Core.Device_needs_CHS_commands_but_I_cant_issue_them_Aborting;

                return true;
            }
        }

        if(_ataReadDmaLba48)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_DMA_EXT_command);
        else if(_ataReadLba48)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_EXT_command);
        else if(_ataReadDmaRetryLba)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_DMA_command_with_retries_LBA);
        else if(_ataReadDmaLba)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_DMA_command_LBA);
        else if(_ataReadRetryLba)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_command_with_retries_LBA);
        else if(_ataReadLba)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_command_LBA);
        else if(_ataReadDmaRetry)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_DMA_command_with_retries_CHS);
        else if(_ataReadDma)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_DMA_command_CHS);
        else if(_ataReadRetry)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_command_with_retries_CHS);
        else if(_ataRead)
            AaruLogging.WriteLine(Localization.Core.Using_ATA_READ_command_CHS);
        else
        {
            ErrorMessage = Localization.Core.Could_not_get_a_working_read_command;

            return true;
        }

        return false;
    }

    bool AtaGetBlockSize()
    {
        if((_ataId.PhysLogSectorSize & 0x8000) == 0x0000 && (_ataId.PhysLogSectorSize & 0x4000) == 0x4000)
        {
            if((_ataId.PhysLogSectorSize & 0x1000) == 0x1000)
            {
                if(_ataId.LogicalSectorWords <= 255 || _ataId.LogicalAlignment == 0xFFFF)
                    LogicalBlockSize = 512;
                else
                    LogicalBlockSize = _ataId.LogicalSectorWords * 2;
            }
            else
                LogicalBlockSize = 512;

            if((_ataId.PhysLogSectorSize & 0x2000) == 0x2000)
                PhysicalBlockSize = LogicalBlockSize * (uint)Math.Pow(2, _ataId.PhysLogSectorSize & 0xF);
            else
                PhysicalBlockSize = LogicalBlockSize;
        }
        else
        {
            LogicalBlockSize  = 512;
            PhysicalBlockSize = 512;
        }

        // TODO: ATA READ LONG
        LongBlockSize = 0;

        return false;
    }

    bool AtaGetBlocksToRead(uint startWithBlocks)
    {
        BlocksToRead = startWithBlocks;

        if(!IsLba)
        {
            BlocksToRead = 1;

            return false;
        }

        var error = true;

        while(IsLba)
        {
            byte[]                 cmdBuf;
            bool                   sense;
            AtaErrorRegistersLba48 errorLba48;

            if(_ataReadDmaLba48)
            {
                sense = _dev.ReadDma(out cmdBuf, out errorLba48, 0, (byte)BlocksToRead, _timeout, out _);
                error = !(!sense && (errorLba48.Status & 0x27) == 0 && errorLba48.Error == 0 && cmdBuf.Length > 0);
            }
            else if(_ataReadLba48)
            {
                sense = _dev.Read(out cmdBuf, out errorLba48, 0, (byte)BlocksToRead, _timeout, out _);
                error = !(!sense && (errorLba48.Status & 0x27) == 0 && errorLba48.Error == 0 && cmdBuf.Length > 0);
            }
            else
            {
                AtaErrorRegistersLba28 errorLba;

                if(_ataReadDmaRetryLba)
                {
                    sense = _dev.ReadDma(out cmdBuf, out errorLba, true, 0, (byte)BlocksToRead, _timeout, out _);
                    error = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0);
                }
                else if(_ataReadDmaLba)
                {
                    sense = _dev.ReadDma(out cmdBuf, out errorLba, false, 0, (byte)BlocksToRead, _timeout, out _);
                    error = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0);
                }
                else if(_ataReadRetryLba)
                {
                    sense = _dev.Read(out cmdBuf, out errorLba, true, 0, (byte)BlocksToRead, _timeout, out _);
                    error = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0);
                }
                else if(_ataReadLba)
                {
                    sense = _dev.Read(out cmdBuf, out errorLba, false, 0, (byte)BlocksToRead, _timeout, out _);
                    error = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && cmdBuf.Length > 0);
                }
            }

            if(error) BlocksToRead /= 2;

            if(!error || BlocksToRead == 1) break;
        }

        if(!error || !IsLba) return false;

        BlocksToRead = 1;

        ErrorMessage = string.Format(Localization.Core.Device_error_0_trying_to_guess_ideal_transfer_length,
                                     _dev.LastError);

        return true;
    }

    bool AtaReadBlocks(out byte[] buffer, ulong block, uint count, out double duration, out bool recoveredError)
    {
        var                    error = true;
        bool                   sense;
        AtaErrorRegistersLba28 errorLba;
        AtaErrorRegistersLba48 errorLba48;
        byte                   status = 0, errorByte = 0;
        buffer         = null;
        duration       = 0;
        recoveredError = false;

        if(_ataReadDmaLba48)
        {
            sense     = _dev.ReadDma(out buffer, out errorLba48, block, (byte)count, _timeout, out duration);
            error     = !(!sense && (errorLba48.Status & 0x27) == 0 && errorLba48.Error == 0 && buffer.Length > 0);
            status    = errorLba48.Status;
            errorByte = errorLba48.Error;

            if(error) _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba48);
        }
        else if(_ataReadLba48)
        {
            sense     = _dev.Read(out buffer, out errorLba48, block, (byte)count, _timeout, out duration);
            error     = !(!sense && (errorLba48.Status & 0x27) == 0 && errorLba48.Error == 0 && buffer.Length > 0);
            status    = errorLba48.Status;
            errorByte = errorLba48.Error;

            if(error) _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba48);
        }
        else if(_ataReadDmaRetryLba)
        {
            sense = _dev.ReadDma(out buffer, out errorLba, true, (uint)block, (byte)count, _timeout, out duration);

            error     = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && buffer.Length > 0);
            status    = errorLba.Status;
            errorByte = errorLba.Error;

            if(error) _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba);
        }
        else if(_ataReadDmaLba)
        {
            sense = _dev.ReadDma(out buffer, out errorLba, false, (uint)block, (byte)count, _timeout, out duration);

            error     = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && buffer.Length > 0);
            status    = errorLba.Status;
            errorByte = errorLba.Error;

            if(error) _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba);
        }
        else if(_ataReadRetryLba)
        {
            sense     = _dev.Read(out buffer, out errorLba, true, (uint)block, (byte)count, _timeout, out duration);
            error     = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && buffer.Length > 0);
            status    = errorLba.Status;
            errorByte = errorLba.Error;

            if(error) _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba);
        }
        else if(_ataReadLba)
        {
            sense     = _dev.Read(out buffer, out errorLba, false, (uint)block, (byte)count, _timeout, out duration);
            error     = !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0 && buffer.Length > 0);
            status    = errorLba.Status;
            errorByte = errorLba.Error;

            if(error) _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba);
        }

        if(!error) return false;

        if((status & 0x04) == 0x04) recoveredError = true;

        AaruLogging.Debug(ATA_MODULE_NAME, Localization.Core.ATA_ERROR_0_STATUS_1, errorByte, status);

        return true;
    }

    bool AtaReadChs(out byte[] buffer, ushort cylinder, byte head, byte sector, out double duration,
                    out bool   recoveredError)
    {
        var                  error = true;
        bool                 sense;
        AtaErrorRegistersChs errorChs;
        byte                 status = 0, errorByte = 0;
        buffer         = null;
        duration       = 0;
        recoveredError = false;

        if(_ataReadDmaRetry)
        {
            sense = _dev.ReadDma(out buffer, out errorChs, true, cylinder, head, sector, 1, _timeout, out duration);

            error     = !(!sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && buffer.Length > 0);
            status    = errorChs.Status;
            errorByte = errorChs.Error;

            if(error) _errorLog?.WriteLine(cylinder, head, sector, _dev.Error, _dev.LastError, errorChs);
        }
        else if(_ataReadDma)
        {
            sense = _dev.ReadDma(out buffer, out errorChs, false, cylinder, head, sector, 1, _timeout, out duration);

            error     = !(!sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && buffer.Length > 0);
            status    = errorChs.Status;
            errorByte = errorChs.Error;

            if(error) _errorLog?.WriteLine(cylinder, head, sector, _dev.Error, _dev.LastError, errorChs);
        }
        else if(_ataReadRetry)
        {
            sense     = _dev.Read(out buffer, out errorChs, true, cylinder, head, sector, 1, _timeout, out duration);
            error     = !(!sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && buffer.Length > 0);
            status    = errorChs.Status;
            errorByte = errorChs.Error;

            if(error) _errorLog?.WriteLine(cylinder, head, sector, _dev.Error, _dev.LastError, errorChs);
        }
        else if(_ataRead)
        {
            sense     = _dev.Read(out buffer, out errorChs, false, cylinder, head, sector, 1, _timeout, out duration);
            error     = !(!sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0 && buffer.Length > 0);
            status    = errorChs.Status;
            errorByte = errorChs.Error;

            if(error) _errorLog?.WriteLine(cylinder, head, sector, _dev.Error, _dev.LastError, errorChs);
        }

        if(!error) return false;

        if((status & 0x04) == 0x04) recoveredError = true;

        AaruLogging.Debug(ATA_MODULE_NAME, Localization.Core.ATA_ERROR_0_STATUS_1, errorByte, status);

        return true;
    }

    bool AtaSeek(ulong block, out double duration)
    {
        bool sense = _dev.Seek(out AtaErrorRegistersLba28 errorLba, (uint)block, _timeout, out duration);

        if(!(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0))
            _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, errorLba);

        return !(!sense && (errorLba.Status & 0x27) == 0 && errorLba.Error == 0);
    }

    bool AtaSeekChs(ushort cylinder, byte head, byte sector, out double duration)
    {
        bool sense = _dev.Seek(out AtaErrorRegistersChs errorChs, cylinder, head, sector, _timeout, out duration);

        if(!(!sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0))
            _errorLog?.WriteLine(cylinder, head, sector, _dev.Error, _dev.LastError, errorChs);

        return !(!sense && (errorChs.Status & 0x27) == 0 && errorChs.Error == 0);
    }
}