// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

using System;

namespace FlaxEngine.GUI
{
    public partial class Control
    {
        /// <summary>
        /// Gets or sets X coordinate of the upper-left corner of the control relative to the upper-left corner of its container.
        /// </summary>
        [HideInEditor, NoSerialize]
        public float X
        {
            get => _bounds.X;
            set => Bounds = new Rectangle(value, Y, _bounds.Size);
        }

        /// <summary>
        /// Gets or sets Y coordinate of the upper-left corner of the control relative to the upper-left corner of its container.
        /// </summary>
        [HideInEditor, NoSerialize]
        public float Y
        {
            get => _bounds.Y;
            set => Bounds = new Rectangle(X, value, _bounds.Size);
        }

        /// <summary>
        /// Gets or sets the normalized position in the parent control that the upper left corner is anchored to (range 0-1).
        /// </summary>
        [Serialize]
        [ExpandGroups, Limit(0.0f, 1.0f, 0.01f), EditorDisplay("Transform"), EditorOrder(990), Tooltip("The normalized position in the parent control that the upper left corner is anchored to (range 0-1).")]
        public Vector2 AnchorMin
        {
            get => _anchorMin;
            set
            {
                if (!_anchorMin.Equals(ref value))
                {
                    var bounds = Bounds;
                    _anchorMin = value;
                    UpdateBounds();
                    Bounds = bounds;
                }
            }
        }

        /// <summary>
        /// Gets or sets the normalized position in the parent control that the bottom right corner is anchored to (range 0-1).
        /// </summary>
        [Serialize]
        [ExpandGroups, Limit(0.0f, 1.0f, 0.01f), EditorDisplay("Transform"), EditorOrder(991), Tooltip("The normalized position in the parent control that the bottom right corner is anchored to (range 0-1).")]
        public Vector2 AnchorMax
        {
            get => _anchorMax;
            set
            {
                if (!_anchorMax.Equals(ref value))
                {
                    var bounds = Bounds;
                    _anchorMax = value;
                    UpdateBounds();
                    Bounds = bounds;
                }
            }
        }

        /// <summary>
        /// Gets or sets the offsets of the corners of the control relative to its anchors.
        /// </summary>
        [Serialize]
        [ExpandGroups, EditorDisplay("Transform"), EditorOrder(992), Tooltip("The offsets of the corners of the control relative to its anchors.")]
        public Margin Offsets
        {
            get => _offsets;
            set
            {
                if (!_offsets.Equals(ref value))
                {
                    _offsets = value;
                    UpdateBounds();
                }
            }
        }

        /// <summary>
        /// Gets or sets coordinates of the upper-left corner of the control relative to the upper-left corner of its container.
        /// </summary>
        [NoSerialize]
        [ExpandGroups, EditorDisplay("Transform"), EditorOrder(1000), Tooltip("The location of the upper-left corner of the control relative to he upper-left corner of its container.")]
        public Vector2 Location
        {
            get => _bounds.Location;
            set => Bounds = new Rectangle(value, _bounds.Size);
        }

        /// <summary>
        /// Gets or sets width of the control.
        /// </summary>
        [NoSerialize, HideInEditor]
        public float Width
        {
            get => _bounds.Width;
            set => Bounds = new Rectangle(_bounds.Location, value, _bounds.Height);
        }

        /// <summary>
        /// Gets or sets height of the control.
        /// </summary>
        [NoSerialize, HideInEditor]
        public float Height
        {
            get => _bounds.Height;
            set => Bounds = new Rectangle(_bounds.Location, _bounds.Width, value);
        }

        /// <summary>
        /// Gets or sets control's size.
        /// </summary>
        [NoSerialize]
        [EditorDisplay("Transform"), EditorOrder(1010), Tooltip("The size of the control bounds.")]
        public Vector2 Size
        {
            get => _bounds.Size;
            set => Bounds = new Rectangle(_bounds.Location, value);
        }

        /// <summary>
        /// Gets Y coordinate of the top edge of the control relative to the upper-left corner of its container.
        /// </summary>
        public float Top => _bounds.Top;

        /// <summary>
        /// Gets Y coordinate of the bottom edge of the control relative to the upper-left corner of its container.
        /// </summary>
        public float Bottom => _bounds.Bottom;

        /// <summary>
        /// Gets X coordinate of the left edge of the control relative to the upper-left corner of its container.
        /// </summary>
        public float Left => _bounds.Left;

        /// <summary>
        /// Gets X coordinate of the right edge of the control relative to the upper-left corner of its container.
        /// </summary>
        public float Right => _bounds.Right;

        /// <summary>
        /// Gets position of the upper left corner of the control relative to the upper-left corner of its container.
        /// </summary>
        public Vector2 UpperLeft => _bounds.UpperLeft;

        /// <summary>
        /// Gets position of the upper right corner of the control relative to the upper-left corner of its container.
        /// </summary>
        public Vector2 UpperRight => _bounds.UpperRight;

        /// <summary>
        /// Gets position of the bottom right corner of the control relative to the upper-left corner of its container.
        /// </summary>
        public Vector2 BottomRight => _bounds.BottomRight;

        /// <summary>
        /// Gets position of the bottom left of the control relative to the upper-left corner of its container.
        /// </summary>
        public Vector2 BottomLeft => _bounds.BottomLeft;

        /// <summary>
        /// Gets center position of the control relative to the upper-left corner of its container.
        /// </summary>
        public Vector2 Center => _bounds.Center;

        /// <summary>
        /// Gets or sets control's bounds rectangle.
        /// </summary>
        [HideInEditor, NoSerialize]
        public Rectangle Bounds
        {
            get => _bounds;
            set
            {
                if (!_bounds.Equals(ref value))
                {
                    // Calculate anchors based on the parent container client area
                    Margin anchors;
                    if (_parent != null)
                    {
                        _parent.GetDesireClientArea(out var parentBounds);
                        anchors = new Margin
                        (
                         _anchorMin.X * parentBounds.Size.X + parentBounds.Location.X,
                         _anchorMax.X * parentBounds.Size.X,
                         _anchorMin.Y * parentBounds.Size.Y + parentBounds.Location.Y,
                         _anchorMax.Y * parentBounds.Size.Y
                        );
                    }
                    else
                    {
                        anchors = Margin.Zero;
                    }

                    // Calculate offsets on X axis
                    _offsets.Left = value.Location.X - anchors.Left;
                    if (_anchorMin.X != _anchorMax.X)
                    {
                        _offsets.Right = anchors.Right - value.Location.X - value.Size.X;
                    }
                    else
                    {
                        _offsets.Right = value.Size.X;
                    }

                    // Calculate offsets on Y axis
                    _offsets.Top = value.Location.Y - anchors.Top;
                    if (_anchorMin.Y != _anchorMax.Y)
                    {
                        _offsets.Bottom = anchors.Bottom - value.Location.Y - value.Size.Y;
                    }
                    else
                    {
                        _offsets.Bottom = value.Size.Y;
                    }

                    // Flush the control bounds
                    UpdateBounds();
                }
            }
        }

        /// <summary>
        /// Gets or sets the scale.
        /// </summary>
        [EditorDisplay("Transform"), Limit(float.MinValue, float.MaxValue, 0.1f), EditorOrder(1020), Tooltip("The control scale parameter.")]
        public Vector2 Scale
        {
            get => _scale;
            set
            {
                if (!_scale.Equals(ref value))
                {
                    SetScaleInternal(ref value);
                }
            }
        }

        /// <summary>
        /// Gets or sets the normalized pivot location (used to transform control around it). Point (0,0) is upper left corner, (0.5,0.5) is center, (1,1) is bottom right corner.
        /// </summary>
        [EditorDisplay("Transform"), Limit(0.0f, 1.0f, 0.1f), EditorOrder(1030), Tooltip("The control rotation pivot location in normalized control size. Point (0,0) is upper left corner, (0.5,0.5) is center, (1,1) is bottom right corner.")]
        public Vector2 Pivot
        {
            get => _pivot;
            set
            {
                if (!_pivot.Equals(ref value))
                {
                    SetPivotInternal(ref value);
                }
            }
        }

        /// <summary>
        /// Gets or sets the shear transform angles (x, y). Defined in degrees.
        /// </summary>
        [EditorDisplay("Transform"), EditorOrder(1040), Tooltip("The shear transform angles (x, y). Defined in degrees.")]
        public Vector2 Shear
        {
            get => _shear;
            set
            {
                if (!_shear.Equals(ref value))
                {
                    SetShearInternal(ref value);
                }
            }
        }

        /// <summary>
        /// Gets or sets the rotation angle (in degrees).
        /// </summary>
        [EditorDisplay("Transform"), EditorOrder(1050), Tooltip("The control rotation angle (in degrees).")]
        public float Rotation
        {
            get => _rotation;
            set
            {
                if (!Mathf.NearEqual(_rotation, value))
                {
                    SetRotationInternal(value);
                }
            }
        }

        /// <summary>
        /// Updates the control cached bounds (based on anchors and offsets).
        /// </summary>
        [NoAnimate]
        public void UpdateBounds()
        {
            var prevBounds = _bounds;

            // Calculate anchors based on the parent container client area
            Margin anchors;
            Vector2 offset;
            if (_parent != null)
            {
                _parent.GetDesireClientArea(out var parentBounds);
                anchors = new Margin
                (
                 _anchorMin.X * parentBounds.Size.X,
                 _anchorMax.X * parentBounds.Size.X,
                 _anchorMin.Y * parentBounds.Size.Y,
                 _anchorMax.Y * parentBounds.Size.Y
                );
                offset = parentBounds.Location;
            }
            else
            {
                anchors = Margin.Zero;
                offset = Vector2.Zero;
            }

            // Calculate position and size on X axis
            _bounds.Location.X = anchors.Left + _offsets.Left;
            if (_anchorMin.X != _anchorMax.X)
            {
                _bounds.Size.X = anchors.Right - _bounds.Location.X - _offsets.Right;
            }
            else
            {
                _bounds.Size.X = _offsets.Right;
            }

            // Calculate position and size on Y axis
            _bounds.Location.Y = anchors.Top + _offsets.Top;
            if (_anchorMin.Y != _anchorMax.Y)
            {
                _bounds.Size.Y = anchors.Bottom - _bounds.Location.Y - _offsets.Bottom;
            }
            else
            {
                _bounds.Size.Y = _offsets.Bottom;
            }

            // Apply the offset
            _bounds.Location += offset;

            // Update cached transformation matrix
            UpdateTransform();

            // Handle location/size changes
            if (_bounds.Location != prevBounds.Location)
            {
                OnLocationChanged();
            }

            if (_bounds.Size != prevBounds.Size)
            {
                OnSizeChanged();
            }
        }

        /// <summary>
        /// Updates the control cached transformation matrix (based on bounds).
        /// </summary>
        [NoAnimate]
        public void UpdateTransform()
        {
            // Actual pivot and negative pivot
            Vector2 v1, v2;
            Vector2.Multiply(ref _pivot, ref _bounds.Size, out v1);
            Vector2.Negate(ref v1, out v2);
            Vector2.Add(ref v1, ref _bounds.Location, out v1);

            // ------ Matrix3x3 based version:

            /*
            // Negative pivot
            Matrix3x3 m1, m2;
            Matrix3x3.Translation2D(ref v2, out m1);

            // Scale
            Matrix3x3.Scaling(_scale.X, _scale.Y, 1, out m2);
            Matrix3x3.Multiply(ref m1, ref m2, out m1);

            // Shear
            Matrix3x3.Shear(ref _shear, out m2);
            Matrix3x3.Multiply(ref m1, ref m2, out m1);

            // Rotation
            Matrix3x3.RotationZ(_rotation * Mathf.DegreesToRadians, out m2);
            Matrix3x3.Multiply(ref m1, ref m2, out m1);

            // Pivot + Location
            Matrix3x3.Translation2D(ref v1, out m2);
            Matrix3x3.Multiply(ref m1, ref m2, out _cachedTransform);
            */

            // ------ Matrix2x2 based version:

            // 2D transformation
            Matrix2x2 m1, m2;
            Matrix2x2.Scale(ref _scale, out m1);
            Matrix2x2.Shear(ref _shear, out m2);
            Matrix2x2.Multiply(ref m1, ref m2, out m1);
            Matrix2x2.Rotation(Mathf.DegreesToRadians * _rotation, out m2);
            Matrix2x2.Multiply(ref m1, ref m2, out m1);

            // Mix all the stuff
            Matrix3x3 m3;
            Matrix3x3.Translation2D(ref v2, out m3);
            Matrix3x3 m4 = (Matrix3x3)m1;
            Matrix3x3.Multiply(ref m3, ref m4, out m3);
            Matrix3x3.Translation2D(ref v1, out m4);
            Matrix3x3.Multiply(ref m3, ref m4, out _cachedTransform);

            // Cache inverted transform
            Matrix3x3.Invert(ref _cachedTransform, out _cachedTransformInv);
        }

        /// <summary>
        /// Sets the anchor preset for the control. Can be use to auto-place the control for a given preset or can preserve the current control bounds.
        /// </summary>
        /// <param name="anchorPreset">The anchor preset to set.</param>
        /// <param name="preserveBounds">True if preserve current control bounds, otherwise will align control position accordingly to the anchor location.</param>
        public void SetAnchorPreset(AnchorPresets anchorPreset, bool preserveBounds)
        {
            for (int i = 0; i < AnchorPresetsData.Length; i++)
            {
                if (AnchorPresetsData[i].Preset == anchorPreset)
                {
                    var anchorMin = AnchorPresetsData[i].Min;
                    var anchorMax = AnchorPresetsData[i].Max;
                    var bounds = _bounds;
                    if (!Vector2.NearEqual(ref _anchorMin, ref anchorMin) ||
                        !Vector2.NearEqual(ref _anchorMax, ref anchorMax))
                    {
                        // Disable scrolling for anchored controls (by default but can be manually restored)
                        if (!anchorMin.IsZero || !anchorMax.IsZero)
                            IsScrollable = false;

                        _anchorMin = anchorMin;
                        _anchorMax = anchorMax;
                        if (preserveBounds)
                        {
                            UpdateBounds();
                            Bounds = bounds;
                        }
                    }
                    if (!preserveBounds)
                    {
                        if (_parent != null)
                        {
                            _parent.GetDesireClientArea(out var parentBounds);
                            switch (anchorPreset)
                            {
                            case AnchorPresets.TopLeft:
                                bounds.Location = Vector2.Zero;
                                break;
                            case AnchorPresets.TopCenter:
                                bounds.Location = new Vector2(parentBounds.Width * 0.5f - bounds.Width * 0.5f, 0);
                                break;
                            case AnchorPresets.TopRight:
                                bounds.Location = new Vector2(parentBounds.Width - bounds.Width, 0);
                                break;
                            case AnchorPresets.MiddleLeft:
                                bounds.Location = new Vector2(0, parentBounds.Height * 0.5f - bounds.Height * 0.5f);
                                break;
                            case AnchorPresets.MiddleCenter:
                                bounds.Location = new Vector2(parentBounds.Width * 0.5f - bounds.Width * 0.5f, parentBounds.Height * 0.5f - bounds.Height * 0.5f);
                                break;
                            case AnchorPresets.MiddleRight:
                                bounds.Location = new Vector2(parentBounds.Width - bounds.Width, parentBounds.Height * 0.5f - bounds.Height * 0.5f);
                                break;
                            case AnchorPresets.BottomLeft:
                                bounds.Location = new Vector2(0, parentBounds.Height - bounds.Height);
                                break;
                            case AnchorPresets.BottomCenter:
                                bounds.Location = new Vector2(parentBounds.Width * 0.5f - bounds.Width * 0.5f, parentBounds.Height - bounds.Height);
                                break;
                            case AnchorPresets.BottomRight:
                                bounds.Location = new Vector2(parentBounds.Width - bounds.Width, parentBounds.Height - bounds.Height);
                                break;
                            case AnchorPresets.VerticalStretchLeft:
                                bounds.Location = Vector2.Zero;
                                bounds.Size = new Vector2(bounds.Width, parentBounds.Height);
                                break;
                            case AnchorPresets.VerticalStretchCenter:
                                bounds.Location = new Vector2(parentBounds.Width * 0.5f - bounds.Width * 0.5f, 0);
                                bounds.Size = new Vector2(bounds.Width, parentBounds.Height);
                                break;
                            case AnchorPresets.VerticalStretchRight:
                                bounds.Location = new Vector2(parentBounds.Width - bounds.Width, 0);
                                bounds.Size = new Vector2(bounds.Width, parentBounds.Height);
                                break;
                            case AnchorPresets.HorizontalStretchTop:
                                bounds.Location = Vector2.Zero;
                                bounds.Size = new Vector2(parentBounds.Width, bounds.Height);
                                break;
                            case AnchorPresets.HorizontalStretchMiddle:
                                bounds.Location = new Vector2(0, parentBounds.Height * 0.5f - bounds.Height * 0.5f);
                                bounds.Size = new Vector2(parentBounds.Width, bounds.Height);
                                break;
                            case AnchorPresets.HorizontalStretchBottom:
                                bounds.Location = new Vector2(0, parentBounds.Height - bounds.Height);
                                bounds.Size = new Vector2(parentBounds.Width, bounds.Height);
                                break;
                            case AnchorPresets.StretchAll:
                                bounds.Location = Vector2.Zero;
                                bounds.Size = parentBounds.Size;
                                break;
                            default: throw new ArgumentOutOfRangeException(nameof(anchorPreset), anchorPreset, null);
                            }
                            bounds.Location += parentBounds.Location;
                        }
                        Bounds = bounds;
                    }
                    return;
                }
            }
        }
    }
}
