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

using System;
using Aaru.CommonTypes.Structs.Devices.SCSI;
using Aaru.Console;
using Aaru.Decoders.SCSI;

namespace Aaru.Core.Devices
{
    internal sealed partial class Reader
    {
        // TODO: Raw reading
        bool _hldtstReadRaw;
        bool _plextorReadRaw;
        bool _read10;
        bool _read12;
        bool _read16;
        bool _read6;
        bool _readLong10;
        bool _readLong16;
        bool _readLongDvd;
        bool _seek10;
        bool _seek6;
        bool _syqReadLong10;
        bool _syqReadLong6;

        ulong ScsiGetBlocks() => ScsiGetBlockSize() ? 0 : Blocks;

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

            if(Blocks == 0)
                return true;

            byte[] senseBuf;
            int    tries      = 0;
            uint   lba        = 0;
            bool   mediumScan = false;

            if(_dev.ScsiType == PeripheralDeviceTypes.OpticalDevice)
            {
                mediumScan = !_dev.MediumScan(out _, true, false, false, false, false, lba, 1, (uint)Blocks,
                                              out uint foundLba, out _, _timeout, out _);

                if(mediumScan)
                    lba = foundLba;
            }

            var rnd = new Random();

            while(tries < 10)
            {
                _read6 = !_dev.Read6(out _, out senseBuf, lba, LogicalBlockSize, _timeout, out _);

                _read10 = !_dev.Read10(out _, out senseBuf, 0, false, false, false, false, lba, LogicalBlockSize, 0, 1,
                                       _timeout, out _);

                _read12 = !_dev.Read12(out _, out senseBuf, 0, false, false, false, false, lba, LogicalBlockSize, 0, 1,
                                       false, _timeout, out _);

                _read16 = !_dev.Read16(out _, out senseBuf, 0, false, false, false, lba, LogicalBlockSize, 0, 1, false,
                                       _timeout, out _);

                if(_read6  ||
                   _read10 ||
                   _read12 ||
                   _read16)
                {
                    break;
                }

                lba = (uint)rnd.Next(1, (int)Blocks);

                if(mediumScan)
                {
                    mediumScan = !_dev.MediumScan(out _, true, false, false, false, false, lba, 1, (uint)Blocks,
                                                  out uint foundLba, out _, _timeout, out _);

                    if(mediumScan)
                        lba = foundLba;
                }

                tries++;
            }

            if(!_read6  &&
               !_read10 &&
               !_read12 &&
               !_read16)
            {
                // Magneto-opticals may have empty LBA 0 but we know they work with READ(12)
                if(_dev.ScsiType == PeripheralDeviceTypes.OpticalDevice)
                {
                    ErrorMessage = "Cannot read medium, aborting scan...";

                    return true;
                }

                _read12 = true;
            }

            if(_read6   &&
               !_read10 &&
               !_read12 &&
               !_read16 &&
               Blocks > 0x001FFFFF + 1)
            {
                ErrorMessage =
                    $"Device only supports SCSI READ (6) but has more than {0x001FFFFF + 1} blocks ({Blocks} blocks total)";

                return true;
            }

            if(Blocks > 0x001FFFFF + 1)
                _read6 = false;

            if(_read10)
                _read12 = false;

            if(!_read16 &&
               Blocks > 0xFFFFFFFF + (long)1)
            {
                ErrorMessage =
                    $"Device only supports SCSI READ (10) but has more than {0xFFFFFFFF + (long)1} blocks ({Blocks} blocks total)";

                return true;
            }

            if(Blocks > 0xFFFFFFFF + (long)1)
            {
                _read10 = false;
                _read12 = false;
            }

            _seek6 = !_dev.Seek6(out senseBuf, lba, _timeout, out _);

            _seek10 = !_dev.Seek10(out senseBuf, lba, _timeout, out _);

            if(CanReadRaw)
            {
                bool testSense;
                CanReadRaw = false;

                if(_dev.ScsiType != PeripheralDeviceTypes.MultiMediaDevice)
                {
                    /*testSense = dev.ReadLong16(out readBuffer, out senseBuf, false, 0, 0xFFFF, timeout, out duration);
                    if (testSense && !dev.Error)
                    {
                        decSense = Decoders.SCSI.Sense.DecodeFixed(senseBuf);
                        if (decSense.HasValue)
                        {
                            if (decSense.Value.SenseKey == Aaru.Decoders.SCSI.SenseKeys.IllegalRequest &&
                                decSense.Value.ASC == 0x24 && decSense.Value.ASCQ == 0x00)
                            {
                                readRaw = true;
                                if (decSense.Value.InformationValid && decSense.Value.ILI)
                                {
                                    longBlockSize = 0xFFFF - (decSense.Value.Information & 0xFFFF);
                                    readLong16 = !dev.ReadLong16(out readBuffer, out senseBuf, false, 0, longBlockSize, timeout, out duration);
                                }
                            }
                        }
                    }*/

                    testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, 0xFFFF, _timeout, out _);
                    DecodedSense? decSense;

                    if(testSense && !_dev.Error)
                    {
                        decSense = Sense.Decode(senseBuf);

                        if(decSense?.SenseKey  == SenseKeys.IllegalRequest &&
                           decSense.Value.ASC  == 0x24                     &&
                           decSense.Value.ASCQ == 0x00)
                        {
                            CanReadRaw = true;

                            bool valid       = decSense?.Fixed?.InformationValid == true;
                            bool ili         = decSense?.Fixed?.ILI              == true;
                            uint information = decSense?.Fixed?.Information ?? 0;

                            if(decSense.Value.Descriptor.HasValue &&
                               decSense.Value.Descriptor.Value.Descriptors.TryGetValue(0, out byte[] desc00))
                            {
                                valid       = true;
                                ili         = true;
                                information = (uint)Sense.DecodeDescriptor00(desc00);

                                if(decSense.Value.Descriptor.Value.Descriptors.TryGetValue(4, out byte[] desc04))
                                    Sense.DecodeDescriptor04(desc04, out _, out _, out ili);
                            }

                            if(valid && ili)
                            {
                                LongBlockSize = 0xFFFF - (information & 0xFFFF);

                                _readLong10 = !_dev.ReadLong10(out _, out senseBuf, false, false, 0,
                                                               (ushort)LongBlockSize, _timeout, out _);
                            }
                        }
                    }

                    if(CanReadRaw && LongBlockSize == LogicalBlockSize)
                        switch(LogicalBlockSize)
                        {
                            case 512:
                            {
                                foreach(ushort testSize in new ushort[]
                                {
                                    // Long sector sizes for floppies
                                    514,

                                    // Long sector sizes for SuperDisk
                                    536, 558,

                                    // Long sector sizes for 512-byte magneto-opticals
                                    600, 610, 630
                                })
                                {
                                    testSense = _dev.ReadLong16(out _, out senseBuf, false, 0, testSize, _timeout,
                                                                out _);

                                    if(!testSense &&
                                       !_dev.Error)
                                    {
                                        _readLong16   = true;
                                        LongBlockSize = testSize;
                                        CanReadRaw    = true;

                                        break;
                                    }

                                    testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, testSize,
                                                                _timeout, out _);

                                    if(testSense || _dev.Error)
                                        continue;

                                    _readLong10   = true;
                                    LongBlockSize = testSize;
                                    CanReadRaw    = true;

                                    break;
                                }

                                break;
                            }
                            case 1024:
                            {
                                foreach(ushort testSize in new ushort[]
                                {
                                    // Long sector sizes for floppies
                                    1026,

                                    // Long sector sizes for 1024-byte magneto-opticals
                                    1200
                                })
                                {
                                    testSense = _dev.ReadLong16(out _, out senseBuf, false, 0, testSize, _timeout,
                                                                out _);

                                    if(!testSense &&
                                       !_dev.Error)
                                    {
                                        _readLong16   = true;
                                        LongBlockSize = testSize;
                                        CanReadRaw    = true;

                                        break;
                                    }

                                    testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, testSize,
                                                                _timeout, out _);

                                    if(testSense || _dev.Error)
                                        continue;

                                    _readLong10   = true;
                                    LongBlockSize = testSize;
                                    CanReadRaw    = true;

                                    break;
                                }

                                break;
                            }
                            case 2048:
                            {
                                testSense = _dev.ReadLong16(out _, out senseBuf, false, 0, 2380, _timeout, out _);

                                if(!testSense &&
                                   !_dev.Error)
                                {
                                    _readLong16   = true;
                                    LongBlockSize = 2380;
                                    CanReadRaw    = true;
                                }
                                else
                                {
                                    testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, 2380, _timeout,
                                                                out _);

                                    if(!testSense &&
                                       !_dev.Error)
                                    {
                                        _readLong10   = true;
                                        LongBlockSize = 2380;
                                        CanReadRaw    = true;
                                    }
                                }

                                break;
                            }
                            case 4096:
                            {
                                testSense = _dev.ReadLong16(out _, out senseBuf, false, 0, 4760, _timeout, out _);

                                if(!testSense &&
                                   !_dev.Error)
                                {
                                    _readLong16   = true;
                                    LongBlockSize = 4760;
                                    CanReadRaw    = true;
                                }
                                else
                                {
                                    testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, 4760, _timeout,
                                                                out _);

                                    if(!testSense &&
                                       !_dev.Error)
                                    {
                                        _readLong10   = true;
                                        LongBlockSize = 4760;
                                        CanReadRaw    = true;
                                    }
                                }

                                break;
                            }
                            case 8192:
                            {
                                testSense = _dev.ReadLong16(out _, out senseBuf, false, 0, 9424, _timeout, out _);

                                if(!testSense &&
                                   !_dev.Error)
                                {
                                    _readLong16   = true;
                                    LongBlockSize = 9424;
                                    CanReadRaw    = true;
                                }
                                else
                                {
                                    testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, 9424, _timeout,
                                                                out _);

                                    if(!testSense &&
                                       !_dev.Error)
                                    {
                                        _readLong10   = true;
                                        LongBlockSize = 9424;
                                        CanReadRaw    = true;
                                    }
                                }

                                break;
                            }
                        }

                    if(!CanReadRaw &&
                       _dev.Manufacturer == "SYQUEST")
                    {
                        testSense = _dev.SyQuestReadLong10(out _, out senseBuf, 0, 0xFFFF, _timeout, out _);

                        if(testSense)
                        {
                            decSense = Sense.Decode(senseBuf);

                            if(decSense.HasValue)
                                if(decSense.Value.SenseKey == SenseKeys.IllegalRequest &&
                                   decSense.Value.ASC      == 0x24                     &&
                                   decSense.Value.ASCQ     == 0x00)
                                {
                                    CanReadRaw = true;

                                    bool valid       = decSense?.Fixed?.InformationValid == true;
                                    bool ili         = decSense?.Fixed?.ILI              == true;
                                    uint information = decSense?.Fixed?.Information ?? 0;

                                    if(decSense.Value.Descriptor.HasValue &&
                                       decSense.Value.Descriptor.Value.Descriptors.TryGetValue(0, out byte[] desc00))
                                    {
                                        valid       = true;
                                        ili         = true;
                                        information = (uint)Sense.DecodeDescriptor00(desc00);

                                        if(decSense.Value.Descriptor.Value.Descriptors.
                                                    TryGetValue(4, out byte[] desc04))
                                            Sense.DecodeDescriptor04(desc04, out _, out _, out ili);
                                    }

                                    if(valid && ili)
                                    {
                                        LongBlockSize = 0xFFFF - (information & 0xFFFF);

                                        _syqReadLong10 =
                                            !_dev.SyQuestReadLong10(out _, out senseBuf, 0, LongBlockSize, _timeout,
                                                                    out _);
                                    }
                                }
                                else
                                {
                                    testSense = _dev.SyQuestReadLong6(out _, out senseBuf, 0, 0xFFFF, _timeout, out _);

                                    if(testSense)
                                    {
                                        decSense = Sense.Decode(senseBuf);

                                        if(decSense?.SenseKey  == SenseKeys.IllegalRequest &&
                                           decSense.Value.ASC  == 0x24                     &&
                                           decSense.Value.ASCQ == 0x00)
                                        {
                                            CanReadRaw = true;

                                            bool valid       = decSense?.Fixed?.InformationValid == true;
                                            bool ili         = decSense?.Fixed?.ILI              == true;
                                            uint information = decSense?.Fixed?.Information ?? 0;

                                            if(decSense.Value.Descriptor.HasValue &&
                                               decSense.Value.Descriptor.Value.Descriptors.
                                                        TryGetValue(0, out byte[] desc00))
                                            {
                                                valid       = true;
                                                ili         = true;
                                                information = (uint)Sense.DecodeDescriptor00(desc00);

                                                if(decSense.Value.Descriptor.Value.Descriptors.
                                                            TryGetValue(4, out byte[] desc04))
                                                    Sense.DecodeDescriptor04(desc04, out _, out _, out ili);
                                            }

                                            if(valid && ili)
                                            {
                                                LongBlockSize = 0xFFFF - (information & 0xFFFF);

                                                _syqReadLong6 =
                                                    !_dev.SyQuestReadLong6(out _, out senseBuf, 0, LongBlockSize,
                                                                           _timeout, out _);
                                            }
                                        }
                                    }
                                }
                        }

                        if(!CanReadRaw &&
                           LogicalBlockSize == 256)
                        {
                            testSense = _dev.SyQuestReadLong6(out _, out senseBuf, 0, 262, _timeout, out _);

                            if(!testSense &&
                               !_dev.Error)
                            {
                                _syqReadLong6 = true;
                                LongBlockSize = 262;
                                CanReadRaw    = true;
                            }
                        }
                    }
                }
                else
                {
                    switch(_dev.Manufacturer)
                    {
                        case "HL-DT-ST":
                            _hldtstReadRaw = !_dev.HlDtStReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _);

                            break;
                        case "PLEXTOR":
                            _plextorReadRaw = !_dev.PlextorReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _);

                            break;
                    }

                    if(_hldtstReadRaw || _plextorReadRaw)
                    {
                        CanReadRaw    = true;
                        LongBlockSize = 2064;
                    }

                    // READ LONG (10) for some DVD drives
                    if(!CanReadRaw &&
                       _dev.Manufacturer == "MATSHITA")
                    {
                        testSense = _dev.ReadLong10(out _, out senseBuf, false, false, 0, 37856, _timeout, out _);

                        if(!testSense &&
                           !_dev.Error)
                        {
                            _readLongDvd  = true;
                            LongBlockSize = 37856;
                            CanReadRaw    = true;
                        }
                    }
                }
            }

            if(CanReadRaw)
            {
                if(_readLong16)
                    AaruConsole.WriteLine("Using SCSI READ LONG (16) command.");
                else if(_readLong10 || _readLongDvd)
                    AaruConsole.WriteLine("Using SCSI READ LONG (10) command.");
                else if(_syqReadLong10)
                    AaruConsole.WriteLine("Using SyQuest READ LONG (10) command.");
                else if(_syqReadLong6)
                    AaruConsole.WriteLine("Using SyQuest READ LONG (6) command.");
                else if(_hldtstReadRaw)
                    AaruConsole.WriteLine("Using HL-DT-ST raw DVD reading.");
                else if(_plextorReadRaw)
                    AaruConsole.WriteLine("Using Plextor raw DVD reading.");
            }
            else if(_read6)
                AaruConsole.WriteLine("Using SCSI READ (6) command.");
            else if(_read10)
                AaruConsole.WriteLine("Using SCSI READ (10) command.");
            else if(_read12)
                AaruConsole.WriteLine("Using SCSI READ (12) command.");
            else if(_read16)
                AaruConsole.WriteLine("Using SCSI READ (16) command.");

            return false;
        }

        bool ScsiGetBlockSize()
        {
            Blocks = 0;

            bool sense = _dev.ReadCapacity(out byte[] cmdBuf, out byte[] senseBuf, _timeout, out _);

            if(!sense)
            {
                Blocks = (ulong)((cmdBuf[0] << 24) + (cmdBuf[1] << 16) + (cmdBuf[2] << 8) + cmdBuf[3]) & 0xFFFFFFFF;
                LogicalBlockSize = (uint)((cmdBuf[4] << 24) + (cmdBuf[5] << 16) + (cmdBuf[6] << 8) + cmdBuf[7]);
            }

            if(sense || Blocks == 0xFFFFFFFF)
            {
                sense = _dev.ReadCapacity16(out cmdBuf, out senseBuf, _timeout, out _);

                if(sense && Blocks == 0)
                    if(_dev.ScsiType != PeripheralDeviceTypes.MultiMediaDevice)
                    {
                        ErrorMessage = "Unable to get media capacity\n" + $"{Sense.PrettifySense(senseBuf)}";

                        return true;
                    }

                if(!sense)
                {
                    byte[] temp = new byte[8];

                    Array.Copy(cmdBuf, 0, temp, 0, 8);
                    Array.Reverse(temp);
                    Blocks           = BitConverter.ToUInt64(temp, 0);
                    LogicalBlockSize = (uint)((cmdBuf[8] << 24) + (cmdBuf[9] << 16) + (cmdBuf[10] << 8) + cmdBuf[11]);
                }
            }

            PhysicalBlockSize = LogicalBlockSize;
            LongBlockSize     = LogicalBlockSize;

            return false;
        }

        bool ScsiGetBlocksToRead(uint startWithBlocks)
        {
            BlocksToRead = startWithBlocks;

            while(true)
            {
                if(_read6)
                {
                    _dev.Read6(out _, out _, 0, LogicalBlockSize, (byte)BlocksToRead, _timeout, out _);

                    if(_dev.Error)
                        BlocksToRead /= 2;
                }
                else if(_read10)
                {
                    _dev.Read10(out _, out _, 0, false, true, false, false, 0, LogicalBlockSize, 0,
                                (ushort)BlocksToRead, _timeout, out _);

                    if(_dev.Error)
                        BlocksToRead /= 2;
                }
                else if(_read12)
                {
                    _dev.Read12(out _, out _, 0, false, false, false, false, 0, LogicalBlockSize, 0, BlocksToRead,
                                false, _timeout, out _);

                    if(_dev.Error)
                        BlocksToRead /= 2;
                }
                else if(_read16)
                {
                    _dev.Read16(out _, out _, 0, false, true, false, 0, LogicalBlockSize, 0, BlocksToRead, false,
                                _timeout, out _);

                    if(_dev.Error)
                        BlocksToRead /= 2;
                }

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

            if(!_dev.Error)
                return false;

            // Magneto-opticals may have LBA 0 empty, we can hard code the value to a safe one
            if(_dev.ScsiType == PeripheralDeviceTypes.OpticalDevice)
            {
                BlocksToRead = 16;

                return false;
            }

            BlocksToRead = 1;
            ErrorMessage = $"Device error {_dev.LastError} trying to guess ideal transfer length.";

            return true;
        }

        bool ScsiReadBlocks(out byte[] buffer, ulong block, uint count, out double duration, out bool recoveredError,
                            out bool blankCheck)
        {
            bool   sense;
            byte[] senseBuf;
            buffer         = null;
            duration       = 0;
            recoveredError = false;
            blankCheck     = false;

            if(CanReadRaw)
                if(_readLong16)
                    sense = _dev.ReadLong16(out buffer, out senseBuf, false, block, LongBlockSize, _timeout,
                                            out duration);
                else if(_readLong10)
                    sense = _dev.ReadLong10(out buffer, out senseBuf, false, false, (uint)block, (ushort)LongBlockSize,
                                            _timeout, out duration);
                else if(_syqReadLong10)
                    sense = _dev.SyQuestReadLong10(out buffer, out senseBuf, (uint)block, LongBlockSize, _timeout,
                                                   out duration);
                else if(_syqReadLong6)
                    sense = _dev.SyQuestReadLong6(out buffer, out senseBuf, (uint)block, LongBlockSize, _timeout,
                                                  out duration);
                else if(_hldtstReadRaw)
                    sense = _dev.HlDtStReadRawDvd(out buffer, out senseBuf, (uint)block, LongBlockSize, _timeout,
                                                  out duration);
                else if(_plextorReadRaw)
                    sense = _dev.PlextorReadRawDvd(out buffer, out senseBuf, (uint)block, LongBlockSize, _timeout,
                                                   out duration);
                else
                    return true;
            else
            {
                if(_read6)
                    sense = _dev.Read6(out buffer, out senseBuf, (uint)block, LogicalBlockSize, (byte)count, _timeout,
                                       out duration);
                else if(_read10)
                    sense = _dev.Read10(out buffer, out senseBuf, 0, false, false, false, false, (uint)block,
                                        LogicalBlockSize, 0, (ushort)count, _timeout, out duration);
                else if(_read12)
                    sense = _dev.Read12(out buffer, out senseBuf, 0, false, false, false, false, (uint)block,
                                        LogicalBlockSize, 0, count, false, _timeout, out duration);
                else if(_read16)
                    sense = _dev.Read16(out buffer, out senseBuf, 0, false, false, false, block, LogicalBlockSize, 0,
                                        count, false, _timeout, out duration);
                else
                    return true;
            }

            if(sense || _dev.Error)
                _errorLog?.WriteLine(block, _dev.Error, _dev.LastError, senseBuf);

            if(!sense &&
               !_dev.Error)
                return false;

            recoveredError = Sense.Decode(senseBuf)?.SenseKey == SenseKeys.RecoveredError;

            blankCheck = Sense.Decode(senseBuf)?.SenseKey == SenseKeys.BlankCheck;

            AaruConsole.DebugWriteLine("SCSI Reader", "READ error:\n{0}", Sense.PrettifySense(senseBuf));

            return sense;
        }

        bool ScsiSeek(ulong block, out double duration)
        {
            bool sense = true;
            duration = 0;

            if(_seek6)
                sense = _dev.Seek6(out _, (uint)block, _timeout, out duration);
            else if(_seek10)
                sense = _dev.Seek10(out _, (uint)block, _timeout, out duration);

            return sense;
        }
    }
}