using System;
using System.Linq;
using System.IO;
using System.Collections.Generic;

namespace Tesses.Http.VFSCollection
{
    public static class Extensions 
    {
        public static void ExtractTessesArchive(this VirtualStorage storage,Stream strm,bool ownStream)
        {
             using(TArchiveReader reader=new TArchiveReader(strm,ownStream))
            {
                reader.ExtractEverything((t,n,len)=>{
                    if(t == EntryType.File)
                    {
                        string pardir = Path.GetDirectoryName(n.TrimStart('.').TrimStart('/'));
                        storage.CreateDirectory(pardir);
                        return storage.Open(pardir,FileMode.Create,FileAccess.Write,FileShare.None);
                    }
                    if(t == EntryType.Directory)
                    {
                        storage.CreateDirectory(n.TrimStart('.').TrimStart('/'));
                    }

                    return Stream.Null;
                });
            }
        }
    }
    public enum EntryType : byte
    {
            File=0b00000000,
            Directory=0b00000001,
            Symlink=0b00000010
    }
    public class TArchiveReader : IDisposable
{
    public delegate Stream ExtractEverythingDelegate(EntryType type,string name,long entLen);
    public delegate void DoneWriting(Stream strm,string name);
    public delegate void Symlink(string linkPath,string linkDest);
    Stream strm;
    bool own;
    public TArchiveReader(Stream strm,bool ownStream=false)
    {
        this.strm=strm;
        own=ownStream;
    }
    public TArchiveReader(string filename)
    {
        this.strm = File.OpenRead(filename);
        own =true;
    }
    private void ReadLittleEndian(byte[] arrayToFill)
    {
        strm.Read(arrayToFill,0,arrayToFill.Length);
       
        if(!BitConverter.IsLittleEndian) 
        {
            Array.Reverse(arrayToFill);
        }
    }
    private bool ReadEntryAttributes(out EntryType type, out string name,out long len)
    {
        //  strm.WriteByte((byte)type);
        //WriteString(name);
        //WriteLittleEndian(BitConverter.GetBytes(len));
        int t=strm.ReadByte();
        if(t == -1)
        {
            type=EntryType.File;
            name="ENDOFSTREAM.BIN";
            len=0;
            return false;
        }
        type=(EntryType)t;
        name=ReadString();
        byte[] len0 = new byte[8];
        ReadLittleEndian(len0);
        len=BitConverter.ToInt64(len0,0);
        return true;
    }

    private string ReadString()
    {
        byte[] len = new byte[4];
        ReadLittleEndian(len);
        byte[] strdat=new byte[BitConverter.ToInt32(len,0)];
        strm.Read(strdat,0,strdat.Length);
        return System.Text.Encoding.UTF8.GetString(strdat);
    }
    private void ReadAndDiscard(long l)
    {
        
        if(strm.CanSeek)
        {
            strm.Seek(l,SeekOrigin.Current);
        }else{
            _copyTo(Stream.Null,l);
        }
    }
    private void _copyTo(Stream strm,long len)
    {
        byte[] buff=new byte[1024];
         long pos=0;
            int read=0;
            do{
                read=(int)Math.Min(1024,len-pos);
                if(read == 0) return;
                read=this.strm.Read(buff,0,read);
                strm.Write(buff,0,read);
                pos+=read;
            }while(read != 0);
    }
   
    public void ExtractStartingWith(string archivePath,string outputDir,Symlink symlink=null)
    {
          Dictionary<string,List<string>> _f=new Dictionary<string, List<string>>(); 
        ExtractEverything((type,name,len)=>{
            if(name.StartsWith(archivePath)){
            switch(type)
            {
                case EntryType.File:
                    string fname=Path.Combine(outputDir,name);
                    string dirname = Path.GetDirectoryName(fname);
                    if(!string.IsNullOrWhiteSpace(dirname))
                    {
                        Directory.CreateDirectory(dirname);
                    }
                    return File.Create(fname);
                case EntryType.Directory:
                    string fname0=Path.Combine(outputDir,name);
                    Directory.CreateDirectory(fname0);
                    break;
            }
            }
            return Stream.Null;
        },(strm0,name)=>{strm0.Dispose();},symlink); 
    }
    public void Extract(string archivePath,Stream strm)
    {
        bool hasGone=false;;
        ExtractEverything((type,name,len)=>{
            if(hasGone) return null;
            if(type == EntryType.File && name==archivePath){
                hasGone=true;
                return strm;
            }
            return Stream.Null;
        });
    }
    
    public void ExtractEverything(string outputDir,Symlink symlink=null)
    {
        Dictionary<string,List<string>> _f=new Dictionary<string, List<string>>(); 
        ExtractEverything((type,name,len)=>{
            switch(type)
            {
                case EntryType.File:
                    string fname=Path.Combine(outputDir,name);
                    
                    string dirname = Path.GetDirectoryName(fname);
                    if(!string.IsNullOrWhiteSpace(dirname))
                    {
                        Directory.CreateDirectory(dirname);
                       
                    }
                    
                    return File.Create(fname);
                case EntryType.Directory:
                    string fname0=Path.Combine(outputDir,name);
                    Directory.CreateDirectory(fname0);
                    break;
            }
            return Stream.Null;
        },(strm0,name)=>{strm0.Dispose();},symlink); 
    }
    public void ExtractEverything(ExtractEverythingDelegate del,DoneWriting doneWriting=null,Symlink symlink=null)
    {
        if(del == null) return;
        if(hasRead && strm.CanSeek)
        {
            strm.Seek(0,SeekOrigin.Begin);
        }
        if(hasRead && !strm.CanSeek)
        {
            throw new Exception("Can't Seek Stream");
        }else{
            bool read=false;
            do{
                EntryType type;
                string name;
                long len;
                read=ReadEntryAttributes(out type,out name,out len); 
                if(read)
                {
                    if(type == EntryType.Symlink)
                    {
                        MemoryStream strm=new MemoryStream();
                        _copyTo(strm,len);
                        using(StreamReader rdr=new StreamReader(strm))
                        {
                           string res= rdr.ReadToEnd();
                           symlink?.Invoke(name,res);
                        }

                    }else{
                    Stream strm=del(type,name,len);
                    if(strm == Stream.Null)
                    {
                        if(len>0)
                        ReadAndDiscard(len);
                    }
                    else if(strm != null)
                    {
                        _copyTo(strm,len);
                        if(doneWriting != null)
                        {
                            doneWriting(strm,name);
                        }else{
                            strm.Close();
                        }
                    }else{
                        hasRead=true;
                        return;
                    }
                }}
            }while(read);

            hasRead=true;
        }

    }
    bool hasRead=false;
    public void Dispose()
    {
        if(own) strm.Dispose();
    }
}   


    public class MemoryStorage : VirtualStorage
    {
        protected override void DeleteEmptyDirectory(string dir)
        {
            string[] _p=dir.Split('/');
            if(_p.Length < 1) return;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }

            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                if(!(dir0.Entries[_p[_p.Length-1]].IsFile))
                dir0.Entries.Remove(_p[_p.Length-1]);
            }
        }
        public override string ActualPath => "mem:///";
        public override void SetCreationTime(string filename, DateTime time)
        {
             string[] _p=filename.Split('/');
            if(_p.Length < 1) return;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                dir0.Entries[_p[_p.Length]].Created=time;
            }
        }
        public override void SetLastAccessTime(string filename, DateTime time)
        {
             string[] _p=filename.Split('/');
            if(_p.Length < 1) return;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                dir0.Entries[_p[_p.Length]].LastAccess=time;
            }
        }
        public override void SetLastWriteTime(string filename, DateTime time)
        {
             string[] _p=filename.Split('/');
            if(_p.Length < 1) return;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                dir0.Entries[_p[_p.Length]].LastWrite=time;
            }
        }
        public override DateTime GetCreationTime(string filename)
        {
             string[] _p=filename.Split('/');
            if(_p.Length < 1) return DateTime.Now;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                return dir0.Entries[_p[_p.Length]].Created;
            }else{
                return DateTime.Now;
            }
        }
         public override DateTime GetLastWriteTime(string filename)
        {
             string[] _p=filename.Split('/');
            if(_p.Length < 1) return DateTime.Now;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                return dir0.Entries[_p[_p.Length]].LastWrite;
            }else{
                return DateTime.Now;
            }
        }
         public override DateTime GetLastAccessTime(string filename)
        {
             string[] _p=filename.Split('/');
            if(_p.Length < 1) return DateTime.Now;
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            if(dir0.Entries.ContainsKey(_p[_p.Length-1]))
            {
                return dir0.Entries[_p[_p.Length]].LastAccess;
            }else{
                return DateTime.Now;
            }
        }
        private abstract class MemoryEntry 
        {
            public abstract bool IsFile {get;}
            
            
            public DateTime Created {get;set;}
            public DateTime LastWrite {get;set;}
            public DateTime LastAccess {get;set;}
        }
        private class MemoryFile : MemoryEntry
        {
            public MemoryFile()
            {
                LastAccess=DateTime.Now;
                LastWrite=DateTime.Now;
                Created=DateTime.Now;
            }
            public bool HasWriteAccess=false;
            public override bool IsFile => true;
            public byte[] Data=new byte[0];

            public void Overwrite(byte[] data)
            {
                lock(this)
                {
                    Data=data;
                    LastWrite = DateTime.Now;
                    
                }
            }
        }
        private class MemoryStream2 : MemoryStream
        {
            MemoryFile _f;
            bool _canWrite;
            
            public MemoryStream2(MemoryFile file,bool canWrite) : base(file.Data.ToArray(),canWrite) //copies data into file
            {
                file.LastAccess = DateTime.Now;
                _f=file;
                _canWrite=canWrite;
                if(file.HasWriteAccess && _canWrite)
                {
                    throw new IOException("MemoryStorage doesn't support simulatious writers");
                }
                if(!file.HasWriteAccess)
                    file.HasWriteAccess=_canWrite;
            }
            
            public override void Close()
            {
                if(_canWrite)
                {
                    _f.Overwrite(this.ToArray());
                    _f.HasWriteAccess=false;
                }    
                base.Close();
            }
        }
        private class MemoryDirectory : MemoryEntry
        {
           
            public MemoryDirectory()
            {
                Created = DateTime.Now;
                LastWrite=DateTime.Now;
                LastAccess=DateTime.Now;
                Entries=new Dictionary<string, MemoryEntry>();
                
            }
            
            public override bool IsFile => false;
            public Dictionary<string,MemoryEntry> Entries {get;set;}
        }
        MemoryDirectory root=new MemoryDirectory();
        public static MemoryStorage FromTessesArchive(Stream strm,bool ownStream)
        {
            MemoryStorage storage=new MemoryStorage();
            storage.ExtractTessesArchive(strm,ownStream);
            //public delegate Stream ExtractEverythingDelegate(EntryType type,string name,long entLen);

            

            return storage;
        }
        private MemoryDirectory _create(MemoryDirectory dir,string p)
        {
            if(!dir.Entries.ContainsKey(p))
            {
                dir.Entries.Add(p,new MemoryDirectory());
            }
            var dir2= dir.Entries[p] as MemoryDirectory;
            return dir2;
        }
        private MemoryEntry _get(MemoryDirectory dir,string p)
        {
            if(!dir.Entries.ContainsKey(p))
            {
                return null;
            }
            
            return dir.Entries[p];
        }
        public override void CreateDirectory(string path)
        {
            string[] _p=path.Split('/');
            var dir = root;
            List<string> __c=new List<string>();
            foreach(var item in _p)
            {
                 dir= _create(root,item);
                 __c.Add(item);
                 if(dir == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
        }

        public override void Delete(string file)
        {
            string[] _p=file.Split('/');
            if(_p.Length < 1) return;
            List<string> __c=new List<string>();
            MemoryDirectory dir=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir=_get(dir,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }

            if(dir.Entries.ContainsKey(_p[_p.Length-1]))
            {
                if(dir.Entries[_p[_p.Length-1]].IsFile)
                dir.Entries.Remove(_p[_p.Length-1]);
            }
        }

        public override bool DirectoryExists(string filename)
        {
            string[] _p=filename.Split('/');
            if(_p.Length < 1) return false;
            List<string> __c=new List<string>();
            MemoryDirectory dir=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir=_get(dir,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }

            if(dir.Entries.ContainsKey(_p[_p.Length-1]))
            {
                return !(dir.Entries[_p[_p.Length-1]].IsFile);
            }
            return false;
        }

        public override IEnumerable<string> EnumerateDirectories(string dir)
        {
            string[] _p=dir.Split('/');
            
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p)
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            
            foreach(var items in dir0.Entries)
            {
                if(!items.Value.IsFile)
                yield return items.Key;
            }
            
            
        }

        public override IEnumerable<string> EnumerateFiles(string dir)
        {
            
            string[] _p=dir.Split('/');
            
            List<string> __c=new List<string>();
            MemoryDirectory dir0=root;
            foreach(var item in _p)
            {
                dir0=_get(dir0,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir0 == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }
            
            foreach(var items in dir0.Entries)
            {
                if(items.Value.IsFile)
                yield return items.Key;
            }
            
            
        

      
        }

        public override bool FileExists(string filename)
        {
            string[] _p=filename.Split('/');
            if(_p.Length < 1) return false;
            List<string> __c=new List<string>();
            MemoryDirectory dir=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir=_get(dir,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir == null)
                 {
                     throw new Exception($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }

            if(dir.Entries.ContainsKey(_p[_p.Length-1]))
            {
                return (dir.Entries[_p[_p.Length-1]].IsFile);
            }
            return false;
        }

        public override Stream Open(string file, FileMode mode, FileAccess access, FileShare share)
        {
            string[] _p=file.Split('/');
            if(_p.Length < 1) throw new IOException("Path empty");
            List<string> __c=new List<string>();
            MemoryDirectory dir=root;
            foreach(var item in _p.Take(_p.Length -1))
            {
                dir=_get(dir,item) as MemoryDirectory;
                __c.Add(item);
                 if(dir == null)
                 {
                     throw new IOException($"The path {PathCombine(__c)} is a file, should be a directory");
                 }
            }

            //we will now try to get the file
            string filename=_p[_p.Length -1];
            if(dir.Entries.ContainsKey(filename))
            {
                if(!dir.Entries[filename].IsFile) throw new IOException("Not a file");
                //the file exists
                if(mode == FileMode.CreateNew) throw new IOException("File Exists");
                
                var _file= dir.Entries[filename] as MemoryFile;
                lock(_file)
                {
                    if(mode == FileMode.Create) {
                        _file.Data=new byte[0];
                    }
                    //we need to open file
                    MemoryStream2 strm=new MemoryStream2(_file,access != FileAccess.Read);
                    
                    if(mode != FileMode.Truncate)
                    {
                       strm.Position=0;
                    }else{
                         strm.Position=strm.Length;
                    }
                    return strm;
                }        

            }else{
                 if(mode == FileMode.Open) throw new IOException("File Doesn't Exist");
                 MemoryFile _file=new MemoryFile(); 
                 dir.Entries.Add(filename,_file);

                 lock(_file)
                 {
                     MemoryStream2 strm=new MemoryStream2(_file,access != FileAccess.Read);
                     return strm;
                 }

                //the file doesnt exist
            }

        }
    }

    public class SSHStorage : VirtualStorage
    {
        public override void CreateDirectory(string path)
        {
            throw new NotImplementedException();
        }

        public override void Delete(string file)
        {
            throw new NotImplementedException();
        }

        public override bool DirectoryExists(string filename)
        {
            throw new NotImplementedException();
        }

        public override IEnumerable<string> EnumerateDirectories(string dir)
        {
            throw new NotImplementedException();
        }

        public override IEnumerable<string> EnumerateFiles(string dir)
        {
            throw new NotImplementedException();
        }

        public override bool FileExists(string filename)
        {
            throw new NotImplementedException();
        }

        public override Stream Open(string file, FileMode mode, FileAccess access, FileShare share)
        {
            throw new NotImplementedException();
        }
    }
}
