using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

using Halo3Map.IO;
using System.Windows.Forms;
using System.Collections;
using System.IO;
using System.Xml;

namespace Halo3Map.Data
{
    /// <summary>
    /// This is the base class used for reading and writing chunks of 
    /// data from a halo 3 map file.
    /// </summary>
    public abstract class Chunk
    {
        private Map.Map map;
        public Map.Map Map { get { return map; } set { map = value; } }

        public Chunk() { }
        public Chunk(Map.Map Map) { map = Map; }

        private FieldInfo[] fields;

        public virtual void Read(EndianReader br)
        {
            fields = this.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
            foreach (FieldInfo fi in fields)
            {
                if (fi.FieldType == typeof(byte))
                    fi.SetValue(this, br.ReadByte());
                else if (fi.FieldType == typeof(char))
                    fi.SetValue(this, br.ReadChar());
                else if (fi.FieldType == typeof(short))
                    fi.SetValue(this, br.ReadInt16());
                else if (fi.FieldType == typeof(ushort))
                    fi.SetValue(this, br.ReadUInt16());
                else if (fi.FieldType == typeof(int))
                    fi.SetValue(this, br.ReadInt32());
                else if (fi.FieldType == typeof(uint))
                    fi.SetValue(this, br.ReadUInt32());
                else if (fi.FieldType == typeof(long))
                    fi.SetValue(this, br.ReadInt64());
                else if (fi.FieldType == typeof(ulong))
                    fi.SetValue(this, br.ReadUInt64());
                else if (fi.FieldType == typeof(double))
                    fi.SetValue(this, br.ReadDouble());
                else if (fi.FieldType == typeof(float))
                    fi.SetValue(this, br.ReadSingle());
                else if (fi.FieldType == typeof(float))
                    fi.SetValue(this, br.ReadSingle());
                else if (fi.FieldType.IsEnum)
                {
                    Type t = Enum.GetUnderlyingType(fi.FieldType);
                    if (t == typeof(byte))
                        fi.SetValue(this, br.ReadByte());
                    else if (t == typeof(short))
                        fi.SetValue(this, br.ReadInt16());
                    else if (t == typeof(int))
                        fi.SetValue(this, br.ReadInt32());
                    else if (t == typeof(long))
                        fi.SetValue(this, br.ReadInt64());
                    else
                    {
                        MessageBox.Show("Unable to read this enum type! Type: " + t.ToString());
                    }
                }
                else if (fi.FieldType == typeof(byte[]))
                {
                    int size = 0;
                    object[] attributes = fi.GetCustomAttributes(typeof(SizeAttribute), true);
                    if (attributes.Length > 0)
                        size = ((SizeAttribute)attributes[0]).Size;

                    fi.SetValue(this, br.ReadBytes(size));
                }
                else if (fi.FieldType == typeof(char[]))
                {
                    int size = 0;
                    object[] attributes = fi.GetCustomAttributes(typeof(SizeAttribute), true);
                    if (attributes.Length > 0)
                        size = ((SizeAttribute)attributes[0]).Size;
                    fi.SetValue(this, br.ReadChars(size));
                }
                else if (fi.FieldType == typeof(string))
                {
                    int size = 32;
                    StringType st = StringType.Ascii;
                    object[] attributes = fi.GetCustomAttributes(typeof(StringAttribute), true);
                    if (attributes.Length > 0)
                    {
                        size = ((StringAttribute)attributes[0]).Size;
                        st = ((StringAttribute)attributes[0]).StringType;
                    }
                    string temp = "";

                    if (st == StringType.Ascii)
                    {
                        ASCIIEncoding ae = new ASCIIEncoding();
                        temp = ae.GetString(br.ReadBytes(size)).Replace("\0", "");
                    }
                    else
                    {
                        UnicodeEncoding ue = new UnicodeEncoding();                        
                        temp = ue.GetString(br.ReadBytes(size)).Replace("\0", "");
                    }

                    fi.SetValue(this, temp);
                }
                else if (fi.FieldType == typeof(StringID))
                {
                    object strID = Activator.CreateInstance(fi.FieldType, new object[] { br, map });
                    fi.SetValue(this, strID);
                }
                else if (fi.FieldType == typeof(TagReference))
                {
                    object tagref = Activator.CreateInstance(fi.FieldType, new object[] {br});
                    fi.SetValue(this, tagref);
                }
                else if (fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(H3Reflexive<>))
                {
                    object reflexive = Activator.CreateInstance(fi.FieldType, new object[] { br, Map });
                    fi.SetValue(this, reflexive);
                }
                else
                {
                    MessageBox.Show("Unable to read this datatype! Type: " + fi.FieldType.ToString());
                }
            }
        }
        
        public virtual void Write(EndianWriter bw)
        {
            foreach (FieldInfo fi in fields)
            {
                if (fi.FieldType == typeof(byte))
                    bw.Write((byte)fi.GetValue(this));
                else if (fi.FieldType == typeof(char))
                    bw.Write((char)fi.GetValue(this));
                else if (fi.FieldType == typeof(short))
                    bw.Write((short)fi.GetValue(this));
                else if (fi.FieldType == typeof(ushort))
                    bw.Write((ushort)fi.GetValue(this));
                else if (fi.FieldType == typeof(int))
                    bw.Write((int)fi.GetValue(this));
                else if (fi.FieldType == typeof(uint))
                    bw.Write((uint)fi.GetValue(this));
                else if (fi.FieldType == typeof(long))
                    bw.Write((long)fi.GetValue(this));
                else if (fi.FieldType == typeof(ulong))
                    bw.Write((ulong)fi.GetValue(this));
                else if (fi.FieldType == typeof(double))
                    bw.Write((double)fi.GetValue(this));
                else if (fi.FieldType == typeof(float))
                    bw.Write((float)fi.GetValue(this));
                else if (fi.FieldType == typeof(float))
                    bw.Write((float)fi.GetValue(this));
                else if (fi.FieldType.IsEnum)
                {
                    Type t = Enum.GetUnderlyingType(fi.FieldType);
                    if (t == typeof(byte))
                        bw.Write((byte)fi.GetValue(this));
                    else if (t == typeof(short))
                        bw.Write((short)fi.GetValue(this));
                    else if (t == typeof(int))
                        bw.Write((int)fi.GetValue(this));
                    else if (t == typeof(long))
                        bw.Write((long)fi.GetValue(this));
                    else
                    {
                        MessageBox.Show("Unable to write this enum type! Type: " + t.ToString());
                    }
                }
                else if (fi.FieldType == typeof(char[]))
                {
                    bw.Write((char[])fi.GetValue(this));
                }
                else if (fi.FieldType == typeof(byte[]))
                {
                    bw.Write((byte[])fi.GetValue(this));
                }
                else if (fi.FieldType == typeof(string))
                {
                    //Set Default properties incase there was any defined in the plugin
                    int size = 32;
                    StringType st = StringType.Ascii;
                    //Get attributes from plugin
                    object[] attributes = fi.GetCustomAttributes(typeof(StringAttribute), true);
                    //If there were attribute returned
                    if (attributes.Length > 0)
                    {
                        //Set properties
                        size = ((StringAttribute)attributes[0]).Size;
                        st = ((StringAttribute)attributes[0]).StringType;
                    }
                    //Write string
                    bw.Write((string)fi.GetValue(this));
                    bw.Write(new byte[size - ((string)fi.GetValue(this)).Length]);
                }
                else if (fi.FieldType == typeof(TagReference))
                {
                    TagReference t = (TagReference)fi.GetValue(this);
                    t.Write(bw);
                }
                else if (fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(H3Reflexive<>))
                {
                    //Remember Pos int Meta
                    int oldpos = (int)bw.BaseStream.Position;
                    //Try incase of error thrown if reflex is null
                    try
                    {
                        //Get our reflex value
                        IList list = (IList)fi.GetValue(this);
                        //If reflex is not null
                        if (list.Count != 0)
                        {
                            //Find our Write method
                            MethodInfo mi = list.GetType().GetMethod("Write");
                            //This will hold our parameters
                            object[] parameters = new object[1];
                            //Set parameteres
                            parameters[0] = bw;
                            //Invoke method
                            mi.Invoke(list, parameters);
                        }
                    }
                    catch { }
                    //Return to our position in meta + 12
                    bw.BaseStream.Position = oldpos + 12;
                }
                else
                {
                    //Prompt user if a datatype was not writeable
                    MessageBox.Show("Unable to write this datatype! Type: " + fi.FieldType.ToString());
                }
            }
        }
        
        public virtual void Extract(EndianWriter bw, XmlTextWriter xtw)
        {           
            foreach (FieldInfo fi in fields)
            {
                if (fi.FieldType == typeof(byte))
                    bw.Write((byte)fi.GetValue(this));
                else if (fi.FieldType == typeof(char))
                    bw.Write((char)fi.GetValue(this));
                else if (fi.FieldType == typeof(short))
                    bw.Write((short)fi.GetValue(this));
                else if (fi.FieldType == typeof(ushort))
                    bw.Write((ushort)fi.GetValue(this));
                else if (fi.FieldType == typeof(int))
                    bw.Write((int)fi.GetValue(this));
                else if (fi.FieldType == typeof(uint))
                    bw.Write((uint)fi.GetValue(this));
                else if (fi.FieldType == typeof(long))
                    bw.Write((long)fi.GetValue(this));
                else if (fi.FieldType == typeof(ulong))
                    bw.Write((ulong)fi.GetValue(this));
                else if (fi.FieldType == typeof(double))
                    bw.Write((double)fi.GetValue(this));
                else if (fi.FieldType == typeof(float))
                    bw.Write((float)fi.GetValue(this));
                else if (fi.FieldType == typeof(float))
                    bw.Write((float)fi.GetValue(this));
                else if (fi.FieldType.IsEnum)
                {
                    Type t = Enum.GetUnderlyingType(fi.FieldType);
                    if (t == typeof(byte))
                        bw.Write((byte)fi.GetValue(this));
                    else if (t == typeof(short))
                        bw.Write((short)fi.GetValue(this));
                    else if (t == typeof(int))
                        bw.Write((int)fi.GetValue(this));
                    else if (t == typeof(long))
                        bw.Write((long)fi.GetValue(this));
                    else
                    {
                        MessageBox.Show("Unable to write this enum type! Type: " + t.ToString());
                    }
                }
                else if (fi.FieldType == typeof(char[]))
                {
                    bw.Write((char[])fi.GetValue(this));
                }
                else if (fi.FieldType == typeof(byte[]))
                {
                    bw.Write((byte[])fi.GetValue(this));
                }
                else if (fi.FieldType == typeof(string))
                {
                    //Set Default properties incase there was any defined in the plugin
                    int size = 32;
                    StringType st = StringType.Ascii;
                    //Get attributes from plugin
                    object[] attributes = fi.GetCustomAttributes(typeof(StringAttribute), true);
                    //If there were attribute returned
                    if (attributes.Length > 0)
                    {
                        //Set properties
                        size = ((StringAttribute)attributes[0]).Size;
                        st = ((StringAttribute)attributes[0]).StringType;
                    }
                    //Write string
                    bw.Write((string)fi.GetValue(this));
                    bw.Write(new byte[size - ((string)fi.GetValue(this)).Length]); 
                }
                else if (fi.FieldType == typeof(TagReference))
                {
                    TagReference t = (TagReference)fi.GetValue(this);
                    t.Write(bw);
                }
                else if (fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(H3Reflexive<>))
                {
                    IList list = (IList)fi.GetValue(this);
                    bw.Write((int)list.GetType().GetProperty("ChunkCount").GetValue(list, null));
                    bw.Write((uint)list.GetType().GetProperty("Pointer").GetValue(list, null));
                    bw.Write(new byte[4]);
                }
                else
                {
                    //Prompt user if a datatype was not writeable
                    MessageBox.Show("Unable to write this datatype! Type: " + fi.FieldType.ToString());
                }                
            }

            foreach (FieldInfo fi in fields)
            {
                if (fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(H3Reflexive<>))
                {
                    //Get our reflex value
                    IList list = (IList)fi.GetValue(this);
                    MethodInfo[] mii = list.GetType().GetMethods();
                    //If reflex is not null
                    if (list.Count != 0)
                    {
                        #region XML
                        xtw.WriteStartElement("Reflexive");
                        xtw.WriteAttributeString("Name", list[0].GetType().Name);
                        xtw.WriteAttributeString("Offset", list.GetType().GetProperty("TagOffset").GetValue(list, null).ToString());
                        xtw.WriteAttributeString("ChunkCount", list.GetType().GetProperty("ChunkCount").GetValue(list, null).ToString());
                        xtw.WriteAttributeString("ChunkSize", list.GetType().GetProperty("ChunkSize").GetValue(list, null).ToString());
                        xtw.WriteAttributeString("Translation", list.GetType().GetProperty("MapOffset").GetValue(list, null).ToString());
                        xtw.WriteAttributeString("ExtractedTranslation", bw.BaseStream.Position.ToString());
                        xtw.WriteEndElement();
                        #endregion
                        //Find our Write method
                        MethodInfo mi = list.GetType().GetMethod("Extract");
                        //This will hold our parameters
                        object[] parameters = new object[2];
                        //Set parameteres
                        parameters[0] = bw;
                        parameters[1] = xtw;
                        //Invoke method
                        mi.Invoke(list, parameters);
                    }
                }
            }
        }

        public virtual void Inject(EndianReader br)
        {
            foreach (FieldInfo fi in fields)
            {
                if (fi.FieldType == typeof(byte))
                    fi.SetValue(this, br.ReadByte());
                else if (fi.FieldType == typeof(char))
                    fi.SetValue(this, br.ReadChar());
                else if (fi.FieldType == typeof(short))
                    fi.SetValue(this, br.ReadInt16());
                else if (fi.FieldType == typeof(ushort))
                    fi.SetValue(this, br.ReadUInt16());
                else if (fi.FieldType == typeof(int))
                    fi.SetValue(this, br.ReadInt32());
                else if (fi.FieldType == typeof(uint))
                    fi.SetValue(this, br.ReadUInt32());
                else if (fi.FieldType == typeof(long))
                    fi.SetValue(this, br.ReadInt64());
                else if (fi.FieldType == typeof(ulong))
                    fi.SetValue(this, br.ReadUInt64());
                else if (fi.FieldType == typeof(double))
                    fi.SetValue(this, br.ReadDouble());
                else if (fi.FieldType == typeof(float))
                    fi.SetValue(this, br.ReadSingle());
                else if (fi.FieldType == typeof(float))
                    fi.SetValue(this, br.ReadSingle());
                else if (fi.FieldType.IsEnum)
                {
                    Type t = Enum.GetUnderlyingType(fi.FieldType);
                    if (t == typeof(byte))
                        fi.SetValue(this, br.ReadByte());
                    else if (t == typeof(short))
                        fi.SetValue(this, br.ReadInt16());
                    else if (t == typeof(int))
                        fi.SetValue(this, br.ReadInt32());
                    else if (t == typeof(long))
                        fi.SetValue(this, br.ReadInt64());
                    else
                    {
                        MessageBox.Show("Unable to read this enum type! Type: " + t.ToString());
                    }
                }
                else if (fi.FieldType == typeof(byte[]))
                {
                    int size = 0;
                    object[] attributes = fi.GetCustomAttributes(typeof(SizeAttribute), true);
                    if (attributes.Length > 0)
                        size = ((SizeAttribute)attributes[0]).Size;

                    fi.SetValue(this, br.ReadBytes(size));
                }
                else if (fi.FieldType == typeof(char[]))
                {
                    int size = 0;
                    object[] attributes = fi.GetCustomAttributes(typeof(SizeAttribute), true);
                    if (attributes.Length > 0)
                        size = ((SizeAttribute)attributes[0]).Size;
                    fi.SetValue(this, br.ReadChars(size));
                }
                else if (fi.FieldType == typeof(string))
                {
                    int size = 32;
                    StringType st = StringType.Ascii;
                    object[] attributes = fi.GetCustomAttributes(typeof(StringAttribute), true);
                    if (attributes.Length > 0)
                    {
                        size = ((StringAttribute)attributes[0]).Size;
                        st = ((StringAttribute)attributes[0]).StringType;
                    }
                    string temp = "";

                    if (st == StringType.Ascii)
                    {
                        ASCIIEncoding ae = new ASCIIEncoding();
                        temp = ae.GetString(br.ReadBytes(size)).Replace("\0", "");
                    }
                    else
                        temp = br.ReadUnicodeString(size);

                    fi.SetValue(this, temp);
                }
                else if (fi.FieldType == typeof(StringID))
                {
                    object strID = Activator.CreateInstance(fi.FieldType, new object[] { br, map });
                    fi.SetValue(this, strID);
                }
                else if (fi.FieldType == typeof(TagReference))
                {
                    TagReference t = (TagReference)fi.GetValue(this);
                    t.TagClass = new string(br.ReadChars(4));
                    t.Unknown = br.ReadBytes(8);
                    t.Ident = br.ReadInt16();
                    t.Index = br.ReadInt16();
                    fi.SetValue(this, t);
                }
                else if (fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(H3Reflexive<>))
                {
                    IList list = (IList)fi.GetValue(this);
                    PropertyInfo[] pi = list.GetType().GetProperties();
                    list.GetType().GetProperty("ChunkCount").SetValue(list, br.ReadInt32(), null);
                    list.GetType().GetProperty("Pointer").SetValue(list, br.ReadUInt32(), null);
                    br.BaseStream.Position += 4;
                    fi.SetValue(this, list);
                }
                else
                {
                    MessageBox.Show("Unable to read this datatype! Type: " + fi.FieldType.ToString());
                }
            }

            foreach (FieldInfo fi in fields)
            {
                if (fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(H3Reflexive<>))
                {
                    //Get our reflex value
                    IList list = (IList)fi.GetValue(this);
                    //If reflex is not null
                    if (list.Count != 0)
                    {
                        //Find our Write method
                        MethodInfo mi = list.GetType().GetMethod("Inject");
                        //This will hold our parameters
                        object[] parameters = new object[1];
                        //Set parameteres
                        parameters[0] = br;
                        //Invoke method
                        mi.Invoke(list, parameters);
                    }

                    fi.SetValue(this, list);
                }
            }
        }
    }
}