using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo
{
///
/// A class containing helper methods to merge objects.
///
public static class Merger
{
///
/// Merge two lists, can keep duplicates or remove them.
///
/// The first enumerable to merge
/// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept
/// Equality function to compare items. If this is null, duplicated elements are kept
/// The two list merged as an array
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T[] MergeLists([CanBeNull] IEnumerable first,
[CanBeNull] IEnumerable second,
[CanBeNull] 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();
}
///
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
///
/// The first dictionary to merge
/// The second dictionary to merge
/// The type of the keys in dictionaries
/// The type of values in the dictionaries
/// The first dictionary with the missing elements of .
///
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary MergeDictionaries([CanBeNull] IDictionary first,
[CanBeNull] IDictionary second)
{
return MergeDictionaries(first, second, out bool _);
}
///
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
///
/// The first dictionary to merge
/// The second dictionary to merge
///
/// true if a new items has been added to the dictionary, false otherwise.
///
/// The type of the keys in dictionaries
/// The type of values in the dictionaries
/// The first dictionary with the missing elements of .
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary MergeDictionaries([CanBeNull] IDictionary first,
[CanBeNull] IDictionary second,
out bool hasChanged)
{
if (first == null)
{
hasChanged = true;
return second;
}
hasChanged = false;
if (second == null)
return first;
foreach ((T key, T2 value) in second)
{
bool success = first.TryAdd(key, value);
hasChanged |= success;
if (success || first[key]?.Equals(default) == false || value?.Equals(default) != false)
continue;
first[key] = value;
hasChanged = true;
}
return first;
}
///
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
///
///
/// The only difference in this function compared to
///
/// is the way is calculated and the order of the arguments.
///
/// MergeDictionaries(first, second);
///
/// will do the same thing as
///
/// CompleteDictionaries(second, first, out bool _);
///
///
/// The first dictionary to merge
/// The second dictionary to merge
///
/// true if a new items has been added to the dictionary, false otherwise.
///
/// The type of the keys in dictionaries
/// The type of values in the dictionaries
///
/// A dictionary with the missing elements of
/// set to those of .
///
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary CompleteDictionaries([CanBeNull] IDictionary first,
[CanBeNull] IDictionary second,
out bool hasChanged)
{
if (first == null)
{
hasChanged = true;
return second;
}
hasChanged = false;
if (second == null)
return first;
hasChanged = second.Any(x => x.Value?.Equals(first[x.Key]) == false);
foreach ((T key, T2 value) in first)
second.TryAdd(key, value);
return second;
}
///
/// 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 containing 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 non-default values of seconds to the corresponding property of second.
/// Dictionaries are handled like anonymous objects with a property per key/pair value
/// (see
///
/// for more details).
/// At the end, the OnMerge method of first will be called if first is a
///
///
/// This does the opposite of .
///
///
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
///
///
/// 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,
[InstantHandle] 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.GetCustomAttribute()?.Value
?? property.PropertyType.GetClrDefault();
if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first)))
continue;
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments;
object[] parameters = {
property.GetValue(first),
value,
false
};
object newDictionary = Utility.RunGenericMethod