using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Models;
using Kyoo.Models.Attributes;
namespace Kyoo
{
///
/// A set of utility functions that can be used everywhere.
///
public static class Utility
{
///
/// Is the lambda expression a member (like x => x.Body).
///
/// The expression that should be checked
/// True if the expression is a member, false otherwise
public static bool IsPropertyExpression(LambdaExpression ex)
{
return ex == null ||
ex.Body is MemberExpression ||
ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression;
}
///
/// Get the name of a property. Usfull for selectors as members ex: Load(x => x.Shows)
///
/// The expression
/// The name of the expression
/// If the expression is not a property, ArgumentException is thrown.
public static string GetPropertyName(LambdaExpression ex)
{
if (!IsPropertyExpression(ex))
throw new ArgumentException($"{ex} is not a property expression.");
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
? ((UnaryExpression)ex.Body).Operand as MemberExpression
: ex.Body as MemberExpression;
return member!.Member.Name;
}
///
/// Get the value of a member (property or field)
///
/// The member value
/// The owner of this member
/// The value boxed as an object
/// if or is null.
/// The member is not a field or a property.
public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj)
{
if (member == null)
throw new ArgumentNullException(nameof(member));
if (obj == null)
throw new ArgumentNullException(nameof(obj));
return member switch
{
PropertyInfo property => property.GetValue(obj),
FieldInfo field => field.GetValue(obj),
_ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).")
};
}
///
/// Slugify a string (Replace spaces by -, Uniformise accents é -> e)
///
/// The string to slugify
/// The slugified string
public static string ToSlug(string str)
{
if (str == null)
return null;
str = str.ToLowerInvariant();
string normalizedString = str.Normalize(NormalizationForm.FormD);
StringBuilder stringBuilder = new();
foreach (char c in normalizedString)
{
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
stringBuilder.Append(c);
}
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled);
str = str.Trim('-', '_');
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
return str;
}
///
/// Set the image of a show using the type.
///
/// The owner of the image
/// The url of the image
/// The type of the image
public static void SetImage(Show show, string imgUrl, ImageType type)
{
switch(type)
{
case ImageType.Poster:
show.Poster = imgUrl;
break;
case ImageType.Logo:
show.Logo = imgUrl;
break;
case ImageType.Background:
show.Backdrop = imgUrl;
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
///
/// Merge two lists, can keep duplicates or remove them.
///
/// The first enumarble to merge
/// The second enumerable to merge, if items from this list are equals to one from the first, they are not keeped
/// Equality function to compare items. If this is null, duplicated elements are kept
/// The two list merged as an array
public static T[] MergeLists(IEnumerable first,
IEnumerable second,
Func isEqual = null)
{
if (first == null)
return second.ToArray();
if (second == null)
return first.ToArray();
if (isEqual == null)
return first.Concat(second).ToArray();
List list = first.ToList();
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
}
///
/// Set every fields of first to those of second. Ignore fields marked with the attribute
/// At the end, the OnMerge method of first will be called if first is a
///
/// The object to assign
/// The object containg new values
/// Fields of T will be used
///
public static T Assign(T first, T second)
{
Type type = typeof(T);
IEnumerable properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
property.SetValue(first, value);
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
///
/// Set every default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}.
/// At the end, the OnMerge method of first will be called if first is a
///
/// The object to complete
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
/// Filter fields that will be merged
/// Fields of T will be completed
///
/// If first is null
public static T Complete([NotNull] T first, [CanBeNull] T second, Func where = null)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
return first;
Type type = typeof(T);
IEnumerable properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
if (value?.Equals(defaultValue) == false && value != property.GetValue(first))
property.SetValue(first, value);
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
///
/// An advanced function.
/// This will set missing values of to the corresponding values of .
/// Enumerable will be merged (concatenated).
/// At the end, the OnMerge method of first will be called if first is a .
///
/// The object to complete
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
/// Fields of T will be merged
///
public static T Merge(T first, T second)
{
if (first == null)
return second;
if (second == null)
return first;
Type type = typeof(T);
IEnumerable properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
foreach (PropertyInfo property in properties)
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string))
{
property.SetValue(first, RunGenericMethod