// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using FlaxEditor.SceneGraph;
using FlaxEngine;
using FlaxEngine.Utilities;

namespace FlaxEditor.Actions
{
    /// <summary>
    /// Implementation of <see cref="IUndoAction"/> used to delete a selection of <see cref="ActorNode"/>.
    /// </summary>
    /// <seealso cref="FlaxEditor.IUndoAction" />
    [Serializable]
    class DeleteActorsAction : IUndoAction
    {
        [Serialize]
        private byte[] _actorsData;

        [Serialize]
        private List<SceneGraphNode.StateData> _nodesData;

        [Serialize]
        private Guid[] _nodeParentsIDs;

        [Serialize]
        private int[] _nodeParentsOrders;

        [Serialize]
        private Guid[] _prefabIds;

        [Serialize]
        private Guid[] _prefabObjectIds;

        [Serialize]
        private bool _isInverted;

        [Serialize]
        private bool _affectsCSG;

        [Serialize]
        private bool _affectsNavigation;

        [Serialize]
        protected List<SceneGraphNode> _nodeParents;

        /// <summary>
        /// Initializes a new instance of the <see cref="DeleteActorsAction"/> class.
        /// </summary>
        /// <param name="actor">The actor.</param>
        /// <param name="isInverted">If set to <c>true</c> action will be inverted - instead of delete it will create actors.</param>
        /// <param name="preserveOrder">If set to <c>true</c> action will be preserve actors order when performing undo.</param>
        internal DeleteActorsAction(Actor actor, bool isInverted = false, bool preserveOrder = true)
        : this(new List<SceneGraphNode>(1) { SceneGraphFactory.FindNode(actor.ID) }, isInverted, preserveOrder)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DeleteActorsAction"/> class.
        /// </summary>
        /// <param name="node">The object.</param>
        /// <param name="isInverted">If set to <c>true</c> action will be inverted - instead of delete it will create actors.</param>
        /// <param name="preserveOrder">If set to <c>true</c> action will be preserve actors order when performing undo.</param>
        internal DeleteActorsAction(SceneGraphNode node, bool isInverted = false, bool preserveOrder = true)
        : this(new List<SceneGraphNode>(1) { node }, isInverted, preserveOrder)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DeleteActorsAction"/> class.
        /// </summary>
        /// <param name="nodes">The objects.</param>
        /// <param name="isInverted">If set to <c>true</c> action will be inverted - instead of delete it will create actors.</param>
        /// <param name="preserveOrder">If set to <c>true</c> action will be preserve actors order when performing undo.</param>
        internal DeleteActorsAction(List<SceneGraphNode> nodes, bool isInverted = false, bool preserveOrder = true)
        {
            _isInverted = isInverted;

            // Collect nodes to delete
            var deleteNodes = new List<SceneGraphNode>(nodes.Count);
            var actors = new List<Actor>(nodes.Count);
            for (int i = 0; i < nodes.Count; i++)
            {
                var node = nodes[i];
                if (node is ActorNode actorNode)
                {
                    deleteNodes.Add(actorNode);
                    actors.Add(actorNode.Actor);
                }
                else
                {
                    deleteNodes.Add(node);
                    if (node.CanUseState)
                    {
                        if (_nodesData == null)
                            _nodesData = new List<SceneGraphNode.StateData>();
                        _nodesData.Add(node.State);
                    }
                }
            }

            // Collect parent nodes to delete
            _nodeParents = new List<SceneGraphNode>(nodes.Count);
            deleteNodes.BuildNodesParents(_nodeParents);
            OnDirtyInit();
            _nodeParentsIDs = new Guid[_nodeParents.Count];
            for (int i = 0; i < _nodeParentsIDs.Length; i++)
                _nodeParentsIDs[i] = _nodeParents[i].ID;
            if (preserveOrder)
            {
                _nodeParentsOrders = new int[_nodeParents.Count];
                for (int i = 0; i < _nodeParentsOrders.Length; i++)
                    _nodeParentsOrders[i] = _nodeParents[i].OrderInParent;
            }

            // Serialize actors
            _actorsData = Actor.ToBytes(actors.ToArray());

            // Cache actors linkage to prefab objects
            _prefabIds = new Guid[actors.Count];
            _prefabObjectIds = new Guid[actors.Count];
            for (int i = 0; i < actors.Count; i++)
            {
                _prefabIds[i] = actors[i].PrefabID;
                _prefabObjectIds[i] = actors[i].PrefabObjectID;
            }
        }

        /// <inheritdoc />
        public string ActionString => _isInverted ? "Create actors" : "Delete actors";

        /// <inheritdoc />
        public void Do()
        {
            if (_isInverted)
                Create();
            else
                Delete();
        }

        /// <inheritdoc />
        public void Undo()
        {
            if (_isInverted)
                Delete();
            else
                Create();
        }

        /// <inheritdoc />
        public void Dispose()
        {
            _actorsData = null;
            _nodeParentsIDs = null;
            _nodeParentsOrders = null;
            _prefabIds = null;
            _prefabObjectIds = null;
            _nodeParents.Clear();
        }

        /// <summary>
        /// Deletes the objects.
        /// </summary>
        protected virtual void Delete()
        {
            // Remove objects
            OnDirty();
            for (int i = 0; i < _nodeParents.Count; i++)
            {
                var node = _nodeParents[i];
                node.Delete();
            }
            _nodeParents.Clear();
            FlaxEngine.Scripting.FlushRemovedObjects();
        }

        /// <summary>
        /// Gets the node.
        /// </summary>
        /// <param name="id">The actor id.</param>
        /// <returns>The scene graph node.</returns>
        protected virtual SceneGraphNode GetNode(Guid id)
        {
            return SceneGraphFactory.FindNode(id);
        }

        /// <summary>
        /// Creates the removed objects (from data).
        /// </summary>
        protected virtual void Create()
        {
            var nodes = new List<SceneGraphNode>();

            // Restore actors
            var actors = Actor.FromBytes(_actorsData);
            if (actors != null)
            {
                nodes.Capacity = Math.Max(nodes.Capacity, actors.Length);

                // Preserve prefab objects linkage
                for (int i = 0; i < actors.Length; i++)
                {
                    Guid prefabId = _prefabIds[i];
                    if (prefabId != Guid.Empty)
                    {
                        Actor.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(actors[i]), ref prefabId, ref _prefabObjectIds[i]);
                    }
                }
            }

            // Restore nodes state
            if (_nodesData != null)
            {
                for (int i = 0; i < _nodesData.Count; i++)
                {
                    var state = _nodesData[i];
                    var type = TypeUtils.GetManagedType(state.TypeName);
                    if (type == null)
                    {
                        Editor.LogError($"Missing type {state.TypeName} for scene graph node undo state restore.");
                        continue;
                    }
                    var method = type.GetMethod(state.CreateMethodName);
                    if (method == null)
                    {
                        Editor.LogError($"Missing method {state.CreateMethodName} from type {state.TypeName} for scene graph node undo state restore.");
                        continue;
                    }
                    var node = method.Invoke(null, new object[] { state });
                    if (node == null)
                    {
                        Editor.LogError($"Failed to restore scene graph node state via method {state.CreateMethodName} from type {state.TypeName}.");
                        continue;
                    }
                }
            }

            // Cache parent nodes ids
            for (int i = 0; i < _nodeParentsIDs.Length; i++)
            {
                var foundNode = GetNode(_nodeParentsIDs[i]);
                if (foundNode is ActorNode node)
                {
                    nodes.Add(node);
                    if (_nodeParentsOrders != null)
                        node.Actor.OrderInParent = _nodeParentsOrders[i];
                }
            }
            nodes.BuildNodesParents(_nodeParents);
            OnDirty();
        }

        private void OnDirtyInit()
        {
            for (int i = 0; i < _nodeParents.Count; i++)
            {
                if (_nodeParents[i] is ActorNode node && node.Actor is BoxBrush)
                {
                    _affectsCSG = true;
                    break;
                }
            }

            for (int i = 0; i < _nodeParents.Count; i++)
            {
                if (_nodeParents[i] is ActorNode actorNode && actorNode.AffectsNavigationWithChildren)
                {
                    _affectsNavigation = true;
                    break;
                }
            }
        }

        private void OnDirty()
        {
            // Mark scene as modified
            foreach (var obj in _nodeParents)
            {
                Editor.Instance.Scene.MarkSceneEdited(obj.ParentScene);
            }

            var editor = Editor.Instance;
            if (editor.StateMachine.IsPlayMode)
                return;
            var options = editor.Options.Options;

            // Auto CSG mesh rebuild
            if (_affectsCSG && options.General.AutoRebuildCSG)
            {
                for (var i = 0; i < _nodeParents.Count; i++)
                {
                    if (_nodeParents[i] is ActorNode node && node.Actor is BoxBrush)
                        node.Actor.Scene.BuildCSG(options.General.AutoRebuildCSGTimeoutMs);
                }
            }

            // Auto NavMesh rebuild
            if (_affectsNavigation && options.General.AutoRebuildNavMesh)
            {
                for (var i = 0; i < _nodeParents.Count; i++)
                {
                    if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren)
                    {
                        var bounds = node.Actor.BoxWithChildren;
                        Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
                    }
                }
            }
        }
    }
}
