using System.Linq;
using System.Xml.Linq;
using SuperTiled2Unity.Editor.Geometry;
using UnityEngine;
using UnityEngine.Assertions;

namespace SuperTiled2Unity.Editor
{
    public class SuperObjectLayerLoader : SuperLayerLoader
    {
        private SuperObjectLayer m_ObjectLayer;
        private float m_AnimationFramerate = 1.0f;

        public SuperObjectLayerLoader(XElement xml, TiledAssetImporter importer)
            : base(xml, importer)
        {
        }

        public ColliderFactory ColliderFactory { get; set; }
        public GlobalTileDatabase GlobalTileDatabase { get; set; }
        public SuperMap SuperMap { get; set; }

        public float AnimationFramerate
        {
            get { return m_AnimationFramerate; }
            set { m_AnimationFramerate = value; }
        }

        public void CreateObjects()
        {
            Assert.IsNotNull(Xml, "Xml exlement is null");
            Assert.IsNotNull(m_ObjectLayer, "Object layer is null");
            Assert.IsNotNull(Importer, "Importer is null");
            Assert.IsNotNull(ColliderFactory, "ColliderFactory is null");

            var xObjects = Xml.Elements("object");
            var drawOrder = Xml.GetAttributeAs<DrawOrder>("draworder", DrawOrder.TopDown);

            if (drawOrder == DrawOrder.TopDown)
            {
                // xObjects need to be ordered by y-value
                xObjects = xObjects.OrderBy(x => x.GetAttributeAs<float>("y", 0.0f));
            }

            foreach (var xObj in xObjects)
            {
                // Ignore invisible objects
                if (!xObj.GetAttributeAs<bool>("visible", true))
                {
                    continue;
                }

                CreateObject(xObj);
            }
        }

        protected override SuperLayer CreateLayerComponent(GameObject go)
        {
            m_ObjectLayer = go.AddComponent<SuperObjectLayer>();
            return m_ObjectLayer;
        }

        protected override void InternalLoadFromXml(GameObject go)
        {
            m_ObjectLayer.m_Color = Xml.GetAttributeAsColor("color", Color.grey);
        }

        private void CreateObject(XElement xObject)
        {
            // Templates may add extra data
            Importer.ApplyTemplateToObject(xObject);

            // Create the super object and fill it out
            var superObject = CreateSuperObject(xObject);
            FillSuperObject(superObject, xObject);

            // Take care of properties
            Importer.AddSuperCustomProperties(superObject.gameObject, xObject.Element("properties"), superObject.m_SuperTile, superObject.m_Type);

            // Post processing after custom properties have been set
            PostProcessObject(superObject.gameObject);
        }

        private SuperObject CreateSuperObject(XElement xObject)
        {
            // Create the object
            GameObject goObject = new GameObject();
            var comp = goObject.AddComponent<SuperObject>();

            // Fill out the attributes
            comp.m_Id = xObject.GetAttributeAs("id", 0);
            comp.m_TiledName = xObject.GetAttributeAs("name", string.Format("Object_{0}", comp.m_Id));

            comp.m_Type = xObject.GetAttributeAs("class", "");

            //As of Tiled 1.9 types have been merged with classes
            //As a simple way to support both version we can fall back like this to the old < 1.9 way
            if (string.IsNullOrWhiteSpace(comp.m_Type))
            {
                comp.m_Type = xObject.GetAttributeAs("type", "");
            }

            comp.m_X = xObject.GetAttributeAs("x", 0.0f);
            comp.m_Y = xObject.GetAttributeAs("y", 0.0f);
            comp.m_Rotation = xObject.GetAttributeAs("rotation", 0.0f);
            comp.m_Width = xObject.GetAttributeAs("width", 0.0f);
            comp.m_Height = xObject.GetAttributeAs("height", 0.0f);
            comp.m_TileId = xObject.GetAttributeAs<uint>("gid", 0);
            comp.m_Visible = xObject.GetAttributeAs("visible", true);
            comp.m_Template = xObject.GetAttributeAs("template", "");

            // Assign the name of our game object
            if (comp.m_TileId != 0)
            {
                // The tile object name is decorated. A descendent will have the "real" object name.
                goObject.name = string.Format("{0} (TRS)", comp.m_TiledName);
            }
            else
            {
                goObject.name = comp.m_TiledName;
            }

            // Position the game object
            var localPosition = new Vector2(comp.m_X, comp.m_Y);
            localPosition = ColliderFactory.TransformPoint(localPosition);
            localPosition = Importer.SuperImportContext.MakePoint(localPosition);

            goObject.transform.localPosition = localPosition;
            goObject.transform.localRotation = Quaternion.Euler(0, 0, Importer.SuperImportContext.MakeRotation(comp.m_Rotation));

            // Add our object to the parent layer
            m_ObjectLayer.gameObject.AddChildWithUniqueName(goObject);

            return comp;
        }

        private void FillSuperObject(SuperObject superObject, XElement xObject)
        {
            // Determine which type of object we are
            var xPolygon = xObject.Element("polygon");
            var xPolyline = xObject.Element("polyline");
            var xEllipse = xObject.Element("ellipse");
            var xPoint = xObject.Element("point");
            var xText = xObject.Element("text");

            bool collisions = Importer.SuperImportContext.LayerIgnoreMode != LayerIgnoreMode.Collision;

            if (superObject.m_TileId != 0)
            {
                ProcessTileObject(superObject, xObject);
            }
            else if (xPolygon != null && collisions)
            {
                ProcessPolygonElement(superObject.gameObject, xPolygon);
            }
            else if (xPolyline != null && collisions)
            {
                ProcessPolylineElement(superObject.gameObject, xPolyline);
            }
            else if (xEllipse != null && collisions)
            {
                ProcessEllipseElement(superObject.gameObject, xObject);
            }
            else if (xText != null)
            {
                // Text objects are not yet supported
            }
            else if (xPoint != null)
            {
                // A point is simply an empty game object out in space.
                // We don't need to add anything else
            }
            else if (collisions)
            {
                // Default object is a rectangle
                ProcessObjectRectangle(superObject.gameObject, xObject);
            }
        }

        private void ProcessTileObject(SuperObject superObject, XElement xObject)
        {
            Assert.IsNull(superObject.m_SuperTile);
            Assert.IsNotNull(GlobalTileDatabase, "Cannot process tile objects without a tileset database");

            SuperTile tile = null;
            var tileId = new TileIdMath(superObject.m_TileId);
            int justTileId = tileId.JustTileId;

            // Are we getting the tile from a template?
            var template = xObject.GetAttributeAs("template", "");
            if (!string.IsNullOrEmpty(template))
            {
                var asset = Importer.RequestDependencyAssetAtPath<ObjectTemplate>(template);
                if (asset == null)
                {
                    return;
                }

                tile = asset.m_Tile;
                if (tile == null)
                {
                    Importer.ReportGenericError($"Missing tile '{justTileId}' from template '{template}' on tile object '{superObject.name}'");
                    return;
                }
            }

            // Are we getting the tile from our tile database?
            if (tile == null)
            {
                GlobalTileDatabase.TryGetTile(justTileId, out tile);

                if (tile == null)
                {
                    Importer.ReportGenericError($"Missing tile '{justTileId}' on tile object '{superObject.name}'");
                    return;
                }
            }

            // Our type may come from the tile as well (this is 'Typed Tiles' in Tiled)
            if (string.IsNullOrEmpty(superObject.m_Type))
            {
                superObject.m_Type = tile.m_Type;
            }

            // Construct the game objects for displaying a single tile
            var inversePPU = Importer.InversePPU;
            bool flip_h = tileId.HasHorizontalFlip;
            bool flip_v = tileId.HasVerticalFlip;

            var scale = Vector3.one;
            scale.x = xObject.GetAttributeAs("width", 1.0f);
            scale.y = xObject.GetAttributeAs("height", 1.0f);

            scale.x /= tile.m_Width;
            scale.y /= tile.m_Height;

            var tileOffset = new Vector3(tile.m_TileOffsetX * inversePPU, -tile.m_TileOffsetY * inversePPU);
            var pivotOffset = ObjectAlignmentToPivot.ToVector3(tile.m_Width, tile.m_Height, inversePPU, SuperMap.m_Orientation, tile.m_ObjectAlignment);

            var translateCenter = new Vector3(tile.m_Width * 0.5f * inversePPU, tile.m_Height * 0.5f * inversePPU);

            // Our root object will contain the translation, rotation, and scale of the tile object
            var goTRS = superObject.gameObject;
            goTRS.transform.localScale = scale;

            // Our pivot object will contain the tileset orientation and offset
            var goPivot = new GameObject();
            goPivot.name = string.Format("{0} (Pivot)", superObject.m_TiledName);
            goPivot.transform.localPosition = tileOffset + pivotOffset;
            goTRS.AddChildWithUniqueName(goPivot);

            // Add another object to handle tile flipping
            // This object will center us into the tile and perform the flips through scaling
            // This object also contains the tile offset in her transform
            var goCF = new GameObject();
            goCF.name = string.Format("{0} (CF)", superObject.m_TiledName);
            goPivot.AddChildWithUniqueName(goCF);

            goCF.transform.localPosition = translateCenter;
            goCF.transform.localRotation = Quaternion.Euler(0, 0, 0);
            goCF.transform.localScale = new Vector3(flip_h ? -1 : 1, flip_v ? -1 : 1, 1);

            var fromCenter = -translateCenter;

            // Add another child, putting our coordinates back into the proper place
            var goTile = new GameObject(superObject.m_TiledName);
            goCF.AddChildWithUniqueName(goTile);
            goTile.transform.localPosition = fromCenter;
            goTile.transform.localRotation = Quaternion.Euler(0, 0, 0);
            goTile.transform.localScale = Vector3.one;

            if (Importer.SuperImportContext.LayerIgnoreMode != LayerIgnoreMode.Visual)
            {
                // Add the renderer
                var renderer = goTile.AddComponent<SpriteRenderer>();
                renderer.sprite = tile.m_Sprite;
                renderer.color = superObject.CalculateColor();
                renderer.spriteSortPoint = SpriteSortPoint.Pivot;
                Importer.AssignMaterial(renderer, m_ObjectLayer.m_TiledName);
                Importer.AssignSpriteSorting(renderer);

                // Add the animator if needed
                if (!tile.m_AnimationSprites.IsEmpty())
                {
                    var tileAnimator = goTile.AddComponent<TileObjectAnimator>();
                    tileAnimator.m_AnimationFramerate = AnimationFramerate;
                    tileAnimator.m_AnimationSprites = tile.m_AnimationSprites;
                }
            }

            if (Importer.SuperImportContext.LayerIgnoreMode != LayerIgnoreMode.Collision)
            {
                // Add any colliders that were set up on the tile in the collision editor
                tile.AddCollidersForTileObject(goTile, Importer.SuperImportContext);
            }

            // Store a reference to our tile object
            superObject.m_SuperTile = tile;
        }

        private void ProcessPolygonElement(GameObject goObject, XElement xPolygon)
        {
            // Get the points of the polygon so we can decompose into a collection of convex polygons
            var points = xPolygon.GetAttributeAsVector2Array("points");
            points = points.Select(p => ColliderFactory.TransformPoint(p)).ToArray();
            points = Importer.SuperImportContext.MakePoints(points);

            // Triangulate the polygon points
            var triangulator = new Triangulator();
            var triangles = triangulator.TriangulatePolygon(points);

            // Gather triangles into a collection of convex polygons
            var composition = new ComposeConvexPolygons();
            var convexPolygons = composition.Compose(triangles);

            PolygonUtils.AddCompositePolygonCollider(goObject, convexPolygons, Importer.SuperImportContext);
        }

        private void ProcessPolylineElement(GameObject goObject, XElement xPolyline)
        {
            var points = xPolyline.GetAttributeAsVector2Array("points");
            ColliderFactory.MakePolyline(goObject, points);
            goObject.AddComponent<SuperColliderComponent>();
        }

        private void ProcessEllipseElement(GameObject goObject, XElement xObject)
        {
            var width = xObject.GetAttributeAs("width", 0f);
            var height = xObject.GetAttributeAs("height", 0f);
            ColliderFactory.MakeEllipse(goObject, width, height);
            goObject.AddComponent<SuperColliderComponent>();
        }

        private void ProcessObjectRectangle(GameObject goObject, XElement xObject)
        {
            var width = xObject.GetAttributeAs("width", 0f);
            var height = xObject.GetAttributeAs("height", 0f);
            ColliderFactory.MakeBox(goObject, width, height);
            goObject.AddComponent<SuperColliderComponent>();
        }

        private void PostProcessObject(GameObject go)
        {
            var properties = go.GetComponent<SuperCustomProperties>();
            var collider = go.GetComponent<Collider2D>();

            if (collider != null)
            {
                CustomProperty isTrigger;
                if (properties.TryGetCustomProperty(StringConstants.Unity_IsTrigger, out isTrigger))
                {
                    collider.isTrigger = Importer.SuperImportContext.GetIsTriggerOverridable(isTrigger.GetValueAsBool());
                }
            }

            // Make sure all children have the same physics layer
            // This is needed for Tile objects in particular that have their colliders in child objects
            go.AssignChildLayers();
        }
    }
}
