using System.Linq.Expressions;
using System.Reflection;
using NadekoBot.Common.Yml;
using NadekoBot.Common.Configs;
namespace NadekoBot.Services;
///
/// Base service for all settings services
///
/// Type of the settings
public abstract class ConfigServiceBase : IConfigService
where TSettings : ICloneable, new()
{
protected readonly string _filePath;
protected readonly IConfigSeria _serializer;
protected readonly IPubSub _pubSub;
private readonly TypedKey _changeKey;
protected TSettings _data;
// todo future config arrays are not copied - they're not protected from mutations
public TSettings Data => _data.Clone();
public abstract string Name { get; }
///
/// Initialized an instance of
///
/// Path to the file where the settings are serialized/deserialized to and from
/// Serializer which will be used
/// Pubsub implementation for signaling when settings are updated
/// Key used to signal changed event
protected ConfigServiceBase(string filePath, IConfigSeria serializer, IPubSub pubSub,
TypedKey changeKey)
{
_filePath = filePath;
_serializer = serializer;
_pubSub = pubSub;
_changeKey = changeKey;
Load();
_pubSub.Sub(_changeKey, OnChangePublished);
}
private void PublishChange()
{
_pubSub.Pub(_changeKey, _data);
}
private ValueTask OnChangePublished(TSettings newData)
{
_data = newData;
OnStateUpdate();
return default;
}
///
/// Loads data from disk. If file doesn't exist, it will be created with default values
///
protected void Load()
{
// if file is deleted, regenerate it with default values
if (!File.Exists(_filePath))
{
_data = new();
Save();
}
_data = _serializer.Deserialize(File.ReadAllText(_filePath));
}
///
/// Loads new data and publishes the new state
///
public void Reload()
{
Load();
_pubSub.Pub(_changeKey, _data);
}
///
/// Doesn't do anything by default. This method will be executed after
/// is reloaded from or new data is recieved
/// from the publish event
///
protected virtual void OnStateUpdate()
{
}
private void Save()
{
var strData = _serializer.Serialize(_data);
File.WriteAllText(_filePath, strData);
}
private readonly Dictionary> _propSetters = new Dictionary>();
private readonly Dictionary> _propSelectors = new Dictionary>();
private readonly Dictionary> _propPrinters = new Dictionary>();
private readonly Dictionary _propComments = new Dictionary();
protected void AddParsedProp(
string key,
Expression> selector,
SettingParser parser,
Func printer,
Func checker = null)
{
checker ??= _ => true;
key = key.ToLowerInvariant();
_propPrinters[key] = obj => printer((TProp)obj);
_propSelectors[key] = () => selector.Compile()(_data);
_propSetters[key] = Magic(selector, parser, checker);
_propComments[key] = ((MemberExpression)selector.Body).Member.GetCustomAttribute()?.Comment;
}
private Func Magic(Expression> selector,
SettingParser parser, Func checker)
=> (target, input) =>
{
if (!parser(input, out var value))
return false;
if (!checker(value))
return false;
object targetObject = target;
var expr = (MemberExpression)selector.Body;
var prop = (PropertyInfo)expr.Member;
var expressions = new List();
while (true)
{
expr = expr.Expression as MemberExpression;
if (expr is null)
break;
expressions.Add(expr);
}
foreach (var memberExpression in expressions.AsEnumerable().Reverse())
{
var localProp = (PropertyInfo) memberExpression.Member;
targetObject = localProp.GetValue(targetObject);
}
prop!.SetValue(targetObject, value, null);
return true;
};
public IReadOnlyList GetSettableProps()
=> _propSetters.Keys.ToList();
public string GetSetting(string prop)
{
prop = prop.ToLowerInvariant();
if (!_propSelectors.TryGetValue(prop, out var selector) ||
!_propPrinters.TryGetValue(prop, out var printer))
return default;
return printer(selector());
}
public string GetComment(string prop)
{
if (_propComments.TryGetValue(prop, out var comment))
return comment;
return null;
}
private bool SetProperty(TSettings target, string key, string value)
=> _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic)
&& magic(target, value);
public bool SetSetting(string prop, string newValue)
{
var success = true;
ModifyConfig(bs =>
{
success = SetProperty(bs, prop, newValue);
});
if(success)
PublishChange();
return success;
}
public void ModifyConfig(Action action)
{
var copy = Data;
action(copy);
_data = copy;
Save();
PublishChange();
}
}