using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using Fsp;
using Fsp.Interop;
using RomRepoMgr.Database.Models;
using FileInfo = Fsp.Interop.FileInfo;

namespace RomRepoMgr.Core.Filesystem;

[SupportedOSPlatform("windows")]
public class Winfsp(Vfs vfs) : FileSystemBase
{
    FileSystemHost _host;

    public static bool IsAvailable
    {
        get
        {
            try
            {
                Version winfspVersion = FileSystemHost.Version();

                if(winfspVersion == null) return false;

                return winfspVersion.Major == 1 && winfspVersion.Minor >= 7;
            }
            catch(Exception)
            {
                return false;
            }
        }
    }

    internal bool Mount(string mountPoint)
    {
        _host = new FileSystemHost(this)
        {
            SectorSize               = 512,
            CasePreservedNames       = true,
            CaseSensitiveSearch      = true,
            FileSystemName           = "romrepomgrfs",
            MaxComponentLength       = 255,
            UnicodeOnDisk            = true,
            SectorsPerAllocationUnit = 1
        };

        if(Directory.Exists(mountPoint)) Directory.Delete(mountPoint);

        int ret = _host.Mount(mountPoint);

        if(ret == STATUS_SUCCESS) return true;

        _host = null;

        return false;
    }

    internal void Umount() => _host?.Unmount();

    public override int SetVolumeLabel(string volumeLabel, out VolumeInfo volumeInfo)
    {
        volumeInfo = default(VolumeInfo);

        return STATUS_MEDIA_WRITE_PROTECTED;
    }

    public override int Create(string     fileName, uint createOptions, uint grantedAccess, uint fileAttributes,
                               byte[]     securityDescriptor, ulong allocationSize, out object fileNode,
                               out object fileDesc, out FileInfo fileInfo, out string normalizedName)
    {
        fileNode       = default(object);
        fileDesc       = default(object);
        fileInfo       = default(FileInfo);
        normalizedName = default(string);

        return STATUS_MEDIA_WRITE_PROTECTED;
    }

    public override int Overwrite(object fileNode, object fileDesc, uint fileAttributes, bool replaceFileAttributes,
                                  ulong  allocationSize, out FileInfo fileInfo)
    {
        fileInfo = default(FileInfo);

        return STATUS_MEDIA_WRITE_PROTECTED;
    }

    public override int Write(object       fileNode,         object fileDesc, IntPtr buffer, ulong offset, uint length,
                              bool         writeToEndOfFile, bool   constrainedIo, out uint bytesTransferred,
                              out FileInfo fileInfo)
    {
        bytesTransferred = default(uint);
        fileInfo         = default(FileInfo);

        return STATUS_MEDIA_WRITE_PROTECTED;
    }

    public override int SetBasicInfo(object fileNode, object fileDesc, uint fileAttributes, ulong creationTime,
                                     ulong lastAccessTime, ulong lastWriteTime, ulong changeTime, out FileInfo fileInfo)
    {
        fileInfo = default(FileInfo);

        return STATUS_MEDIA_WRITE_PROTECTED;
    }

    public override int SetFileSize(object       fileNode, object fileDesc, ulong newSize, bool setAllocationSize,
                                    out FileInfo fileInfo)
    {
        fileInfo = default(FileInfo);

        return STATUS_MEDIA_WRITE_PROTECTED;
    }

    public override int CanDelete(object fileNode, object fileDesc, string fileName) => STATUS_MEDIA_WRITE_PROTECTED;

    public override int Rename(object fileNode, object fileDesc, string fileName, string newFileName,
                               bool   replaceIfExists) => STATUS_MEDIA_WRITE_PROTECTED;

    public override int GetVolumeInfo(out VolumeInfo volumeInfo)
    {
        volumeInfo = new VolumeInfo();

        vfs.GetInfo(out _, out ulong totalSize);

        volumeInfo.FreeSize  = 0;
        volumeInfo.TotalSize = totalSize;

        return STATUS_SUCCESS;
    }

    public override int Open(string     fileName, uint         createOptions, uint grantedAccess, out object fileNode,
                             out object fileDesc, out FileInfo fileInfo,      out string normalizedName)
    {
        fileNode       = default(object);
        fileDesc       = default(object);
        fileInfo       = default(FileInfo);
        normalizedName = default(string);

        string[] pieces = vfs.SplitPath(fileName);

        // Root directory
        if(pieces.Length == 0)
        {
            fileInfo = new FileInfo
            {
                CreationTime   = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                LastWriteTime  = (ulong)DateTime.UtcNow.ToFileTimeUtc()
            };

            normalizedName = "";

            fileNode = new FileNode
            {
                FileName    = normalizedName,
                IsDirectory = true,
                Info        = fileInfo,
                Path        = fileName
            };

            return STATUS_SUCCESS;
        }

        long romSetId = vfs.GetRomSetId(pieces[0]);

        if(romSetId <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

        RomSet romSet = vfs.GetRomSet(romSetId);

        if(romSet == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        // ROM Set
        if(pieces.Length == 1)
        {
            fileInfo = new FileInfo
            {
                CreationTime   = (ulong)romSet.CreatedOn.ToUniversalTime().ToFileTimeUtc(),
                FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                LastWriteTime  = (ulong)romSet.UpdatedOn.ToUniversalTime().ToFileTimeUtc()
            };

            normalizedName = Path.GetFileName(fileName);

            fileNode = new FileNode
            {
                FileName    = normalizedName,
                IsDirectory = true,
                Info        = fileInfo,
                Path        = fileName,
                RomSetId    = romSet.Id,
                ParentInfo = new FileInfo
                {
                    CreationTime   = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                    FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                    LastWriteTime  = (ulong)DateTime.UtcNow.ToFileTimeUtc()
                }
            };

            return STATUS_SUCCESS;
        }

        CachedMachine machine = vfs.GetMachine(romSetId, pieces[1]);

        if(machine == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        // Machine
        if(pieces.Length == 2)
        {
            fileInfo = new FileInfo
            {
                CreationTime   = (ulong)machine.CreationDate.ToUniversalTime().ToFileTimeUtc(),
                FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                LastWriteTime  = (ulong)machine.ModificationDate.ToUniversalTime().ToFileTimeUtc()
            };

            normalizedName = Path.GetFileName(fileName);

            fileNode = new FileNode
            {
                FileName    = normalizedName,
                IsDirectory = true,
                Info        = fileInfo,
                Path        = fileName,
                MachineId   = machine.Id,
                ParentInfo = new FileInfo
                {
                    CreationTime   = (ulong)romSet.CreatedOn.ToUniversalTime().ToFileTimeUtc(),
                    FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                    LastWriteTime  = (ulong)romSet.UpdatedOn.ToUniversalTime().ToFileTimeUtc()
                }
            };

            return STATUS_SUCCESS;
        }

        long       handle = 0;
        CachedFile file   = vfs.GetFile(machine.Id, pieces[2]);

        if(file != null)
        {
            if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;

            if(file.Sha384 == null) return STATUS_OBJECT_NAME_NOT_FOUND;

            handle = vfs.Open(file.Sha384, (long)file.Size);

            if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

            normalizedName = Path.GetFileName(fileName);

            // TODO: Real allocation size
            fileInfo = new FileInfo
            {
                ChangeTime     = (ulong)file.UpdatedOn.ToFileTimeUtc(),
                AllocationSize = (file.Size + 511) / 512,
                FileSize       = file.Size,
                CreationTime   = (ulong)file.CreatedOn.ToFileTimeUtc(),
                FileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
                IndexNumber    = file.Id,
                LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                LastWriteTime  = (ulong)(file.FileLastModification?.ToFileTimeUtc() ?? file.UpdatedOn.ToFileTimeUtc())
            };

            fileNode = new FileNode
            {
                FileName = normalizedName,
                Info     = fileInfo,
                Path     = fileName,
                Handle   = handle
            };

            return STATUS_SUCCESS;
        }

        CachedDisk disk = vfs.GetDisk(machine.Id, pieces[2]);

        if(disk != null)
        {
            if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;

            if(disk.Sha1 == null && disk.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;

            handle = vfs.OpenDisk(disk.Sha1, disk.Md5);

            if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

            normalizedName = Path.GetFileName(fileName);

            // TODO: Real allocation size
            fileInfo = new FileInfo
            {
                ChangeTime     = (ulong)disk.UpdatedOn.ToFileTimeUtc(),
                AllocationSize = (disk.Size + 511) / 512,
                FileSize       = disk.Size,
                CreationTime   = (ulong)disk.CreatedOn.ToFileTimeUtc(),
                FileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
                IndexNumber    = disk.Id,
                LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                LastWriteTime  = (ulong)disk.UpdatedOn.ToFileTimeUtc()
            };

            fileNode = new FileNode
            {
                FileName = normalizedName,
                Info     = fileInfo,
                Path     = fileName,
                Handle   = handle
            };

            return STATUS_SUCCESS;
        }

        CachedMedia media = vfs.GetMedia(machine.Id, pieces[2]);

        if(media == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;

        if(media.Sha256 == null && media.Sha1 == null && media.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        handle = vfs.OpenMedia(media.Sha256, media.Sha1, media.Md5);

        if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

        normalizedName = Path.GetFileName(fileName);

        // TODO: Real allocation size
        fileInfo = new FileInfo
        {
            ChangeTime     = (ulong)media.UpdatedOn.ToFileTimeUtc(),
            AllocationSize = (media.Size + 511) / 512,
            FileSize       = media.Size,
            CreationTime   = (ulong)media.CreatedOn.ToFileTimeUtc(),
            FileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
            IndexNumber    = media.Id,
            LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
            LastWriteTime  = (ulong)media.UpdatedOn.ToFileTimeUtc()
        };

        fileNode = new FileNode
        {
            FileName = normalizedName,
            Info     = fileInfo,
            Path     = fileName,
            Handle   = handle
        };

        return STATUS_SUCCESS;
    }

    public override void Close(object fileNode, object fileDesc)
    {
        if(fileNode is not FileNode node) return;

        if(node.Handle <= 0) return;

        vfs.Close(node.Handle);
    }

    public override int Read(object   fileNode, object fileDesc, IntPtr buffer, ulong offset, uint length,
                             out uint bytesTransferred)
    {
        bytesTransferred = 0;

        if(fileNode is not FileNode { Handle: > 0 } node) return STATUS_INVALID_HANDLE;

        byte[] buf = new byte[length];

        int ret = vfs.Read(node.Handle, buf, (long)offset);

        if(ret < 0) return STATUS_INVALID_HANDLE;

        Marshal.Copy(buf, 0, buffer, ret);

        bytesTransferred = (uint)ret;

        return STATUS_SUCCESS;
    }

    public override int GetFileInfo(object fileNode, object fileDesc, out FileInfo fileInfo)
    {
        fileInfo = default(FileInfo);

        if(fileNode is not FileNode node) return STATUS_INVALID_HANDLE;

        fileInfo = node.Info;

        return STATUS_SUCCESS;
    }

    public override bool ReadDirectoryEntry(object     fileNode, object     fileDesc, string pattern, string marker,
                                            ref object context,  out string fileName, out FileInfo fileInfo)
    {
        fileName = default(string);
        fileInfo = default(FileInfo);

        if(fileNode is not FileNode { IsDirectory: true } node) return false;

        if(context is not IEnumerator<FileEntry> enumerator)
        {
            if(node.MachineId > 0)
            {
                ConcurrentDictionary<string, CachedFile> cachedMachineFiles = vfs.GetFilesFromMachine(node.MachineId);

                ConcurrentDictionary<string, CachedDisk> cachedMachineDisks = vfs.GetDisksFromMachine(node.MachineId);

                ConcurrentDictionary<string, CachedMedia> cachedMachineMedias =
                    vfs.GetMediasFromMachine(node.MachineId);

                node.Children =
                [
                    new FileEntry
                    {
                        FileName = ".",
                        Info     = node.Info
                    },
                    new FileEntry
                    {
                        FileName = "..",
                        Info     = node.ParentInfo
                    }
                ];

                node.Children.AddRange(cachedMachineFiles.Select(file => new FileEntry
                {
                    FileName = file.Key,
                    Info = new FileInfo
                    {
                        ChangeTime     = (ulong)file.Value.UpdatedOn.ToFileTimeUtc(),
                        AllocationSize = (file.Value.Size + 511) / 512,
                        FileSize       = file.Value.Size,
                        CreationTime   = (ulong)file.Value.CreatedOn.ToFileTimeUtc(),
                        FileAttributes =
                            (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
                        IndexNumber    = file.Value.Id,
                        LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                        LastWriteTime = (ulong)(file.Value.FileLastModification?.ToFileTimeUtc() ??
                                                file.Value.UpdatedOn.ToFileTimeUtc())
                    }
                }));

                node.Children.AddRange(cachedMachineDisks.Select(disk => new FileEntry
                {
                    FileName = disk.Key + ".chd",
                    Info = new FileInfo
                    {
                        ChangeTime     = (ulong)disk.Value.UpdatedOn.ToFileTimeUtc(),
                        AllocationSize = (disk.Value.Size + 511) / 512,
                        FileSize       = disk.Value.Size,
                        CreationTime   = (ulong)disk.Value.CreatedOn.ToFileTimeUtc(),
                        FileAttributes =
                            (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
                        IndexNumber    = disk.Value.Id,
                        LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                        LastWriteTime  = (ulong)disk.Value.UpdatedOn.ToFileTimeUtc()
                    }
                }));

                node.Children.AddRange(cachedMachineMedias.Select(media => new FileEntry
                {
                    FileName = media.Key + ".aif",
                    Info = new FileInfo
                    {
                        ChangeTime     = (ulong)media.Value.UpdatedOn.ToFileTimeUtc(),
                        AllocationSize = (media.Value.Size + 511) / 512,
                        FileSize       = media.Value.Size,
                        CreationTime   = (ulong)media.Value.CreatedOn.ToFileTimeUtc(),
                        FileAttributes =
                            (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
                        IndexNumber    = media.Value.Id,
                        LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
                        LastWriteTime  = (ulong)media.Value.UpdatedOn.ToFileTimeUtc()
                    }
                }));
            }
            else if(node.RomSetId > 0)
            {
                ConcurrentDictionary<string, CachedMachine> machines = vfs.GetMachinesFromRomSet(node.RomSetId);

                node.Children =
                [
                    new FileEntry
                    {
                        FileName = ".",
                        Info     = node.Info
                    },
                    new FileEntry
                    {
                        FileName = "..",
                        Info     = node.ParentInfo
                    }
                ];

                node.Children.AddRange(machines.Select(machine => new FileEntry
                {
                    FileName = machine.Key,
                    Info = new FileInfo
                    {
                        CreationTime   = (ulong)machine.Value.CreationDate.ToUniversalTime().ToFileTimeUtc(),
                        FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                        LastWriteTime  = (ulong)machine.Value.ModificationDate.ToUniversalTime().ToFileTimeUtc()
                    }
                }));
            }
            else
            {
                node.Children = [];

                node.Children.AddRange(vfs.GetRootEntries()
                                          .Select(e => new FileEntry
                                           {
                                               FileName = e,
                                               IsRomSet = true
                                           }));
            }

            if(marker != null)
            {
                int idx = node.Children.FindIndex(f => f.FileName == marker);

                if(idx >= 0) node.Children.RemoveRange(0, idx + 1);
            }

            context = enumerator = node.Children.GetEnumerator();
        }

        while(enumerator.MoveNext())
        {
            FileEntry entry = enumerator.Current;

            if(entry is null) continue;

            if(entry.IsRomSet)
            {
                long romSetId = vfs.GetRomSetId(entry.FileName);

                if(romSetId <= 0) continue;

                RomSet romSet = vfs.GetRomSet(romSetId);

                if(romSet is null) continue;

                entry.Info = new FileInfo
                {
                    CreationTime   = (ulong)romSet.CreatedOn.ToUniversalTime().ToFileTimeUtc(),
                    FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
                    LastWriteTime  = (ulong)romSet.UpdatedOn.ToUniversalTime().ToFileTimeUtc()
                };
            }

            fileName = entry.FileName;
            fileInfo = entry.Info;

            return true;
        }

        return false;
    }

    public override int GetSecurityByName(string fileName, out uint fileAttributes, ref byte[] securityDescriptor)
    {
        fileAttributes = 0;

        string[] pieces = vfs.SplitPath(fileName);

        // Root directory
        if(pieces.Length == 0)
        {
            fileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed);

            if(securityDescriptor == null) return STATUS_SUCCESS;

            string rootSddl = "O:BAG:BAD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;WD)";

            var    rootSecurityDescriptor = new RawSecurityDescriptor(rootSddl);
            byte[] fileSecurity           = new byte[rootSecurityDescriptor.BinaryLength];
            rootSecurityDescriptor.GetBinaryForm(fileSecurity, 0);
            securityDescriptor = fileSecurity;

            return STATUS_SUCCESS;
        }

        long romSetId = vfs.GetRomSetId(pieces[0]);

        if(romSetId <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

        RomSet romSet = vfs.GetRomSet(romSetId);

        if(romSet == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        // ROM Set
        if(pieces.Length == 1)
        {
            fileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed);

            return STATUS_SUCCESS;
        }

        CachedMachine machine = vfs.GetMachine(romSetId, pieces[1]);

        if(machine == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        // Machine
        if(pieces.Length == 2)
        {
            fileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed);

            return STATUS_SUCCESS;
        }

        long       handle = 0;
        CachedFile file   = vfs.GetFile(machine.Id, pieces[2]);

        if(file != null)
        {
            if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;

            if(file.Sha384 == null) return STATUS_OBJECT_NAME_NOT_FOUND;

            handle = vfs.Open(file.Sha384, (long)file.Size);

            if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

            fileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly);

            return STATUS_SUCCESS;
        }

        CachedDisk disk = vfs.GetDisk(machine.Id, pieces[2]);

        if(disk != null)
        {
            if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;

            if(disk.Sha1 == null && disk.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;

            handle = vfs.OpenDisk(disk.Sha1, disk.Md5);

            if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

            fileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly);

            return STATUS_SUCCESS;
        }

        CachedMedia media = vfs.GetMedia(machine.Id, pieces[2]);

        if(media == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;

        if(media.Sha256 == null && media.Sha1 == null && media.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;

        handle = vfs.OpenMedia(media.Sha256, media.Sha1, media.Md5);

        if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;

        fileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly);

        return STATUS_SUCCESS;
    }

    sealed class FileEntry
    {
        public string   FileName { get; set; }
        public FileInfo Info     { get; set; }
        public bool     IsRomSet { get; set; }
    }

    sealed class FileNode
    {
        public FileInfo        Info        { get; set; }
        public FileInfo        ParentInfo  { get; set; }
        public string          FileName    { get; set; }
        public string          Path        { get; set; }
        public long            Handle      { get; set; }
        public List<FileEntry> Children    { get; set; }
        public bool            IsDirectory { get; set; }
        public long            RomSetId    { get; set; }
        public ulong           MachineId   { get; set; }
    }
}