using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using GitConverter.Lib.Factories;
using GitConverter.Lib.Logging;
using GitConverter.Lib.Models;
namespace GitConverter.Lib.Converters
{
///
/// High-level conversion orchestrator.
///
///
/// Responsibilities
/// - Validate paths (input/output exists, 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.
///
public static class ConversionService
{
private static readonly Dictionary _s_archiveRequirements = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ "EsriJson", new[] { ".json", ".esrijson" } },
{ "GeoJson", new[] { ".geojson", ".json" } },
{ "GeoJsonSeq", new[] { ".json" } },
{ "Kml", new[] { ".kml" } },
{ "Kmz", new[] { ".kml" } },
{ "Shapefile", new[] { ".shp", ".shx", ".dbf" } },
{ "Osm", new[] { ".osm" } },
{ "Gdb", new[] { ".gdb" } },
{ "Gpx", new[] { ".gpx" } },
{ "TopoJson", new[] { ".json" } },
{ "MapInfoInterchange", new[] { ".mif" } },
{ "MapInfoTab", new[] { ".tab", ".dat", ".map", ".id" } },
{ "Csv", new[] { ".csv" } },
{ "GeoPackage", new[] { ".gpkg" } },
};
private static readonly Dictionary _s_extensionToConverter = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ ".geojson", "GeoJson" },
{ ".esrijson", "EsriJson" },
{ ".kml", "Kml" },
{ ".kmz", "Kmz" },
{ ".shp", "Shapefile" },
{ ".osm", "Osm" },
{ ".gpx", "Gpx" },
{ ".gml", "Gml" },
{ ".gdb", "Gdb" },
{ ".mif", "MapInfoInterchange" },
{ ".tab", "MapInfoTab" },
{ ".map", "MapInfoTab" },
{ ".dat", "MapInfoTab" },
{ ".id", "MapInfoTab" },
{ ".csv", "Csv" },
{ ".gpkg", "GeoPackage" },
};
///
/// Orchestrate a conversion given paths and a factory.
/// Note: outputFolderPath is expected to be a folder path (not a file path).
///
public static ConversionResult Run(string gisInputFilePath, string outputFolderPath, string tempFolderPath, IConverterFactory factory = null)
{
try
{
Log.Info("ConversionService: Run invoked.");
// Require an output FOLDER path (tests and callers expect folder semantics).
if (string.IsNullOrWhiteSpace(outputFolderPath))
{
Log.Error("ConversionService: output folder path is required.");
return ConversionResult.Failure("Output folder path is required.");
}
// Reject file-like paths: caller must provide a folder, not a file with extension.
if (Path.HasExtension(outputFolderPath))
{
Log.Error($"ConversionService: output path '{outputFolderPath}' appears to be a file. Provide a folder path instead.");
return ConversionResult.Failure("Output path must be a folder path (no filename/extension).");
}
var outFolderForValidation = outputFolderPath;
// Validate inputs and prepare folders (ensure output folder writable and temp ready)
var prep = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outFolderForValidation, 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.");
}
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(...).");
// converters expect an output folder path; pass the folder
return conv.Convert(gisInputFilePath, matchedConverter, outputFolderPath, tempFolderPath);
}
else
{
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))
{
// JSON detection: prefer throwing variant then fallback to lightweight sniff
JsonFormatDetector.Format jsonFmt;
try
{
jsonFmt = JsonFormatDetector.DetectFromFile(gisInputFilePath);
}
catch (Exception detEx)
{
Log.Debug($"JsonFormatDetector.DetectFromFile failed: {detEx.Message}. Falling back to lightweight sniff.");
jsonFmt = JsonFormatDetector.Format.Unknown;
// lightweight fallback: read a small head of the file and look for decisive tokens
try
{
var head = File.ReadAllText(gisInputFilePath, Encoding.UTF8);
// First, try to detect NDJSON / GeoJsonSeq by checking the first non-empty line.
string firstLine = null;
using (var sr = new StringReader(head))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (!string.IsNullOrWhiteSpace(line))
{
firstLine = line.Trim();
break;
}
}
}
// If the first non-empty line looks like a standalone JSON object/array,
// treat as GeoJsonSeq (NDJSON) unless it clearly is a FeatureCollection/topology.
if (!string.IsNullOrWhiteSpace(firstLine))
{
// If the first line or the head contains FeatureCollection -> treat as GeoJson (full doc).
if (firstLine.IndexOf("FeatureCollection", StringComparison.OrdinalIgnoreCase) >= 0 ||
head.IndexOf("FeatureCollection", StringComparison.OrdinalIgnoreCase) >= 0)
{
jsonFmt = JsonFormatDetector.Format.GeoJson;
}
else if (firstLine.IndexOf("\"Topology\"", StringComparison.OrdinalIgnoreCase) >= 0 ||
head.IndexOf("\"Topology\"", StringComparison.OrdinalIgnoreCase) >= 0)
{
jsonFmt = JsonFormatDetector.Format.TopoJson;
}
else if (firstLine.IndexOf("\"spatialReference\"", StringComparison.OrdinalIgnoreCase) >= 0 ||
head.IndexOf("\"spatialReference\"", StringComparison.OrdinalIgnoreCase) >= 0)
{
jsonFmt = JsonFormatDetector.Format.EsriJson;
}
else
{
// If first non-empty line starts with "{" or "[" assume NDJSON/GeoJsonSeq
var flTrim = firstLine.TrimStart();
if (flTrim.StartsWith("{") || flTrim.StartsWith("["))
{
jsonFmt = JsonFormatDetector.Format.GeoJsonSeq;
}
}
}
else
{
// No lines found — leave as Unknown
}
}
catch (Exception readEx)
{
Log.Debug($"Fallback JSON sniff failed: {readEx.Message}");
}
}
if (jsonFmt == JsonFormatDetector.Format.Unknown)
{
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).");
}
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;
default:
converterKeyForJson = null;
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, outputFolderPath, tempFolderPath);
}
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(...).");
return convNonJson.Convert(gisInputFilePath, converterKeyNonJson, outputFolderPath, tempFolderPath);
}
}
catch (Exception ex)
{
Log.Error($"Unexpected error in ConversionService.Run: {ex.Message}", ex);
return ConversionResult.Failure($"Unexpected error: {ex.Message}");
}
}
private static string DetectConverterFromArchiveEntries(IEnumerable entries)
{
var exts = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var e in entries)
{
try
{
if (string.IsNullOrWhiteSpace(e)) continue;
// Normal extension from the entry path (file or dir entry)
var ext = Path.GetExtension(e);
if (!string.IsNullOrEmpty(ext))
exts.Add(ext.ToLowerInvariant());
// Also inspect path segments for folder names that end with a known extension,
// e.g. "Some.gdb/..." inside a zip. Path.GetExtension on "Some.gdb/entry" returns "".
// Split on both separators to be robust across archive entry styles.
var segments = e.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var seg in segments)
{
if (seg.EndsWith(".gdb", StringComparison.OrdinalIgnoreCase))
{
exts.Add(".gdb");
}
}
}
catch
{
// ignore malformed names
}
}
Log.Debug($"Archive contains {exts.Count} distinct extensions (or folder markers): {string.Join(", ", exts)}");
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 'Kmz' converter.");
return "Kmz";
}
Log.Debug("No archive-based converter match found.");
return null;
}
}
}