using System.Globalization;
using System.Text;

namespace Sunfish.Shares;

/// <summary>
/// Alias 8.3 para DOS y Windows 3.1 (DOC/CLIENTES.md). Son estables: dependen del nombre (un hash),
/// no del orden de la carpeta, asi que un listado o un enlace antiguo no acaban apuntando a otro
/// fichero cuando se anaden o quitan otros. "Fotos del viaje.jpeg" -> "FOTxxx~1.JPE" (xxx: el hash).
/// </summary>
public static class ShortNames
{
    private const string Allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'()-@^_`{}~";

    /// <summary>El nombre ya es 8.3 valido (sin distinguir mayusculas).</summary>
    public static bool Is83(string name)
    {
        int dot = name.IndexOf('.');
        string stem = dot < 0 ? name : name[..dot];
        string ext = dot < 0 ? "" : name[(dot + 1)..];
        return stem.Length is >= 1 and <= 8 && ext.Length <= 3 && ext.IndexOf('.') < 0 &&
               (dot < 0 || ext.Length > 0) &&
               stem.ToUpperInvariant().All(c => Allowed.Contains(c)) && ext.ToUpperInvariant().All(c => Allowed.Contains(c));
    }

    /// <summary>Alias de un nombre suelto (sin tener en cuenta colisiones).</summary>
    public static string Alias(string name, int number = 1)
    {
        if (number == 1 && Is83(name))
            return name;
        int dot = name.LastIndexOf('.');
        string stem = dot > 0 ? name[..dot] : name;
        string ext = dot > 0 ? Clean(name[(dot + 1)..], 3) : "";
        string prefix = Clean(stem, 3);
        if (prefix.Length == 0)
            prefix = "_";
        string hash = (Fnv1a(name) % 0x1000).ToString("X3", CultureInfo.InvariantCulture);
        string tail = "~" + number.ToString(CultureInfo.InvariantCulture);
        string baseName = (prefix + hash)[..Math.Min(prefix.Length + 3, 8 - tail.Length)] + tail;
        return ext.Length > 0 ? baseName + "." + ext : baseName;
    }

    /// <summary>
    /// Alias de todos los nombres de una carpeta. Si dos coinciden, el segundo en orden alfabetico
    /// pasa a ~2, y asi sucesivamente (muy raro).
    /// </summary>
    public static Dictionary<string, string> Map(IEnumerable<string> names)
    {
        var result = new Dictionary<string, string>(StringComparer.Ordinal);
        var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var ordered = names.OrderBy(n => n, StringComparer.Ordinal).ToList();
        // Primero los que ya son 8.3: conservan su nombre tal cual (DOS no distingue mayusculas).
        foreach (var name in ordered.Where(Is83))
        {
            result[name] = name;
            used.Add(result[name]);
        }
        foreach (var name in ordered.Where(n => !Is83(n)))
        {
            for (int n = 1; ; n++)
            {
                string alias = Alias(name, n);
                if (used.Add(alias))
                {
                    result[name] = alias;
                    break;
                }
            }
        }
        return result;
    }

    /// <summary>Nombre real dentro de una carpeta a partir de su alias (null si no hay ninguno).</summary>
    public static string? Resolve(string physicalDirectory, string alias)
    {
        if (!Directory.Exists(physicalDirectory))
            return null;
        var names = new DirectoryInfo(physicalDirectory).EnumerateFileSystemInfos().Select(i => i.Name).ToList();
        foreach (var (name, a) in Map(names))
            if (a.Equals(alias, StringComparison.OrdinalIgnoreCase))
                return name;
        return null;
    }

    /// <summary>Solo los caracteres validos en 8.3, transliterados a ASCII y en mayusculas.</summary>
    private static string Clean(string text, int max)
    {
        var sb = new StringBuilder();
        foreach (char c in text.Normalize(NormalizationForm.FormD))
        {
            if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark)
                continue;
            char u = char.ToUpperInvariant(c);
            if (Allowed.Contains(u) && u != '~')
                sb.Append(u);
            if (sb.Length == max)
                break;
        }
        return sb.ToString();
    }

    private static uint Fnv1a(string text)
    {
        uint hash = 2166136261;
        foreach (byte b in Encoding.UTF8.GetBytes(text))
        {
            hash ^= b;
            hash *= 16777619;
        }
        return hash;
    }
}
