using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ServiceStack.Text;
using ServiceStack.VirtualPath;

namespace ServiceStack.IO
{
    public class MemoryVirtualFiles 
        : AbstractVirtualPathProviderBase, IVirtualFiles
    {
        public MemoryVirtualFiles()
        {
            this.files = new List<InMemoryVirtualFile>();
            this.rootDirectory = new InMemoryVirtualDirectory(this, null);
        }

        public const char DirSep = '/';

        private List<InMemoryVirtualFile> files;
        public List<InMemoryVirtualFile> Files => files;

        private readonly InMemoryVirtualDirectory rootDirectory;

        public override IVirtualDirectory RootDirectory => rootDirectory;

        public override string VirtualPathSeparator => "/";

        public override string RealPathSeparator => "/";

        protected override void Initialize() {}

        public override IVirtualFile GetFile(string virtualPath)
        {
            if (Files.Count == 0)
                return null;
                
            var filePath = SanitizePath(virtualPath);
            return Files.FirstOrDefault(x => x.FilePath == filePath);
        }

        public override IVirtualDirectory GetDirectory(string virtualPath) => GetDirectory(virtualPath, forceDir: false);

        public IVirtualDirectory GetDirectory(string virtualPath, bool forceDir)
        {
            var dirPath = SanitizePath(virtualPath);
            if (string.IsNullOrEmpty(dirPath))
                return rootDirectory;
            
            var dir = new InMemoryVirtualDirectory(this, dirPath, GetParentDirectory(dirPath));
            return forceDir || dir.HasFiles()
                ? dir
                : null;
        }

        public IVirtualDirectory GetParentDirectory(string dirPath)
        {
            if (string.IsNullOrEmpty(dirPath))
                return null;

            var lastDirPos = dirPath.LastIndexOf('/');
            if (lastDirPos >= 0)
            {
                var parentDir = dirPath.Substring(0, lastDirPos);
                if (!string.IsNullOrEmpty(parentDir))
                    return GetDirectory(parentDir, forceDir:true);
            }

            return this.rootDirectory;
        }

        public override bool DirectoryExists(string virtualPath)
        {
            return GetDirectory(virtualPath) != null;
        }

        private IVirtualDirectory CreateDirectory(string dirPath)
        {
            return new InMemoryVirtualDirectory(this, dirPath, GetParentDirectory(dirPath));
        }

        public void WriteFile(string filePath, string textContents)
        {
            filePath = SanitizePath(filePath);
            AddFile(new InMemoryVirtualFile(this, CreateDirectory(GetDirPath(filePath)))
            {
                FilePath = filePath,
                TextContents = textContents,
                FileLastModified = DateTime.UtcNow,
            });
        }

        public void WriteFile(string filePath, Stream stream)
        {
            filePath = SanitizePath(filePath);
            var byteContents = stream.ReadFully();
            AddFile(new InMemoryVirtualFile(this, CreateDirectory(GetDirPath(filePath)))
            {
                FilePath = filePath,
                ByteContents = byteContents,
                FileLastModified = DateTime.UtcNow,
            });
        }

        public void WriteFiles(IEnumerable<IVirtualFile> files, Func<IVirtualFile, string> toPath = null)
        {
            this.CopyFrom(files, toPath);
        }

        public void AppendFile(string filePath, string textContents)
        {
            filePath = SanitizePath(filePath);

            var existingFile = GetFile(filePath);
            var text = existingFile != null
                ? existingFile.ReadAllText() + textContents
                : textContents;

            DeleteFile(filePath);

            AddFile(new InMemoryVirtualFile(this, CreateDirectory(GetDirPath(filePath)))
            {
                FilePath = filePath,
                TextContents = text,
                FileLastModified = DateTime.UtcNow,
            });
        }

        public void AppendFile(string filePath, Stream stream)
        {
            filePath = SanitizePath(filePath);

            var existingFile = GetFile(filePath);
            var bytes = existingFile != null
                ? existingFile.ReadAllBytes().Combine(stream.ReadFully())
                : stream.ReadFully();

            DeleteFile(filePath);

            AddFile(new InMemoryVirtualFile(this, CreateDirectory(GetDirPath(filePath)))
            {
                FilePath = filePath,
                ByteContents = bytes,
                FileLastModified = DateTime.UtcNow,
            });
        }

        public void AddFile(InMemoryVirtualFile file)
        {
            List<InMemoryVirtualFile> snapshot, newFiles;
            do
            {
                snapshot = files;
                newFiles = new List<InMemoryVirtualFile>(files.Where(x => x.FilePath != file.FilePath)) {
                    file
                };
            } while (!ReferenceEquals(
                Interlocked.CompareExchange(ref files, newFiles, snapshot), snapshot));
        }

        public void DeleteFile(string filePath) => DeleteFiles(new[]{ filePath });

        public void DeleteFiles(IEnumerable<string> filePaths)
        {
            var sanitizedFilePaths = filePaths.Select(SanitizePath).ToSet();
            
            List<InMemoryVirtualFile> snapshot, newFiles;
            do
            {
                snapshot = files;
                newFiles = files.Where(x => !sanitizedFilePaths.Contains(x.FilePath)).ToList();
            } while (!ReferenceEquals(
                Interlocked.CompareExchange(ref files, newFiles, snapshot), snapshot));
        }

        public void DeleteFolder(string dirPath)
        {
            var subFiles = Files.Where(x => x.DirPath.StartsWith(dirPath));
            DeleteFiles(subFiles.Map(x => x.VirtualPath));            
        }

        public IEnumerable<InMemoryVirtualDirectory> GetImmediateDirectories(string fromDirPath)
        {
            if (Files.Count == 0)
                return TypeConstants<InMemoryVirtualDirectory>.EmptyArray;
            
            var dirPaths = Files
                .Map(x => x.DirPath)
                .Distinct()
                .Map(x => GetImmediateSubDirPath(fromDirPath, x))
                .Where(x => x != null)
                .Distinct();

            return dirPaths.Map(x => new InMemoryVirtualDirectory(this, x, GetParentDirectory(x)));
        }

        public IEnumerable<InMemoryVirtualFile> GetImmediateFiles(string fromDirPath)
        {
            if (Files.Count == 0)
                return TypeConstants<InMemoryVirtualFile>.EmptyArray;
            
            return Files.Where(x => x.DirPath == fromDirPath);
        }

        public override IEnumerable<IVirtualFile> GetAllFiles()
        {
            return Files;
        }

        public string GetDirPath(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
                return null;

            var lastDirPos = filePath.LastIndexOf(DirSep);
            return lastDirPos >= 0
                ? filePath.Substring(0, lastDirPos)
                : null;
        }

        public string GetImmediateSubDirPath(string fromDirPath, string subDirPath)
        {
            if (string.IsNullOrEmpty(subDirPath))
                return null;

            if (fromDirPath == null)
            {
                return subDirPath.CountOccurrencesOf(DirSep) == 0 
                    ? subDirPath
                    : subDirPath.LeftPart(DirSep);
            }

            if (!subDirPath.StartsWith(fromDirPath))
                return null;

            return fromDirPath.CountOccurrencesOf(DirSep) == subDirPath.CountOccurrencesOf(DirSep) - 1 
                ? subDirPath
                : null;
        }

        public void Clear() => Files.Clear();
    }

    public class InMemoryVirtualDirectory : AbstractVirtualDirectoryBase
    {
        private readonly MemoryVirtualFiles pathProvider;

        public InMemoryVirtualDirectory(MemoryVirtualFiles pathProvider, string dirPath, IVirtualDirectory parentDir=null) 
            : base(pathProvider, parentDir)
        {
            this.pathProvider = pathProvider;
            this.DirPath = dirPath;
        }
        
        public DateTime DirLastModified { get; set; }
        public override DateTime LastModified => DirLastModified;

        public override IEnumerable<IVirtualFile> Files => pathProvider.GetImmediateFiles(DirPath);

        public override IEnumerable<IVirtualDirectory> Directories => pathProvider.GetImmediateDirectories(DirPath);

        public string DirPath { get; set; }

        public override string VirtualPath => DirPath;

        public override string Name => DirPath?.LastRightPart(MemoryVirtualFiles.DirSep);

        public override IVirtualFile GetFile(string virtualPath)
        {
            return pathProvider.GetFile(DirPath.CombineWith(virtualPath));
        }

        public override IEnumerator<IVirtualNode> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        protected override IVirtualFile GetFileFromBackingDirectoryOrDefault(string fileName)
        {
            return GetFile(fileName);
        }

        protected override IEnumerable<IVirtualFile> GetMatchingFilesInDir(string globPattern)
        {
            if (pathProvider.Files.Count == 0)
                return TypeConstants<IVirtualFile>.EmptyArray;
            
            var matchingFilesInBackingDir = EnumerateFiles(globPattern);
            return matchingFilesInBackingDir;
        }

        public IEnumerable<InMemoryVirtualFile> EnumerateFiles(string pattern)
        {
            foreach (var file in pathProvider.GetImmediateFiles(DirPath).Where(f => f.Name.Glob(pattern)))
            {
                yield return file;
            }
        }

        protected override IVirtualDirectory GetDirectoryFromBackingDirectoryOrDefault(string directoryName)
        {
            var subDir = DirPath.CombineWith(directoryName);
            return new InMemoryVirtualDirectory(pathProvider, subDir, this);
        }

        public void AddFile(string filePath, string contents)
        {
            pathProvider.WriteFile(DirPath.CombineWith(filePath), contents);
        }

        public void AddFile(string filePath, Stream stream)
        {
            pathProvider.WriteFile(DirPath.CombineWith(filePath), stream);
        }

        public bool HasFiles()
        {
            if (pathProvider.Files.Count == 0)
                return false;
                
            if (IsRoot)
                return pathProvider.Files.Count > 0;
            
            var ret = pathProvider.Files.Any(x => x.DirPath != null && x.DirPath.StartsWith(DirPath));
            return ret;
        }

        public override IEnumerable<IVirtualFile> GetAllMatchingFiles(string globPattern, int maxDepth = int.MaxValue)
        {
            if (pathProvider.Files.Count == 0)
                return TypeConstants<IVirtualFile>.EmptyArray;
            
            if (IsRoot)
                return pathProvider.Files.Where(x => 
                    (x.DirPath == null || x.DirPath.CountOccurrencesOf('/') < maxDepth-1)
                    && x.Name.Glob(globPattern));
            
            return pathProvider.Files.Where(x => 
                x.DirPath != null
                && x.DirPath.CountOccurrencesOf('/') < maxDepth-1
                && x.DirPath.StartsWith(DirPath)
                && x.Name.Glob(globPattern));
        }

    }

    public class InMemoryVirtualFile : AbstractVirtualFileBase
    {

        public InMemoryVirtualFile(IVirtualPathProvider owningProvider, IVirtualDirectory directory) 
            : base(owningProvider, directory)
        {
            this.FileLastModified = DateTime.MinValue;            
        }

        public string DirPath => base.Directory.VirtualPath;

        public string FilePath { get; set; }

        public override string Name => FilePath.LastRightPart(MemoryVirtualFiles.DirSep);

        public override string VirtualPath => FilePath;

        public DateTime FileLastModified { get; set; }
        public override DateTime LastModified => FileLastModified;

        public override long Length => (ByteContents?.Length ?? 0);

        private string textContents;
        public string TextContents
        {
            get => textContents;
            set
            {
                textContents = value;
                ByteContents = value?.ToUtf8Bytes();
            }
        }

        public byte[] ByteContents { get; set; }

        public void SetContents(string text, byte[] bytes)
        {
            if (bytes != null)
            {
                this.textContents = null;
                this.ByteContents = bytes;
            }
            else
            {
                this.ByteContents = null;
                this.TextContents = text;
            }
        }

        public override Stream OpenRead()
        {
            if (ByteContents == null)
                throw new ArgumentNullException(nameof(ByteContents));
            return MemoryStreamFactory.GetStream(ByteContents);
        }

        public override object GetContents()
        {
            return TextContents != null 
                ? (object) TextContents.AsMemory() 
                : ByteContents != null ? new ReadOnlyMemory<byte>(ByteContents) : null;
        }

        public override byte[] ReadAllBytes() =>
            ByteContents ?? MemoryProvider.Instance.ToUtf8Bytes(TextContents.AsSpan());

        public override void Refresh()
        {
            if (base.VirtualPathProvider.GetFile(VirtualPath) is InMemoryVirtualFile file && !ReferenceEquals(file, this))
            {
                this.FilePath = file.FilePath;
                this.FileLastModified = file.FileLastModified;
                SetContents(file.TextContents, file.ByteContents);
            }
        }
    }
}