// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : Compression.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Apple Hierarchical File System Plus plugin.
//
// --[ 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.IO;
using System.IO.Compression;
using Aaru.CommonTypes.Enums;
using Aaru.Compression;
using Aaru.Helpers;
using Aaru.Logging;

namespace Aaru.Filesystems;

// Information from Apple TechNote 1150: https://developer.apple.com/legacy/library/technotes/tn/tn1150.html
/// <summary>Implements HFS+ transparent decompression support</summary>
public sealed partial class AppleHFSPlus
{
    /// <summary>Checks if a file is compressed</summary>
    /// <param name="fileEntry">File entry to check</param>
    /// <param name="compressionType">Output compression type if compressed</param>
    /// <param name="uncompressedSize">Output uncompressed size if compressed</param>
    /// <returns>True if file is compressed</returns>
    private bool IsFileCompressed(FileEntry fileEntry, out HFSCompressionType compressionType,
                                  out ulong uncompressedSize)
    {
        compressionType  = 0;
        uncompressedSize = 0;

        // Check if the file has the decmpfs extended attribute
        ErrorNumber err = GetAttributeFromBTree(fileEntry.CNID, DECMPFS_XATTR_NAME, out byte[] decmpfsData);

        if(err != ErrorNumber.NoError || decmpfsData == null || decmpfsData.Length < 16) return false;

        // Parse the decmpfs header
        var magic = BigEndianBitConverter.ToUInt32(decmpfsData, 0);

        if(magic != DECMPFS_MAGIC) return false;

        compressionType  = (HFSCompressionType)BigEndianBitConverter.ToUInt32(decmpfsData, 4);
        uncompressedSize = BigEndianBitConverter.ToUInt64(decmpfsData, 8);

        return true;
    }

    /// <summary>Decompresses a compressed file</summary>
    /// <param name="fileEntry">File entry to decompress</param>
    /// <param name="buf">Output buffer with decompressed data</param>
    /// <returns>Error number</returns>
    private ErrorNumber DecompressFile(FileEntry fileEntry, out byte[] buf)
    {
        buf = null;

        // Get compression information
        if(!IsFileCompressed(fileEntry, out HFSCompressionType compressionType, out ulong uncompressedSize))
            return ErrorNumber.InvalidArgument;

        AaruLogging.Debug(MODULE_NAME,
                          "DecompressFile: Decompressing file CNID={0}, type={1}, uncompressed size={2}",
                          fileEntry.CNID,
                          compressionType,
                          uncompressedSize);

        // Get the decmpfs xattr data
        ErrorNumber err = GetAttributeFromBTree(fileEntry.CNID, DECMPFS_XATTR_NAME, out byte[] decmpfsData);

        if(err != ErrorNumber.NoError || decmpfsData == null) return err;

        switch(compressionType)
        {
            case HFSCompressionType.Uncompressed:
                // Type 1: Uncompressed data stored inline in xattr
                return DecompressType1(decmpfsData, uncompressedSize, out buf);

            case HFSCompressionType.ZlibInline:
                // Type 3: ZLIB compressed data stored inline in xattr
                return DecompressType3(decmpfsData, uncompressedSize, out buf);

            case HFSCompressionType.LzvnInline:
                // Type 4: LZVN compressed data stored inline in xattr
                return DecompressType4(decmpfsData, uncompressedSize, out buf);

            case HFSCompressionType.LzvnResourceFork:
            case HFSCompressionType.ZlibResourceFork:
            case HFSCompressionType.Lzvn2ResourceFork:
            case HFSCompressionType.LzfseResourceFork:
                // Types 7-10: Compressed data stored in resource fork
                return DecompressResourceFork(fileEntry, compressionType, uncompressedSize, out buf);

            default:
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressFile: Unknown compression type {0} for CNID {1}",
                                  compressionType,
                                  fileEntry.CNID);

                return ErrorNumber.NotSupported;
        }
    }

    /// <summary>Decompresses type 1 (uncompressed inline)</summary>
    private ErrorNumber DecompressType1(byte[] decmpfsData, ulong uncompressedSize, out byte[] buf)
    {
        buf = null;

        // Type 1: Uncompressed data follows the 16-byte header
        if(decmpfsData.Length < 16) return ErrorNumber.InvalidArgument;

        int dataSize = decmpfsData.Length - 16;

        if(dataSize != (int)uncompressedSize)
        {
            AaruLogging.Debug(MODULE_NAME,
                              "DecompressType1: Size mismatch - xattr data size {0} != uncompressed size {1}",
                              dataSize,
                              uncompressedSize);
        }

        buf = new byte[uncompressedSize];
        Array.Copy(decmpfsData, 16, buf, 0, Math.Min(dataSize, (int)uncompressedSize));

        return ErrorNumber.NoError;
    }

    /// <summary>Decompresses type 3 (ZLIB inline)</summary>
    private ErrorNumber DecompressType3(byte[] decmpfsData, ulong uncompressedSize, out byte[] buf)
    {
        buf = null;

        // Type 3: ZLIB compressed data follows the 16-byte header
        if(decmpfsData.Length < 16) return ErrorNumber.InvalidArgument;

        int compressedSize = decmpfsData.Length - 16;

        try
        {
            buf = new byte[uncompressedSize];

            using var compressedStream = new MemoryStream(decmpfsData, 16, compressedSize);
            using var zlibStream       = new DeflateStream(compressedStream, CompressionMode.Decompress);

            var totalRead = 0;

            while(totalRead < (int)uncompressedSize)
            {
                int read = zlibStream.Read(buf, totalRead, (int)uncompressedSize - totalRead);

                if(read == 0) break;

                totalRead += read;
            }

            if(totalRead != (int)uncompressedSize)
            {
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressType3: Decompressed size {0} != expected size {1}",
                                  totalRead,
                                  uncompressedSize);
            }

            return ErrorNumber.NoError;
        }
        catch(Exception ex)
        {
            AaruLogging.Debug(MODULE_NAME, "DecompressType3: Exception - {0}", ex.Message);

            return ErrorNumber.InvalidArgument;
        }
    }

    /// <summary>Decompresses type 4 (LZVN inline)</summary>
    private static ErrorNumber DecompressType4(byte[] decmpfsData, ulong uncompressedSize, out byte[] buf)
    {
        buf = null;

        // Type 4: LZVN compressed data follows the 16-byte header
        if(decmpfsData.Length < 16) return ErrorNumber.InvalidArgument;

        int compressedSize = decmpfsData.Length - 16;

        try
        {
            var compressedData = new byte[compressedSize];
            Array.Copy(decmpfsData, 16, compressedData, 0, compressedSize);

            buf = new byte[uncompressedSize];
            int decompressedSize = LZVN.DecodeBuffer(compressedData, buf);

            if(decompressedSize <= 0)
            {
                AaruLogging.Debug(MODULE_NAME, "DecompressType4: LZVN decompression failed");

                return ErrorNumber.InvalidArgument;
            }

            if(decompressedSize != (int)uncompressedSize)
            {
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressType4: Decompressed size {0} != expected size {1}",
                                  decompressedSize,
                                  uncompressedSize);
            }

            return ErrorNumber.NoError;
        }
        catch(Exception ex)
        {
            AaruLogging.Debug(MODULE_NAME, "DecompressType4: Exception - {0}", ex.Message);

            return ErrorNumber.InvalidArgument;
        }
    }

    /// <summary>Decompresses types 7-10 (resource fork based)</summary>
    private ErrorNumber DecompressResourceFork(FileEntry fileEntry,        HFSCompressionType compressionType,
                                               ulong     uncompressedSize, out byte[]         buf)
    {
        buf = null;

        // Read the resource fork
        ErrorNumber err = ReadResourceFork(fileEntry, out byte[] resourceForkData);

        if(err                     != ErrorNumber.NoError ||
           resourceForkData        == null                ||
           resourceForkData.Length < DECMPFS_RESOURCE_HEADER_SIZE)
        {
            AaruLogging.Debug(MODULE_NAME,
                              "DecompressResourceFork: Failed to read resource fork for CNID {0}: {1}",
                              fileEntry.CNID,
                              err);

            return err;
        }

        // Parse resource fork header
        var headerSize = BigEndianBitConverter.ToUInt32(resourceForkData, 0);
        var totalSize  = BigEndianBitConverter.ToUInt32(resourceForkData, 4);
        var dataSize   = BigEndianBitConverter.ToUInt32(resourceForkData, 8);

        if(headerSize != DECMPFS_RESOURCE_HEADER_SIZE)
        {
            AaruLogging.Debug(MODULE_NAME,
                              "DecompressResourceFork: Invalid header size {0} for CNID {1}",
                              headerSize,
                              fileEntry.CNID);

            return ErrorNumber.InvalidArgument;
        }

        // Compressed data starts after the header
        var compressedDataOffset = (int)headerSize;

        if(compressedDataOffset + dataSize > resourceForkData.Length)
        {
            AaruLogging.Debug(MODULE_NAME,
                              "DecompressResourceFork: Compressed data size {0} exceeds resource fork size {1} for CNID {2}",
                              dataSize,
                              resourceForkData.Length,
                              fileEntry.CNID);

            return ErrorNumber.InvalidArgument;
        }

        // Decompress based on type
        switch(compressionType)
        {
            case HFSCompressionType.ZlibResourceFork:
                return DecompressZlibResourceFork(resourceForkData,
                                                  compressedDataOffset,
                                                  (int)dataSize,
                                                  uncompressedSize,
                                                  out buf);

            case HFSCompressionType.LzfseResourceFork:
                return DecompressLzfseResourceFork(resourceForkData,
                                                   compressedDataOffset,
                                                   (int)dataSize,
                                                   uncompressedSize,
                                                   out buf);

            case HFSCompressionType.LzvnResourceFork:
            case HFSCompressionType.Lzvn2ResourceFork:
                return DecompressLzvnResourceFork(resourceForkData,
                                                  compressedDataOffset,
                                                  (int)dataSize,
                                                  uncompressedSize,
                                                  out buf);

            default:
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressResourceFork: Unsupported compression type {0} for CNID {1}",
                                  compressionType,
                                  fileEntry.CNID);

                return ErrorNumber.NotSupported;
        }
    }

    /// <summary>Decompresses ZLIB compressed data from resource fork</summary>
    private ErrorNumber DecompressZlibResourceFork(byte[] resourceForkData, int        offset, int compressedSize,
                                                   ulong  uncompressedSize, out byte[] buf)
    {
        buf = null;

        try
        {
            buf = new byte[uncompressedSize];

            using var compressedStream = new MemoryStream(resourceForkData, offset, compressedSize);
            using var zlibStream       = new DeflateStream(compressedStream, CompressionMode.Decompress);

            var totalRead = 0;

            while(totalRead < (int)uncompressedSize)
            {
                int read = zlibStream.Read(buf, totalRead, (int)uncompressedSize - totalRead);

                if(read == 0) break;

                totalRead += read;
            }

            if(totalRead != (int)uncompressedSize)
            {
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressZlibResourceFork: Decompressed size {0} != expected size {1}",
                                  totalRead,
                                  uncompressedSize);
            }

            return ErrorNumber.NoError;
        }
        catch(Exception ex)
        {
            AaruLogging.Debug(MODULE_NAME, "DecompressZlibResourceFork: Exception - {0}", ex.Message);

            return ErrorNumber.InvalidArgument;
        }
    }

    /// <summary>Decompresses LZFSE compressed data from resource fork</summary>
    private static ErrorNumber DecompressLzfseResourceFork(byte[] resourceForkData, int offset, int compressedSize,
                                                           ulong  uncompressedSize, out byte[] buf)
    {
        buf = null;

        try
        {
            var compressedData = new byte[compressedSize];
            Array.Copy(resourceForkData, offset, compressedData, 0, compressedSize);

            // Use Aaru.Compression.LZFSE to decompress
            buf = new byte[uncompressedSize];
            int decompressedSize = LZFSE.DecodeBuffer(compressedData, buf);

            if(decompressedSize <= 0)
            {
                AaruLogging.Debug(MODULE_NAME, "DecompressLzfseResourceFork: LZFSE decompression failed");

                return ErrorNumber.InvalidArgument;
            }

            if(decompressedSize != (int)uncompressedSize)
            {
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressLzfseResourceFork: Decompressed size {0} != expected size {1}",
                                  decompressedSize,
                                  uncompressedSize);
            }

            return ErrorNumber.NoError;
        }
        catch(Exception ex)
        {
            AaruLogging.Debug(MODULE_NAME, "DecompressLzfseResourceFork: Exception - {0}", ex.Message);

            return ErrorNumber.InvalidArgument;
        }
    }

    /// <summary>Decompresses LZVN compressed data from resource fork (types 7 and 9)</summary>
    private static ErrorNumber DecompressLzvnResourceFork(byte[] resourceForkData, int offset, int compressedSize,
                                                          ulong  uncompressedSize, out byte[] buf)
    {
        buf = null;

        try
        {
            var compressedData = new byte[compressedSize];
            Array.Copy(resourceForkData, offset, compressedData, 0, compressedSize);

            // Use Aaru.Compression.LZVN to decompress
            buf = new byte[uncompressedSize];
            int decompressedSize = LZVN.DecodeBuffer(compressedData, buf);

            if(decompressedSize <= 0)
            {
                AaruLogging.Debug(MODULE_NAME, "DecompressLzvnResourceFork: LZVN decompression failed");

                return ErrorNumber.InvalidArgument;
            }

            if(decompressedSize != (int)uncompressedSize)
            {
                AaruLogging.Debug(MODULE_NAME,
                                  "DecompressLzvnResourceFork: Decompressed size {0} != expected size {1}",
                                  decompressedSize,
                                  uncompressedSize);
            }

            return ErrorNumber.NoError;
        }
        catch(Exception ex)
        {
            AaruLogging.Debug(MODULE_NAME, "DecompressLzvnResourceFork: Exception - {0}", ex.Message);

            return ErrorNumber.InvalidArgument;
        }
    }
}