namespace Sunfish.Mac;

/// <summary>
/// Mac clasico de forma transparente sobre el disco (DOC/MAC-CLASICO.md): al subir, un MacBinary
/// o un BinHex validos se separan en "nombre" + "._nombre" (AppleDouble); al descargar se vuelven
/// a juntar. Si el fichero no tiene nada propio de Mac no se toca.
/// </summary>
public static class MacFiles
{
    /// <summary>Limite para BinHex (se decodifica en memoria).</summary>
    public const long MaxBinHex = 256L * 1024 * 1024;

    public enum Unpacked { NotMac, Unpacked, ExistsKept }

    /// <summary>
    /// Si el fichero recien subido es MacBinary (II/III, o I si <paramref name="acceptVersion1"/>)
    /// o BinHex con CRC correctos, lo separa en data fork + AppleDouble con el nombre que trae.
    /// Si ese nombre ya existe y no se permite sobrescribir, deja el fichero codificado como
    /// estaba (no se pierde nada). Devuelve que ha pasado y el nombre final.
    /// </summary>
    public static async Task<(Unpacked Result, string Name)> TryUnpackAsync(string path, bool overwrite, bool acceptVersion1, CancellationToken ct)
    {
        string directory = Path.GetDirectoryName(path)!;
        string original = Path.GetFileName(path);
        var info = new FileInfo(path);
        if (!info.Exists || info.Length < MacBinary.HeaderSize)
            return (Unpacked.NotMac, original);

        var head = new byte[MacBinary.HeaderSize];
        await using (var probe = File.OpenRead(path))
            await probe.ReadExactlyAsync(head, ct);

        bool v1 = acceptVersion1 && original.EndsWith(".bin", StringComparison.OrdinalIgnoreCase);
        if (MacBinary.Parse(head, info.Length, v1) is { } header)
        {
            string name = SafeName(header.File.Name, original, ".bin");
            string target = Path.Combine(directory, name);
            if (!CanWrite(target, path, overwrite))
                return (Unpacked.ExistsKept, original);
            string temp = Path.Combine(directory, Shares.FileOperations.TempPrefix + Guid.NewGuid().ToString("N") + ".tmp");
            MacFile file;
            await using (var input = File.OpenRead(path))
            await using (var output = File.Create(temp))
                file = await MacBinary.DecodeAsync(input, header, output, ct);
            Finish(path, temp, target, file);
            return (Unpacked.Unpacked, name);
        }

        if (info.Length <= MaxBinHex && BinHex.LooksLikeBinHex(await ReadStartAsync(path, ct)) &&
            BinHex.Decode(await File.ReadAllBytesAsync(path, ct)) is { } hqx)
        {
            string name = SafeName(hqx.File.Name, original, ".hqx");
            string target = Path.Combine(directory, name);
            if (!CanWrite(target, path, overwrite))
                return (Unpacked.ExistsKept, original);
            string temp = Path.Combine(directory, Shares.FileOperations.TempPrefix + Guid.NewGuid().ToString("N") + ".tmp");
            await File.WriteAllBytesAsync(temp, hqx.DataFork, ct);
            Finish(path, temp, target, hqx.File);
            return (Unpacked.Unpacked, name);
        }
        return (Unpacked.NotMac, original);
    }

    private static async Task<byte[]> ReadStartAsync(string path, CancellationToken ct)
    {
        var buffer = new byte[4096];
        await using var stream = File.OpenRead(path);
        int n = await stream.ReadAtLeastAsync(buffer, buffer.Length, throwOnEndOfStream: false, ct);
        return buffer[..n];
    }

    private static bool CanWrite(string target, string source, bool overwrite) =>
        string.Equals(target, source, StringComparison.OrdinalIgnoreCase) || overwrite || (!File.Exists(target) && !Directory.Exists(target));

    /// <summary>Coloca el data fork, escribe el AppleDouble si hace falta y borra el codificado.</summary>
    private static void Finish(string encoded, string temp, string target, MacFile file)
    {
        File.Move(temp, target, overwrite: true);
        if (file.Modified is { } modified)
            File.SetLastWriteTime(target, modified);
        string companion = AppleDouble.CompanionOf(target);
        if (file.HasMacData)
            File.WriteAllBytes(companion, AppleDouble.Write(file));
        else if (File.Exists(companion))
            File.Delete(companion);
        if (!string.Equals(encoded, target, StringComparison.OrdinalIgnoreCase))
            File.Delete(encoded);
    }

    /// <summary>
    /// Nombre de destino a partir del que trae la cabecera: valido en este sistema; si no lo es,
    /// el del fichero subido sin la extension de codificacion.
    /// </summary>
    internal static string SafeName(string macName, string uploaded, string encodingExtension)
    {
        string clean = new string(macName.Select(c => c is '/' or '\\' or ':' || char.IsControl(c) ? '_' : c).ToArray()).Trim().TrimEnd('.');
        if (Shares.FileOperations.IsCreatableName(clean))
            return clean;
        string fallback = uploaded.EndsWith(encodingExtension, StringComparison.OrdinalIgnoreCase) ? uploaded[..^encodingExtension.Length] : uploaded;
        return Shares.FileOperations.IsCreatableName(fallback) ? fallback : uploaded;
    }

    /// <summary>El fichero con su AppleDouble, o con tipo y creador por extension si no lo tiene.</summary>
    public static MacFile Describe(string physical)
    {
        var file = AppleDouble.ReadCompanion(physical) ?? new MacFile();
        file.Name = Path.GetFileName(physical);
        if (file.FinderInfo.Take(8).All(b => b == 0))
        {
            var (type, creator) = MacNames.TypeFor(file.Name);
            file.Type = type;
            file.Creator = creator;
        }
        file.Modified ??= File.GetLastWriteTime(physical);
        return file;
    }

    /// <summary>Tiene un AppleDouble con resource fork o datos del Finder.</summary>
    public static bool HasCompanion(string physical) => File.Exists(AppleDouble.CompanionOf(physical));
}
