using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using DynamicData;
using FileTime.Core.ContentAccess;
using FileTime.Core.Enums;
using FileTime.Core.Models;
using FileTime.Core.Models.Extensions;
using FileTime.Core.Timeline;

namespace FileTime.Providers.Local;

public sealed partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider
{
    private readonly ITimelessContentProvider _timelessContentProvider;
    private readonly bool _isCaseInsensitive;

    public LocalContentProvider(ITimelessContentProvider timelessContentProvider)
        : base(LocalContentProviderConstants.ContentProviderId, timelessContentProvider)
    {
        _timelessContentProvider = timelessContentProvider;
        _isCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

        SupportsContentStreams = true;

        RefreshRootDirectories();
    }

    public override Task OnEnter()
    {
        RefreshRootDirectories();

        return Task.CompletedTask;
    }

    private void RefreshRootDirectories()
    {
        var rootDirectories = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
            ? new DirectoryInfo("/").GetDirectories()
            : Environment.GetLogicalDrives().Select(d => new DirectoryInfo(d));

        Items.Clear();
        Items.AddRange(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));

        /*Items.Edit(actions =>
        {
            actions.Clear();
            actions.AddOrUpdate(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
        });*/
    }

    public override bool CanHandlePath(NativePath path)
    {
        var rootDrive = Items
            .FirstOrDefault(r =>
                path.Path.StartsWith(
                    GetNativePath(r.Path).Path,
                    _isCaseInsensitive
                        ? StringComparison.InvariantCultureIgnoreCase
                        : StringComparison.InvariantCulture
                )
            );

        return rootDrive is not null;
    }

    public override Task<IItem> GetItemByNativePathAsync(NativePath nativePath,
        PointInTime pointInTime,
        bool forceResolve = false,
        AbsolutePathType forceResolvePathType = AbsolutePathType.Unknown,
        ItemInitializationSettings? itemInitializationSettings = null)
    {
        var path = nativePath.Path;
        Exception? innerException;
        try
        {
            if (path.Length == 0)
            {
                return Task.FromResult((IItem) this);
            }
            else if (Directory.Exists(path))
            {
                return Task.FromResult((IItem) DirectoryToContainer(
                    new DirectoryInfo(path!.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar),
                    pointInTime,
                    itemInitializationSettings)
                );
            }
            else if (File.Exists(path))
            {
                return Task.FromResult((IItem) FileToElement(new FileInfo(path), pointInTime));
            }

            var type = forceResolvePathType switch
            {
                AbsolutePathType.Container => "Directory",
                AbsolutePathType.Element => "File",
                _ => "Directory or file"
            };

            innerException = forceResolvePathType switch
            {
                AbsolutePathType.Container => new DirectoryNotFoundException($"{type} not found: '{path}'"),
                _ => new FileNotFoundException(type + " not found", path)
            };
        }
        catch (Exception e)
        {
            if (!forceResolve)
            {
                throw new Exception(
                    $"Could not resolve path '{nativePath.Path}' and {nameof(forceResolve)} is false.",
                    e
                );
            }

            innerException = e;
        }

        return forceResolvePathType switch
        {
            AbsolutePathType.Container => Task.FromResult(
                (IItem) CreateEmptyContainer(
                    nativePath,
                    pointInTime,
                    new List<Exception>() {innerException}
                )
            ),
            AbsolutePathType.Element => Task.FromResult(CreateEmptyElement(nativePath)),
            _ => throw new Exception(
                $"Could not resolve path '{nativePath.Path}' and could not force create, because {nameof(forceResolvePathType)} is {nameof(AbsolutePathType.Unknown)}.",
                innerException)
        };
    }

    private Container CreateEmptyContainer(NativePath nativePath,
        PointInTime pointInTime,
        IEnumerable<Exception>? initialExceptions = null)
    {
        var exceptions = new ObservableCollection<Exception>();
        if (initialExceptions is not null)
        {
            exceptions.AddRange(initialExceptions);
        }

        var name = nativePath.Path.Split(Path.DirectorySeparatorChar).LastOrDefault() ?? "???";
        var fullName = GetFullName(nativePath);

        var parentFullName = fullName.GetParent();
        var parent =
            parentFullName is null
                ? null
                : new AbsolutePath(
                    _timelessContentProvider,
                    pointInTime,
                    parentFullName,
                    AbsolutePathType.Container);

        return new Container(
            name,
            name,
            fullName,
            nativePath,
            parent,
            false,
            true,
            DateTime.MinValue,
            DateTime.MinValue,
            SupportsDelete.False,
            false,
            "???",
            this,
            true,
            pointInTime,
            exceptions,
            new ExtensionCollection().AsReadOnly(),
            new ObservableCollection<AbsolutePath>()
        );
    }

    private IItem CreateEmptyElement(NativePath nativePath)
    {
        throw new NotImplementedException();
    }

    private AbsolutePath DirectoryToAbsolutePath(DirectoryInfo directoryInfo, PointInTime pointInTime)
    {
        var fullName = GetFullName(directoryInfo);
        return new AbsolutePath(_timelessContentProvider, pointInTime, fullName, AbsolutePathType.Container);
    }

    private AbsolutePath FileToAbsolutePath(FileInfo file, PointInTime pointInTime)
    {
        var fullName = GetFullName(file);
        return new AbsolutePath(_timelessContentProvider, pointInTime, fullName, AbsolutePathType.Element);
    }

    private Container DirectoryToContainer(DirectoryInfo directoryInfo, PointInTime pointInTime,
        ItemInitializationSettings? initializationSettings = null)
    {
        initializationSettings ??= new();

        var fullName = GetFullName(directoryInfo.FullName);
        var parentFullName = fullName.GetParent();
        var parent =
            initializationSettings.Parent
            ?? (parentFullName is null
                ? null
                : new AbsolutePath(
                    _timelessContentProvider,
                    pointInTime,
                    parentFullName,
                    AbsolutePathType.Container));
        var exceptions = new ObservableCollection<Exception>();

        var children = new ObservableCollection<AbsolutePath>();

        var container = new Container(
            directoryInfo.Name,
            directoryInfo.Name,
            fullName,
            new NativePath(directoryInfo.FullName),
            parent,
            (directoryInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden,
            directoryInfo.Exists,
            directoryInfo.CreationTime,
            directoryInfo.LastWriteTime,
            SupportsDelete.True,
            true,
            GetDirectoryAttributes(directoryInfo),
            this,
            true,
            pointInTime,
            exceptions,
            new ExtensionCollection().AsReadOnly(),
            children
        );

        if (!initializationSettings.SkipChildInitialization)
        {
            Task.Run(async () => await LoadChildren(container, directoryInfo, children, pointInTime, exceptions));
        }

        return container;
    }

    private async Task LoadChildren(Container container,
        DirectoryInfo directoryInfo,
        ObservableCollection<AbsolutePath> children,
        PointInTime pointInTime,
        ObservableCollection<Exception> exceptions)
    {
        var lockObj = new object();
        var loadingIndicatorCancellation = new CancellationTokenSource();

        Task.Run(async () => await DelayedLoadingIndicator());
        await LoadChildrenInternal();

        lock (lockObj)
        {
            loadingIndicatorCancellation.Cancel();
            container.StopLoading();
        }

        Task LoadChildrenInternal()
        {
            try
            {
                foreach (var directory in directoryInfo.EnumerateDirectories())
                {
                    try
                    {
                        if (container.LoadingCancellationToken.IsCancellationRequested) break;
                        var absolutePath = DirectoryToAbsolutePath(directory, pointInTime);
                        children.Add(absolutePath);
                    }
                    catch (Exception e)
                    {
                        exceptions.Add(e);
                    }
                }

                foreach (var file in directoryInfo.EnumerateFiles())
                {
                    try
                    {
                        if (container.LoadingCancellationToken.IsCancellationRequested) break;
                        var absolutePath = FileToAbsolutePath(file, pointInTime);
                        children.Add(absolutePath);
                    }
                    catch (Exception e)
                    {
                        exceptions.Add(e);
                    }
                }
            }
            catch (Exception e)
            {
                exceptions.Add(e);
            }

            return Task.CompletedTask;
        }

        async Task DelayedLoadingIndicator()
        {
            var token = loadingIndicatorCancellation.Token;
            try
            {
                await Task.Delay(2000, token);
            }
            catch
            {
            }

            lock (lockObj)
            {
                if (token.IsCancellationRequested) return;
                container.StartLoading();
            }
        }
    }

    private List<AbsolutePath> GetItemsByContainer(DirectoryInfo directoryInfo, PointInTime pointInTime)
        => directoryInfo
            .GetDirectories()
            .Select(d => DirectoryToAbsolutePath(d, pointInTime))
            .Concat(
                directoryInfo
                    .GetFiles()
                    .Select(f => FileToAbsolutePath(f, pointInTime))
            )
            .ToList();

    private Element FileToElement(FileInfo fileInfo, PointInTime pointInTime)
    {
        var fullName = GetFullName(fileInfo);
        var parentFullName = fullName.GetParent() ??
                             throw new Exception($"Path does not have parent: '{fileInfo.FullName}'");
        var parent = new AbsolutePath(_timelessContentProvider, pointInTime, parentFullName,
            AbsolutePathType.Container);

        var extensions = new ExtensionCollection()
        {
            new FileExtension(fileInfo.Length)
        };

        return new Element(
            fileInfo.Name,
            fileInfo.Name,
            fullName,
            new NativePath(fileInfo.FullName),
            parent,
            (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden,
            fileInfo.Exists,
            fileInfo.CreationTime,
            fileInfo.LastWriteTime,
            SupportsDelete.True,
            true,
            GetFileAttributes(fileInfo),
            this,
            pointInTime,
            new ObservableCollection<Exception>(),
            extensions.AsReadOnly()
        );
    }

    private FullName GetFullName(DirectoryInfo directoryInfo) => GetFullName(directoryInfo.FullName);

    private FullName GetFullName(FileInfo fileInfo) => GetFullName(fileInfo.FullName);
    public override FullName GetFullName(NativePath nativePath) => GetFullName(nativePath.Path);

    private FullName GetFullName(string nativePath) =>
        FullName.CreateSafe((Name + Constants.SeparatorChar +
                             string.Join(Constants.SeparatorChar,
                                 nativePath.TrimStart(Constants.SeparatorChar).Split(Path.DirectorySeparatorChar)))
            .TrimEnd(Constants.SeparatorChar))!;

    public override NativePath GetNativePath(FullName fullName)
    {
        var path = string.Join(Path.DirectorySeparatorChar, fullName.Path.Split(Constants.SeparatorChar).Skip(1));
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !path.StartsWith("/")) path = "/" + path;
        return new NativePath(path);
    }

    public override async Task<byte[]?> GetContentAsync(IElement element, int? maxLength = null,
        CancellationToken cancellationToken = default)
    {
        if (cancellationToken.IsCancellationRequested) return null;
        if (!File.Exists(element.NativePath!.Path))
            throw new FileNotFoundException("File does not exist", element.NativePath.Path);

        await using var reader = new FileStream(element.NativePath!.Path, FileMode.Open, FileAccess.Read,
            FileShare.Read,
            bufferSize: 1, // bufferSize == 1 used to avoid unnecessary buffer in FileStream
            FileOptions.Asynchronous | FileOptions.SequentialScan);

        var size = new FileInfo(element.NativePath!.Path).Length;

        if (maxLength.HasValue && maxLength.Value < size)
        {
            size = maxLength.Value;
        }

        var finalSize = size switch
        {
            > int.MaxValue => int.MaxValue,
            _ => (int) size
        };
        var buffer = new byte[finalSize];
        var realSize = await reader.ReadAsync(buffer.AsMemory(0, finalSize), cancellationToken);

        if (realSize == buffer.Length) return buffer;

        var finalData = new byte[realSize];
        Array.Copy(buffer, finalData, realSize);

        return buffer;
    }
}