using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using System.Xml.Linq;

namespace Barotrauma
{
    public enum Anchor
    {
        TopLeft, TopCenter, TopRight,
        CenterLeft, Center, CenterRight,
        BottomLeft, BottomCenter, BottomRight
    }

    public enum Pivot
    {
        TopLeft, TopCenter, TopRight,
        CenterLeft, Center, CenterRight,
        BottomLeft, BottomCenter, BottomRight
    }

    public class RectTransform
    {
        #region Fields and Properties
        /// <summary>
        /// Should be assigned only by GUIComponent.
        /// Note that RectTransform is created first and the GUIComponent after that.
        /// This means the GUIComponent is not set before the GUIComponent is initialized.
        /// </summary>
        public GUIComponent GUIComponent { get; set; }

        private RectTransform parent;
        public RectTransform Parent
        {
            get { return parent; }
            set
            {
                if (parent == value || value == this) { return; }
                // Remove the child from the old parent
                RemoveFromHierarchy(displayErrors: false);
                parent = value;
                if (parent != null && !parent.children.Contains(this))
                {
                    parent.children.Add(this);
                    RecalculateAll(false, true, true);
                    ParentChanged?.Invoke(parent);
                    Parent.ChildrenChanged?.Invoke(this);
                }
            }
        }

        private List<RectTransform> children = new List<RectTransform>();
        public IEnumerable<RectTransform> Children => children;

        public int CountChildren => children.Count;

        private Vector2 relativeSize = Vector2.One;
        /// <summary>
        /// Relative to the parent rect.
        /// </summary>
        public Vector2 RelativeSize
        {
            get { return relativeSize; }
            set
            {
                if (relativeSize.NearlyEquals(value)) { return; }
                relativeSize = value;
                RecalculateAll(resize: true, scale: false, withChildren: true);
            }
        }

        private Point? minSize;
        /// <summary>
        /// Min size in pixels.
        /// Does not affect scaling.
        /// </summary>
        public Point MinSize
        {
            get { return minSize ?? Point.Zero; }
            set
            {
                if (minSize == value) { return; }
                minSize = value;
                RecalculateAll(true, false, true);
            }
        }

        private static Point maxPoint = new Point(int.MaxValue, int.MaxValue);
        private Point? maxSize;

        /// <summary>
        /// Max size in pixels.
        /// Does not affect scaling.
        /// </summary>
        public Point MaxSize
        {
            get { return maxSize ?? maxPoint; }
            set
            {
                if (maxSize == value) { return; }
                maxSize = value;
                RecalculateAll(true, false, true);
            }
        }

        private Point nonScaledSize;
        /// <summary>
        /// Size before scale multiplications.
        /// </summary>
        public Point NonScaledSize
        {
            get { return nonScaledSize; }
            set
            {
                if (nonScaledSize == value) { return; }
                nonScaledSize = value.Clamp(MinSize, MaxSize);
                RecalculateRelativeSize();
                RecalculateAnchorPoint();
                RecalculatePivotOffset();
                RecalculateChildren(resize: true, scale: false);
            }
        }
        /// <summary>
        /// Size after scale multiplications.
        /// </summary>
        public Point ScaledSize => NonScaledSize.Multiply(Scale);

        /// <summary>
        /// Applied to all RectTransforms.
        /// The elements are not automatically resized, if the global scale changes.
        /// You have to manually call RecalculateScale() for all elements after changing the global scale.
        /// This is because there is currently no easy way to inform all the elements without having a reference to them.
        /// Having a reference (static list, or event) is problematic, because deconstructing the elements is not handled manually.
        /// This means that the uncleared references would bloat the memory.
        /// We could recalculate the scale each time it's needed, 
        /// but in that case the calculation would need to be very lightweight and garbage free, which it currently is not.
        /// </summary>
        public static Vector2 globalScale = Vector2.One;

        private Vector2 localScale = Vector2.One;
        public Vector2 LocalScale
        {
            get { return localScale; }
            set
            {
                if (localScale.NearlyEquals(value)) { return; }
                localScale = value;
                RecalculateAll(resize: false, scale: true, withChildren: true);
                ScaleChanged?.Invoke();
            }
        }

        public Vector2 Scale { get; private set; }

        private Vector2 relativeOffset = Vector2.Zero;
        private Point absoluteOffset = Point.Zero;
        private Point screenSpaceOffset = Point.Zero;
        /// <summary>
        /// Defined as portions of the parent size.
        /// Also the direction of the offset is relative, calculated away from the anchor point.
        /// </summary>
        public Vector2 RelativeOffset
        {
            get { return relativeOffset; }
            set
            {
                if (relativeOffset.NearlyEquals(value)) { return; }
                relativeOffset = value;
                RecalculateChildren(false, false);
            }
        }
        /// <summary>
        /// Absolute in pixels but relative to the anchor point.
        /// Calculated away from the anchor point, like a padding.
        /// Use RelativeOffset to set an amount relative to the parent size.
        /// </summary>
        public Point AbsoluteOffset
        {
            get { return absoluteOffset; }
            set
            {
                if (absoluteOffset == value) { return; }
                absoluteOffset = value;
                recalculateRect = true;
                RecalculateChildren(false, false);
            }
        }
        /// <summary>
        /// Screen space offset. From top left corner. In pixels.
        /// </summary>
        public Point ScreenSpaceOffset
        {
            get { return screenSpaceOffset; }
            set
            {
                if (screenSpaceOffset == value) { return; }
                screenSpaceOffset = value;
                recalculateRect = true;
                RecalculateChildren(false, false);
            }
        }
        /// <summary>
        /// Calculated from the selected pivot. In pixels.
        /// </summary>
        public Point PivotOffset { get; private set; }
        /// <summary>
        /// Screen space point in pixels.
        /// </summary>
        public Point AnchorPoint { get; private set; }

        public Point TopLeft
        {
            get
            {
                Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor);
                Point relativeOffset = ParentRect.MultiplySize(RelativeOffset);
                relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor);
                return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset;
            }
        }

        protected Point NonScaledTopLeft
        {
            get
            {
                Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor);
                Point relativeOffset = NonScaledParentRect.MultiplySize(RelativeOffset);
                relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor);
                return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset;
            }
        }

        private bool recalculateRect = true;
        private Rectangle _rect;
        public Rectangle Rect
        {
            get
            {
                if (recalculateRect)
                {
                    _rect = new Rectangle(TopLeft, ScaledSize);
                    recalculateRect = false;
                }
                return _rect;
            }
        }
        public Rectangle ParentRect => Parent != null ? Parent.Rect : ScreenRect;

        protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize);
        protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : ScreenRect;
        protected Rectangle ScreenRect => new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);

        private Pivot pivot;
        /// <summary>
        /// Does not automatically calculate children.
        /// Note also that if you change the pivot point with this property, the pivot does not automatically match the anchor.
        /// You can use SetPosition to change everything automatcally or MatchPivotToAnchor to match the pivot to anchor.
        /// </summary>
        public Pivot Pivot
        {
            get { return pivot; }
            set
            {
                if (pivot == value) { return; }
                pivot = value;
                RecalculatePivotOffset();
            }
        }

        private Anchor anchor;
        /// <summary>
        /// Does not automatically calculate children.
        /// Note also that if you change the anchor point with this property, the pivot does not automatically match the anchor.
        /// You can use SetPosition to change everything automatically or MatchPivotToAnchor to match the pivot to anchor.
        /// </summary>
        public Anchor Anchor
        {
            get { return anchor; }
            set
            {
                if (anchor == value) { return; }
                anchor = value;
                RecalculateAnchorPoint();
            }
        }

        public bool IsLastChild
        {
            get
            {
                if (Parent == null) { return false; }
                var last = Parent.Children.LastOrDefault();
                if (last == null) { return false; }
                return last == this;
            }
        }

        public bool IsFirstChild
        {
            get
            {
                if (Parent == null) { return false; }
                var first = Parent.Children.FirstOrDefault();
                if (first == null) { return false; }
                return first == this;
            }
        }
        #endregion

        #region Events
        public event Action<RectTransform> ParentChanged;
        /// <summary>
        /// The element provided as the argument is the changed child. It may be new in the hierarchy or just repositioned.
        /// </summary>
        public event Action<RectTransform> ChildrenChanged;
        public event Action ScaleChanged;
        public event Action SizeChanged;
        #endregion

        #region Initialization
        public RectTransform(Vector2 relativeSize, RectTransform parent, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null)
        {
            Init(parent, anchor, pivot);
            this.relativeSize = relativeSize;
            this.minSize = minSize;
            this.maxSize = maxSize;
            RecalculateScale();
            RecalculateAbsoluteSize();
            RecalculateAnchorPoint();
            RecalculatePivotOffset();
            parent?.ChildrenChanged?.Invoke(this);
        }

        /// <summary>
        /// By default, elements defined with an absolute size (in pixels), will be treated as fixed sized.
        /// This can be changed by setting IsFixedSize to false.
        /// </summary>
        public RectTransform(Point absoluteSize, RectTransform parent = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null)
        {
            Init(parent, anchor, pivot);
            this.nonScaledSize = absoluteSize;
            RecalculateScale();
            RecalculateRelativeSize();            
            RecalculateAnchorPoint();
            RecalculatePivotOffset();
            IsFixedSize = true;
            parent?.ChildrenChanged?.Invoke(this);
        }

        public static RectTransform Load(XElement element, RectTransform parent)
        {
            Enum.TryParse(element.GetAttributeString("anchor", "Center"), out Anchor anchor);
            Enum.TryParse(element.GetAttributeString("pivot", anchor.ToString()), out Pivot pivot);

            Point? minSize = null, maxSize = null;
            if (element.Attribute("minsize") != null) minSize = element.GetAttributePoint("minsize", Point.Zero);
            if (element.Attribute("maxsize") != null) maxSize = element.GetAttributePoint("maxsize", new Point(1000, 1000));

            RectTransform rectTransform;
            if (element.Attribute("relativesize") != null)
            {
                rectTransform = new RectTransform(element.GetAttributeVector2("relativesize", Vector2.One), parent, anchor, pivot, minSize, maxSize);
            }
            else
            {
                rectTransform = new RectTransform(element.GetAttributePoint("absolutesize", new Point(1000, 1000)), parent, anchor, pivot)
                {
                    minSize = minSize,
                    maxSize = maxSize
                };
            }
            rectTransform.RelativeOffset = element.GetAttributeVector2("relativeoffset", Vector2.Zero);
            rectTransform.AbsoluteOffset = element.GetAttributePoint("absoluteoffset", Point.Zero);
            return rectTransform;
        }

        private void Init(RectTransform parent = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null)
        {
            this.parent = parent;
            parent?.children.Add(this);
            Anchor = anchor;
            Pivot = pivot ?? MatchPivotToAnchor(Anchor);
        }
        #endregion

        #region Protected methods
        protected void RecalculateScale()
        {
            var scale = LocalScale * globalScale;
            var parents = GetParents();
            Scale = parents.Any() ? parents.Select(rt => rt.LocalScale).Aggregate((parent, child) => parent * child) * scale : scale;
            recalculateRect = true;
            ScaleChanged?.Invoke();
        }

        protected void RecalculatePivotOffset()
        {
            PivotOffset = CalculatePivotOffset(Pivot, ScaledSize);
            recalculateRect = true;
        }

        protected void RecalculateAnchorPoint()
        {
            AnchorPoint = CalculateAnchorPoint(Anchor, ParentRect);
            recalculateRect = true;
        }

        protected void RecalculateRelativeSize()
        {
            relativeSize = new Vector2(NonScaledSize.X, NonScaledSize.Y) / new Vector2(NonScaledParentRect.Width, NonScaledParentRect.Height);
            recalculateRect = true;
            SizeChanged?.Invoke();
        }

        protected void RecalculateAbsoluteSize()
        {
            nonScaledSize = NonScaledParentRect.Size.Multiply(RelativeSize).Clamp(MinSize, MaxSize);
            recalculateRect = true;
            SizeChanged?.Invoke();
        }

        /// <summary>
        /// If false, the element will resize if the parent is resized (with the children).
        /// If true, the element will resize only when explicitly resized.
        /// Note that scaling always affects the elements.
        /// </summary>
        public bool IsFixedSize { get; set; }

        protected void RecalculateAll(bool resize, bool scale = true, bool withChildren = true)
        {
            if (scale)
            {
                RecalculateScale();
            }
            if (resize && !IsFixedSize)
            {
                RecalculateAbsoluteSize();
            }
            RecalculateAnchorPoint();
            RecalculatePivotOffset();
            if (withChildren)
            {
                RecalculateChildren(resize, scale);
            }
        }

        private bool RemoveFromHierarchy(bool displayErrors = true, bool recalculate = true)
        {
            if (Parent == null)
            {
                if (displayErrors)
                {
                    DebugConsole.ThrowError("Parent null" + Environment.StackTrace);
                }
                return false;
            }
            if (!Parent.children.Contains(this))
            {
                if (displayErrors)
                {
                    DebugConsole.ThrowError("The children of the parent does not contain this child. This should not be possible! " + Environment.StackTrace);
                }
                return false;
            }
            if (!Parent.children.Remove(this))
            {
                if (displayErrors)
                {
                    DebugConsole.ThrowError("Unable to remove the child from the parent. " + Environment.StackTrace);
                }
                return false;
            }
            return true;
        }
        #endregion

        #region Public instance methods
        public void SetPosition(Anchor anchor, Pivot? pivot = null)
        {
            Anchor = anchor;
            Pivot = pivot ?? MatchPivotToAnchor(anchor);
            ScreenSpaceOffset = Point.Zero;
            recalculateRect = true;
            RecalculateChildren(false, false);
        }

        public void Resize(Point absoluteSize, bool resizeChildren = true)
        {
            nonScaledSize = absoluteSize;
            RecalculateRelativeSize();
            RecalculateAll(resize: false, scale: false, withChildren: false);
            RecalculateChildren(resizeChildren, false);
        }

        public void Resize(Vector2 relativeSize, bool resizeChildren = true)
        {
            this.relativeSize = relativeSize;
            RecalculateAll(resize: true, scale: false, withChildren: false);
            RecalculateChildren(resizeChildren, false);
        }

        public void ChangeScale(Vector2 newScale)
        {
            LocalScale = newScale;
        }

        public void ResetScale()
        {
            ChangeScale(Vector2.One);
        }

        /// <summary>
        /// Currently this needs to be manually called only when the global scale changes.
        /// If the local scale changes, the scale is automatically recalculated.
        /// </summary>
        public void RecalculateScale(bool withChildren)
        {
            RecalculateScale();
            if (withChildren)
            {
                RecalculateChildren(resize: false, scale: true);
            }
        }

        /// <summary>
        /// Manipulates ScreenSpaceOffset. 
        /// If you want to manipulate some other offset, access the property setters directly.
        /// </summary>
        public void Translate(Point translation)
        {
            ScreenSpaceOffset += translation;
        }

        /// <summary>
        /// Returns all parent elements in the hierarchy.
        /// </summary>
        public IEnumerable<RectTransform> GetParents()
        {
            var parents = new List<RectTransform>();
            if (Parent != null)
            {
                parents.Add(Parent);
                return parents.Concat(Parent.GetParents());
            }
            else
            {
                return parents;
            }
        }

        /// <summary>
        /// Returns all child elements in the hierarchy.
        /// </summary>
        public IEnumerable<RectTransform> GetAllChildren()
        {
            return children.SelectManyRecursive(c => c.children);
        }

        public int GetChildIndex(RectTransform rectT)
        {
            return children.IndexOf(rectT);
        }

        public RectTransform GetChild(int index)
        {
            return children[index];
        }

        public bool IsParentOf(RectTransform rectT, bool recursive = true)
        {
            return children.Contains(rectT) || (recursive && children.Any(c => c.IsParentOf(rectT)));
        }

        public void ClearChildren()
        {
            children.ForEachMod(c => c.Parent = null);
        }

        public void SortChildren(Comparison<RectTransform> comparison)
        {
            children.Sort(comparison);
            RecalculateAll(false, true, true);
            Parent.ChildrenChanged?.Invoke(this);
        }

        public void SetAsLastChild()
        {
            if (IsLastChild) { return; }
            if (!RemoveFromHierarchy(displayErrors: true)) { return; }
            parent.children.Add(this);
            RecalculateAll(false, true, true);
            parent.ChildrenChanged?.Invoke(this);
        }

        public void SetAsFirstChild()
        {
            if (IsFirstChild) { return; }
            RepositionChildInHierarchy(0);
        }

        public bool RepositionChildInHierarchy(int index)
        {
            if (!RemoveFromHierarchy(displayErrors: true)) { return false; }
            try
            {
                Parent.children.Insert(index, this);
            }
            catch (ArgumentOutOfRangeException e)
            {
                DebugConsole.ThrowError(e.ToString());
                return false;
            }
            RecalculateAll(false, true, true);
            Parent.ChildrenChanged?.Invoke(this);
            return true;
        }

        public void RecalculateChildren(bool resize, bool scale = true)
        {
            for (int i = 0; i < children.Count; i++)
            {
                children[i].RecalculateAll(resize, scale, withChildren: true);
            }
        }

        public void AddChildrenToGUIUpdateList(bool ignoreChildren = false, int order = 0)
        {
            for (int i = 0; i < children.Count; i++)
            {
                children[i].GUIComponent.AddToGUIUpdateList(ignoreChildren, order);
            }
        }
        #endregion

        #region Static methods
        public static Pivot MatchPivotToAnchor(Anchor anchor)
        {
            if (!Enum.TryParse(anchor.ToString(), out Pivot pivot))
            {
                throw new Exception($"[RectTransform] Cannot match pivot to anchor {anchor}");
            }
            return pivot;
        }

        /// <summary>
        /// Converts the offset so that the direction is always away from the anchor point.
        /// </summary>
        public static Point ConvertOffsetRelativeToAnchor(Point offset, Anchor anchor)
        {
            switch (anchor)
            {
                case Anchor.BottomRight:
                    return offset.Inverse();
                case Anchor.BottomLeft:
                case Anchor.BottomCenter:
                    return new Point(offset.X, -offset.Y);
                case Anchor.TopRight:
                case Anchor.CenterRight:
                    return new Point(-offset.X, offset.Y);
                default:
                    return offset;
            }
        }

        public static Point CalculatePivotOffset(Pivot pivot, Point size)
        {
            int width = size.X;
            int height = size.Y;
            switch (pivot)
            {
                case Pivot.TopLeft:
                    return Point.Zero;
                case Pivot.TopCenter:
                    return new Point(-width / 2, 0);
                case Pivot.TopRight:
                    return new Point(-width, 0);
                case Pivot.CenterLeft:
                    return new Point(0, -height / 2);
                case Pivot.Center:
                    return size.Divide(2).Inverse();
                case Pivot.CenterRight:
                    return new Point(-width, -height / 2);
                case Pivot.BottomLeft:
                    return new Point(0, -height);
                case Pivot.BottomCenter:
                    return new Point(-width / 2, -height);
                case Pivot.BottomRight:
                    return new Point(-width, -height);
                default:
                    throw new NotImplementedException(pivot.ToString());
            }
        }

        public static Point CalculateAnchorPoint(Anchor anchor, Rectangle parent)
        {
            switch (anchor)
            {
                case Anchor.TopLeft:
                    return parent.Location;
                case Anchor.TopCenter:
                    return new Point(parent.Center.X, parent.Top);
                case Anchor.TopRight:
                    return new Point(parent.Right, parent.Top);
                case Anchor.CenterLeft:
                    return new Point(parent.Left, parent.Center.Y);
                case Anchor.Center:
                    return parent.Center;
                case Anchor.CenterRight:
                    return new Point(parent.Right, parent.Center.Y);
                case Anchor.BottomLeft:
                    return new Point(parent.Left, parent.Bottom);
                case Anchor.BottomCenter:
                    return new Point(parent.Center.X, parent.Bottom);
                case Anchor.BottomRight:
                    return new Point(parent.Right, parent.Bottom);
                default:
                    throw new NotImplementedException(anchor.ToString());
            }
        }

        /// <summary>
        /// The elements are not automatically resized, if the global scale changes.
        /// You have to manually call RecalculateScale() for all elements after changing the global scale.
        /// This is because there is currently no easy way to inform all the elements without having a reference to them.
        /// Having a reference (static list, or event) is problematic, because deconstructing the elements is not handled manually.
        /// This means that the uncleared references would bloat the memory.
        /// We could recalculate the scale each time it's needed, 
        /// but in that case the calculation would need to be very lightweight and garbage free, which it currently is not.
        /// </summary>
        public static void ResetGlobalScale()
        {
            globalScale = Vector2.One;
        }
        #endregion
    }
}
