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
///
///
/// Design goals
/// - Lightweight, testable factory that maps human-friendly display keys (e.g. "EsriJson") to
/// converter factory delegates (Func).
/// - Normalization uses so callers may provide
/// keys with varying case, punctuation or spacing.
///
/// Thread-safety and lifetime
/// - Instances are effectively read-only after construction: registrations are applied in the
/// constructor and stored in an internal dictionary. Concurrent readers (Create/TryCreate)
/// are safe for typical usage. Do not modify the factory's internal state after construction.
///
/// Logging
/// - The factory accepts an optional . When none is provided the static
/// is used by default. Logging calls are best-effort and should not
/// change application control-flow.
///
/// Error handling
/// - Use when calling code prefers not to throw;
/// throws on unsupported options
/// and includes a friendly suggestion when available.
///
/// Testability
/// - For unit tests provide a custom registrations dictionary via the constructor to avoid depending
/// on built-in converters. Prefer injecting an implementation (TestLogger)
/// so tests can assert emitted diagnostics without touching global logging.
///
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.
///
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 EsriJsonConverter());
AddSingle("GeoJson", () => new GeoJsonConverter());
AddSingle("GeoJsonSeq", () => new GeoJsonSeqConverter());
AddSingle("Kml", () => new KmlConverter());
AddSingle("Kmz", () => new KmzConverter());
AddSingle("Shapefile", () => new ShapefileConverter());
AddSingle("Osm", () => new OsmConverter());
AddSingle("Gpx", () => new GpxConverter());
AddSingle("Gml", () => new GmlConverter());
AddSingle("Gdb", () => new GbdConverter());
AddSingle("TopoJson", () => new TopoJsonConverter());
AddSingle("MapInfoInterchange", () => new MapInfoInterchangeConverter());
AddSingle("MapInfoTab", () => new MapInfoTabConverter());
AddSingle("Csv", () => new CsvConverter());
AddSingle("GeoPackage", () => new GeoPackageConverter());
// 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)}");
}
///
/// 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;
}
}