using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Xml;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.GamerServices;
using MegaManRipoff.UI;

namespace MegaManRipoff.MainGameClasses
{
    /// <summary>
    /// Handles reading a given XML file whose contents contain details of a
    /// level. This class constructs the level and returns it so that it can
    /// be played.
    /// </summary>
    class LevelLoader
    {
        #region Member Variables

        /// <summary>
        /// The name of the current level XML file.
        /// </summary>
        string _xmlName;

        /// <summary>
        /// The extension used for each room's array of tiles file.
        /// </summary>
        const string RoomFileExtension = ".room";

        /// <summary>
        /// The current game instance.
        /// </summary>
        MainGame _mainGame;

        /// <summary>
        /// The complete path of the level currently being loaded.
        /// </summary>
        string _levelPath;

        /// <summary>
        /// Holds all of the types of tiles that the level loader knows how to deal with.
        /// </summary>
        enum TileType
        {
            Solid, Background, Ice
        }

        #endregion

        /// <summary>
        /// Creates an instance that will read the data required to create a new level.
        /// </summary>
        /// <param name="game">The current game instance.</param>
        /// <param name="levelPath">The name of the level's folder, excluding \\Levels\\. This must end with \\.</param>
        /// <param name="xmlName">The name of the level's XML document. This must end with .xml.</param>
        public LevelLoader(MainGame mainGame, string levelPath, string xmlName)
        {
            _mainGame = mainGame;

            //Test that the path and relevant XML file exists.
            if (!Directory.Exists(ContentPath() + levelPath))
                throw new Exception("The specified level directory \"" + ContentPath() + levelPath +
                    "\" was not found.");
            if (!File.Exists(ContentPath() + levelPath + xmlName))
                throw new Exception("The level's XML document \"" + ContentPath() + levelPath + xmlName +
                    "\" was not found.");

            _levelPath = levelPath;
            _xmlName = xmlName;
        }

        #region Methods

        /// <summary>
        /// Get the game's content folder, ending with \.
        /// </summary>
        /// <returns>Returns the path of the content folder, ending with \.</returns>
        private string ContentPath()
        {
            return _mainGame.Content.RootDirectory + "\\Levels\\";
        }

        /// <summary>
        /// A stupid workaround for XmlTextReader.GetAttribute() and bool.Parse(). 
        /// bool.Parse() keeps throwing a null reference exception when I call it
        /// and I really can't be bothered to figure out why. I think its to do with
        /// GetAttribute() not evaluating before calling bool.Parse, but whatever.
        /// This method combines the two.
        /// </summary>
        /// <param name="input">The string form of the boolean.</param>
        /// <returns>True if the input is "True", and false if the input is "False".</returns>
        private bool GetAttributeAsBool(XmlReader xmlReader, string attributeName)
        {
            return (xmlReader.GetAttribute(attributeName) == bool.TrueString);
        }

        /// <summary>
        /// Creates a dictionary of tile definitions by loading them from the XML document.
        /// </summary>
        /// <param name="xmlReader">The reader instance of the Tileset subtree.</param>
        /// <returns>Returns the newly made dictionary.</returns>
        private Dictionary<char, Tuple<TileType, Point, Rectangle>> CreateTileset(XmlReader xmlReader)
        {
            //Throw an exception if we're not currently on a tileset element.
            xmlReader.MoveToContent();
            if (xmlReader.LocalName != "Tileset")
                throw new Exception("The current element " + xmlReader.LocalName +
                    "is not a Tileset element.");

            Dictionary<char, Tuple<TileType, Point, Rectangle>> tileset = new Dictionary<char, Tuple<TileType, Point, Rectangle>>();

            //If the tileset element is not empty (i.e. <Tileset />)
            if (!xmlReader.IsEmptyElement)
            {
                //Iterate through all of the background elements.
                xmlReader.ReadToFollowing("Tile");

                do
                {
                    //Get the tile's details from the attributes.
                    char key = char.Parse(xmlReader.GetAttribute("char"));
                    TileType tileType = (TileType)Enum.Parse(typeof(TileType), xmlReader.GetAttribute("type"));
                    Point textureOffset = new Point(int.Parse(xmlReader.GetAttribute("textureX")),
                                                    int.Parse(xmlReader.GetAttribute("textureY")));

                    //Check to see if a special hitbox has been defined; if so use the defined
                    //rectangle in the tile's definition.
                    string hitboxOffsetXString, hitboxOffsetYString, hitboxWidthString, hitboxHeightString;
                    Tuple<TileType, Point, Rectangle> tileTriple;

                    if ((hitboxWidthString = xmlReader.GetAttribute("hitboxW")) != null
                        | (hitboxHeightString = xmlReader.GetAttribute("hitboxH")) != null)
                    {
                        //Parse the strings as ints.
                        int hitboxWidth = int.Parse(hitboxWidthString);
                        int hitboxHeight = int.Parse(hitboxHeightString);

                        //If the offsets have been defined, use those.
                        if ((hitboxOffsetXString = xmlReader.GetAttribute("hitboxX")) != null
                            | (hitboxOffsetYString = xmlReader.GetAttribute("hitboxY")) != null)
                        {
                            int hitboxOffsetX = int.Parse(hitboxWidthString);
                            int hitboxOffsetY = int.Parse(hitboxHeightString);

                            tileTriple = new Tuple<TileType, Point, Rectangle>(
                                tileType,
                                textureOffset,
                                new Rectangle(hitboxOffsetX, hitboxOffsetY, hitboxWidth, hitboxHeight));
                        }
                        //Otherwise, assume the offset is (0, 0).
                        else
                        {
                            tileTriple = new Tuple<TileType, Point, Rectangle>(
                                tileType,
                                textureOffset,
                                new Rectangle(0, 0, hitboxWidth, hitboxHeight));
                        }
                    }
                    //Otherwise, use the default width and height and offset (0, 0).
                    else
                    {
                        tileTriple = new Tuple<TileType, Point, Rectangle>(
                            tileType,
                            textureOffset,
                            new Rectangle(0, 0, Tile.DEFAULT_WIDTH, Tile.DEFAULT_HEIGHT));
                    }

                    //Add the tile definition to the dictionary.
                    tileset.Add(key, tileTriple);
                                                            
                }
                while (xmlReader.ReadToNextSibling("Tile"));
            }

            return tileset;
        }

        /// <summary>
        /// Creates a dictionary of backgrounds by loading their details from the XML document.
        /// </summary>
        /// <param name="xmlReader">The reader instance of the Backgrounds subtree.</param>
        /// <returns>Returns the newly made dictionary.</returns>
        private Dictionary<string, Background> CreateBackgrounds(XmlReader xmlReader)
        {
            //Throw an exception if we're not currently on a backgrounds element.
            xmlReader.MoveToContent();
            if (xmlReader.LocalName != "Backgrounds")
                throw new Exception("The current element " + xmlReader.LocalName +
                    "is not a Backgrounds element.");

            Dictionary<string, Background> backgrounds = new Dictionary<string, Background>();

            //If the background element is not empty (i.e. <Background />)
            if (!xmlReader.IsEmptyElement)
            {
                //Iterate through all of the background elements.
                xmlReader.ReadToFollowing("Background");

                do
                {
                    //Add a new background and its asset name to the dictionary.
                    Background currentBackground = new Background();
                    string backgroundName = xmlReader.GetAttribute("name");

                    backgrounds.Add(xmlReader.GetAttribute("name"), currentBackground);

                    //Iterate through all of the background element elements.
                    xmlReader.ReadToFollowing("BackgroundElement");
                    do
                    {
                        //Add a background element to the current background by loading all
                        //the attributes of the element from 
                        currentBackground.AddBackgroundElement(new BackgroundElement(currentBackground,
                            xmlReader.GetAttribute("texture"),
                            new Vector2(float.Parse(xmlReader.GetAttribute("destX")),
                                        float.Parse(xmlReader.GetAttribute("destY"))),
                            GetAttributeAsBool(xmlReader, "tileX"),  //For some reason bool.Parse()
                            GetAttributeAsBool(xmlReader, "tileY"),  //doesn't work here...
                            new Vector2(float.Parse(xmlReader.GetAttribute("speedX")),
                                        float.Parse(xmlReader.GetAttribute("speedY"))),
                            new Vector2(float.Parse(xmlReader.GetAttribute("scrollFactorX")),
                                        float.Parse(xmlReader.GetAttribute("scrollFactorY")))));
                    }
                    while (xmlReader.ReadToNextSibling("BackgroundElement"));
                }
                while (xmlReader.ReadToNextSibling("Background"));
            }

            return backgrounds;
        }

        /// <summary>
        /// Reads the details for a single entity from the XML document.
        /// </summary>
        /// <param name="xmlReader">The reader instance of the Entity subtree.</param>
        /// <param name="offset">The absolute offset of the room.</param>
        private Tuple<Type, Vector2, object[]> CreateEntityDetail(XmlReader xmlReader, Vector2 offset)
        {
            //Throw an exception if we're not currently on an entity element.
            xmlReader.MoveToContent();
            if (xmlReader.LocalName != "Entity")
                throw new Exception("The current element " + xmlReader.LocalName +
                    "is not a Entity element.");

            //Hold the new entity.
            Tuple<Type, Vector2, object[]> newEntity;

            //Read the type and position of the entity.
            string entityType = xmlReader.GetAttribute("type");
            Vector2 position = new Vector2(float.Parse(xmlReader.GetAttribute("x")),
                                           float.Parse(xmlReader.GetAttribute("y")));

            //Begin the biiiiiigggg if else statement that checks the entity type attribute and
            //creates the relevant details for the entity. These details are then later used by
            //the room to create the instances of the entities as soon as the room is scrolled
            //into.
            if (entityType == "Metool")
            {
                //Also read in the extra attribute.
                bool shouldRun = GetAttributeAsBool(xmlReader, "shouldRun");
                newEntity = new Tuple<Type, Vector2, object[]>
                    (typeof(Metool), position + offset, new object[1] { shouldRun });
            }
            else
                if (entityType == "PipiGenerator")
                {
                    //Also read in the extra attributes.
                    int width = int.Parse(xmlReader.GetAttribute("width"));
                    int height = int.Parse(xmlReader.GetAttribute("height"));
                    newEntity = new Tuple<Type, Vector2, object[]>
                        (typeof(PipiGenerator),
                        Vector2.Zero,
                        new object[1] { new Rectangle((int)(position.X + offset.X),
                                                              (int)(position.Y + offset.Y),
                                                              width,
                                                              height)});
                }
                else
                    if (entityType == "SmallHealth")
                    {
                        newEntity = new Tuple<Type, Vector2, object[]>
                            (typeof(SmallHealth), position + offset, null);
                    }
                    else
                        if (entityType == "LargeHealth")
                        {
                            newEntity = new Tuple<Type, Vector2, object[]>
                                (typeof(LargeHealth), position + offset, null);
                        }
                        else
                            if (entityType == "ExtraLife")
                            {
                                newEntity = new Tuple<Type, Vector2, object[]>
                                    (typeof(ExtraLife), position + offset, null);
                            }
                            else
                                if (entityType == "BigEvilKillingThing")
                                {
                                    newEntity = new Tuple<Type, Vector2, object[]>
                                        (typeof(BigEvilKillingThing), position + offset, null);
                                }
                                else
                                    if (entityType == "YellowDevil")
                                    {
                                        newEntity = new Tuple<Type, Vector2, object[]>
                                            (typeof(YellowDevil), position + offset, null);
                                    }
                                    else
                                        if (entityType == "PitLurker")
                                        {
                                            newEntity = new Tuple<Type, Vector2, object[]>
                                                (typeof(PitLurker), position + offset, null);
                                        }
                                        else
                                            if (entityType == "Tripropellan")
                                            {
                                                newEntity = new Tuple<Type, Vector2, object[]>
                                                    (typeof(Tripropellan), position + offset, null);
                                            }
                                            else
                                                if (entityType == "SniperJoe")
                                                {
                                                    newEntity = new Tuple<Type, Vector2, object[]>
                                                        (typeof(SniperJoe), position + offset, null);
                                                }
                                                else
                                                    if (entityType == "QuickManLaserGenerator")
                                                    {
                                                        //Also read in the extra attributes.
                                                        int delay = int.Parse(xmlReader.GetAttribute("delay"));
                                                        Entity.Direction direction = (Entity.Direction)Enum.Parse(typeof(Entity.Direction),
                                                            xmlReader.GetAttribute("direction"));
                                                        newEntity = new Tuple<Type, Vector2, object[]>
                                                            (typeof(QuickManLaserGenerator),
                                                            position + offset,
                                                            new object[2] { delay,
                                                                            direction });
                                                    }
                                                    else
                                                        if (entityType == "DarkArea")
                                                        {
                                                            //Also read in the extra attributes.
                                                            int width = int.Parse(xmlReader.GetAttribute("width"));
                                                            int height = int.Parse(xmlReader.GetAttribute("height"));
                                                            newEntity = new Tuple<Type, Vector2, object[]>
                                                                (typeof(DarkArea),
                                                                Vector2.Zero,
                                                                new object[1] { new Rectangle((int)(position.X + offset.X),
                                                              (int)(position.Y + offset.Y),
                                                              width,
                                                              height)});
                                                        }
                                                        else
                                                            if (entityType == "Cake")
                                                            {
                                                                newEntity = new Tuple<Type, Vector2, object[]>
                                                                    (typeof(Cake), position + offset, null);
                                                            }
                                                            else
                                                                throw new Exception("Specified entity type \"" + entityType
                                                                    + "\" does not exist.");

            return newEntity;
        }

        /// <summary>
        /// Checks if an entity is set to spawn on the current difficulty setting.
        /// </summary>
        /// <param name="xmlReader">The reader instance of the Entity subtree.</param>
        /// <param name="gameDifficulty">The game's current difficulty setting.</param>
        /// <returns>Returns true if the current difficulty matches one of the settings in
        /// the entity element, and false otherwise.</returns>
        private bool CheckDifficultySetting(XmlReader xmlReader, GameDifficulty gameDifficulty)
        {
            //Throw an exception if we're not currently on an entity element.
            xmlReader.MoveToContent();
            if (xmlReader.LocalName != "Entity")
                throw new Exception("The current element " + xmlReader.LocalName +
                    "is not a Entity element.");

            //Read to the next difficulties element.
            xmlReader.ReadToFollowing("Difficulties");

            //If the room element is empty (i.e. <Difficulties />), return false.
            if (xmlReader.IsEmptyElement)
                return false;
            else
            {
                //Iterate through all difficulty settings.
                while (xmlReader.ReadToFollowing("Difficulty"))
                {
                    //Read the setting attribute passed as a GameDifficulty enum.
                    GameDifficulty setting = (GameDifficulty)Enum.Parse(typeof(GameDifficulty),
                                                                        xmlReader.GetAttribute("setting"));

                    //Check if the settings match and return true if they do.
                    if (setting == gameDifficulty)
                        return true;
                }
            }

            //If we've got this far, none of the difficulty settings have matched, so return false.
            return false;
        }

        /// <summary>
        /// Creates a list of entity definitions by loading their details from the XML document.
        /// </summary>
        /// <param name="xmlReader">The reader containing the current room's subtree.</param>
        /// <param name="offset">The absolute offset of the room in the level.</param>
        /// <param name="gameDifficulty">The game's current difficulty setting.</param>
        /// <returns>Returns a list of triples containing the entity's type, starting position
        /// and any extra arguments.</returns>
        private List<Tuple<Type, Vector2, object[]>> CreateOriginalEntities(XmlReader xmlReader,
            Vector2 offset, GameDifficulty gameDifficulty)
        {
            //Throw an exception if we're not currently on a room element.
            xmlReader.MoveToContent();
            if (xmlReader.LocalName != "Room")
                throw new Exception("The current element " + xmlReader.LocalName +
                    "is not a Room element.");

            List<Tuple<Type, Vector2, object[]>> entities = new List<Tuple<Type, Vector2, object[]>>();

            //If the room element is not empty (i.e. <Room />)
            if (!xmlReader.IsEmptyElement)
            {
                //Iterate through all entities.
                while (xmlReader.ReadToFollowing("Entity"))
                {
                    //Read an entity's details.
                    Tuple<Type, Vector2, object[]> entity = CreateEntityDetail(xmlReader, offset);

                    //If the current element is empty, assume it will appear on all difficulties -
                    //add it to the original entities list immediately.
                    if (xmlReader.IsEmptyElement)
                    {
                        entities.Add(entity);
                    }
                    //Otherwise, check the entity's difficulty settings, and check if this matches
                    //the current difficulty setting. If so, add the entity to the list.
                    else
                    {
                        if (CheckDifficultySetting((XmlReader)xmlReader.ReadSubtree(),
                                                   gameDifficulty))
                        {
                            entities.Add(entity);
                        }
                    }
                }
            }

            return entities;
        }

        /// <summary>
        /// Creates a list of tiles by iterating through an array stored in a .room file.
        /// </summary>
        /// <param name="roomPath">The complete path of the .room file.</param>
        /// <param name="offset">The absolute offset of the room in the level.</param>
        /// <param name="tileset">The set of tile definitions to use.</param>
        /// <returns>Returns the newly made list.</returns>
        private List<Tile> CreateTiles(string roomPath, Vector2 offset,
            Dictionary<char, Tuple<TileType, Point, Rectangle>> tileset)
        {
            List<Tile> tiles = new List<Tile>();

            //Create a stream reader and attempt to load the .room file.
            StreamReader streamReader = new StreamReader(roomPath);
            string readLine;

            //The vector to create the tile at.
            Vector2 position = Vector2.Zero;

            while ((readLine = streamReader.ReadLine()) != null)
            {
                //Make an array from the line, and iterate through it.
                char[] characters = readLine.ToCharArray();
                foreach (char character in characters)
                {
                    //Look up the found character in the dictionary. (If it's not found,
                    //no tile is created at this position).
                    if (tileset.Keys.Contains<char>(character))
                    {
                        //Get the tile's definition from the dictionary, then use the TileType enum
                        //value to determine what type of tile should be created.
                        Tuple<TileType, Point, Rectangle> definition = tileset[character];

                        switch (definition.Item1)
                        {
                            case TileType.Solid:
                                tiles.Add(new SolidTile(position + offset, definition.Item2, definition.Item3));
                                break;
                            case TileType.Background:
                                tiles.Add(new BackgroundTile(position + offset, definition.Item2));
                                break;
                            case TileType.Ice:
                                tiles.Add(new IceTile(position + offset, definition.Item2, definition.Item3));
                                break;
                            default:
                                throw new NotImplementedException();
                        }
                    }

                    //Increment the x-position.
                    position.X += Tile.DEFAULT_WIDTH;
                }

                //Increment the y-position and reset the x-position.
                position.X = 0;
                position.Y += Tile.DEFAULT_HEIGHT;
            }

            return tiles;
        }

        /// <summary>
        /// Creates a list of rooms by loading their details from the XML document.
        /// </summary>
        /// <param name="xmlReader">The reader instance of the Rooms subtree.</param>
        /// <param name="backgrounds">The previously loaded dictionary of backgrounds.</param>
        /// <param name="tileset">The previously loaded set tile definitions.</param>
        /// <returns>Returns the newly made list of rooms.</returns>
        private List<Room> CreateRooms(XmlReader xmlReader,
                                                         Dictionary<string, Background> backgrounds,
                                                         Dictionary<char, Tuple<TileType, Point, Rectangle>> tileset)
        {
            List<Room> rooms = new List<Room>();

            //The offset at which to create the new room.
            Vector2 offset = Vector2.Zero;

            //The width, height and location of the previous room - these are used when the offset
            //of the next room depends on the dimensions of the previous (NextRoomLocation.BottomRight)
            int previousWidth = 0,
                previousHeight = 0;
            NextRoomLocation previousRoomLocation = NextRoomLocation.None;

            //The current index of the room.
            int index = 0;

            //Iterate through all of the rooms.
            while (xmlReader.ReadToFollowing("Room"))
            {
                //The new room.
                Room room;

                //Get the name of the file that stores the array of tiles.
                string name = xmlReader.GetAttribute("name");

                //Get the dimensions.
                int width = int.Parse(xmlReader.GetAttribute("width"));
                int height = int.Parse(xmlReader.GetAttribute("height"));

                //If, in a previous iteration, this room was to be placed such that entering
                //from the previous room would cause the player to appear at the bottom of the
                //new room (NextRoomLocation.BottomRight) reset the offset because we now know
                //the dimensions of this room.
                if (previousRoomLocation == NextRoomLocation.BottomRight)
                {
                    offset += new Vector2(previousWidth, previousHeight - height);

                    //Now forget the previous room location.
                    previousRoomLocation = NextRoomLocation.None;
                }

                //Get the background to use.
                Background background = backgrounds[xmlReader.GetAttribute("background")];

                //Get where the next room is located.
                NextRoomLocation nextRoomLocation = (NextRoomLocation)Enum.Parse(typeof(NextRoomLocation),
                                                                                 xmlReader.GetAttribute("nextRoomLocation"));

                //Attempt to read in the checkpoint attributes.
                Vector2 checkpoint = Room.NO_CHECKPOINT;
                string checkpointXString, checkpointYString;

                if ((checkpointXString = xmlReader.GetAttribute("checkpointX")) != null
                    | (checkpointYString = xmlReader.GetAttribute("checkpointY")) != null)
                {
                    checkpoint = new Vector2(int.Parse(checkpointXString), int.Parse(checkpointYString));
                }

                //Call the entity and tile list making methods.
                List<Tuple<Type, Vector2, object[]>> originalEntities = CreateOriginalEntities((XmlReader)xmlReader.ReadSubtree(), offset, _mainGame.GameDifficulty);
                List<Tile> tiles = CreateTiles(ContentPath() + _levelPath + name + RoomFileExtension, offset, tileset);

                //Instantiate the room.
                room = new Room(new Rectangle((int)offset.X, (int)offset.Y, width, height),
                                background,
                                tiles,
                                originalEntities,
                                nextRoomLocation,
                                checkpoint);
                rooms.Add(room);

                //Increment the position of the next room based on the dimensions and scroll
                //direction of this one.
                switch (nextRoomLocation)
                {
                    case NextRoomLocation.TopRight:
                        offset += new Vector2(width, 0);
                        break;
                    case NextRoomLocation.Down:
                        offset += new Vector2(width - Camera.VIEWPORT_WIDTH, height);
                        break;
                    case NextRoomLocation.BottomRight:
                        //Store the current width, height and location for the next iteration.
                        previousWidth = width;
                        previousHeight = height;
                        previousRoomLocation = nextRoomLocation;
                        break;
                }   

                //Increase the index.
                index++;
            }

            return rooms;
        }

        /// <summary>
        /// Creates a level by loading the details from an XML document.
        /// </summary>
        /// <param name="levelPath">The full path of the level's directory.</param>
        public Level CreateLevel()
        {
            //Create a new reader instance and load the XML file.
            XmlTextReader xmlTextReader = null;
            Level level = null;

            //Load the XML document.
            try
            {
                xmlTextReader = new XmlTextReader(ContentPath() + _levelPath + _xmlName);

                //Read the background music's asset name.
                xmlTextReader.ReadToFollowing("BackgroundMusic");
                string backgroundMusicAssetName = xmlTextReader.GetAttribute("name");

                //Create a dictionary of the tileset so that the tile creation method
                //knows what character corresponds to what tile.
                xmlTextReader.ReadToFollowing("Tileset");
                string tileAssetName = xmlTextReader.GetAttribute("texture");  //But first get the texture name.
                Dictionary<char, Tuple<TileType, Point, Rectangle>> tileset = CreateTileset((XmlReader)xmlTextReader.ReadSubtree());

                //Create a dictionary of backgrounds so that the rooms (that are later
                //to be created) can reference a background by using a string.
                xmlTextReader.ReadToFollowing("Backgrounds");
                Dictionary<string, Background> backgrounds = CreateBackgrounds((XmlReader)xmlTextReader.ReadSubtree());

                //Read the rooms.
                xmlTextReader.ReadToFollowing("Rooms");
                List<Room> rooms = CreateRooms((XmlReader)xmlTextReader.ReadSubtree(), backgrounds, tileset);

                //Convert the dictionary to a list.
                List<Background> backgroundList = new List<Background>();
                foreach (Background background in backgrounds.Values)
                    backgroundList.Add(background);

                //Finally, make the level.
                level = new Level(_mainGame, "Levels\\" + _levelPath, tileAssetName,
                    backgroundMusicAssetName, rooms, backgroundList);
            }
            finally
            {
                //Close the reader if necessary.
                if (xmlTextReader != null)
                {
                    xmlTextReader.Close();
                }
            }

            //Return the level.
            return level;
        }

        #endregion
    }
}
