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

using System;
using System.Globalization;
using System.IO;
using System.Text;
using FlaxEngine.GUI;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace FlaxEngine
{
    partial class UIControl
    {
        private Control _control;

        /// <summary>
        /// Gets or sets the GUI control used by this actor.
        /// </summary>
        /// <remarks>
        /// When changing the control, the previous one is disposed. Use <see cref="UnlinkControl"/> to manage it on your own.
        /// </remarks>
        [EditorDisplay("Control", EditorDisplayAttribute.InlineStyle), CustomEditorAlias("FlaxEditor.CustomEditors.Dedicated.UIControlControlEditor"), EditorOrder(50)]
        public Control Control
        {
            get => _control;
            set
            {
                if (_control == value)
                    return;

                // Cleanup previous
                if (_control != null)
                {
                    _control.LocationChanged -= OnControlLocationChanged;
                    _control.Dispose();
                }

                // Set value
                _control = value;

                // Link the new one (events and parent)
                if (_control != null)
                {
                    var containerControl = _control as ContainerControl;
                    if (containerControl != null)
                        containerControl.UnlockChildrenRecursive();

                    _control.Parent = GetParent();
                    _control.IndexInParent = OrderInParent;
                    _control.Location = new Vector2(LocalPosition);
                    // TODO: sync control order in parent with actor order in parent (think about special cases like Panel with scroll bars used as internal controls)
                    _control.LocationChanged += OnControlLocationChanged;

                    if (containerControl != null && IsActiveInHierarchy)
                    {
                        var children = ChildrenCount;
                        for (int i = 0; i < children; i++)
                        {
                            var child = GetChild(i) as UIControl;
                            if (child != null && child.IsActiveInHierarchy && child.HasControl)
                            {
                                child.Control.Parent = containerControl;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether this actor has control.
        /// </summary>
        public bool HasControl => _control != null;

        /// <summary>
        /// Gets the world-space oriented bounding box that contains a 3D control.
        /// </summary>
        public OrientedBoundingBox Bounds
        {
            get
            {
                // Pick a parent canvas
                var canvasRoot = Control?.Root as CanvasRootControl;
                if (canvasRoot == null || canvasRoot.Canvas.Is2D)
                    return new OrientedBoundingBox();

                // Find control bounds limit points in canvas-space
                var p1 = Vector2.Zero;
                var p2 = new Vector2(0, Control.Height);
                var p3 = new Vector2(Control.Width, 0);
                var p4 = Control.Size;
                var c = Control;
                while (c != canvasRoot)
                {
                    p1 = c.PointToParent(ref p1);
                    p2 = c.PointToParent(ref p2);
                    p3 = c.PointToParent(ref p3);
                    p4 = c.PointToParent(ref p4);

                    c = c.Parent;
                }
                var min = Vector2.Min(Vector2.Min(p1, p2), Vector2.Min(p3, p4));
                var max = Vector2.Max(Vector2.Max(p1, p2), Vector2.Max(p3, p4));
                var size = max - min;

                // Calculate bounds
                OrientedBoundingBox bounds = new OrientedBoundingBox();
                bounds.Extents = new Vector3(size * 0.5f, Mathf.Epsilon);
                Matrix world;
                canvasRoot.Canvas.GetWorldMatrix(out world);
                Matrix offset;
                Matrix.Translation(min.X + size.X * 0.5f, min.Y + size.Y * 0.5f, 0, out offset);
                Matrix.Multiply(ref offset, ref world, out bounds.Transformation);
                return bounds;
            }
        }

        /// <summary>
        /// Gets the control object cased to the given type.
        /// </summary>
        /// <typeparam name="T">The type of the control.</typeparam>
        /// <returns>The control object.</returns>
        public T Get<T>() where T : Control
        {
            return (T)_control;
        }

        /// <summary>
        /// Checks if the control object is of the given type.
        /// </summary>
        /// <typeparam name="T">The type of the control.</typeparam>
        /// <returns>True if control object is of the given type.</returns>
        public bool Is<T>() where T : Control
        {
            return _control is T;
        }

        /// <summary>
        /// Creates a new UIControl with the control of the given type and links it to this control as a child.
        /// </summary>
        /// <remarks>
        /// The current actor has to have a valid container control.
        /// </remarks>
        /// <typeparam name="T">Type of the child control to add.</typeparam>
        /// <returns>The created UIControl that contains a new control of the given type.</returns>
        public UIControl AddChildControl<T>() where T : Control
        {
            if (!(_control is ContainerControl))
                throw new InvalidOperationException("To add child to the control it has to be ContainerControl.");

            var child = new UIControl();
            child.Parent = this;
            child.Control = (Control)Activator.CreateInstance(typeof(T));
            return child;
        }

        /// <summary>
        /// Unlinks the control from the actor without disposing it or modifying.
        /// </summary>
        [NoAnimate]
        public void UnlinkControl()
        {
            if (_control != null)
            {
                _control.LocationChanged -= OnControlLocationChanged;
                _control = null;
            }
        }

        private void OnControlLocationChanged(Control control)
        {
            LocalPosition = new Vector3(control.Location, LocalPosition.Z);
        }

        /// <summary>
        /// The fallback callback used to handle <see cref="UIControl"/> parent container control to link when it fails to find the default parent. Can be used to link the controls into a custom control.
        /// </summary>
        public static Func<UIControl, ContainerControl> FallbackParentGetDelegate;

        private ContainerControl GetParent()
        {
            // Don't link disabled actors
            if (!IsActiveInHierarchy)
                return null;

            var parent = Parent;
            if (parent is UIControl uiControl && uiControl.Control is ContainerControl uiContainerControl)
                return uiContainerControl;
            if (parent is UICanvas uiCanvas)
                return uiCanvas.GUI;
            return FallbackParentGetDelegate?.Invoke(this);
        }

        internal string Serialize(out string controlType)
        {
            if (_control == null)
            {
                controlType = null;
                return null;
            }

            var type = _control.GetType();

            JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(Json.JsonSerializer.Settings);
            jsonSerializer.Formatting = Formatting.Indented;

            StringBuilder sb = new StringBuilder(1024);
            StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture);
            using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
            {
                // Prepare writer settings
                jsonWriter.IndentChar = '\t';
                jsonWriter.Indentation = 1;
                jsonWriter.Formatting = jsonSerializer.Formatting;
                jsonWriter.DateFormatHandling = jsonSerializer.DateFormatHandling;
                jsonWriter.DateTimeZoneHandling = jsonSerializer.DateTimeZoneHandling;
                jsonWriter.FloatFormatHandling = jsonSerializer.FloatFormatHandling;
                jsonWriter.StringEscapeHandling = jsonSerializer.StringEscapeHandling;
                jsonWriter.Culture = jsonSerializer.Culture;
                jsonWriter.DateFormatString = jsonSerializer.DateFormatString;

                JsonSerializerInternalWriter serializerWriter = new JsonSerializerInternalWriter(jsonSerializer);

                serializerWriter.Serialize(jsonWriter, _control, type);
            }

            controlType = type.FullName;
            return sw.ToString();
        }

        internal string SerializeDiff(out string controlType, UIControl other)
        {
            if (_control == null)
            {
                controlType = null;
                return null;
            }
            var type = _control.GetType();
            if (other._control == null || other._control.GetType() != type)
            {
                return Serialize(out controlType);
            }

            JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(Json.JsonSerializer.Settings);
            jsonSerializer.Formatting = Formatting.Indented;

            StringBuilder sb = new StringBuilder(1024);
            StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture);
            using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
            {
                // Prepare writer settings
                jsonWriter.IndentChar = '\t';
                jsonWriter.Indentation = 1;
                jsonWriter.Formatting = jsonSerializer.Formatting;
                jsonWriter.DateFormatHandling = jsonSerializer.DateFormatHandling;
                jsonWriter.DateTimeZoneHandling = jsonSerializer.DateTimeZoneHandling;
                jsonWriter.FloatFormatHandling = jsonSerializer.FloatFormatHandling;
                jsonWriter.StringEscapeHandling = jsonSerializer.StringEscapeHandling;
                jsonWriter.Culture = jsonSerializer.Culture;
                jsonWriter.DateFormatString = jsonSerializer.DateFormatString;

                JsonSerializerInternalWriter serializerWriter = new JsonSerializerInternalWriter(jsonSerializer);

                serializerWriter.SerializeDiff(jsonWriter, _control, type, other._control);
            }

            controlType = string.Empty;
            return sw.ToString();
        }

        internal void Deserialize(string json, Type controlType)
        {
            if (_control == null || _control.GetType() != controlType)
            {
                if (controlType != null)
                    Control = (Control)Activator.CreateInstance(controlType);
            }

            if (_control != null)
            {
                Json.JsonSerializer.Deserialize(_control, json);
            }
        }

        internal void ParentChanged()
        {
            if (_control != null)
            {
                _control.Parent = GetParent();
                _control.IndexInParent = OrderInParent;
            }
        }

        internal void TransformChanged()
        {
            if (_control != null)
                _control.Location = new Vector2(LocalPosition);
        }

        internal void ActiveInTreeChanged()
        {
            if (_control != null)
            {
                // Link or unlink control (won't modify Enable/Visible state)
                _control.Parent = GetParent();
                _control.IndexInParent = OrderInParent;
            }
        }

        internal void OrderInParentChanged()
        {
            if (_control != null)
                _control.IndexInParent = OrderInParent;
        }

        internal void BeginPlay()
        {
            if (_control != null)
            {
                _control.Parent = GetParent();
            }
        }

        internal void EndPlay()
        {
            if (_control != null)
            {
                _control.Dispose();
                _control = null;
            }
        }
    }
}
