// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.StaticWebAssets
{
    internal sealed class ManifestStaticWebAssetFileProvider : IFileProvider
    {
        private static readonly StringComparison _fsComparison = OperatingSystem.IsWindows() ?
            StringComparison.OrdinalIgnoreCase :
            StringComparison.Ordinal;

        private static readonly IEqualityComparer<IFileInfo> _nameComparer = new FileNameComparer();

        private readonly IFileProvider[] _fileProviders;
        private readonly StaticWebAssetNode _root;

        public ManifestStaticWebAssetFileProvider(StaticWebAssetManifest manifest, Func<string, IFileProvider> fileProviderFactory)
        {
            _fileProviders = new IFileProvider[manifest.ContentRoots.Length];

            for (int i = 0; i < manifest.ContentRoots.Length; i++)
            {
                _fileProviders[i] = fileProviderFactory(manifest.ContentRoots[i]);
            }

            _root = manifest.Root;
        }

        // For testing purposes only
        internal IFileProvider[] FileProviders => _fileProviders;

        public IDirectoryContents GetDirectoryContents(string subpath)
        {
            if (subpath == null)
            {
                throw new ArgumentNullException(nameof(subpath));
            }

            var segments = Normalize(subpath).Split('/', StringSplitOptions.RemoveEmptyEntries);
            var candidate = _root;

            // Iterate over the path segments until we reach the destination. Whenever we encounter
            // a pattern, we start tracking it as well as the content root directory. On every segment
            // we evalutate the directory to see if there is a subfolder with the current segment and
            // replace it on the dictionary if it exists or remove it if it does not.
            // When we reach our destination we enumerate the files in that folder and evalute them against
            // the pattern to compute the final list.
            HashSet<IFileInfo>? files = null;
            for (var i = 0; i < segments.Length; i++)
            {
                files = GetFilesForCandidatePatterns(segments, candidate, files);

                if (candidate.HasChildren() && candidate.Children.TryGetValue(segments[i], out var child))
                {
                    candidate = child;
                }
                else
                {
                    candidate = null;
                    break;
                }
            }

            if ((candidate == null || (!candidate.HasChildren() && !candidate.HasPatterns())) && files == null)
            {
                return NotFoundDirectoryContents.Singleton;
            }
            else
            {
                // We do this to make sure we account for the patterns on the last segment which are not covered by the loop above
                files = GetFilesForCandidatePatterns(segments, candidate, files);
                if (candidate != null && candidate.HasChildren())
                {
                    files ??= new(_nameComparer);
                    GetCandidateFilesForNode(candidate, files);
                }

                return new StaticWebAssetsDirectoryContents((files as IEnumerable<IFileInfo>) ?? Array.Empty<IFileInfo>());
            }

            HashSet<IFileInfo>? GetFilesForCandidatePatterns(string[] segments, StaticWebAssetNode? candidate, HashSet<IFileInfo>? files)
            {
                if (candidate != null && candidate.HasPatterns())
                {
                    var depth = candidate.Patterns[0].Depth;
                    var candidateDirectoryPath = string.Join('/', segments[depth..]);
                    foreach (var pattern in candidate.Patterns)
                    {
                        var contentRoot = _fileProviders[pattern.ContentRoot];
                        var matcher = new Matcher(_fsComparison);
                        matcher.AddInclude(pattern.Pattern);
                        foreach (var result in contentRoot.GetDirectoryContents(candidateDirectoryPath))
                        {
                            var fileCandidate = string.IsNullOrEmpty(candidateDirectoryPath) ? result.Name : $"{candidateDirectoryPath}/{result.Name}";
                            if (result.Exists && (result.IsDirectory || matcher.Match(fileCandidate).HasMatches))
                            {
                                files ??= new(_nameComparer);
                                if (!files.Contains(result))
                                {
                                    // Multiple patterns might match the same file (even at different locations on disk) at runtime. We don't
                                    // try to disambiguate anything here, since there is already a build step for it. We just pick the first
                                    // file that matches the pattern. The manifest entries are ordered, so while this choice is random, it is
                                    // nonetheless deterministic.
                                    files.Add(result);
                                }
                            }
                        }
                    }
                }

                return files;
            }

            void GetCandidateFilesForNode(StaticWebAssetNode candidate, HashSet<IFileInfo> files)
            {
                foreach (var child in candidate.Children!)
                {
                    var match = child.Value.Match;
                    if (match == null)
                    {
                        // This is a folder
                        var file = new StaticWebAssetsDirectoryInfo(child.Key);
                        // Entries from the manifest always win over any content based on patterns,
                        // so remove any potentially existing file or folder in favor of the manifest
                        // entry.
                        files.Remove(file);
                        files.Add(file);
                    }
                    else
                    {
                        // This is a file.
                        files.RemoveWhere(f => string.Equals(match.Path, f.Name, _fsComparison));
                        var file = _fileProviders[match.ContentRoot].GetFileInfo(match.Path);

                        files.Add(string.Equals(child.Key, match.Path, _fsComparison) ? file :
                            // This means that this file was mapped, there is a chance that we added it to the list
                            // of files by one of the patterns, so we need to replace it with the mapped file.
                            new StaticWebAssetsFileInfo(child.Key, file));
                    }
                }
            }
        }

        private string Normalize(string path)
        {
            return path.Replace('\\', '/');
        }

        public IFileInfo GetFileInfo(string subpath)
        {
            if (subpath == null)
            {
                throw new ArgumentNullException(nameof(subpath));
            }

            var segments = subpath.Split('/', StringSplitOptions.RemoveEmptyEntries);
            StaticWebAssetNode? candidate = _root;
            List<StaticWebAssetPattern>? patterns = null;

            // Iterate over the path segments until we reach the destination, collecting
            // all pattern candidates along the way except for any pattern at the root.
            for (var i = 0; i < segments.Length; i++)
            {
                if (candidate.HasPatterns())
                {
                    patterns ??= new();
                    patterns.AddRange(candidate.Patterns);
                }
                if (candidate.HasChildren() && candidate.Children.TryGetValue(segments[i], out var child))
                {
                    candidate = child;
                }
                else
                {
                    candidate = null;
                    break;
                }
            }

            var match = candidate?.Match;
            if (match != null)
            {
                // If we found a file, that wins over anything else. If there are conflicts with files added after
                // we've built the manifest, we'll be notified the next time we do a build. This is not different
                // from previous Static Web Assets versions.
                var file = _fileProviders[match.ContentRoot].GetFileInfo(match.Path);
                if (!file.Exists || string.Equals(subpath, Normalize(match.Path), _fsComparison))
                {
                    return file;
                }
                else
                {
                    return new StaticWebAssetsFileInfo(segments[^1], file);
                }
            }

            // The list of patterns is ordered by pattern depth, so we compute the string to check for patterns only
            // once per level. We don't aim to solve conflicts here where multiple files could match a given path,
            // we have a build check that takes care of that.
            var currentDepth = -1;
            var candidatePath = subpath;

            if (patterns != null)
            {
                for (var i = 0; i < patterns.Count; i++)
                {
                    var pattern = patterns[i];
                    if (pattern.Depth != currentDepth)
                    {
                        currentDepth = pattern.Depth;
                        candidatePath = string.Join('/', segments[currentDepth..]);
                    }

                    var result = _fileProviders[pattern.ContentRoot].GetFileInfo(candidatePath);
                    if (result.Exists)
                    {
                        if (!result.IsDirectory)
                        {
                            var matcher = new Matcher();
                            matcher.AddInclude(pattern.Pattern);
                            if (!matcher.Match(candidatePath).HasMatches)
                            {
                                continue;
                            }

                            return result;
                        }
                    }
                }
            }

            return new NotFoundFileInfo(subpath);
        }

        public IChangeToken Watch(string filter) => NullChangeToken.Singleton;

        private sealed class StaticWebAssetsDirectoryContents : IDirectoryContents
        {
            private readonly IEnumerable<IFileInfo> _files;

            public StaticWebAssetsDirectoryContents(IEnumerable<IFileInfo> files) =>
                _files = files;

            public bool Exists => true;

            public IEnumerator<IFileInfo> GetEnumerator() => _files.GetEnumerator();

            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        }

        private sealed class StaticWebAssetsDirectoryInfo : IFileInfo
        {
            private static readonly DateTimeOffset _lastModified = DateTimeOffset.FromUnixTimeSeconds(0);

            public StaticWebAssetsDirectoryInfo(string name)
            {
                Name = name;
            }

            public bool Exists => true;

            public long Length => 0;

            public string? PhysicalPath => null;

            public DateTimeOffset LastModified => _lastModified;

            public bool IsDirectory => true;

            public string Name { get; }

            public Stream CreateReadStream() => throw new InvalidOperationException("Can not create a stream for a directory.");
        }

        private sealed class StaticWebAssetsFileInfo : IFileInfo
        {
            private readonly IFileInfo _source;

            public StaticWebAssetsFileInfo(string name, IFileInfo source)
            {
                Name = name;
                _source = source;
            }
            public bool Exists => _source.Exists;

            public long Length => _source.Length;

            public string PhysicalPath => _source.PhysicalPath;

            public DateTimeOffset LastModified => _source.LastModified;

            public bool IsDirectory => _source.IsDirectory;

            public string Name { get; }

            public Stream CreateReadStream() => _source.CreateReadStream();
        }

        private sealed class FileNameComparer : IEqualityComparer<IFileInfo>
        {
            public bool Equals(IFileInfo? x, IFileInfo? y) => string.Equals(x?.Name, y?.Name, _fsComparison);

            public int GetHashCode(IFileInfo obj) => obj.Name.GetHashCode(_fsComparison);
        }

        internal sealed class StaticWebAssetManifest
        {
            internal static readonly StringComparer PathComparer =
                OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

            public string[] ContentRoots { get; set; } = Array.Empty<string>();

            public StaticWebAssetNode Root { get; set; } = null!;

            internal static StaticWebAssetManifest Parse(Stream manifest)
            {
                return JsonSerializer.Deserialize<StaticWebAssetManifest>(manifest)!;
            }
        }

        internal sealed class StaticWebAssetNode
        {
            [JsonPropertyName("Asset")]
            public StaticWebAssetMatch? Match { get; set; }

            [JsonConverter(typeof(OSBasedCaseConverter))]
            public Dictionary<string, StaticWebAssetNode>? Children { get; set; }

            public StaticWebAssetPattern[]? Patterns { get; set; }

            [MemberNotNullWhen(true, nameof(Children))]
            internal bool HasChildren() => Children != null && Children.Count > 0;

            [MemberNotNullWhen(true, nameof(Patterns))]
            internal bool HasPatterns() => Patterns != null && Patterns.Length > 0;
        }

        internal sealed class StaticWebAssetMatch
        {
            [JsonPropertyName("ContentRootIndex")]
            public int ContentRoot { get; set; }

            [JsonPropertyName("SubPath")]
            public string Path { get; set; } = null!;
        }

        internal sealed class StaticWebAssetPattern
        {
            [JsonPropertyName("ContentRootIndex")]
            public int ContentRoot { get; set; }

            public int Depth { get; set; }

            public string Pattern { get; set; } = null!;
        }

        private sealed class OSBasedCaseConverter : JsonConverter<Dictionary<string, StaticWebAssetNode>>
        {
            public override Dictionary<string, StaticWebAssetNode> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                var parsed = JsonSerializer.Deserialize<IDictionary<string, StaticWebAssetNode>>(ref reader, options)!;
                var result = new Dictionary<string, StaticWebAssetNode>(StaticWebAssetManifest.PathComparer);
                MergeChildren(parsed, result);
                return result;

                static void MergeChildren(
                    IDictionary<string, StaticWebAssetNode> newChildren,
                    IDictionary<string, StaticWebAssetNode> existing)
                {
                    foreach (var (key, value) in newChildren)
                    {
                        if (!existing.TryGetValue(key, out var existingNode))
                        {
                            existing.Add(key, value);
                        }
                        else
                        {
                            if (value.Patterns != null)
                            {
                                if (existingNode.Patterns == null)
                                {
                                    existingNode.Patterns = value.Patterns;
                                }
                                else
                                {
                                    if (value.Patterns.Length > 0)
                                    {
                                        var newList = new StaticWebAssetPattern[existingNode.Patterns.Length + value.Patterns.Length];
                                        existingNode.Patterns.CopyTo(newList, 0);
                                        value.Patterns.CopyTo(newList, existingNode.Patterns.Length);
                                        existingNode.Patterns = newList;
                                    }
                                }
                            }

                            if (value.Children != null)
                            {
                                if (existingNode.Children == null)
                                {
                                    existingNode.Children = value.Children;
                                }
                                else
                                {
                                    if (value.Children.Count > 0)
                                    {
                                        MergeChildren(value.Children, existingNode.Children);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            public override void Write(Utf8JsonWriter writer, Dictionary<string, StaticWebAssetNode> value, JsonSerializerOptions options)
            {
                JsonSerializer.Serialize(writer, value, options);
            }
        }
    }
}
