using GitConverter.Lib.Converters;
using GitConverter.Lib.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace GitConverter.Lib.Factories
{
///
/// Registry-based, non-static converter factory.
/// - Normalizes CLI input keys (flexible matching)
/// - Provides TryCreate to avoid throwing from CLI flows
/// - Offers suggestions for close matches
///
///
///
/// ConverterFactory maps human-friendly display keys (for example "Shapefile" or "GeoJson")
/// to factory delegates that produce concrete instances. It is intended
/// to be a lightweight, testable component used by the ConsoleApp and higher-level services to
/// resolve converters based on user-supplied option strings.
///
///
/// Key normalization
/// - Input keys are normalized using so callers
/// can supply options in a forgiving manner (different case, extra punctuation, spacing or aliases).
/// - Registration keys may include comma-separated aliases (for example "Kml,Kmz") when constructed
/// via the registrations dictionary overload.
///
///
/// Registration model & lifetime
/// - Registrations are applied in the constructor and stored in an internal, case-insensitive
/// dictionary keyed by the normalized key. After construction the instance is effectively
/// read-only and safe for concurrent readers. Do not attempt to mutate the internal registry
/// after construction; for dynamic scenarios create a new factory instance.
/// - The default constructor registers the built-in converters used by the project. For unit
/// tests prefer the overload that accepts an explicit registrations dictionary so tests can
/// register small, deterministic factories (e.g., fakes) and avoid instantiating heavy
/// dependencies like Aspose drivers.
///
///
/// Thread-safety
/// - The factory is safe for concurrent Create/TryCreate calls. Registration operations occur
/// only during construction. The internal logger is expected to be thread-safe.
///
///
/// Error handling and API choices
/// - is a non-throwing operation intended for
/// CLI flows and tests where callers prefer to handle unsupported options gracefully.
/// - is a convenience that throws a
/// when the option is unsupported. When a close match is available the exception message
/// includes a suggestion to improve UX.
/// - Instantiation exceptions thrown by factory delegates are caught in
/// and logged at Warn level; the method returns false so callers can decide how to surface
/// the error to users.
///
///
/// Logging
/// - The factory accepts an optional . When none is provided it falls
/// back to the static . Logging is best-effort — diagnostic calls
/// should not affect control flow.
/// - The constructor logs each registration at Debug level and emits an Info-level summary of
/// registered options. Lookup misses, instantiation failures and warnings are logged at
/// Debug/Warn levels to aid troubleshooting in tests and production.
///
///
/// Testing guidance
/// - Provide a small registrations dictionary and a test logger to isolate unit tests from
/// heavy converter implementations. Use to assert the
/// available display keys when verifying help text.
/// - Tests that exercise suggestion logic can assert the message produced by
/// when a close match exists.
///
///
/// Example usage
///
/// var factory = new ConverterFactory();
/// if (factory.TryCreate("Shapefile", out var converter))
/// {
/// var result = converter.Convert(...);
/// }
/// else
/// {
/// // Present usage / suggestion to user
/// }
///
///
///
public class ConverterFactory : IConverterFactory
{
private readonly IDictionary> _registry;
private readonly IReadOnlyCollection _displayKeys;
private readonly IAppLogger _logger;
///
/// Default ctor registers built-in converters.
/// For testability you can use .
///
///
/// - Registers the library's built-in converters (EsriJson, GeoJson, Kml, etc.).
/// - Uses the default logger () when no logger is supplied.
/// - For unit tests prefer passing a custom registrations dictionary to avoid creating real converters.
///
public ConverterFactory() : this(null, null) { }
///
/// Construct factory with optional external registrations (displayKey -> factory) and optional logger.
/// Keys will be normalized; a key may contain comma-separated aliases.
///
///
/// Optional custom registrations. The dictionary key may contain a single display key or comma-separated aliases.
/// Values are factory delegates that create an instance.
///
/// Optional logger; when null the static is used.
///
/// - Validation: blank display keys or null factories will throw during construction (ArgumentException/ArgumentNullException).
/// - Duplicate normalized keys cause an ArgumentException after logging the duplicate attempt.
/// - Useful for tests: pass a small registrations map and a TestLogger to avoid instantiating built-in converters.
///
public ConverterFactory(IDictionary> registrations, IAppLogger logger = null)
{
// Use provided logger or fall back to static Log.Current so behaviour remains compatible.
_logger = logger ?? Log.Current;
_registry = new Dictionary>(StringComparer.OrdinalIgnoreCase);
var displayList = new List();
void AddSingle(string displayKey, Func factory)
{
if (string.IsNullOrWhiteSpace(displayKey)) throw new ArgumentException("displayKey is required.", nameof(displayKey));
if (factory == null) throw new ArgumentNullException(nameof(factory));
var key = FactoryHelpers.NormalizeKey(displayKey);
if (_registry.ContainsKey(key))
{
// Log duplicate registration attempt before throwing
_logger.Error($"ConverterFactory duplicate registration attempt. NormalizedKey='{key}', displayKey='{displayKey}'");
throw new ArgumentException($"Duplicate registration for normalized key '{key}' (display: '{displayKey}').");
}
_registry[key] = factory;
displayList.Add(displayKey);
// Log each registration at Debug level (helpful during startup)
_logger.Debug($"ConverterFactory registered '{displayKey}' (normalized='{key}').");
}
void AddAliases(IEnumerable displayKeys, Func factory)
{
foreach (var dk in displayKeys)
AddSingle(dk, factory);
}
// Log purpose of following built-in registrations (helps startup diagnostics).
_logger.Info("ConverterFactory: registering built-in converters (display keys) - these map user-facing option names to concrete converter implementations.");
// Built-in registrations (lightweight stubs or real implementations)
AddSingle("EsriJson", () => new UniversalGisConverter());
AddSingle("GeoJson", () => new UniversalGisConverter());
AddSingle("GeoJsonSeq", () => new UniversalGisConverter());
AddSingle("Kml", () => new UniversalGisConverter());
AddSingle("Kmz", () => new UniversalGisConverter());
AddSingle("Shapefile", () => new UniversalGisConverter());
AddSingle("Osm", () => new UniversalGisConverter());
AddSingle("Gpx", () => new UniversalGisConverter());
AddSingle("Gml", () => new UniversalGisConverter());
AddSingle("Gdb", () => new UniversalGisConverter());
AddSingle("TopoJson", () => new UniversalGisConverter());
AddSingle("MapInfoInterchange", () => new UniversalGisConverter());
AddSingle("MapInfoTab", () => new UniversalGisConverter());
AddSingle("Csv", () => new UniversalGisConverter());
AddSingle("GeoPackage", () => new UniversalGisConverter());
// Apply external registrations (if provided).
if (registrations != null)
{
foreach (var kv in registrations)
{
if (string.IsNullOrWhiteSpace(kv.Key)) continue;
var aliases = kv.Key.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
if (!aliases.Any())
AddSingle(kv.Key, kv.Value);
else
AddAliases(aliases, kv.Value);
}
}
_displayKeys = displayList.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList().AsReadOnly();
// Log final registration summary at Info level (one-time)
_logger.Info($"ConverterFactory initialized with options: {string.Join(", ", _displayKeys)}");
_logger.Info(Environment.NewLine);
}
///
/// Create a converter for the given format option or throw if unsupported.
///
/// User-provided option (case/format tolerant).
/// An instance.
///
/// - Preferred when callers expect a converter or want an exception on unsupported options.
/// - When an unsupported option is provided the method will throw .
/// If a close suggestion exists the exception message will include it to improve UX.
/// - Logs useful diagnostics at Warn/Debug levels to assist troubleshooting.
///
public IConverter Create(string formatOption)
{
if (string.IsNullOrWhiteSpace(formatOption)) throw new ArgumentException("formatOption is required.", nameof(formatOption));
if (!TryCreate(formatOption, out var converter))
{
var suggestion = FactoryHelpers.SuggestClosest(formatOption, _displayKeys);
if (suggestion != null)
{
// Log suggestion before throwing to aid diagnostics
_logger.Warn($"Unsupported format option '{formatOption}'. Suggestion: '{suggestion}'");
throw new KeyNotFoundException($"Unsupported format option '{formatOption}'. Did you mean '{suggestion}'?");
}
// Log unsupported option and available options
_logger.Warn($"Unsupported format option '{formatOption}'. Supported: {string.Join(", ", _displayKeys)}");
throw new KeyNotFoundException($"Unsupported conversion option '{formatOption}'. Supported: {string.Join(", ", _displayKeys)}");
}
// Successful creation - brief debug log
_logger.Debug($"ConverterFactory.Create resolved '{formatOption}' to '{converter.GetType().FullName}'");
return converter;
}
///
/// Try to create a converter for the given option. Returns false if unsupported.
///
/// User-provided option string.
/// When true returned, the resolved converter instance.
/// True when a converter was created; false otherwise.
///
/// - Safe, non-throwing alternative to suitable for CLI flows and tests.
/// - Performs normalization via and looks up the registered factory.
/// - Instantiation is delegated to the stored factory delegate; any exception during instantiation is caught,
/// logged at Warn level and the method returns false.
/// - Use this method when callers want to handle unsupported options gracefully.
///
public bool TryCreate(string formatOption, out IConverter converter)
{
converter = null;
if (string.IsNullOrWhiteSpace(formatOption)) return false;
var key = FactoryHelpers.NormalizeKey(formatOption);
if (!_registry.TryGetValue(key, out var factory))
{
// Debug-level lookup miss (useful to diagnose why inputs don't match)
_logger.Debug($"ConverterFactory lookup miss for input='{formatOption}', normalized='{key}'");
return false;
}
try
{
converter = factory();
// Debug-level instantiation success
_logger.Debug(converter != null
? $"ConverterFactory instantiated converter for '{formatOption}' ({converter.GetType().FullName})"
: $"ConverterFactory factory returned null for '{formatOption}'");
return converter != null;
}
catch (Exception ex)
{
// Defensive: log and return false so caller can handle gracefully.
_logger.Warn($"ConverterFactory failed to instantiate converter for '{formatOption}': {ex.Message}");
converter = null;
return false;
}
}
///
/// Returns the supported option keys (human-friendly).
///
///
/// - The returned collection is ordered and read-only; suitable for displaying help text to users.
///
public IReadOnlyCollection GetSupportedOptions() => _displayKeys;
}
}