// a greatly modified parser , originaly based on the targa reader by David Polomis (paloma_sw@cox.net)
// now modified to extract image files from the ps2 version of LucasArts gladius.

using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace xexuxjy
{
    public class GladiusHeader
    {
        private short sColorMapFirstEntryIndex = 0;
        private short sColorMapLength = 0;
        private byte bColorMapEntrySize = 0;
        public int Width = 0;
        public int Height = 0;
        private byte bPixelDepth = 0;
        private byte bImageDescriptor = 0;
        public byte[] RawColourMap;
        public byte[] SwizzledColourMap;
        public PixelFormat PixelFormat;

        public static char[] PTTPHeader = new char[] { 'P', 'T', 'T', 'P' };
        public static char[] NMPTHeader = new char[] { 'N', 'M', 'P', 'T' };
        public static char[] r2d2Header = new char[] { 'R', '2', 'D', '2', 'p', 's', 'x', '2' };
        public static char[] tmapHeader = new char[] { 't', 'm', 'a', 'p' };


        public static void TestGladius()
        {
            GladiusImage targaImage = new GladiusImage();

            String targetDirectory = @"C:/gladius-extracted/test-extract/";
            //String filepath = @"D:\gladius-extracted\ps2-decompressed\converted1\";
            //String filepath = @"D:\gladius-extracted\ps2-decompressed\PTTP\";
            //String errorFile = @"D:\gladius-extracted\ps2-decompressed-errors";

            String filepath = @"C:\gladius-extracted\ps2-decompressed\PTTP\";
            String errorFile = @"C:\gladius-extracted\ps2-decompressed-errors";

            System.IO.DirectoryInfo targetInfo = new DirectoryInfo(targetDirectory);
            if (!targetInfo.Exists)
            {
                targetInfo.Create();
            }

            bool doDelete = true;
            if (doDelete)
            {
                foreach (FileInfo file in targetInfo.GetFiles())
                {
                    file.Delete();
                }
            }

            string[] filePaths = Directory.GetFiles(filepath);

            DirectoryInfo d = new DirectoryInfo(filepath);
            FileInfo[] files = d.GetFiles(); //Getting Text files
            using (StreamWriter errorStream = new StreamWriter(new FileStream(errorFile, FileMode.OpenOrCreate)))
            {
                GladiusImage image = null;
                foreach (FileInfo file in files)
                {
                    //if (file.Name != "File_000957")
                    //if (file.Name != "File_000230")
                    if (file.Name != "File_000024")
                    {
                        //continue;
                    }
                    using (FileStream fs = new FileStream(filepath + file.Name, FileMode.Open))
                    using (BinaryReader binReader = new BinaryReader(fs))
                    {
                        int headerPadding = 0;

                        int subImageCounter = 0;

                        List<String> textureNameList = BuildImageList(binReader);

                        // if there were no texture names then reset the position of the stream to find the data.
                        if (textureNameList.Count == 0)
                        {
                            binReader.BaseStream.Position = 0;
                        }

                        while (ReadToNextTMapBlock(binReader, ref subImageCounter))
                        {
                            int adjustedCounter = subImageCounter-1;

                            image = new GladiusImage();

                            image.Header.LoadHeaderInfo(binReader, errorStream, file.Name);

                            bool saveImage = true;
                            String outputFileName = null;
                            if (textureNameList.Count > 0 && adjustedCounter < textureNameList.Count)
                            {
                                //outputFileName = file.Name + "-" + textureNameList[adjustedCounter];
                                outputFileName = textureNameList[adjustedCounter];
                            }
                            else
                            {
                                outputFileName = file.Name + "-" + (adjustedCounter);
                            }

                            //outputFileName = file.Name + "-" + (subImageCounter);

                            errorStream.WriteLine(String.Format("Extracting [{0}][{1}][{2}]", file.Name, adjustedCounter, outputFileName));

                            int imagePadding = 0x13;// 0x24;

                            if (image.Header.PixelFormat == PixelFormat.Format8bppIndexed)
                            {
                                //image.Header.Se
                                image.Header.ColorMapLength = 256;
                                image.Header.ColorMapEntrySize = 32;

                                if (binReader.BaseStream.Position + 1024 > binReader.BaseStream.Length)
                                {
                                    // not enough room for a colour map?
                                    break;
                                }
                                image.Header.PixelDepth = 8;
                                image.LoadColourMapInfo(binReader);

                            }
                            else if (image.Header.PixelFormat == PixelFormat.Format32bppArgb)
                            {
                                image.Header.PixelDepth = 32;
                            }
                            else
                            {
                                int ibreak = 0;
                            }

                            if (image.Header.ValidSize() && image.Header.KnownFormat())
                            {


                                binReader.BaseStream.Seek(imagePadding, SeekOrigin.Current);
                                try
                                {
                                    image.LoadGladiusImage(binReader);
                                    if (saveImage)
                                    {
                                        image.Image.Save(targetDirectory + outputFileName + ".png", ImageFormat.Png);
                                    }
                                }
                                catch (AccessViolationException e)
                                {
                                    // bleugh.
                                }

                                bool writePalette = false;
                                if (writePalette)
                                {
                                    using (var fs2 = new FileStream(targetDirectory + file.Name + "-pal.bin", FileMode.CreateNew))
                                    {
                                        using (var bw = new BinaryWriter(fs2))
                                        {
                                            bw.Write(image.Header.RawColourMap);
                                        }
                                    }

                                    using (var fs2 = new FileStream(targetDirectory + file.Name + "-cpal.bin", FileMode.CreateNew))
                                    {
                                        using (var bw = new BinaryWriter(fs2))
                                        {
                                            bw.Write(image.Header.SwizzledColourMap);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

        }

        //readpfhd!


        /// <summary>
        /// Gets total number of color map entries included.
        /// </summary>
        public short ColorMapLength
        {
            get;set;
        }

        /// <summary>
        /// Gets the number of bits per entry in the Color Map. Typically 15, 16, 24 or 32-bit values are used.
        /// </summary>
        public byte ColorMapEntrySize
        {
            get;set;
        }


        /// <summary>
        /// Gets the number of bits per pixel. This number includes
        /// the Attribute or Alpha channel bits. Common values are 8, 16, 24 and 32.
        /// </summary>
        public byte PixelDepth
        {
            get;set;
        }


        /// <summary>
        /// Gets the number of bytes per pixel.
        /// </summary>
        public int BytesPerPixel
        {
            get
            {
                return (int)this.PixelDepth / 8;
            }
        }

        public static bool FindCharsInStream(BinaryReader binReader, char[] charsToFind)
        {
            bool found = false;
            char c = ' ';
            int lastFoundIndex = 0;
            try
            {
                while (true)
                {
                    c = (char)binReader.ReadByte();
                    if (c == charsToFind[lastFoundIndex])
                    {
                        lastFoundIndex++;
                        if (lastFoundIndex == charsToFind.Length)
                        {
                            found = true;
                            break;
                        }
                    }
                    else
                    {
                        lastFoundIndex = 0;
                    }
                }
            }
            catch (Exception e)
            {
                int ibreak = 0;
            }
            return found;

        }


        public static bool ReadToNextTMapBlock(BinaryReader binReader, ref int imageCount)
        {
            bool foundR2D2 = FindCharsInStream(binReader, GladiusHeader.r2d2Header);
            bool foundTmap = false;
            byte[] extra = new byte[8];

            while (foundR2D2)
            {
                imageCount++;

                binReader.Read(extra, 0, extra.Length);
                bool shouldHaveTmap = (extra[5] != 0x00);
                if (shouldHaveTmap)
                {
                    foundTmap = FindCharsInStream(binReader, GladiusHeader.tmapHeader);
                    //if (foundTmap)
                    //{
                    //    imageCount++;
                    //}
                    break;
                }
                foundR2D2 = FindCharsInStream(binReader, GladiusHeader.r2d2Header);
            }

            return foundTmap;
        }


        public static List<String> BuildImageList(BinaryReader binReader)
        {
            List<String> textureNameList = new List<string>();

            List<String> ignoreList = new List<string>();

            // for some reason these seem to be referenced but not included in all(?) the files so ignore them.
            //ignoreList.Add("teeth.tga");
            //ignoreList.Add("skygold_R.rm.tga");

            // load header and map of image names.
            if (FindCharsInStream(binReader, GladiusHeader.PTTPHeader))
            {
                if (FindCharsInStream(binReader, GladiusHeader.NMPTHeader))
                {
                    int unknown1 = binReader.ReadInt32();
                    int unknown2 = binReader.ReadInt32();
                    int numTextures = binReader.ReadInt32();

                    for (int i = 0; i < numTextures; ++i)
                    {
                        StringBuilder sb = new StringBuilder();
                        char b = (char)binReader.ReadByte();
                        while (b != 0x00)
                        {
                            if (b != 0x00)
                            {
                                sb.Append(b);
                            }
                            b = (char)binReader.ReadByte();
                        }
                        String n = sb.ToString();
                        if (!ignoreList.Contains(n))
                        {
                            textureNameList.Add(n);
                        }
                    }
                }

            }
            return textureNameList;
        }


       

        public bool KnownFormat()
        {
            return PixelFormat == PixelFormat.Format8bppIndexed || PixelFormat == PixelFormat.Format32bppArgb;
        }

        public bool ValidSize()
        {
            return !(Width <= 0 || Height <= 0 || Width > 1024 || Height > 1024);
        }


        public void LoadHeaderInfo(BinaryReader binReader, StreamWriter errorStream, String file)
        {
            int headerPadding = 0;

            byte[] extraHeader = new byte[24];
            binReader.Read(extraHeader, 0, extraHeader.Length);

            //19th extra byte should determine image format (8bppIndexed,32rgba)
            //20th extra byte should be FF
            int imageSize = extraHeader[2];
            int imageFormat = extraHeader[22];

            if (imageFormat == 0xB0)
            {
                if (imageSize == 0x10)
                {
                    Width = Height = 256;
                }
                else if (imageSize == 0x08)
                {
                    Width = Height = 0x20;
                }
                else if (imageSize == 0x04)
                {
                    Width = Height = 64;
                }
                else
                {
                    errorStream.WriteLine(String.Format("Unknown size [{0}] File [{1}]", imageSize, file));
                }

                PixelFormat = PixelFormat.Format32bppArgb;
                headerPadding = 101;
            }
            else if (imageFormat == 0xD8 || imageFormat == 0x60)
            {
                PixelFormat = PixelFormat.Format8bppIndexed;
                headerPadding = 0x78 - 20;
            }
            else
            {
                errorStream.WriteLine(String.Format("Unknown Output Format [{0}] File [{1}] Position[{2}]", imageFormat, file, binReader.BaseStream.Position));
            }

            {
                binReader.BaseStream.Seek(headerPadding, SeekOrigin.Current);
                //return binReader.BaseStream.Position;

            }
        }

        public static bool PositionReaderAtNextImage(BinaryReader binReader, StreamWriter errorStream, String file)
        {
            return FindCharsInStream(binReader, GladiusHeader.r2d2Header) && FindCharsInStream(binReader, GladiusHeader.tmapHeader);
        }

        static int Main(string[] args)
        {
            GladiusHeader.TestGladius();
            return 0;
        }

    }



    public class GladiusImage : IDisposable
    {
        private GladiusHeader gladiusHeader = null;
        private Bitmap bitmapImage = null;
        private string strFileName = string.Empty;
        private int intStride = 0;
        private GCHandle ImageByteHandle;
        private System.Collections.Generic.List<System.Collections.Generic.List<byte>> rows = new System.Collections.Generic.List<System.Collections.Generic.List<byte>>();
        private System.Collections.Generic.List<byte> row = new System.Collections.Generic.List<byte>();

        /// <summary>
        /// Creates a new instance of the TargaImage object.
        /// </summary>
        public GladiusImage()
        {
            this.gladiusHeader = new GladiusHeader();
            this.bitmapImage = null;
        }


        /// <summary>
        /// Gets a TargaHeader object that holds the Targa Header information of the loaded file.
        /// </summary>
        public GladiusHeader Header
        {
            get { return this.gladiusHeader; }
            set { this.gladiusHeader = value; }
        }

        /// <summary>
        /// Gets a Bitmap representation of the loaded file.
        /// </summary>
        public Bitmap Image
        {
            get { return this.bitmapImage; }
        }

        /// <summary>
        /// Gets the full path and filename of the loaded file.
        /// </summary>
        public string FileName
        {
            get { return this.strFileName; }
        }


        /// <summary>
        /// Gets the byte offset between the beginning of one scan line and the next. Used when loading the image into the Image Bitmap.
        /// </summary>
        /// <remarks>
        /// The memory allocated for Microsoft Bitmaps must be aligned on a 32bit boundary.
        /// The stride refers to the number of bytes allocated for one scanline of the bitmap.
        /// </remarks>
        public int Stride
        {
            get { return this.intStride; }
        }

        ~GladiusImage()
        {
            Dispose(false);
        }

        public void LoadColourMapInfo(BinaryReader binReader)
        {
            int paletteSize = 1024;

            this.gladiusHeader.RawColourMap = new byte[paletteSize];
            this.gladiusHeader.SwizzledColourMap = new byte[paletteSize];

            int rawCounter = 0;

            while (rawCounter < paletteSize)
            {
                int a = 0;
                int r = 0;
                int g = 0;
                int b = 0;

                byte rbyte = binReader.ReadByte();
                byte gbyte = binReader.ReadByte();
                byte bbyte = binReader.ReadByte();
                byte abyte = binReader.ReadByte();

                this.gladiusHeader.RawColourMap[rawCounter++] = rbyte;
                this.gladiusHeader.RawColourMap[rawCounter++] = gbyte;
                this.gladiusHeader.RawColourMap[rawCounter++] = bbyte;
                this.gladiusHeader.RawColourMap[rawCounter++] = abyte;
            }

            byte[] src = this.gladiusHeader.RawColourMap;
            byte[] tgt = this.gladiusHeader.SwizzledColourMap;

            int blockSize = 8;
            int blockIncrement = blockSize * 4;
            int offset = 0;
            while (offset < src.Length)
            {

                for (int j = 0; j < blockSize; ++j)
                {
                    tgt[offset + 0] = src[offset + 2];
                    tgt[offset + 1] = src[offset + 1];
                    tgt[offset + 2] = src[offset + 0];
                    tgt[offset + 3] = src[offset + 3];
                    offset += 4;
                }

                for (int j = 0; j < blockSize; ++j)
                {
                    tgt[offset + 0] = src[blockIncrement + offset + 2];
                    tgt[offset + 1] = src[blockIncrement + offset + 1];
                    tgt[offset + 2] = src[blockIncrement + offset + 0];
                    tgt[offset + 3] = src[blockIncrement + offset + 3];
                    offset += 4;
                }

                for (int j = 0; j < blockSize; ++j)
                {
                    tgt[offset + 0] = src[offset + 2 - (blockIncrement * 1)];
                    tgt[offset + 1] = src[offset + 1 - (blockIncrement * 1)];
                    tgt[offset + 2] = src[offset + 0 - (blockIncrement * 1)];
                    tgt[offset + 3] = src[offset + 3 - (blockIncrement * 1)];
                    offset += 4;
                }

                for (int j = 0; j < blockSize; ++j)
                {
                    tgt[offset + 0] = src[offset + 2];
                    tgt[offset + 1] = src[offset + 1];
                    tgt[offset + 2] = src[offset + 0];
                    tgt[offset + 3] = src[offset + 3];
                    offset += 4;
                }

            }

            // skip 12?
            int a1 = binReader.ReadInt32();
            int a2 = binReader.ReadInt32();
            
            this.gladiusHeader.Width = binReader.ReadInt16();
            this.gladiusHeader.Height = binReader.ReadInt16();
        }

        // assumes we've position the stream.
        private byte[] LoadImageBytes(BinaryReader binReader)
        {
            byte[] data = null;
            int intImageRowByteSize = (int)this.gladiusHeader.Width * ((int)this.gladiusHeader.BytesPerPixel);
            
            // get the size in bytes of the whole image
            int intImageByteSize = intImageRowByteSize * (int)this.gladiusHeader.Height;
            data = new byte[intImageByteSize];

            try
            {
                if(Header.PixelFormat == PixelFormat.Format8bppIndexed)
                {
                    for (int i = 0; i < data.Length; ++i)
                    {

                        data[i] = binReader.ReadByte();
                    }
                }
                else if (Header.PixelFormat == PixelFormat.Format32bppArgb)
                {
                    byte[] subBytes = new byte[4];
                    int adjustedLength = data.Length / 4;
                    int counter = 0;
                    for (int i = 0; i < adjustedLength; ++i)
                    {
                        subBytes[0] = binReader.ReadByte ();
                        subBytes[1] = binReader.ReadByte ();
                        subBytes[2] = binReader.ReadByte ();
                        subBytes[3] = binReader.ReadByte ();

                        //bgr
                        //Array.Reverse(subBytes);
                        subBytes[3] = 0xff;
                        data[counter++] = subBytes[2];
                        data[counter++] = subBytes[1];
                        data[counter++] = subBytes[0];
                        data[counter++] = subBytes[3];
                    }

                }




            }
            catch (Exception e)
            {
                // if we read past the end, somethings wrong, but try and return as much data as we could get..
            }
            // return the image byte array
            return data;

        }

        public void LoadGladiusImage(BinaryReader binReader)
        {
            this.intStride = (((int)this.gladiusHeader.Width * (int)this.gladiusHeader.PixelDepth + 31) & ~31) >> 3; // width in bytes

            byte[] bimagedata = this.LoadImageBytes(binReader);

            this.ImageByteHandle = GCHandle.Alloc(bimagedata, GCHandleType.Pinned);

            PixelFormat pf = this.Header.PixelFormat;


            this.bitmapImage = new Bitmap((int)this.gladiusHeader.Width,
                                            (int)this.gladiusHeader.Height,
                                            this.intStride,
                                            pf,
                                            this.ImageByteHandle.AddrOfPinnedObject());

            if (pf == PixelFormat.Format8bppIndexed)
            {
                int numColourEntries = gladiusHeader.RawColourMap.Length / 4;
                if (numColourEntries > 0)
                {
                    ColorPalette pal = this.bitmapImage.Palette;

                    byte[] map = gladiusHeader.RawColourMap;
                    map = gladiusHeader.SwizzledColourMap;
                    for (int i = 0; i < numColourEntries; i++)
                    {
                        Color c = Color.FromArgb(255, map[i * 4], map[(i * 4) + 1], map[(i * 4) + 2]);
                        Color c2 = Color.FromArgb(c.A, c.B, c.G, c.R);
                        pal.Entries[i] = c2;
                    }
                    this.bitmapImage.Palette = pal;
                }
            }
        }

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            // Take yourself off the Finalization queue 
            // to prevent finalization code for this object
            // from executing a second time.
            //GC.SuppressFinalize(this);

        }


        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            // If disposing equals true, dispose all managed 
            // and unmanaged resources.
            if (disposing)
            {
                // Dispose managed resources.
                if (this.bitmapImage != null)
                {
                    this.bitmapImage.Dispose();
                }

                if (this.ImageByteHandle != null)
                {
                    if (this.ImageByteHandle.IsAllocated)
                    {
                        this.ImageByteHandle.Free();
                    }

                }
            }
        }


        #endregion
    }




}
