// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Reflection;

namespace System.ComponentModel.Design;

public abstract partial class UndoEngine
{
    /// <summary>
    ///  This class embodies a unit of undoable work.  The undo engine creates an undo unit when a change to the designer is about to be made.  The undo unit is responsible for tracking changes.  The undo engine will call Close on the unit when it no longer needs to track changes.
    /// </summary>
    protected partial class UndoUnit
    {
        private List<UndoEvent>? _events; // the list of events we've captured
        private List<ChangeUndoEvent>? _changeEvents; // the list of change events we're currently capturing.  Only valid until Commit is called.
        private List<AddRemoveUndoEvent>? _removeEvents; // the list of remove events we're currently capturing.  Only valid until a matching Removed is encountered.
        private List<IComponent>? _ignoreAddingList; // the list of objects that are currently being added.  We ignore change events between adding and added.
        private List<IComponent>? _ignoreAddedList; // the list of objects that are added. We do not serialize before state for change events that happen in the same transaction
        private bool _reverse; // if true, we walk the events list from the bottom up
        private readonly Dictionary<string, IContainer>? _lastSelection; // the selection as it was before we gathered undo info

        public UndoUnit(UndoEngine engine, string? name)
        {
            Name = name ?? string.Empty;

            UndoEngine = engine.OrThrowIfNull();
            _reverse = true;
            if (UndoEngine.TryGetService(out ISelectionService? ss))
            {
                ICollection selection = ss.GetSelectedComponents();
                Dictionary<string, IContainer> selectedNames = [];
                foreach (object sel in selection)
                {
                    if (sel is IComponent { Site: ISite site })
                    {
                        selectedNames[site.Name!] = site.Container!;
                    }
                }

                _lastSelection = selectedNames;
            }
        }

        public string Name { get; }

        /// <summary>
        ///  This returns true if the undo unit has nothing in it to undo.  The unit will be discarded.
        /// </summary>
        public virtual bool IsEmpty => _events is null || _events.Count == 0;

        protected UndoEngine UndoEngine { get; }

        /// <summary>
        ///  Adds the given event to our event list.
        /// </summary>
        private void AddEvent(UndoEvent e)
        {
            _events ??= [];

            _events.Add(e);
        }

        /// <summary>
        ///  Called by the undo engine when it wants to close this unit.  The unit should do any final work it needs to do to close.
        /// </summary>
        public virtual void Close()
        {
            if (_changeEvents is not null)
            {
                foreach (ChangeUndoEvent e in _changeEvents)
                {
                    e.Commit();
                }
            }

            if (_removeEvents is not null)
            {
                foreach (AddRemoveUndoEvent e in _removeEvents)
                {
                    e.Commit();
                }
            }

            // At close time we are done with this list.  All change events were simultaneously added to the _events list.
            _changeEvents = null;
            _removeEvents = null;
            _ignoreAddingList = null;
            _ignoreAddedList = null;
        }

        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component added event.
        /// </summary>
        public virtual void ComponentAdded(ComponentEventArgs e)
        {
            if (e.Component!.Site?.Container is INestedContainer)
            {
                // do nothing
            }
            else
            {
                AddEvent(new AddRemoveUndoEvent(UndoEngine, e.Component, true));
            }

            _ignoreAddingList?.Remove(e.Component);

            _ignoreAddedList ??= [];

            _ignoreAddedList.Add(e.Component);
        }

        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component adding event.
        /// </summary>
        public virtual void ComponentAdding(ComponentEventArgs e)
        {
            _ignoreAddingList ??= [];

            _ignoreAddingList.Add(e.Component!);
        }

        private static bool ChangeEventsSymmetric(
            [NotNullWhen(true)] ComponentChangingEventArgs? changing,
            [NotNullWhen(true)] ComponentChangedEventArgs? changed)
        {
            if (changing is null || changed is null)
            {
                return false;
            }

            return changing.Component == changed.Component && changing.Member == changed.Member;
        }

        private bool CanRepositionEvent(int startIndex, ComponentChangedEventArgs e)
        {
            bool containsAdd = false;
            bool containsRename = false;
            bool containsSymmetricChange = false;
            for (int i = startIndex + 1; i < _events!.Count; i++)
            {
                if (_events[i] is AddRemoveUndoEvent addEvt && !addEvt.NextUndoAdds)
                {
                    containsAdd = true;
                }
                else if (_events[i] is ChangeUndoEvent changeEvt && ChangeEventsSymmetric(changeEvt.ComponentChangingEventArgs, e))
                {
                    containsSymmetricChange = true;
                }
                else if (_events[i] is RenameUndoEvent)
                {
                    containsRename = true;
                }
            }

            return containsAdd && !containsRename && !containsSymmetricChange;
        }

        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component changed event.
        /// </summary>
        public virtual void ComponentChanged(ComponentChangedEventArgs e)
        {
            if (_events is not null && e is not null)
            {
                for (int i = 0; i < _events.Count; i++)
                {
                    // Determine if we've located the UndoEvent which was  created as a result of a corresponding ComponentChanging event.
                    // If so, reposition to the "Changed" spot in the list if the following is true:
                    //          - It must be for a DSV.Content property
                    //          - There must be a AddEvent between the Changing and Changed
                    //          - There are no renames in between Changing and Changed.
                    if (_events[i] is ChangeUndoEvent ce && ChangeEventsSymmetric(ce.ComponentChangingEventArgs, e) && i != _events.Count - 1)
                    {
                        if (e.Member is not null && e.Member.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content) &&
                            CanRepositionEvent(i, e))
                        {
                            _events.RemoveAt(i);
                            _events.Add(ce);
                        }
                    }
                }
            }
        }

        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component changing event.
        /// </summary>
        public virtual void ComponentChanging(ComponentChangingEventArgs e)
        {
            // If we are in the process of adding this component, ignore any changes to it.  The ending "Added" event will capture the component's state.  This not just an optimization.  If we get a change during an add, we can have an undo order that specifies a remove, and then a change to a removed component.
            if (_ignoreAddingList is not null && _ignoreAddingList.Contains(e.Component))
            {
                return;
            }

            _changeEvents ??= [];

            // The site check here is done because the data team is calling us for components that are not yet sited.  We end up writing them out as Guid-named locals.  That's fine, except that we cannot capture after state for these types of things so we assert.
            if (UndoEngine.GetName(e.Component, false) is not null)
            {
                // The caller provided us with a component.  This is the common case.  We will add a new change event provided there is not already one open for this component.
                bool hasChange = false;

                for (int idx = 0; idx < _changeEvents.Count; idx++)
                {
                    ChangeUndoEvent ce = _changeEvents[idx];
                    if (ce.OpenComponent == e.Component && ce.ContainsChange(e.Member))
                    {
                        hasChange = true;
                        break;
                    }
                }

                if (!hasChange ||
                    (e.Member?.Attributes is not null && e.Member.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content)))
                {
#if DEBUG
                    string? name = UndoEngine.GetName(e.Component, false);

                    if (name is not null)
                    {
                        string memberName = e.Member?.Name ?? "(none)";
                    }
                    else
                    {
                        Debug.Fail("UndoEngine: GetName is failing on successive calls");
                    }
#endif
                    ChangeUndoEvent? changeEvent = null;
                    bool serializeBeforeState = true;
                    // perf: if this object was added in this undo unit we do not want to serialize before state for ChangeEvent since undo will remove it anyway
                    if (_ignoreAddedList is not null && _ignoreAddedList.Contains(e.Component))
                    {
                        serializeBeforeState = false;
                    }

                    if (e.Component is IComponent { Site: not null })
                    {
                        changeEvent = new ChangeUndoEvent(UndoEngine, e, serializeBeforeState);
                    }
                    else if (e.Component is not null)
                    {
                        if (GetService(typeof(IReferenceService)) is IReferenceService rs)
                        {
                            IComponent? owningComp = rs.GetComponent(e.Component);

                            if (owningComp is not null)
                            {
                                changeEvent = new ChangeUndoEvent(UndoEngine, new ComponentChangingEventArgs(owningComp, null), serializeBeforeState);
                            }
                        }
                    }

                    if (changeEvent is not null)
                    {
                        AddEvent(changeEvent);
                        _changeEvents.Add(changeEvent);
                    }
                }
            }
        }

        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component removed event.
        /// </summary>
        public virtual void ComponentRemoved(ComponentEventArgs e)
        {
            // We should gather undo state in ComponentRemoved, but by this time the component's designer has been destroyed so it's too late.  Instead, we captured state in the Removing method.  But, it is possible for there to be component changes to other objects that happen between removing and removed,  so we need to reorder the removing event so it's positioned after any changes.
            if (_events is not null && e is not null)
            {
                ChangeUndoEvent? changeEvt = null;
                int changeIdx = -1;
                for (int idx = _events.Count - 1; idx >= 0; idx--)
                {
                    if (changeEvt is null)
                    {
                        changeEvt = _events[idx] as ChangeUndoEvent;
                        changeIdx = idx;
                    }

                    if (_events[idx] is AddRemoveUndoEvent evt && evt.OpenComponent == e.Component)
                    {
                        evt.Commit();
                        // We should only reorder events if there  are change events coming between OnRemoving and OnRemoved.
                        // If there are other events (such as AddRemoving), the serialization  done in OnComponentRemoving might refer to components that aren't available.
                        if (idx != _events.Count - 1 && changeEvt is not null)
                        {
                            // ensure only change change events exist between these two events
                            bool onlyChange = true;
                            for (int i = idx + 1; i < changeIdx; i++)
                            {
                                if (_events[i] is not ChangeUndoEvent)
                                {
                                    onlyChange = false;
                                    break;
                                }
                            }

                            if (onlyChange)
                            {
                                // reposition event after final ComponentChangingEvent
                                _events.RemoveAt(idx);
                                _events.Insert(changeIdx, evt);
                            }
                        }

                        break;
                    }
                }
            }
        }

        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component removing event.
        /// </summary>
        public virtual void ComponentRemoving(ComponentEventArgs e)
        {
            if (e.Component!.Site is INestedContainer)
            {
                return;
            }

            _removeEvents ??= [];

            try
            {
                AddRemoveUndoEvent evt = new(UndoEngine, e.Component, false);
                AddEvent(evt);
                _removeEvents.Add(evt);
            }
            catch (TargetInvocationException) { }
        }

        /// <summary>
        ///  The undo engine will cal this on the active undo unit in response to a component rename event.
        /// </summary>
        public virtual void ComponentRename(ComponentRenameEventArgs e) =>
            AddEvent(new RenameUndoEvent(e.OldName, e.NewName));

        /// <summary>
        ///  Returns an instance of the requested service.
        /// </summary>
        protected object? GetService(Type serviceType) => UndoEngine.GetService(serviceType);

        /// <summary>
        ///  Override for object.ToString()
        /// </summary>
        public override string ToString() => Name;

        /// <summary>
        ///  Either performs undo, or redo, depending on the state of the unit.  UndoUnit initially assumes that the undoable work has already been "done", so the first call to undo will undo the work.  The next call will undo the "undo", performing a redo.
        /// </summary>
        public void Undo()
        {
            UndoUnit? savedUnit = UndoEngine._executingUnit;
            UndoEngine._executingUnit = this;
            DesignerTransaction? transaction = null;
            try
            {
                if (savedUnit is null)
                {
                    UndoEngine.OnUndoing(EventArgs.Empty);
                }

                // create a transaction here so things that do work on componentchanged can ignore that while the transaction is opened...big perf win.
                transaction = UndoEngine._host.CreateTransaction();
                UndoCore();
            }
            catch (CheckoutException)
            {
                transaction!.Cancel();
                transaction = null;
                throw;
            }
            finally
            {
                transaction?.Commit();

                UndoEngine._executingUnit = savedUnit;
                if (savedUnit is null)
                {
                    UndoEngine.OnUndone(EventArgs.Empty);
                }
            }
        }

        /// <summary>
        ///  The undo method invokes this method to perform the actual undo / redo work.  You should never call this method directly; override it if you wish, but always call the public Undo method to perform undo work.  Undo notifies the undo engine to suspend undo data gathering until  this undo is completed, which prevents new undo units from being created in response to this unit doing work.
        /// </summary>
        protected virtual void UndoCore()
        {
            if (_events is not null)
            {
                if (_reverse)
                {
                    // How does BeforeUndo work?  You'd think you should just call this in one pass, and then call Undo in another, but you'd be wrong. The complexity arises because there are undo events that have dependencies on other undo events.  There are also undo events that have side effects with respect to other events.  Here are examples:
                    // Rename is an undo event that other undo events depend on, because they store names.  It must be performed in the right order and it must be  performed before any subsequent event's BeforeUndo is called.
                    // Property change is an undo event that may have an unknown side effect if changing the property results in other property changes (for example, reparenting a control removes the control from its former parent).  A property change undo event must have all BeforeUndo methods called before any Undo method is called. To do this, we have a property on UndoEvent called CausesSideEffects.
                    // As we run through UndoEvents, consecutive events that return true from this property are grouped so that their BeforeUndo methods are all called before their Undo methods.  For events that do not have  side effects, their BeforeUndo and Undo are invoked immediately.
                    for (int idx = _events.Count - 1; idx >= 0; idx--)
                    {
                        int groupEndIdx = idx;
                        for (int groupIdx = idx; groupIdx >= 0; groupIdx--)
                        {
                            if (_events[groupIdx].CausesSideEffects)
                            {
                                groupEndIdx = groupIdx;
                            }
                            else
                            {
                                break;
                            }
                        }

                        for (int beforeIdx = idx; beforeIdx >= groupEndIdx; beforeIdx--)
                        {
                            (_events[beforeIdx]).BeforeUndo(UndoEngine);
                        }

                        for (int undoIdx = idx; undoIdx >= groupEndIdx; undoIdx--)
                        {
                            (_events[undoIdx]).Undo(UndoEngine);
                        }

                        Debug.Assert(idx >= groupEndIdx, "We're going backwards");
                        idx = groupEndIdx;
                    }

                    // Now, if we have a selection, apply it.
                    if (_lastSelection is not null)
                    {
                        if (UndoEngine.TryGetService(out ISelectionService? ss))
                        {
                            List<IComponent> list = new(_lastSelection.Count);
                            foreach ((string name, IContainer container) in _lastSelection)
                            {
                                IComponent? comp = container.Components[name];
                                if (comp is not null)
                                {
                                    list.Add(comp);
                                }
                            }

                            ss.SetSelectedComponents(list, SelectionTypes.Replace);
                        }
                    }
                }
                else
                {
                    int count = _events.Count;
                    for (int idx = 0; idx < count; idx++)
                    {
                        int groupEndIdx = idx;

                        for (int groupIdx = idx; groupIdx < count; groupIdx++)
                        {
                            if (_events[groupIdx].CausesSideEffects)
                            {
                                groupEndIdx = groupIdx;
                            }
                            else
                            {
                                break;
                            }
                        }

                        for (int beforeIdx = idx; beforeIdx <= groupEndIdx; beforeIdx++)
                        {
                            (_events[beforeIdx]).BeforeUndo(UndoEngine);
                        }

                        for (int undoIdx = idx; undoIdx <= groupEndIdx; undoIdx++)
                        {
                            (_events[undoIdx]).Undo(UndoEngine);
                        }

                        Debug.Assert(idx <= groupEndIdx, "We're going backwards");
                        idx = groupEndIdx;
                    }
                }
            }

            _reverse = !_reverse;
        }
    }
}
