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

namespace ANX.Tools.XNBInspector
{
    public class InspectReader : BinaryReader
    {
        private InspectReader(Stream input)
            : base(input)
        {
        }

        public static string TryInspectXNB(Stream input, IInspectLogger result)
        {
            try
            {
                InspectXNB(input, result);
            }
            catch (Exception e)
            {
                result.AppendLine();
                result.AppendLine(Severity.Error, e.Message);
                result.AppendLine(Severity.Error, e.StackTrace);
            }
            return result.ToString();
        }

        public static void InspectXNB(Stream input, IInspectLogger result)
        {
            // read the XNB file information
            //
            // | Type   |   Description        | example/value
            // |--------|----------------------|--------------------------------
            // | Byte   | Format identifier    | X (88)
            // |--------|----------------------|--------------------------------
            // | Byte   | Format identifier    | N (78)
            // |--------|----------------------|--------------------------------
            // | Byte   | Format identifier    | B (66)
            // |--------|----------------------|--------------------------------
            // | Byte   | Target platform	   | w = Microsoft Windows
            // |        |                      | m = Windows Phone 7
            // |        |                      | x = Xbox 360
            // |--------|----------------------|--------------------------------
            // | Byte   | XNB format version   | 5 = XNA Game Studio 4.0
            // |        |                      | 6 = future XNA version 
            // |        |                      |     OR anx version if the next three bytes are ANX
            // |--------|----------------------|--------------------------------
            // | Byte   | Flag bits            | Bit 0x01 = content is for HiDef profile (otherwise Reach)
            // |        |                      | Bit 0x80 = asset data is compressed
            // |--------|----------------------|--------------------------------
            // | UInt32 | Compressed file size | Total size of the (optionally compressed) 
            // |        |                      | .xnb file as stored on disk (including this header block)

            InspectReader reader = new InspectReader(input);

            byte magicX = reader.ReadByte(); // X
            byte magicN = reader.ReadByte(); // N
            byte magicB = reader.ReadByte(); // B
            byte[] magicBytes = new byte[] { magicX, magicN, magicB };
            byte[] magicWants = new byte[] { 88, 78, 66 };

            for (int i = 0; i < magicBytes.Length; i++)
            {
                result.Append(Severity.None, "Format identifier: ");
                if (magicBytes[i] != magicWants[i])
                {
                    result.AppendFormat(Severity.Error, "{0} (should be {1}[{2}])", magicBytes[i], magicWants[i], (char)magicWants[i]);
                }
                else
                {
                    result.AppendFormat(Severity.Success, "{0}[{1}]", magicWants[i], (char)magicWants[i]);
                }
                result.AppendLine();
            }
            
            byte targetPlattform = reader.ReadByte();
            // w = Microsoft Windows
            // m = Windows Phone 7
            // x = Xbox 360
            // ANX-EXTENSIONS:
            // a = Android
            // i = iOS
            // l = Linux
            // o = MacOs
            // p = PS Vita / Mobile
            // 8 = Windows 8 Metro
            result.Append(Severity.None, "Target platform  : ");
            switch ((char)targetPlattform)
            {
                case 'w':
                    result.AppendFormat(Severity.Success, "{0}[w] (Microsoft Windows)", targetPlattform);
                    break;
                case 'm':
                    result.AppendFormat(Severity.Success, "{0}[m] (Windows Phone 7)", targetPlattform);
                    break;
                case 'x':
                    result.AppendFormat(Severity.Success, "{0}[x] (Xbox 360)", targetPlattform);
                    break;
                case 'a':
                    result.AppendFormat(Severity.Success, "{0}[a] (Android) | ANX extension", targetPlattform);
                    break;
                case 'i':
                    result.AppendFormat(Severity.Success, "{0}[i] (iOS) | ANX extension", targetPlattform);
                    break;
                case 'l':
                    result.AppendFormat(Severity.Success, "{0}[l] (Linux) | ANX extension", targetPlattform);
                    break;
                case 'o':
                    result.AppendFormat(Severity.Success, "{0}[o] (MacOS) | ANX extension", targetPlattform);
                    break;
                case 'p':
                    result.AppendFormat(Severity.Success, "{0}[p] (PS Vita / mobile) | ANX extension", targetPlattform);
                    break;
                case '8':
                    result.AppendFormat(Severity.Success, "{0}[8] (Windows 8 metro) | ANX extension", targetPlattform);
                    break;
                default:
                    result.AppendFormat(Severity.Error, "{0} (Unknown or non XNA/ANX platform)", targetPlattform);
                    break;
            }
            result.AppendLine();

            byte formatVersion = reader.ReadByte();
            // 5 = XNA Game Studio 4.0
            result.Append(Severity.None, "Format version   : ");
            switch (formatVersion)
            {
                case 1:
                    result.Append(Severity.Success, "1 (XNA Game Studio 1.0)");
                    break;
                case 2:
                    result.Append(Severity.Success, "2 (XNA Game Studio 2.0)");
                    break;
                case 3:
                    result.Append(Severity.Success, "3 (XNA Game Studio 3.0)");
                    break;
                case 4:
                    result.Append(Severity.Success, "4 (XNA Game Studio 3.1)");
                    break;
                case 5:
                    result.Append(Severity.Success, "5 (XNA Game Studio 4.0)");
                    break;
                case 6:
                    byte magicVA = reader.ReadByte(); // A
                    byte magicVN = reader.ReadByte(); // N
                    byte magicVX = reader.ReadByte(); // X

                    if (magicVA == 'A' && magicVN == 'N' && magicVX == 'X')
                    {
                        result.Append(Severity.Success, "6 with additional magic bytes (ANX.Framework 1.0)");
                    }
                    else
                    {
                        reader.BaseStream.Seek(-3, SeekOrigin.Current);
                        result.AppendFormat(Severity.Warning, "{0} (Unknown or non XNA/ANX content)", formatVersion);

                    }
                    break;
                default:
                    result.AppendFormat(Severity.Warning, "{0} (Unknown or non XNA/ANX content)", formatVersion);
                    break;
            }
            result.AppendLine();

            byte flags = reader.ReadByte();
            result.AppendFormat(Severity.None, "Flags            : 0x{0:X4}\n", flags);
            if ((flags & 0x01) == 0x01)
            {
                // HiDef Profile
                result.AppendLine(Severity.None, " - HiDef Profile");
            }
            else
            {
                // Reach Profile
                result.AppendLine(Severity.None, " - Reach Profile");
            }

            bool isCompressed = (flags & 0x80) != 0;
            result.AppendFormat(Severity.None, " - Compressed {0}", isCompressed);
            result.AppendLine();

            int sizeOnDisk = reader.ReadInt32();
            result.Append(Severity.None, "Size on disk     : ");
            if (sizeOnDisk != input.Length)
            {
                result.AppendFormat(Severity.Error, "{0} bytes [{1}]", sizeOnDisk, ToHumanSize(sizeOnDisk));
                result.AppendFormat(Severity.Error, " (Should be {0} bytes [{1}])", input.Length, ToHumanSize(input.Length));
            }
            else
            {
                result.AppendFormat(Severity.Success, "{0} bytes [{1}]", sizeOnDisk, ToHumanSize(sizeOnDisk));
            }
            result.AppendLine();

            if (isCompressed)
            {
                long position = reader.BaseStream.Position;
                int sizeOfdata = reader.ReadInt32();
                reader.BaseStream.Seek(position, SeekOrigin.Begin);

                result.AppendFormat(Severity.None, "Uncompressed     : {0} bytes [{1}]", sizeOfdata, ToHumanSize(sizeOfdata));
                result.AppendLine();

                input = ANX.Framework.Content.Decompressor.DecompressStream(reader, input, sizeOnDisk);
                reader = new InspectReader(input);
            }

            int numTypes = reader.Read7BitEncodedInt();
            result.AppendFormat(Severity.None, "Type readers     : {0}", numTypes);
            result.AppendLine();

            for (int i = 0; i < numTypes; i++)
            {
                string readerTypeName = reader.ReadString();
                int readerVersionNumber = reader.ReadInt32();
                result.AppendFormat(Severity.None, " - Version: {1}    Type: {2}", i, readerVersionNumber, readerTypeName);
                result.AppendLine();
            }

            int numSharedResources = reader.Read7BitEncodedInt();
            result.AppendFormat(Severity.None, "Shared resources : {0}", numSharedResources);
            result.AppendLine();
        }

        private static string ToHumanSize(long bytes)
        {
            double s = bytes;
            string[] format = new string[]
                  {
                      "{0:0.00} bytes", 
                      "{0:0.00} KB",  
                      "{0:0.00} MB", 
                      "{0:0.00} GB", 
                      "{0:0.00} TB", 
                      "{0:0.00} PB", 
                      "{0:0.00} EB"
                  };

            int i = 0;

            while (i < format.Length && s >= 1024)
            {
                s = (long)(100 * s / 1024.0) / 100.0;
                i++;
            }
            return string.Format(format[i], s);
        }
    }
}