using Org.BouncyCastle.Crypto.Digests;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using static PSVIMGTOOLS.SceIoStat;

namespace PSVIMGTOOLS
{
    class PSVIMGBuilder
    {
        private byte[] IV = new byte[0x10];
        private byte[] KEY;
        private Random rnd = new Random();
        private Stream mainStream;
        private Sha256Digest shaCtx;
        private byte[] blockData;
        private MemoryStream blockStream;
        private long contentSize = 0;


        //async
        private int blocksWritten = 0;
        private bool finished = false;
        
        public Int64 ContentSize
        {
            get
            {
                return contentSize;
            }
        }
        public Int32 BlocksWritten
        {
            get
            {
                return blocksWritten;
            }
        }

        public Boolean HasFinished
        {
            get
            {
                return finished;
            }
        }

        //Footer
        private long totalBytes = 0;

        private byte[] aes_cbc_encrypt(byte[] plainText, byte[] IV, byte[] KEY, int size=-1)
        {
            if (size < 0)
            {
                size = plainText.Length;
            }

            MemoryStream ms = new MemoryStream();
           /*
            *- DEBUG Disable Encryption
            ms.Write(plainText, 0x00, size);
            ms.Seek(0x00,SeekOrigin.Begin);
            return ms.ToArray();*/

            Aes alg = Aes.Create();
            alg.Mode = CipherMode.CBC;
            alg.Padding = PaddingMode.None;
            alg.KeySize = 256;
            alg.BlockSize = 128;
            alg.Key = KEY;
            alg.IV = IV;
            CryptoStream cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write);
            cs.Write(plainText, 0, size);
            cs.Close();
            byte[] cipherText = ms.ToArray();
            return cipherText;
        }

        private byte[] aes_ecb_encrypt(byte[] plainText, byte[] KEY, int size = -1)
        {
            if (size < 0)
            {
                size = plainText.Length;
            }

            MemoryStream ms = new MemoryStream();
            /*
             *- DEBUG Disable Encryption
             ms.Write(plainText, 0x00, size);
             ms.Seek(0x00,SeekOrigin.Begin);
             return ms.ToArray();*/

            Aes alg = Aes.Create();
            alg.Mode = CipherMode.ECB;
            alg.Padding = PaddingMode.None;
            alg.KeySize = 256;
            alg.BlockSize = 128;
            alg.Key = KEY;
            CryptoStream cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write);
            cs.Write(plainText, 0, size);
            cs.Close();
            byte[] cipherText = ms.ToArray();
            return cipherText;
        }

        private void writeUInt64(Stream dst,UInt64 value)
        {
            byte[] ValueBytes = BitConverter.GetBytes(value);
            dst.Write(ValueBytes, 0x00, 0x8);
        }
        private void writeInt64(Stream dst,Int64 value)
        {
            byte[] ValueBytes = BitConverter.GetBytes(value);
            dst.Write(ValueBytes, 0x00, 0x8);
        }
        private void writeUInt16(Stream dst, UInt16 value)
        {
            byte[] ValueBytes = BitConverter.GetBytes(value);
            dst.Write(ValueBytes, 0x00, 0x2);
        }
        private void writeInt16(Stream dst, Int16 value)
        {
            byte[] ValueBytes = BitConverter.GetBytes(value);
            dst.Write(ValueBytes, 0x00, 0x2);
        }

        private void writeInt32(Stream dst, Int32 value)
        {
            byte[] ValueBytes = BitConverter.GetBytes(value);
            dst.Write(ValueBytes, 0x00, 0x4);
        }
        private void writeUInt32(Stream dst, UInt32 value)
        {
            byte[] ValueBytes = BitConverter.GetBytes(value);
            dst.Write(ValueBytes, 0x00, 0x4);
        }

        private SceDateTime dateTimeToSceDateTime(DateTime dt)
        {
            SceDateTime sdt  = new SceDateTime();
            sdt.Day = Convert.ToUInt16(dt.Day);
            sdt.Month = Convert.ToUInt16(dt.Month);
            sdt.Year = Convert.ToUInt16(dt.Year);


            sdt.Hour = Convert.ToUInt16(dt.Hour);
            sdt.Minute = Convert.ToUInt16(dt.Minute);
            sdt.Second = Convert.ToUInt16(dt.Second);
            sdt.Microsecond = Convert.ToUInt32(dt.Millisecond * 1000);
            return sdt;
        }

        private SceIoStat sceIoStat(string path)
        {
            SceIoStat stats = new SceIoStat();
            FileAttributes attrbutes = File.GetAttributes(path);
            
            if (attrbutes.HasFlag(FileAttributes.Directory))
            {
                stats.Mode |= Modes.Directory;
                stats.Size = 0;
            }
            else
            {
                stats.Mode |= Modes.File;
                stats.Size = Convert.ToUInt64(new FileInfo(path).Length);
                
            }
            
            if(attrbutes.HasFlag(FileAttributes.ReadOnly))
            {
                stats.Mode |= Modes.GroupRead;
                
                stats.Mode |= Modes.OthersRead;

                stats.Mode |= Modes.UserRead;
            }
            else
            {
                stats.Mode |= Modes.GroupRead;
                stats.Mode |= Modes.GroupWrite;
                
                stats.Mode |= Modes.OthersRead;
                stats.Mode |= Modes.OthersWrite;

                stats.Mode |= Modes.UserRead;
                stats.Mode |= Modes.UserWrite;
            }

            stats.CreationTime = dateTimeToSceDateTime(File.GetCreationTimeUtc(path));
            stats.AccessTime = dateTimeToSceDateTime(File.GetLastAccessTimeUtc(path));
            stats.ModificaionTime = dateTimeToSceDateTime(File.GetLastWriteTimeUtc(path));
            
            return stats;
        }

        private void writeSceDateTime(Stream dst,SceDateTime time)
        {
            writeUInt16(dst, time.Year);
            writeUInt16(dst, time.Month);
            writeUInt16(dst, time.Day);

            writeUInt16(dst, time.Hour);
            writeUInt16(dst, time.Minute);
            writeUInt16(dst, time.Second);
            writeUInt32(dst, time.Microsecond);
        }

        private void writeSceIoStat(Stream dst, SceIoStat stats)
        {
            writeUInt32(dst, Convert.ToUInt32(stats.Mode));
            writeUInt32(dst, Convert.ToUInt32(stats.Attributes));
            writeUInt64(dst, stats.Size);
            writeSceDateTime(dst, stats.CreationTime);
            writeSceDateTime(dst, stats.AccessTime);
            writeSceDateTime(dst, stats.ModificaionTime);
            foreach(UInt32 i in stats.Private)
            {
                writeUInt32(dst,i);
            }
        }

        private void memset(byte[] buf, byte content, long length)
        {
            for(int i = 0; i < length; i++)
            {
                buf[i] = content;
            }
        }

        private void writeStringWithPadding(Stream dst, string str, int padSize, byte padByte = 0x78)
        {
            int StrLen = str.Length;
            if(StrLen > padSize)
            {
                StrLen = padSize;
            }

            int PaddingLen = (padSize - StrLen)-1;
            writeString(dst, str, StrLen);
            dst.WriteByte(0x00);
            writePadding(dst, padByte, PaddingLen);
        }

        private void writeString(Stream dst, string str, int len=-1)
        {
            if(len < 0)
            {
                len = str.Length;
            }

            byte[] StrBytes = Encoding.UTF8.GetBytes(str);
            dst.Write(StrBytes, 0x00, len);
        }

        private void writePadding(Stream dst, byte paddingByte, long paddingLen)
        {
            byte[] paddingData = new byte[paddingLen];
            memset(paddingData, paddingByte, paddingLen);
            dst.Write(paddingData, 0x00, paddingData.Length);
        }
        private byte[] getHeader(string FilePath, string ParentPath, string PathRel)
        {
            using (MemoryStream Header = new MemoryStream())
            {
                writeInt64(Header, DateTime.UtcNow.Ticks); // SysTime
                writeInt64(Header, 0); // Flags
                writeSceIoStat(Header, sceIoStat(FilePath));
                writeStringWithPadding(Header, ParentPath, 256); // Parent Path
                writeUInt32(Header, 1); //unk_16C
                writeStringWithPadding(Header, PathRel, 256); //Relative Path
                writePadding(Header, 0x78, 904); //'x'
                writeString(Header, PSVIMGConstants.PSVIMG_HEADER_END); //EndOfHeader
                Header.Seek(0x00, SeekOrigin.Begin);
                return Header.ToArray();
            }       
        }

        private void startNewBlock()
        {
            blockData = new byte[PSVIMGConstants.FULL_PSVIMG_SIZE];
            blockStream = new MemoryStream(blockData, 0x00, PSVIMGConstants.FULL_PSVIMG_SIZE);
        }


        private byte[] shaBlock(int length = PSVIMGConstants.PSVIMG_BLOCK_SIZE,bool final=false)
        {
            byte[] outbytes = new byte[PSVIMGConstants.SHA256_BLOCK_SIZE];
            shaCtx.BlockUpdate(blockData, 0x00, length);
            Sha256Digest shaTmp = (Sha256Digest)shaCtx.Copy();
            shaTmp.DoFinal(outbytes,0x00);
            return outbytes;
        }

        private void finishBlock(bool final = false)
        {
            int len = Convert.ToInt32(blockStream.Position);
            byte[] shaBytes = shaBlock(len, final);
            blockStream.Write(shaBytes, 0x00, PSVIMGConstants.SHA256_BLOCK_SIZE);
            len += PSVIMGConstants.SHA256_BLOCK_SIZE;
            
            //Get next IV
            byte[] encryptedBlock = aes_cbc_encrypt(blockData, IV, KEY, len);
            for (int i = 0; i < IV.Length; i++)
            {
                int encBlockOffset = (encryptedBlock.Length - IV.Length)+i;
                IV[i] = encryptedBlock[encBlockOffset];
            }

            mainStream.Write(encryptedBlock, 0x00, encryptedBlock.Length);
            totalBytes += encryptedBlock.Length;

            blockStream.Dispose();
        }

        private int remainingBlockSize()
        {
            return Convert.ToInt32((PSVIMGConstants.PSVIMG_BLOCK_SIZE - blockStream.Position));
        }

        private void writeBlock(byte[] data, bool update=false)
        {
            long dLen = data.Length;
            long writeTotal = 0;
            while (dLen > 0)
            {
                int remaining = remainingBlockSize();

                if (dLen > remaining)
                {
                    byte[] dataRemains = new byte[remaining];
                    Array.Copy(data, writeTotal, dataRemains, 0, remaining);
                    blockStream.Write(dataRemains, 0x00, remaining);

                    writeTotal += remaining;
                    dLen -= remaining;
                    

                    finishBlock();
                    startNewBlock();
                    if (update)
                    {
                        blocksWritten += 1;
                    }
                }
                else
                {
                    byte[] dataRemains = new byte[dLen];
                    Array.Copy(data, writeTotal, dataRemains, 0, dLen);
                    blockStream.Write(dataRemains, 0x00, Convert.ToInt32(dLen));
                    
                    writeTotal += dLen;
                    dLen -= dLen;
                }
            }
        }
        private byte[] getPadding(long size)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                long paddingSize = PSVIMGPadding.GetPadding(size);
                if(paddingSize != 0)
                {
                    writePadding(ms, 0x2B, paddingSize-PSVIMGConstants.PSVIMG_PADDING_END.Length);
                    writeString(ms, PSVIMGConstants.PSVIMG_PADDING_END);
                }
                ms.Seek(0x00, SeekOrigin.Begin);
                return ms.ToArray();
            }
        }

        private byte[] getTailer()
        {
            using (MemoryStream ms = new MemoryStream())
            {
                writeUInt64(ms, 0x00);
                writePadding(ms, 0x7a, 1004);
                writeString(ms, PSVIMGConstants.PSVIMG_TAILOR_END);

                ms.Seek(0x00, SeekOrigin.Begin);
                return ms.ToArray();
            }
        }

        private void writeStream(Stream dst)
        {
            while(dst.Position < dst.Length)
            {
                byte[] work_buf;
                Int64 bytes_remain = (dst.Length - dst.Position);
                if (bytes_remain > 0x33554432)
                {
                    work_buf = new byte[0x33554432];
                }
                else
                {
                    work_buf = new byte[bytes_remain];
                }
                dst.Read(work_buf, 0x00, work_buf.Length);
                writeBlock(work_buf, true);
            }
        }
        
        private byte[] getFooter()
        {
            using (MemoryStream ms = new MemoryStream())
            {
                totalBytes += 0x10; //number of bytes used by this footer.
                
                writeInt32(ms, 0x00); // int padding (idk wht this is)
                writeUInt32(ms, 0x00);
                writeInt64(ms, totalBytes);
                ms.Seek(0x00, SeekOrigin.Begin);
                return aes_cbc_encrypt(ms.ToArray(), IV, KEY);
            }
            
        }
   
        public void AddFileAsync(string FilePath, string ParentPath, string PathRel)
        {
            finished = false;
            new Thread(() =>
            {
                long sz = Convert.ToInt64(sceIoStat(FilePath).Size);
                writeBlock(getHeader(FilePath, ParentPath, PathRel));
                using (FileStream fs = File.OpenRead(FilePath))
                {
                    writeStream(fs);
                }
                writeBlock(getPadding(sz));
                writeBlock(getTailer());
                contentSize += sz;
                finished = true;
            }).Start();

        }
        public void AddFile(string FilePath, string ParentPath, string PathRel)
        {

            long sz = Convert.ToInt64(sceIoStat(FilePath).Size);
            writeBlock(getHeader(FilePath, ParentPath, PathRel));
            using (FileStream fs = File.OpenRead(FilePath))
            {
                writeStream(fs);
            }
            writeBlock(getPadding(sz));
            writeBlock(getTailer());
            contentSize += sz;
        }

        public void AddDir(string DirPath, string ParentPath, string PathRel)
        {
            writeBlock(getHeader(DirPath, ParentPath, PathRel));
            writeBlock(getPadding(0));
            writeBlock(getTailer());
        }
        public long Finish()
        {
            finishBlock(true);
            byte[] footer = getFooter();
            mainStream.Write(footer, 0x00, footer.Length);

            blockStream.Dispose();
            mainStream.Dispose();
            return contentSize;
        }


        public PSVIMGBuilder(Stream dst, byte[] Key)
        {
            totalBytes = 0;
            contentSize = 0;
            shaCtx = new Sha256Digest();
            mainStream = dst;
            KEY = Key;

            rnd.NextBytes(IV);
            IV = aes_ecb_encrypt(IV, Key);

            mainStream.Write(IV, 0x00, IV.Length);
            totalBytes += IV.Length;

            startNewBlock();
        }
    }
}
