using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using JD.Core.Collections; using JD.Core.Helpers; using JetBrains.Annotations; namespace JD.Serialization { public class PictogramModel : INotifyPropertyChanged { #region Variables private string _name; [NotNull] private readonly Tape _tape; private Path _pictoPath = new Path(""); private int _instancesCount; private float _beatDuration; private readonly NullingExpandoObject _extensions = new NullingExpandoObject(); private static readonly char[] _validModelNameChars = Enumerable.Range('a', 26) .Union(Enumerable.Range('A', 26)) .Union(Enumerable.Range('0', 10)) .Union(new int[] { '_' }) .Select(x => (char)x).ToArray(); #endregion #region Constructor public PictogramModel(Tape tape, Path pictoPath) { if (tape == null) { throw new ArgumentNullException(nameof(tape)); } _tape = tape; PictoPath = pictoPath; _name = GetPictogramNameFromPictogramPath(PictoPath); InstancesCount = Instances.Count(); } #endregion #region Properties [Browsable(false)] public IEnumerable Instances { get { return _tape.Clips.OfType().Where(clip => clip.PictoPath.Equals(PictoPath)); } } [Browsable(true)] public string Name { get { return _name; } set { if (_name != value) { string sanitizedValue = SanitizePictoModelName(value); string uniqueNewMoveName = sanitizedValue.MakeUniqueName(Tape.PictogramModels().Where(x => x != this).Select(x => x.Name).ToList()); if (uniqueNewMoveName != sanitizedValue) { PictogramNameMadeUnique?.Invoke(uniqueNewMoveName); } _name = uniqueNewMoveName != sanitizedValue ? uniqueNewMoveName : sanitizedValue; OnPropertyChanged(); PictoPath = new Path(GetPictogramPathFromPictogramName(Tape, _name)); } } } [Browsable(true)] [ReadOnly(true)] public Path PictoPath { get { return _pictoPath; } set { if (!_pictoPath.Equals(value)) { PictoPathChanged?.Invoke(this, _pictoPath.getAbsolute(), value.getAbsolute()); _pictoPath = value; _name = GetPictogramNameFromPictogramPath(PictoPath); OnPropertyChanged(); } } } [DisplayName("Duration (in beats)")] [Description("Duration in beats")] [Browsable(false)] public float BeatDuration { get { return _beatDuration; } set { if (!_beatDuration.AlmostEquals(value)) { _beatDuration = value; OnPropertyChanged(); } } } [Browsable(true)] [ReadOnly(true)] public int InstancesCount { get { return _instancesCount; } set { if (_instancesCount != value) { InstancesCountChanged?.Invoke(this, _instancesCount, value); _instancesCount = value; OnPropertyChanged(); } } } public Tape Tape { get { return _tape; } } public PictogramFlag Flags { get { return GetFlagsFromName(Name); } set { if (GetFlagsFromName(Name) != value) { Name = GetNameFromFlags(Name, value); OnPropertyChanged(); } } } /// /// Allows to add additional values to the object since the classes are generated. /// Typically, an extension method can reuse this to get and set value. /// public dynamic Extensions => _extensions; #endregion #region UIProperties public string PictogramTypeFriendlyName { get { return InstancesCount > 0 ? "Pictogram" : "Orphan pictogram"; } } #endregion #region Methods public override string ToString() { return Name; } /// /// Get the name of the pictogram from its path /// internal static string GetPictogramNameFromPictogramPath(Path pictoPath) { return System.IO.Path.GetFileNameWithoutExtension(pictoPath.getAbsolute()); } /// /// Get the path of the pictogram from its name /// internal static string GetPictogramPathFromPictogramName(Tape tape, string pictoName) { string classifierFileName = pictoName + ".png"; string classifierPath = System.IO.Path.Combine(tape.MapInfo?.PictosFolderRelative, classifierFileName); classifierPath = classifierPath.Replace("\\", "/"); return classifierPath; } /// /// Gets the Pictogram informations by parsing its name /// private static PictogramFlag GetFlagsFromName(string name) { var flags = PictogramFlag.None; var elements = name.Split('_'); foreach (PictogramFlag flag in Enum.GetValues(typeof(PictogramFlag))) { foreach (string nomenclature in flag.GetNomenclatures()) { if (elements.Contains(nomenclature, StringComparer.CurrentCultureIgnoreCase)) { flags |= flag; } } } return flags; } /// /// Gets the name of a pictogram from a current name and its informations /// private static string GetNameFromFlags(string name, PictogramFlag flags) { var elements = name.Split('_').ToList(); var pictoFlags = Enum.GetValues(typeof(PictogramFlag)).Cast().Where(flag => flag != PictogramFlag.None).ToArray(); var pictoFlagsNames = pictoFlags.SelectMany(flag => flag.GetNomenclatures()).ToArray(); int countSuffix; Func isCount = s => s == elements.LastOrDefault() && int.TryParse(s, out countSuffix); var names = elements.Where(part => !pictoFlagsNames.Contains(part, StringComparer.CurrentCultureIgnoreCase) && !isCount(part)); var newFlags = pictoFlags.Where(flag => flags.HasFlag(flag)).Select(flag => flag.GetNomenclatures()[0]); var count = elements.Where(isCount); return string.Join("_", names.Concat(newFlags).Concat(count)); } private static string SanitizePictoModelName(string modelName) { string sanitizedValue = modelName; var invalidChars = modelName.ToCharArray().Distinct().Where(x => !_validModelNameChars.Contains(x)).ToList(); foreach (var invalidChar in invalidChars) { sanitizedValue = sanitizedValue.Replace(invalidChar.ToString(), string.Empty); } return sanitizedValue; } public static bool IsNameValid(string modelName) { return modelName.ToCharArray().All(@char => _validModelNameChars.Contains(@char)); } #endregion #region Events public delegate void PictogramNameMadeUniqueFunc(string newMoveName); public static event PictogramNameMadeUniqueFunc PictogramNameMadeUnique; public delegate void PictoPathChangedFunc(PictogramModel model, string oldPath, string newPath); public static event PictoPathChangedFunc PictoPathChanged; public delegate void InstancesCountChangedFunc(PictogramModel model, int oldValue, int newValue); public static event InstancesCountChangedFunc InstancesCountChanged; public event PropertyChangedEventHandler PropertyChanged; public void NotifyPropertyChanged(string propertyName) { OnPropertyChanged(propertyName); } //[NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChangedEventHandler handler = PropertyChanged; handler?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion } public class PictogramModelCollection : UAFList { } #region Manager public static class PictogramModelManager { private static void CreatePictogramModelCollection(this Tape tape) { tape.Extensions.PictogramModels = new PictogramModelCollection(); tape.Clips.CollectionChanged += tape.ClipsOnCollectionChanged; tape.Clips.ItemChanged += tape.ClipsOnItemChanged; tape.PropertyChanged += tape.tape_PropertyChanged; } static void tape_PropertyChanged(this Tape tape, object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "IsLoading" && !tape.IsLoading) { RefreshInstancesCount(tape); } } private static void ClipsOnItemChanged(this Tape tape, object sender, ItemChangedEventArgs args) { if (args.PropertyName == "Model" && !tape.IsLoading) { RefreshInstancesCount(tape); } } static void ClipsOnCollectionChanged(this Tape tape, object sender, NotifyCollectionChangedEventArgs e) { if (!tape.IsLoading) { if (e.NewItems != null) { foreach (var model in e.NewItems.OfType().Select(x => x.Model).Distinct()) { RefreshInstancesCount(tape, model); } } if (e.OldItems != null) { foreach (var model in e.OldItems.OfType().Select(x => x.Model).Distinct()) { RefreshInstancesCount(tape, model); } } } } private static void RefreshInstancesCount(this Tape tape) { foreach (var model in tape.PictogramModels().ToList()) { RefreshInstancesCount(tape, model); } } private static void RefreshInstancesCount(this Tape tape, PictogramModel model) { model.InstancesCount = tape.Clips.OfType().Count(x => x.Model == model); if (model.InstancesCount == 0) { tape.PictogramModels().Remove(model); } } public static PictogramModel GetOrCreatePictogramModel(this Tape tape, Path pictogramPath) { var models = tape.PictogramModels(); PictogramModel model = models.FirstOrDefault(x => x.PictoPath.Equals(pictogramPath)); if (model == null) { model = new PictogramModel(tape, pictogramPath); models.Add(model); } return model; } public static PictogramModel GetOrCreatePictogramModel(this Tape tape, string modelName) { return GetOrCreatePictogramModel(tape, new Path(PictogramModel.GetPictogramPathFromPictogramName(tape, modelName))); } public static PictogramModelCollection PictogramModels(this Tape tape) { if (tape.Extensions.PictogramModels == null) { tape.CreatePictogramModelCollection(); } return tape.Extensions.PictogramModels; } public static void ClearPictogramModels(this Tape tape) { PictogramModelCollection models = tape.Extensions.PictogramModels; models?.Clear(); } } #endregion #region PictogramFlag [Flags] public enum PictogramFlag { None = 0, Pose = 2, Move = 4, Arrow = 8, Ghost = 16, Shake = 32, Clap = 64, Punch = 128, Object = 256, Jump = 512, Gold = 1024 } public static class PictogramFlagExtensions { public static string[] GetNomenclatures(this PictogramFlag flag) { switch (flag) { case PictogramFlag.Pose: return new []{"po", "pose", "P"}; case PictogramFlag.Arrow: return new[] { "ar", "arrow", "F" }; case PictogramFlag.Ghost: return new[] { "gh", "ghost", "G" }; case PictogramFlag.Gold: return new[] { "gold" }; } return new[] { flag.ToString().ToLower().Substring(0, 2), flag.ToString().ToLower() }; } } #endregion }