// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Constructor.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Direct device access.
//
// --[ Description ] ----------------------------------------------------------
//
//     Prepares a device for direct access.
//
// --[ License ] --------------------------------------------------------------
//
//     This library is free software; you can redistribute it and/or modify
//     it under the terms of the GNU Lesser General Public License as
//     published by the Free Software Foundation; either version 2.1 of the
//     License, or (at your option) any later version.
//
//     This library 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
//     Lesser General Public License for more details.
//
//     You should have received a copy of the GNU Lesser General Public
//     License along with this library; if not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs.Devices.ATA;
using Aaru.CommonTypes.Structs.Devices.SCSI;
using Aaru.Decoders.SCSI;
using Aaru.Decoders.SCSI.MMC;
using Aaru.Helpers;
using Sentry;
using Inquiry = Aaru.CommonTypes.Structs.Devices.SCSI.Inquiry;

namespace Aaru.Devices;

/// <summary>Implements a device or media containing drive</summary>
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global")]
public partial class Device : IDisposable
{
    // Pointer to send CDB to device
    protected unsafe void* CdbPtr;
    protected unsafe void* SensePtr;

    protected Device()
    {
        unsafe
        {
            CdbPtr   = NativeMemory.AlignedAlloc(16, 64);
            SensePtr = NativeMemory.AlignedAlloc(64, 64);
            NativeMemory.Clear(CdbPtr,   16);
            NativeMemory.Clear(SensePtr, 64);
        }
    }

    // Span to make pointer usable as data. Fixed size CDB is maximum 16 bytes. Variable size CDB is another problem.
    public unsafe Span<byte> CdbBuffer   => new(CdbPtr, 16);
    public unsafe Span<byte> SenseBuffer => new(SensePtr, 64);

#region IDisposable Members

    /// <inheritdoc />
    public unsafe void Dispose()
    {
        void* p = CdbPtr;
        CdbPtr = null;
        if(p != null) NativeMemory.AlignedFree(p);
        GC.SuppressFinalize(this);
    }

#endregion

    /// <summary>Opens the device for sending direct commands</summary>
    /// <param name="devicePath">Device path</param>
    /// <param name="errno">Sets the error if a device cannot be opened</param>
    /// <returns>Device</returns>
    public static Device Create(string devicePath, out ErrorNumber errno)
    {
        Device dev = null;
        Uri    aaruUri;
        errno = ErrorNumber.NoError;

        try
        {
            aaruUri = new Uri(devicePath);
        }
        catch(Exception ex)
        {
            SentrySdk.CaptureException(ex);

            aaruUri = null;
        }

        if(aaruUri?.Scheme is "dic" or "aaru")
            dev = Remote.Device.Create(aaruUri, out errno);
        else if(OperatingSystem.IsLinux())
            dev = Linux.Device.Create(devicePath, out errno);
        else if(OperatingSystem.IsWindows())
            dev = Windows.Device.Create(devicePath, out errno);
        else
            errno = ErrorNumber.NotSupported;

        if(dev is null) return null;

        if(dev.Type is DeviceType.SCSI or DeviceType.ATAPI)
        {
            dev.ScsiInquiry(out byte[] inqBuf, out _);

            Inquiry? inquiry = Inquiry.Decode(inqBuf);

            dev.Type = DeviceType.SCSI;
            bool serialSense = dev.ScsiInquiry(out inqBuf, out _, 0x80);

            if(!serialSense) dev.Serial = EVPD.DecodePage80(inqBuf);

            if(inquiry.HasValue)
            {
                string tmp = StringHandlers.CToString(inquiry.Value.ProductRevisionLevel);

                if(tmp != null) dev.FirmwareRevision = tmp.Trim();

                tmp = StringHandlers.CToString(inquiry.Value.ProductIdentification);

                if(tmp != null) dev.Model = tmp.Trim();

                tmp = StringHandlers.CToString(inquiry.Value.VendorIdentification);

                if(tmp != null) dev.Manufacturer = tmp.Trim();

                dev.IsRemovable = inquiry.Value.RMB;

                dev.ScsiType = (PeripheralDeviceTypes)inquiry.Value.PeripheralDeviceType;
            }

            bool atapiSense = dev.AtapiIdentify(out byte[] ataBuf, out _);

            if(!atapiSense)
            {
                dev.Type = DeviceType.ATAPI;
                Identify.IdentifyDevice? ataId = Identify.Decode(ataBuf);

                if(ataId.HasValue) dev.Serial = ataId.Value.SerialNumber;
            }

            dev.LastError = 0;
            dev.Error     = false;
        }

        if(dev.Type != DeviceType.SCSI && dev.Type != DeviceType.ATAPI && !(dev.IsUsb || dev.IsFireWire) ||
           dev.Manufacturer == "ATA")
        {
            bool ataSense = dev.AtaIdentify(out byte[] ataBuf, out _);

            if(!ataSense)
            {
                dev.Type = DeviceType.ATA;
                Identify.IdentifyDevice? ataid = Identify.Decode(ataBuf);

                if(ataid.HasValue)
                {
                    string[] separated = ataid.Value.Model.Split(' ');

                    if(separated.Length == 1)
                        dev.Model = separated[0];
                    else
                    {
                        dev.Manufacturer = separated[0];
                        dev.Model        = separated[^1];
                    }

                    dev.FirmwareRevision = ataid.Value.FirmwareRevision;
                    dev.Serial           = ataid.Value.SerialNumber;

                    dev.ScsiType = PeripheralDeviceTypes.DirectAccess;

                    if((ushort)ataid.Value.GeneralConfiguration != 0x848A)
                    {
                        dev.IsRemovable |=
                            (ataid.Value.GeneralConfiguration & Identify.GeneralConfigurationBit.Removable) ==
                            Identify.GeneralConfigurationBit.Removable;
                    }
                    else
                        dev.IsCompactFlash = true;
                }
            }
        }

        if(dev.Type == DeviceType.Unknown)
        {
            dev.Manufacturer     = null;
            dev.Model            = null;
            dev.FirmwareRevision = null;
            dev.Serial           = null;
        }

        if(dev.IsUsb)
        {
            if(string.IsNullOrEmpty(dev.Manufacturer)) dev.Manufacturer = dev.UsbManufacturerString;

            if(string.IsNullOrEmpty(dev.Model)) dev.Model = dev.UsbProductString;

            if(string.IsNullOrEmpty(dev.Serial))
                dev.Serial = dev.UsbSerialString;
            else
                foreach(char c in dev.Serial.Where(static c => !char.IsControl(c)))
                    dev.Serial = $"{dev.Serial}{c:X2}";
        }

        if(dev.IsFireWire)
        {
            if(string.IsNullOrEmpty(dev.Manufacturer)) dev.Manufacturer = dev.FireWireVendorName;

            if(string.IsNullOrEmpty(dev.Model)) dev.Model = dev.FireWireModelName;

            if(string.IsNullOrEmpty(dev.Serial))
                dev.Serial = $"{dev.FirewireGuid:X16}";
            else
                foreach(char c in dev.Serial.Where(static c => !char.IsControl(c)))
                    dev.Serial = $"{dev.Serial}{c:X2}";
        }

        // Some optical drives are not getting the correct serial, and IDENTIFY PACKET DEVICE is blocked without
        // administrator privileges
        if(dev.ScsiType != PeripheralDeviceTypes.MultiMediaDevice) return dev;

        bool featureSense = dev.GetConfiguration(out byte[] featureBuffer,
                                                 out _,
                                                 0x0108,
                                                 MmcGetConfigurationRt.Single,
                                                 dev.Timeout,
                                                 out _);

        if(featureSense) return dev;

        Features.SeparatedFeatures features = Features.Separate(featureBuffer);

        if(features.Descriptors?.Length != 1 || features.Descriptors[0].Code != 0x0108) return dev;

        Feature_0108? serialFeature = Features.Decode_0108(features.Descriptors[0].Data);

        if(serialFeature is null) return dev;

        dev.Serial = serialFeature.Value.Serial;

        return dev;
    }
}