// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename       : File.cs
// Author(s)      : Natalia Portillo <claunia@claunia.com>
//
// Component      : Professional File System 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.Collections.Generic;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Helpers;
using Aaru.Logging;

namespace Aaru.Filesystems;

/// <inheritdoc />
public sealed partial class PFS
{
    /// <inheritdoc />
    public ErrorNumber ReadLink(string path, out string dest)
    {
        dest = null;

        if(!_mounted) return ErrorNumber.AccessDenied;

        AaruLogging.Debug(MODULE_NAME, "ReadLink: path='{0}'", path);

        // Find the file entry
        ErrorNumber errno = GetEntryForPath(path, out DirEntryCacheItem entry);

        if(errno != ErrorNumber.NoError) return errno;

        // Check if it's a symbolic link
        if(entry.Type != EntryType.SoftLink)
        {
            AaruLogging.Debug(MODULE_NAME, "ReadLink: '{0}' is not a symbolic link (type={1})", path, entry.Type);

            return ErrorNumber.InvalidArgument;
        }

        // Get the link's anode to find the data block
        errno = GetAnode(entry.Anode, out Anode linkAnode);

        if(errno != ErrorNumber.NoError)
        {
            AaruLogging.Debug(MODULE_NAME, "ReadLink: Error getting anode {0}: {1}", entry.Anode, errno);

            return errno;
        }

        // Read the block containing the symbolic link target
        errno = ReadBlock(linkAnode.blocknr, out byte[] blockData);

        if(errno != ErrorNumber.NoError)
        {
            AaruLogging.Debug(MODULE_NAME, "ReadLink: Error reading block {0}: {1}", linkAnode.blocknr, errno);

            return errno;
        }

        // The fsize field contains the length of the symbolic link path
        // The path is stored at the beginning of the data block
        var linkLength = (int)entry.Size;

        if(linkLength <= 0 || linkLength > blockData.Length)
        {
            AaruLogging.Debug(MODULE_NAME, "ReadLink: Invalid link length {0}", linkLength);

            return ErrorNumber.InvalidArgument;
        }

        // Extract the symbolic link target
        dest = _encoding.GetString(blockData, 0, linkLength);

        AaruLogging.Debug(MODULE_NAME, "ReadLink: '{0}' -> '{1}'", path, dest);

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber OpenFile(string path, out IFileNode node)
    {
        node = null;

        if(!_mounted) return ErrorNumber.AccessDenied;

        AaruLogging.Debug(MODULE_NAME, "OpenFile: path='{0}'", path);

        // Find the file entry
        ErrorNumber errno = GetEntryForPath(path, out DirEntryCacheItem entry);

        if(errno != ErrorNumber.NoError) return errno;

        // Check if it's a file
        if(entry.Type == EntryType.Directory || entry.Type == EntryType.HardLinkDir)
        {
            AaruLogging.Debug(MODULE_NAME, "OpenFile: '{0}' is a directory", path);

            return ErrorNumber.IsDirectory;
        }

        // Get the file's starting anode
        errno = GetAnode(entry.Anode, out Anode fileAnode);

        if(errno != ErrorNumber.NoError)
        {
            AaruLogging.Debug(MODULE_NAME, "OpenFile: Error getting anode {0}: {1}", entry.Anode, errno);

            return errno;
        }

        // Create file node
        node = new PFSFileNode
        {
            Path         = path,
            Length       = entry.Size,
            Offset       = 0,
            StartAnode   = entry.Anode,
            CurrentAnode = fileAnode,
            AnodeOffset  = 0,
            BlockOffset  = 0,
            FileSize     = entry.Size
        };

        AaruLogging.Debug(MODULE_NAME,
                          "OpenFile: Opened file '{0}', size={1}, anode={2}",
                          path,
                          entry.Size,
                          entry.Anode);

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber CloseFile(IFileNode node)
    {
        if(!_mounted) return ErrorNumber.AccessDenied;

        if(node is not PFSFileNode) return ErrorNumber.InvalidArgument;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read)
    {
        read = 0;

        if(!_mounted) return ErrorNumber.AccessDenied;

        if(buffer is null || buffer.Length < length) return ErrorNumber.InvalidArgument;

        if(node is not PFSFileNode pfsNode) return ErrorNumber.InvalidArgument;

        // Can't read past end of file
        if(pfsNode.Offset >= pfsNode.Length) return ErrorNumber.NoError;

        // Limit read to remaining file size
        if(length > pfsNode.Length - pfsNode.Offset) length = pfsNode.Length - pfsNode.Offset;

        var  bufferOffset   = 0;
        long bytesRemaining = length;

        while(bytesRemaining > 0)
        {
            // Calculate current block number within the filesystem
            uint currentBlock = pfsNode.CurrentAnode.blocknr + pfsNode.AnodeOffset;

            // Read the current data block
            ErrorNumber errno = ReadBlock(currentBlock, out byte[] blockData);

            if(errno != ErrorNumber.NoError)
            {
                AaruLogging.Debug(MODULE_NAME, "ReadFile: Error reading block {0}: {1}", currentBlock, errno);

                return errno;
            }

            // Calculate how much data to read from this block
            var dataInBlock = (int)(_blockSize - pfsNode.BlockOffset);

            if(dataInBlock > bytesRemaining) dataInBlock = (int)bytesRemaining;

            // Copy data to buffer
            Array.Copy(blockData, (int)pfsNode.BlockOffset, buffer, bufferOffset, dataInBlock);

            bufferOffset   += dataInBlock;
            bytesRemaining -= dataInBlock;
            pfsNode.Offset += dataInBlock;

            // Update position within block
            pfsNode.BlockOffset += (uint)dataInBlock;

            // If we've read past the end of this block, move to the next
            if(pfsNode.BlockOffset >= _blockSize)
            {
                pfsNode.BlockOffset = 0;
                pfsNode.AnodeOffset++;

                // Check if we need to move to the next anode in the chain
                if(pfsNode.AnodeOffset >= pfsNode.CurrentAnode.clustersize)
                {
                    // Move to next anode
                    if(pfsNode.CurrentAnode.next == ANODE_EOF)
                    {
                        // End of file
                        break;
                    }

                    errno = GetAnode(pfsNode.CurrentAnode.next, out Anode nextAnode);

                    if(errno != ErrorNumber.NoError)
                    {
                        AaruLogging.Debug(MODULE_NAME,
                                          "ReadFile: Error getting next anode {0}: {1}",
                                          pfsNode.CurrentAnode.next,
                                          errno);

                        break;
                    }

                    pfsNode.CurrentAnode = nextAnode;
                    pfsNode.AnodeOffset  = 0;
                }
            }
        }

        read = bufferOffset;

        return ErrorNumber.NoError;
    }

    /// <inheritdoc />
    public ErrorNumber Stat(string path, out FileEntryInfo stat)
    {
        stat = null;

        if(!_mounted) return ErrorNumber.AccessDenied;

        AaruLogging.Debug(MODULE_NAME, "Stat: path='{0}'", path);

        // Normalize the path
        string normalizedPath = path ?? "/";

        if(normalizedPath == "" || normalizedPath == ".") normalizedPath = "/";

        // Root directory
        if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
        {
            stat = new FileEntryInfo
            {
                Attributes = FileAttributes.Directory,
                Inode      = ANODE_ROOTDIR,
                Links      = 1,
                BlockSize  = _blockSize,
                CreationTimeUtc =
                    DateHandlers.AmigaToDateTime(_rootBlock.creationday,
                                                 _rootBlock.creationminute,
                                                 _rootBlock.creationtick),
                LastWriteTimeUtc = DateHandlers.AmigaToDateTime(_rootBlock.creationday,
                                                                _rootBlock.creationminute,
                                                                _rootBlock.creationtick)
            };

            return ErrorNumber.NoError;
        }

        // Find the entry
        ErrorNumber errno = GetEntryForPath(normalizedPath, out DirEntryCacheItem entry);

        if(errno != ErrorNumber.NoError) return errno;

        // Build stat from entry
        stat = BuildStatFromEntry(entry);

        return ErrorNumber.NoError;
    }

    /// <summary>Gets the directory entry for a given path</summary>
    /// <param name="path">The path to find</param>
    /// <param name="entry">The directory entry</param>
    /// <returns>Error code</returns>
    ErrorNumber GetEntryForPath(string path, out DirEntryCacheItem entry)
    {
        entry = null;

        // Remove leading slash
        string pathWithoutLeadingSlash = path.StartsWith("/", StringComparison.Ordinal) ? path[1..] : path;

        string[] pathComponents = pathWithoutLeadingSlash.Split('/', StringSplitOptions.RemoveEmptyEntries);

        if(pathComponents.Length == 0) return ErrorNumber.InvalidArgument;

        // Start from root directory cache
        Dictionary<string, DirEntryCacheItem> currentEntries = _rootDirectoryCache;
        DirEntryCacheItem                     currentEntry   = null;

        // Traverse each path component
        for(var i = 0; i < pathComponents.Length; i++)
        {
            string component = pathComponents[i];

            // Find the component in current directory (case-insensitive)
            string foundKey = null;

            foreach(string key in currentEntries.Keys)
            {
                if(string.Equals(key, component, StringComparison.OrdinalIgnoreCase))
                {
                    foundKey = key;

                    break;
                }
            }

            if(foundKey == null)
            {
                AaruLogging.Debug(MODULE_NAME, "GetEntryForPath: Component '{0}' not found", component);

                return ErrorNumber.NoSuchFile;
            }

            currentEntry = currentEntries[foundKey];

            // If not the last component, it must be a directory
            if(i < pathComponents.Length - 1)
            {
                if(currentEntry.Type != EntryType.Directory && currentEntry.Type != EntryType.HardLinkDir)
                {
                    AaruLogging.Debug(MODULE_NAME,
                                      "GetEntryForPath: '{0}' is not a directory (type={1})",
                                      component,
                                      currentEntry.Type);

                    return ErrorNumber.NotDirectory;
                }

                // Get the anode for this directory
                ErrorNumber errno = GetAnode(currentEntry.Anode, out Anode dirAnode);

                if(errno != ErrorNumber.NoError) return errno;

                // Read directory contents
                currentEntries = new Dictionary<string, DirEntryCacheItem>();
                errno          = ReadDirectoryBlocks(dirAnode, currentEntries);

                if(errno != ErrorNumber.NoError) return errno;
            }
        }

        entry = currentEntry;

        return ErrorNumber.NoError;
    }

    /// <summary>Builds a FileEntryInfo from a cached directory entry</summary>
    /// <param name="entry">The directory entry</param>
    /// <returns>FileEntryInfo structure</returns>
    FileEntryInfo BuildStatFromEntry(DirEntryCacheItem entry)
    {
        var stat = new FileEntryInfo
        {
            Inode           = entry.Anode,
            Links           = 1,
            BlockSize       = _blockSize,
            CreationTimeUtc = DateHandlers.AmigaToDateTime(entry.CreationDay, entry.CreationMinute, entry.CreationTick),
            LastWriteTimeUtc =
                DateHandlers.AmigaToDateTime(entry.CreationDay, entry.CreationMinute, entry.CreationTick),
            Mode = ProtectionToUnixMode(entry.Protection)
        };

        // Determine attributes from entry type
        switch(entry.Type)
        {
            case EntryType.Directory:
            case EntryType.HardLinkDir:
                stat.Attributes = FileAttributes.Directory;

                break;

            case EntryType.File:
            case EntryType.HardLinkFile:
            case EntryType.RolloverFile:
                stat.Attributes = FileAttributes.File;
                stat.Length     = entry.Size;

                break;

            case EntryType.SoftLink:
                stat.Attributes = FileAttributes.Symlink;

                break;

            default:
                stat.Attributes = FileAttributes.File;
                stat.Length     = entry.Size;

                break;
        }

        // Apply Amiga protection flags to attributes
        // Archive bit (active high)
        if(entry.Protection.HasFlag(ProtectionBits.Archive)) stat.Attributes |= FileAttributes.Archive;

        // Pure bit - maps to System (resident)
        if(entry.Protection.HasFlag(ProtectionBits.Pure)) stat.Attributes |= FileAttributes.System;

        // Calculate blocks used (for files)
        if(stat.Length > 0) stat.Blocks = (stat.Length + _blockSize - 1) / _blockSize;

        return stat;
    }

    /// <summary>Converts Amiga protection bits to Unix-style mode</summary>
    /// <param name="protect">Amiga protection bits</param>
    /// <returns>Unix-style mode</returns>
    static uint ProtectionToUnixMode(ProtectionBits protect)
    {
        // Amiga owner bits are active-low (0 = allowed), Unix are active-high
        uint mode = 0;

        // Owner permissions (active-low, so check if NOT set means allowed)
        if(!protect.HasFlag(ProtectionBits.Read)) // Read allowed
            mode |= 0x100;                        // S_IRUSR

        if(!protect.HasFlag(ProtectionBits.Write)) // Write allowed
            mode |= 0x080;                         // S_IWUSR

        if(!protect.HasFlag(ProtectionBits.Execute)) // Execute allowed
            mode |= 0x040;                           // S_IXUSR

        // Note: Group and Other permissions would require ExtendedProtectionBits
        // which are stored separately in ExtraFields. For basic PFS, we just
        // mirror owner permissions to group and other.
        if(!protect.HasFlag(ProtectionBits.Read))
        {
            mode |= 0x020; // S_IRGRP
            mode |= 0x004; // S_IROTH
        }

        if(!protect.HasFlag(ProtectionBits.Write))
        {
            mode |= 0x010; // S_IWGRP
            mode |= 0x002; // S_IWOTH
        }

        if(!protect.HasFlag(ProtectionBits.Execute))
        {
            mode |= 0x008; // S_IXGRP
            mode |= 0x001; // S_IXOTH
        }

        return mode;
    }
}