using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Spine;
using Spine.Interfaces;
using SpineViewer.Models;
using System.Collections;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace SpineViewer.ViewModels.MainWindow
{
    public class SpineObjectTabViewModel : ObservableObject
    {
        private SpineObjectModel[] _selectedObjects = [];
        private readonly ObservableCollection<SkinViewModel> _skins = [];
        private readonly ObservableCollection<SlotViewModel> _slots = [];
        private readonly ObservableCollection<AnimationTrackViewModel> _animationTracks = [];

        public static ImmutableArray<ISkeleton.Physics> PhysicsOptions { get; } = Enum.GetValues<ISkeleton.Physics>().ToImmutableArray();

        public SpineObjectModel[] SelectedObjects
        {
            get => _selectedObjects;
            set
            {
                if (ReferenceEquals(_selectedObjects, value)) return;

                // 清空之前的所有内容
                foreach (var obj in _selectedObjects)
                {
                    obj.PropertyChanged -= SingleModel_PropertyChanged;
                    obj.TrackPropertyChanged -= SingleModel_TrackPropChanged;
                }
                _skins.Clear();
                _slots.Clear();
                _animationTracks.Clear();

                // 生成新的内容
                _selectedObjects = value ?? [];
                if (_selectedObjects.Length > 0)
                {
                    foreach (var obj in _selectedObjects)
                    {
                        obj.PropertyChanged += SingleModel_PropertyChanged;
                        obj.TrackPropertyChanged += SingleModel_TrackPropChanged;
                    }

                    IEnumerable<string> commonSkinNames = _selectedObjects[0].Skins;
                    foreach (var obj in _selectedObjects.Skip(1)) commonSkinNames = commonSkinNames.Intersect(obj.Skins);
                    foreach (var name in commonSkinNames.Order()) _skins.Add(new(name, _selectedObjects));

                    IEnumerable<string> commonSlotNames = _selectedObjects[0].SlotAttachments.Keys;
                    foreach (var obj in _selectedObjects.Skip(1)) commonSlotNames = commonSlotNames.Intersect(obj.SlotAttachments.Keys);
                    foreach (var name in commonSlotNames.Order()) _slots.Add(new(name, _selectedObjects));

                    IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
                    foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
                    foreach (var idx in commonTrackIndices.Order()) _animationTracks.Add(new(idx, _selectedObjects));
                }

                OnPropertyChanged();

                Cmd_AppendTrack.NotifyCanExecuteChanged();

                OnPropertyChanged(nameof(Version));
                OnPropertyChanged(nameof(AssetsDir));
                OnPropertyChanged(nameof(SkelPath));
                OnPropertyChanged(nameof(AtlasPath));
                OnPropertyChanged(nameof(Name));
                OnPropertyChanged(nameof(FileVersion));

                OnPropertyChanged(nameof(IsShown));
                OnPropertyChanged(nameof(UsePma));
                OnPropertyChanged(nameof(Physics));
                OnPropertyChanged(nameof(TimeScale));

                OnPropertyChanged(nameof(Scale));
                OnPropertyChanged(nameof(FlipX));
                OnPropertyChanged(nameof(FlipY));
                OnPropertyChanged(nameof(X));
                OnPropertyChanged(nameof(Y));

                OnPropertyChanged(nameof(DebugTexture));
                OnPropertyChanged(nameof(DebugBounds));
                OnPropertyChanged(nameof(DebugBones));
                OnPropertyChanged(nameof(DebugRegions));
                OnPropertyChanged(nameof(DebugMeshHulls));
                OnPropertyChanged(nameof(DebugMeshes));
                OnPropertyChanged(nameof(DebugBoundingBoxes));
                OnPropertyChanged(nameof(DebugPaths));
                OnPropertyChanged(nameof(DebugPoints));
                OnPropertyChanged(nameof(DebugClippings));
            }
        }

        public SpineVersion? Version
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].Version;
                if (_selectedObjects.Skip(1).Any(it => it.Version != val)) return null;
                return val;
            }
        }

        public string? AssetsDir
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].AssetsDir;
                if (_selectedObjects.Skip(1).Any(it => it.AssetsDir != val)) return null;
                return val;
            }
        }

        public string? SkelPath
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].SkelPath;
                if (_selectedObjects.Skip(1).Any(it => it.SkelPath != val)) return null;
                return val;
            }
        }

        public string? AtlasPath
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].AtlasPath;
                if (_selectedObjects.Skip(1).Any(it => it.AtlasPath != val)) return null;
                return val;
            }
        }

        public string? Name
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].Name;
                if (_selectedObjects.Skip(1).Any(it => it.Name != val)) return null;
                return val;
            }
        }

        public string? FileVersion
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].FileVersion;
                if (_selectedObjects.Skip(1).Any(it => it.FileVersion != val)) return null;
                return val;
            }
        }

        public bool? IsShown
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].IsShown;
                if (_selectedObjects.Skip(1).Any(it => it.IsShown != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.IsShown = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? UsePma
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].UsePma;
                if (_selectedObjects.Skip(1).Any(it => it.UsePma != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.UsePma = (bool)value;
                OnPropertyChanged();
            }
        }

        public ISkeleton.Physics? Physics
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].Physics;
                if (_selectedObjects.Skip(1).Any(it => it.Physics != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.Physics = (ISkeleton.Physics)value;
                OnPropertyChanged();
            }
        }

        public float? TimeScale
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].TimeScale;
                if (_selectedObjects.Skip(1).Any(it => it.TimeScale != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.TimeScale = (float)value;
                OnPropertyChanged();
            }
        }

        public float? Scale
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].Scale;
                if (_selectedObjects.Skip(1).Any(it => it.Scale != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.Scale = (float)value;
                OnPropertyChanged();
            }
        }

        public bool? FlipX
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].FlipX;
                if (_selectedObjects.Skip(1).Any(it => it.FlipX != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.FlipX = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? FlipY
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].FlipY;
                if (_selectedObjects.Skip(1).Any(it => it.FlipY != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.FlipY = (bool)value;
                OnPropertyChanged();
            }
        }

        public float? X
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].X;
                if (_selectedObjects.Skip(1).Any(it => it.X != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.X = (float)value;
                OnPropertyChanged();
            }
        }

        public float? Y
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].Y;
                if (_selectedObjects.Skip(1).Any(it => it.Y != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.Y = (float)value;
                OnPropertyChanged();
            }
        }

        public ObservableCollection<SkinViewModel> Skins => _skins;

        public RelayCommand<IList?> Cmd_EnableSkins => _cmd_EnableSkins ??= new (
            args => { if (args is null) return; foreach (var s in args.OfType<SkinViewModel>()) s.Status = true; },
            args => { return args is not null && args.OfType<SkinViewModel>().Any(); }
        );
        private RelayCommand<IList?> _cmd_EnableSkins;

        public RelayCommand<IList?> Cmd_DisableSkins => _cmd_DisableSkins ??= new (
            args => { if (args is null) return; foreach (var s in args.OfType<SkinViewModel>()) s.Status = false; },
            args => { return args is not null && args.OfType<SkinViewModel>().Any(); }
        );
        private RelayCommand<IList?> _cmd_DisableSkins;

        public RelayCommand Cmd_EnableAllSkins => _cmd_EnableAllSkins ??= new(
            () => { if (_skins.Count <= 0) return; foreach (var s in _skins) s.Status = true; },
            () => { return _skins.Count > 0; }
        );
        private RelayCommand _cmd_EnableAllSkins;

        public RelayCommand Cmd_DisableAllSkins => _cmd_DisableAllSkins ??= new(
            () => { if (_skins.Count <= 0) return; foreach (var s in _skins) s.Status = false; },
            () => { return _skins.Count > 0; }
        );
        private RelayCommand _cmd_DisableAllSkins;

        public ObservableCollection<SlotViewModel> Slots => _slots;

        public RelayCommand<IList?> Cmd_EnableSlots => _cmd_EnableSlots ??= new (
            args => { if (args is null) return; foreach (var s in args.OfType<SlotViewModel>()) s.Visible = true; },
            args => { return args is not null && args.OfType<SlotViewModel>().Any(); }
        );
        private RelayCommand<IList?> _cmd_EnableSlots;

        public RelayCommand<IList?> Cmd_DisableSlots => _cmd_DisableSlots ??= new (
            args => { if (args is null) return; foreach (var s in args.OfType<SlotViewModel>()) s.Visible = false; },
            args => { return args is not null && args.OfType<SlotViewModel>().Any(); }
        );
        private RelayCommand<IList?> _cmd_DisableSlots;

        public RelayCommand Cmd_EnableAllSlots => _cmd_EnableAllSlots ??= new(
            () => { if (_slots.Count <= 0) return; foreach (var s in _slots) s.Visible = true; },
            () => { return _slots.Count > 0; }
        );
        private RelayCommand _cmd_EnableAllSlots;

        public RelayCommand Cmd_DisableAllSlots => _cmd_DisableAllSlots ??= new(
            () => { if (_slots.Count <= 0) return; foreach (var s in _slots) s.Visible = false; },
            () => { return _slots.Count > 0; }
        );
        private RelayCommand _cmd_DisableAllSlots;

        public ObservableCollection<AnimationTrackViewModel> AnimationTracks => _animationTracks;

        public RelayCommand Cmd_AppendTrack => _cmd_AppendTrack ??= new(
            () =>
            {
                if (_selectedObjects.Length != 1) return;
                var sp = _selectedObjects[0];
                if (sp.Animations.Length <= 0) return;
                sp.SetAnimation(sp.GetTrackIndices().LastOrDefault(-1) + 1, sp.Animations[0]);
            },
            () => { return _selectedObjects.Length == 1; }
        );
        private RelayCommand? _cmd_AppendTrack;

        public RelayCommand<IList?> Cmd_InsertTrack => _cmd_InsertTrack ??= new(
            args =>
            {
                if (_selectedObjects.Length != 1) return;
                var sp = _selectedObjects[0];

                if (sp.Animations.Length <= 0) return;
                if (args is null) return;
                if (args.Count != 1) return;
                if (args[0] is not AnimationTrackViewModel vm) return;
                var idx = vm.TrackIndex;

                if (idx <= 0) return;
                if (sp.GetTrackIndices().Contains(idx - 1)) return;
                sp.SetAnimation(idx - 1, sp.Animations[0]);
            },
            args =>
            {
                if (_selectedObjects.Length != 1) return false;
                var sp = _selectedObjects[0];

                if (sp.Animations.Length <= 0) return false;
                if (args is null) return false;
                if (args.Count != 1) return false;
                if (args[0] is not AnimationTrackViewModel vm) return false;
                var idx = vm.TrackIndex;

                if (idx <= 0) return false;
                if (sp.GetTrackIndices().Contains(idx - 1)) return false;
                return true;
            }
        );
        private RelayCommand<IList?>? _cmd_InsertTrack;

        public RelayCommand<IList?>? Cmd_ClearTrack => _cmd_ClearTrack ??= new(
            args =>
            {
                if (_selectedObjects.Length <= 0) return;
                if (args is null) return;
                if (args.Count <= 0) return;

                foreach (var vm in args.OfType<AnimationTrackViewModel>())
                    foreach (var sp in _selectedObjects)
                        sp.ClearTrack(vm.TrackIndex);
            },
            args =>
            {
                if (_selectedObjects.Length <= 0) return false;
                if (args is null) return false;
                if (args.Count <= 0) return false;
                return true;
            }
        );
        private RelayCommand<IList?>? _cmd_ClearTrack;

        public bool? DebugTexture
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugTexture;
                if (_selectedObjects.Skip(1).Any(it => it.DebugTexture != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugTexture = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugBounds
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugBounds;
                if (_selectedObjects.Skip(1).Any(it => it.DebugBounds != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugBounds = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugBones
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugBones;
                if (_selectedObjects.Skip(1).Any(it => it.DebugBones != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugBones = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugRegions
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugRegions;
                if (_selectedObjects.Skip(1).Any(it => it.DebugRegions != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugRegions = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugMeshHulls
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugMeshHulls;
                if (_selectedObjects.Skip(1).Any(it => it.DebugMeshHulls != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugMeshHulls = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugMeshes
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugMeshes;
                if (_selectedObjects.Skip(1).Any(it => it.DebugMeshes != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugMeshes = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugBoundingBoxes
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugBoundingBoxes;
                if (_selectedObjects.Skip(1).Any(it => it.DebugBoundingBoxes != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugBoundingBoxes = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugPaths
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugPaths;
                if (_selectedObjects.Skip(1).Any(it => it.DebugPaths != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugPaths = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugPoints
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugPoints;
                if (_selectedObjects.Skip(1).Any(it => it.DebugPoints != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugPoints = (bool)value;
                OnPropertyChanged();
            }
        }

        public bool? DebugClippings
        {
            get
            {
                if (_selectedObjects.Length <= 0) return null;
                var val = _selectedObjects[0].DebugClippings;
                if (_selectedObjects.Skip(1).Any(it => it.DebugClippings != val)) return null;
                return val;
            }

            set
            {
                if (_selectedObjects.Length <= 0) return;
                if (value is null) return;
                foreach (var sp in _selectedObjects) sp.DebugClippings = (bool)value;
                OnPropertyChanged();
            }
        }

        private static readonly Dictionary<string, string> _singleModelPropertyMap = new()
        {
            { nameof(SpineObjectModel.IsShown), nameof(IsShown) },
            { nameof(SpineObjectModel.UsePma), nameof(UsePma) },
            { nameof(SpineObjectModel.Physics), nameof(Physics) },
            { nameof(SpineObjectModel.TimeScale), nameof(TimeScale) },

            { nameof(SpineObjectModel.Scale), nameof(Scale) },
            { nameof(SpineObjectModel.FlipX), nameof(FlipX) },
            { nameof(SpineObjectModel.FlipY), nameof(FlipY) },
            { nameof(SpineObjectModel.X), nameof(X) },
            { nameof(SpineObjectModel.Y), nameof(Y) },

            // Skins 变化在 SkinViewModel 中监听
            // Slots 变化在 SlotAttachmentViewModel 中监听
            // AnimationTracks 变化在 AnimationTrackViewModel 中监听

            { nameof(SpineObjectModel.DebugTexture), nameof(DebugTexture) },
            { nameof(SpineObjectModel.DebugBounds), nameof(DebugBounds) },
            { nameof(SpineObjectModel.DebugBones), nameof(DebugBones) },
            { nameof(SpineObjectModel.DebugRegions), nameof(DebugRegions) },
            { nameof(SpineObjectModel.DebugMeshHulls), nameof(DebugMeshHulls) },
            { nameof(SpineObjectModel.DebugMeshes), nameof(DebugMeshes) },
            { nameof(SpineObjectModel.DebugBoundingBoxes), nameof(DebugBoundingBoxes) },
            { nameof(SpineObjectModel.DebugPaths), nameof(DebugPaths) },
            { nameof(SpineObjectModel.DebugPoints), nameof(DebugPoints) },
            { nameof(SpineObjectModel.DebugClippings), nameof(DebugClippings) },
        };

        /// <summary>
        /// 监听单个模型属性发生变化, 则更新聚合属性值
        /// </summary>
        private void SingleModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
        {
            if (_singleModelPropertyMap.TryGetValue(e.PropertyName, out var targetProperty))
            {
                OnPropertyChanged(targetProperty);
            }
        }

        /// <summary>
        /// 监听单个模型动画轨道发生变化, 则重建聚合后的动画列表
        /// </summary>
        private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName))
            {
                // XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表
                // 但是目前无法识别是否增加了轨道, 因此总是重建列表

                // 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
                Application.Current.Dispatcher.BeginInvoke(
                    () =>
                    {
                        _animationTracks.Clear();
                        IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
                        foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
                        foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
                    }
                );
            }
        }

        public class SkinViewModel : ObservableObject
        {
            private readonly SpineObjectModel[] _spines;
            private readonly string _name;

            public SkinViewModel(string name, SpineObjectModel[] spines)
            {
                _spines = spines;
                _name = name;

                // 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
                foreach (var sp in _spines)
                {
                    WeakEventManager<SpineObjectModel, SkinStatusChangedEventArgs>.AddHandler(
                        sp,
                        nameof(sp.SkinStatusChanged),
                        SingleModel_SkinStatusChanged
                    );
                }
            }

            public string Name => _name;

            public bool? Status
            {
                get
                {
                    if (_spines.Length <= 0) return null;
                    var val = _spines[0].GetSkinStatus(_name);
                    if (_spines.Skip(1).Any(it => it.GetSkinStatus(_name) != val)) return null;
                    return val;
                }

                set
                {
                    if (_spines.Length <= 0) return;
                    if (value is null) return;
                    bool changed = false;
                    foreach (var sp in _spines) if (sp.SetSkinStatus(_name, (bool)value)) changed = true;
                    if (changed) OnPropertyChanged();
                }
            }

            private void SingleModel_SkinStatusChanged(object? sender, SkinStatusChangedEventArgs e)
            {
                if (e.Name == _name) OnPropertyChanged(nameof(Status));
            }
        }

        public class SlotViewModel : ObservableObject
        {
            private readonly SpineObjectModel[] _spines;
            private readonly string[] _attachmentNames = [];
            private readonly string _slotName;

            public SlotViewModel(string slotName, SpineObjectModel[] spines)
            {
                _spines = spines;
                _slotName = slotName;

                if (_spines.Length > 0)
                {
                    IEnumerable<string> attachmentNames = _spines[0].SlotAttachments[_slotName];
                    foreach (var sp in _spines.Skip(1))
                        attachmentNames = attachmentNames.Union(sp.SlotAttachments[_slotName]);
                    _attachmentNames = attachmentNames.ToArray();
                }

                // 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
                foreach (var sp in _spines)
                {
                    WeakEventManager<SpineObjectModel, SlotVisibleChangedEventArgs>.AddHandler(
                        sp,
                        nameof(sp.SlotVisibleChanged),
                        SingleModel_SlotVisibleChanged
                    );
                    WeakEventManager<SpineObjectModel, SlotAttachmentChangedEventArgs>.AddHandler(
                        sp,
                        nameof(sp.SlotAttachmentChanged),
                        SingleModel_SlotAttachmentChanged
                    );
                    WeakEventManager<SpineObjectModel, SkinStatusChangedEventArgs>.AddHandler(
                        sp,
                        nameof(sp.SkinStatusChanged),
                        SingleModel_SkinStatusChanged
                    );
                }
            }

            public ReadOnlyCollection<string> AttachmentNames => _attachmentNames.AsReadOnly();

            public string SlotName => _slotName;

            public string? AttachmentName
            {
                get
                {
                    if (_spines.Length <= 0) return null;
                    var val = _spines[0].GetAttachment(_slotName);
                    if (_spines.Skip(1).Any(it => it.GetAttachment(_slotName) != val)) return null;
                    return val;
                }

                set
                {
                    if (_spines.Length <= 0) return;
                    bool changed = false;
                    foreach (var sp in _spines) if (sp.SetAttachment(_slotName, value)) changed = true;
                    if (changed) OnPropertyChanged();
                }
            }

            public bool? Visible
            {
                get
                {
                    if (_spines.Length <= 0) return null;
                    var val = _spines[0].GetSlotVisible(_slotName);
                    if (_spines.Skip(1).Any(it => it.GetSlotVisible(_slotName) != val)) return null;
                    return val;
                }

                set
                {
                    if (_spines.Length <= 0) return;
                    if (value is null) return;
                    foreach (var sp in _spines) sp.SetSlotVisible(_slotName, (bool)value);
                    OnPropertyChanged();
                }
            }

            private void SingleModel_SlotVisibleChanged(object? sender, SlotVisibleChangedEventArgs e)
            {
                if (e.SlotName == _slotName) OnPropertyChanged(nameof(Visible));
            }

            private void SingleModel_SlotAttachmentChanged(object? sender, SlotAttachmentChangedEventArgs e)
            {
                if (e.SlotName == _slotName) OnPropertyChanged(nameof(AttachmentName));
            }

            private void SingleModel_SkinStatusChanged(object? sender, SkinStatusChangedEventArgs e)
            {
                // 如果皮肤发生改变, 则直接触发附件属性变化事件
                OnPropertyChanged(nameof(AttachmentName));
            }
        }

        public class AnimationTrackViewModel : ObservableObject
        {
            private readonly SpineObjectModel[] _spines;
            private readonly string[] _animationNames = [];
            private readonly int _trackIndex;

            public AnimationTrackViewModel(int trackIndex, SpineObjectModel[] spines)
            {
                _spines = spines;
                _trackIndex = trackIndex;

                if (_spines.Length > 0)
                {
                    IEnumerable<string> animationNames = _spines[0].Animations;
                    foreach (var sp in _spines.Skip(1))
                        animationNames = animationNames.Union(sp.Animations);
                    _animationNames = animationNames.ToArray();
                }

                // 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
                foreach (var sp in _spines)
                {
                    WeakEventManager<SpineObjectModel, TrackPropertyChangedEventArgs>.AddHandler(
                        sp,
                        nameof(sp.TrackPropertyChanged),
                        SingleModel_TrackPropChanged
                    );
                }
            }

            public ReadOnlyCollection<string> AnimationNames => _animationNames.AsReadOnly();

            public int TrackIndex => _trackIndex;

            public float? AnimationDuration
            {
                get
                {
                    if (_spines.Length <= 0) return null;
                    var ani = _spines[0].GetAnimation(_trackIndex);
                    if (ani is null) return null;
                    var val = _spines[0].GetAnimationDuration(ani);
                    foreach (var sp in _spines.Skip(1))
                    {
                        var a = sp.GetAnimation(_trackIndex);
                        if (a is null) return null;
                        if (sp.GetAnimationDuration(a) != val) return null;
                    }
                    return val;
                }
            }

            public string? AnimationName
            {
                get
                {
                    // XXX: 空轨道和多选不相同都会返回 null
                    if (_spines.Length <= 0) return null;
                    var val = _spines[0].GetAnimation(_trackIndex);
                    if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null;
                    return val;
                }

                set
                {
                    if (_spines.Length <= 0) return;
                    if (value is null) return;
                    foreach (var sp in _spines) sp.SetAnimation(_trackIndex, value);
                    OnPropertyChanged();
                    OnPropertyChanged(nameof(AnimationDuration));
                }
            }

            public float? TrackTimeScale
            {
                get
                {
                    // XXX: 空轨道和多选不相同都会返回 null
                    if (_spines.Length <= 0) return null;
                    var val = _spines[0].GetTrackTimeScale(_trackIndex);
                    if (_spines.Skip(1).Any(it => it.GetTrackTimeScale(_trackIndex) != val)) return null;
                    return val;
                }

                set
                {
                    if (_spines.Length <= 0) return;
                    if (value is null) return;
                    foreach (var sp in _spines) sp.SetTrackTimeScale(_trackIndex, (float)value);
                    OnPropertyChanged();
                }
            }

            public float? TrackAlpha
            {
                get
                {
                    // XXX: 空轨道和多选不相同都会返回 null
                    if (_spines.Length <= 0) return null;
                    var val = _spines[0].GetTrackAlpha(_trackIndex);
                    if (_spines.Skip(1).Any(it => it.GetTrackAlpha(_trackIndex) != val)) return null;
                    return val;
                }

                set
                {
                    if (_spines.Length <= 0) return;
                    if (value is null) return;
                    foreach (var sp in _spines) sp.SetTrackAlpha(_trackIndex, (float)value);
                    OnPropertyChanged();
                }
            }

            private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e)
            {
                if (e.TrackIndex == _trackIndex)
                {
                    if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName)) OnPropertyChanged(nameof(AnimationName));
                    else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.TimeScale)) OnPropertyChanged(nameof(TrackTimeScale));
                    else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.Alpha)) OnPropertyChanged(nameof(TrackAlpha));
                }
            }
        }
    }
}