// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see .
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Core.Api;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Linq;
namespace Kyoo.Core.Controllers
{
public class ConfigurationManager : IConfigurationManager
{
///
/// The configuration to retrieve and edit.
///
private readonly IConfiguration _configuration;
///
/// The application running Kyoo, it is used to retrieve the configuration file.
///
private readonly IApplication _application;
///
/// The strongly typed list of options
///
private readonly Dictionary _references;
///
/// Create a new using the given configuration.
///
/// The configuration to use.
/// The strongly typed option list.
/// The application running Kyoo, it is used to retrieve the configuration file.
public ConfigurationManager(IConfiguration configuration, IEnumerable references, IApplication application)
{
_configuration = configuration;
_application = application;
_references = references.ToDictionary(x => x.Path, x => x.Type, StringComparer.OrdinalIgnoreCase);
}
///
/// Transform the configuration section in nested expando objects.
///
/// The section to convert
/// The converted section
private static object _ToUntyped(IConfigurationSection config)
{
ExpandoObject obj = new();
foreach (IConfigurationSection section in config.GetChildren())
{
obj.TryAdd(section.Key, _ToUntyped(section));
}
if (!obj.Any())
return config.Value;
return obj;
}
///
public void AddTyped(string path)
{
foreach (ConfigurationReference confRef in ConfigurationReference.CreateReference(path))
_references.Add(confRef.Path, confRef.Type);
}
///
public void AddUntyped(string path)
{
ConfigurationReference config = ConfigurationReference.CreateUntyped(path);
_references.Add(config.Path, config.Type);
}
///
public void Register(string path, Type type)
{
if (type == null)
{
ConfigurationReference config = ConfigurationReference.CreateUntyped(path);
_references.Add(config.Path, config.Type);
}
else
{
foreach (ConfigurationReference confRef in ConfigurationReference.CreateReference(path, type))
_references.Add(confRef.Path, confRef.Type);
}
}
///
/// Get the type of the resource at the given path
///
/// The path of the resource
/// The path is not editable or readable
/// No configuration exists for the given path
/// The type of the resource at the given path
private Type _GetType(string path)
{
path = path.Replace("__", ":");
// TODO handle lists and dictionaries.
if (_references.TryGetValue(path, out Type type))
{
if (type != null)
return type;
throw new ArgumentException($"The configuration at {path} is not editable or readable.");
}
string parent = path.Contains(':') ? path[..path.IndexOf(':')] : null;
if (parent != null && _references.TryGetValue(parent, out type) && type == null)
throw new ArgumentException($"The configuration at {path} is not editable or readable.");
throw new ItemNotFoundException($"No configuration exists for the name: {path}");
}
///
public object GetValue(string path)
{
path = path.Replace("__", ":");
// TODO handle lists and dictionaries.
Type type = _GetType(path);
object ret = _configuration.GetValue(type, path);
if (ret != null)
return ret;
object option = Activator.CreateInstance(type);
_configuration.Bind(path, option);
return option;
}
///
public T GetValue(string path)
{
path = path.Replace("__", ":");
// TODO handle lists and dictionaries.
Type type = _GetType(path);
if (typeof(T).IsAssignableFrom(type))
{
throw new InvalidCastException($"The type {typeof(T).Name} is not valid for " +
$"a resource of type {type.Name}.");
}
return (T)GetValue(path);
}
///
public async Task EditValue(string path, object value)
{
path = path.Replace("__", ":");
Type type = _GetType(path);
value = JObject.FromObject(value).ToObject(type);
if (value == null)
throw new ArgumentException("Invalid value format.");
ExpandoObject config = _ToObject(_configuration);
IDictionary configDic = config;
configDic[path] = value;
JObject obj = JObject.FromObject(config);
await using StreamWriter writer = new(_application.GetConfigFile());
await writer.WriteAsync(obj.ToString());
}
///
/// Transform a configuration to a strongly typed object (the root configuration is an
/// but child elements are using strong types.
///
/// The configuration to transform
/// A strongly typed representation of the configuration.
[SuppressMessage("ReSharper", "RedundantJumpStatement",
Justification = "A catch block should not be empty.")]
private ExpandoObject _ToObject(IConfiguration config)
{
ExpandoObject obj = new();
foreach (IConfigurationSection section in config.GetChildren())
{
try
{
Type type = _GetType(section.Path);
obj.TryAdd(section.Key, section.Get(type));
}
catch (ArgumentException)
{
obj.TryAdd(section.Key, _ToUntyped(section));
}
catch
{
continue;
}
}
return obj;
}
}
}