using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GitConverter.Lib.Factories;
using GitConverter.Lib.Logging;
using GitConverter.Lib.Models;
namespace GitConverter.Lib.Converters
{
///
/// High-level conversion orchestrator.
///
///
/// Responsibilities
/// - Validate paths (input exists, output/temp can be created).
/// - Inspect input: single file or archive.
/// - Detect the best matching converter based on input extension or archive contents (required file extensions).
/// - Resolve converter using ConverterFactory (TryCreate) and invoke its Convert method.
/// - Log each step and return a friendly ConversionResult on expected failures (validation, unknown option,
/// missing required files) rather than throwing.
///
/// Design notes / best practices
/// - The service is intentionally small and deterministic; the detection rules are explicit and easy to extend.
/// - The factory is used for converter resolution; tests may inject a custom ConverterFactory into the Factory
/// instance returned by the default constructor if a test seam is added in the ConsoleApp (or tests can
/// construct their own ConverterFactory and call converters directly).
/// - Do not swallow unexpected exceptions at the orchestration layer; log and return a Failure result so callers
/// can surface a helpful message. (We catch and convert to Failure below to keep the CLI robust.)
///
public static class ConversionService
{
// Known converters and their required file extensions when input is an archive.
// Keys are display names used by ConverterFactory.
// Values: required extensions (lowercase, include leading dot) that must be present in the archive.
private static readonly Dictionary _s_archiveRequirements = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
// JSON / GeoJSON-like formats (archives containing .json / .geojson)
{ "EsriJson", new[] { ".json", ".esrijson" } },
{ "GeoJson", new[] { ".geojson", ".json" } },
{ "GeoJsonSeq", new[] { ".json" } },
// KML / KMZ (KMZ is a zip containing .kml)
{ "Kml", new[] { ".kml" } },
{ "Kmz", new[] { ".kml" } },
// Shapefile requires .shp, .shx, .dbf
{ "Shapefile", new[] { ".shp", ".shx", ".dbf" } },
// OSM (OpenStreetMap XML)
{ "Osm", new[] { ".osm" } },
// File Geodatabase
{ "Gdb", new[] { ".gdb" } },
// GPX
{ "Gpx", new[] { ".gpx" } },
// GPX
{ "TopoJson", new[] { ".json" } },
// MapInfo formats
{ "MapInfoInterchange", new[] { ".mif" } },
{ "MapInfoTab", new[] { ".tab", ".dat", ".map", ".id" } },
// CSV
{ "Csv", new[] { ".csv" } },
// GeoPackage
{ "GeoPackage", new[] { ".gpkg" } },
};
// Mapping of single-file extensions to converter display names.
// Covers the requested set of GIS formats so single-file inputs are dispatched to the expected converters.
private static readonly Dictionary _s_extensionToConverter = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
// JSON / GeoJSON family - JSON is handled specially by JsonFormatDetector at runtime
{ ".geojson", "GeoJson" },
{ ".esrijson", "EsriJson" },
// KML / KMZ
{ ".kml", "Kml" },
{ ".kmz", "Kmz" },
// Shapefile (single-file entry not common; usually archive or folder)
{ ".shp", "Shapefile" },
// OSM
{ ".osm", "Osm" },
// GPX
{ ".gpx", "Gpx" },
// GML
{ ".gml", "Gml" },
// File Geodatabase (folder - mapped by extension .gdb where applicable)
{ ".gdb", "Gdb" },
// MapInfo
{ ".mif", "MapInfoInterchange" },
{ ".tab", "MapInfoTab" },
{ ".map", "MapInfoTab" },
{ ".dat", "MapInfoTab" },
{ ".id", "MapInfoTab" },
// Tabular / package formats
{ ".csv", "Csv" },
{ ".gpkg", "GeoPackage" },
};
///
/// Orchestrate a conversion given paths and a factory.
///
/// Input file or archive.
/// Output folder for converter results.
/// Working folder for extraction / intermediate files.
/// ConverterFactory instance to resolve converters (optional). When null a default ConverterFactory is used.
/// ConversionResult describing the outcome; friendly messages are returned for validation or detection failures.
public static ConversionResult Run(string gisInputFilePath, string outFolderPath, string tempFolderPath, IConverterFactory factory = null)
{
try
{
Log.Info("ConversionService: Run invoked.");
// Validate inputs and prepare folders
var prep = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outFolderPath, tempFolderPath);
if (prep != null) return prep; // validation failure
if (factory == null)
{
factory = new ConverterFactory();
}
// Determine input kind
if (ConverterUtils.IsArchiveFile(gisInputFilePath))
{
Log.Info($"Input '{gisInputFilePath}' detected as archive. Inspecting contents.");
var entries = ConverterUtils.TryListArchiveEntries(gisInputFilePath);
if (entries == null)
{
Log.Error("Failed to list archive entries.");
return ConversionResult.Failure("Failed to inspect archive contents.");
}
// Discover candidate converter by checking archive requirements
var matchedConverter = DetectConverterFromArchiveEntries(entries);
if (string.IsNullOrEmpty(matchedConverter))
{
Log.Warn("No converter matched archive contents.");
return ConversionResult.Failure("No converter matched archive contents or required files are missing.");
}
Log.Info($"Archive matched converter '{matchedConverter}'. Attempting to resolve converter instance.");
if (!factory.TryCreate(matchedConverter, out var conv))
{
Log.Error($"ConverterFactory failed to resolve converter '{matchedConverter}'.");
return ConversionResult.Failure($"Converter for '{matchedConverter}' is not available.");
}
Log.Info($"Converter '{matchedConverter}' resolved. Invoking Convert(...).");
// pass the resolved converter key as gisTargetFormatOption (four-argument signature)
return conv.Convert(gisInputFilePath, matchedConverter, outFolderPath, tempFolderPath);
}
else
{
// Single-file path: map extension to converter, with special handling for JSON-family files
var ext = Path.GetExtension(gisInputFilePath);
Log.Info($"Input '{gisInputFilePath}' detected as single file with extension '{ext}'.");
if (!string.IsNullOrWhiteSpace(ext) && ext.EndsWith("json", StringComparison.OrdinalIgnoreCase))
{
// Use JsonFormatDetector to determine specific JSON format
if (!JsonFormatDetector.TryDetectFromFile(gisInputFilePath, out var jsonFmt))
{
Log.Error("Unable to parse JSON input to determine specific JSON GIS format.");
return ConversionResult.Failure("Unable to determine JSON format (GeoJson / EsriJson / GeoJsonSeq / TopoJson).");
}
switch (jsonFmt)
{
case JsonFormatDetector.Format.GeoJson:
ext = ".geojson";
break;
case JsonFormatDetector.Format.EsriJson:
ext = ".esrijson";
break;
case JsonFormatDetector.Format.GeoJsonSeq:
ext = ".geojsonseq";
break;
case JsonFormatDetector.Format.TopoJson:
ext = ".topojson";
break;
default:
Log.Error("JSON format detected as Unknown.");
return ConversionResult.Failure("Unrecognized JSON format; provide a GeoJSON, EsriJSON, TopoJSON or GeoJSON sequence file.");
}
// Map the detected JSON format to converter key
string converterKeyForJson = null;
switch (jsonFmt)
{
case JsonFormatDetector.Format.GeoJson: converterKeyForJson = "GeoJson"; break;
case JsonFormatDetector.Format.EsriJson: converterKeyForJson = "EsriJson"; break;
case JsonFormatDetector.Format.GeoJsonSeq: converterKeyForJson = "GeoJsonSeq"; break;
case JsonFormatDetector.Format.TopoJson: converterKeyForJson = "TopoJson"; break;
}
if (string.IsNullOrWhiteSpace(converterKeyForJson))
{
Log.Error("Failed to map detected JSON format to a converter key.");
return ConversionResult.Failure("Failed to map JSON format to converter.");
}
Log.Info($"Detected JSON format '{jsonFmt}'. Resolving converter '{converterKeyForJson}'.");
if (!factory.TryCreate(converterKeyForJson, out var convJson))
{
Log.Error($"ConverterFactory failed to resolve converter '{converterKeyForJson}'.");
return ConversionResult.Failure($"Converter for '{converterKeyForJson}' is not available.");
}
return convJson.Convert(gisInputFilePath, converterKeyForJson, outFolderPath, tempFolderPath);
}
// Not JSON-family: use extension mapping
if (!_s_extensionToConverter.TryGetValue(ext, out var converterKeyNonJson))
{
Log.Warn($"No converter mapping for extension '{ext}'.");
return ConversionResult.Failure($"Unknown input file type '{ext}'.");
}
Log.Info($"Mapped extension '{ext}' to converter '{converterKeyNonJson}'. Attempting to resolve.");
if (!factory.TryCreate(converterKeyNonJson, out var convNonJson))
{
Log.Error($"ConverterFactory failed to resolve converter '{converterKeyNonJson}'.");
return ConversionResult.Failure($"Converter for '{converterKeyNonJson}' is not available.");
}
Log.Info($"Converter '{converterKeyNonJson}' resolved. Invoking Convert(...).");
// pass the resolved converter key as gisConversionOption (four-argument signature)
return convNonJson.Convert(gisInputFilePath, converterKeyNonJson, outFolderPath, tempFolderPath);
}
}
catch (Exception ex)
{
Log.Error($"Unexpected error in ConversionService.Run: {ex.Message}", ex);
return ConversionResult.Failure($"Unexpected error: {ex.Message}");
}
}
///
/// Inspect archive entries and attempt to match a known converter based on required extensions.
///
/// Archive entry names (relative paths inside archive).
/// Display name of matching converter or null if none match.
private static string DetectConverterFromArchiveEntries(IEnumerable entries)
{
// Normalize entry extensions
var exts = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var e in entries)
{
try
{
var ext = Path.GetExtension(e);
if (!string.IsNullOrEmpty(ext))
exts.Add(ext.ToLowerInvariant());
}
catch
{
// ignore malformed names
}
}
Log.Debug($"Archive contains {exts.Count} distinct extensions: {string.Join(", ", exts)}");
// Check each known converter's requirements
foreach (var kv in _s_archiveRequirements)
{
var required = kv.Value;
var allPresent = required.All(r => exts.Contains(r));
if (allPresent)
{
Log.Debug($"Archive satisfies requirements for '{kv.Key}'.");
return kv.Key;
}
}
// Special-case: KMZ archive typically contains a .kml
if (exts.Contains(".kml"))
{
Log.Debug("Archive contains .kml; selecting 'Kml' converter.");
return "Kml";
}
// No match
Log.Debug("No archive-based converter match found.");
return null;
}
}
}