namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;

/// <summary>
///     Parsed FOCAS address covering the three addressing spaces a driver touches:
///     <see cref="FocasAreaKind.Pmc"/> (letter + byte + optional bit — <c>X0.0</c>, <c>R100</c>,
///     <c>F20.3</c>), <see cref="FocasAreaKind.Parameter"/> (CNC parameter number —
///     <c>PARAM:1020</c>, <c>PARAM:1815/0</c> for bit 0), and <see cref="FocasAreaKind.Macro"/>
///     (macro variable number — <c>MACRO:100</c>, <c>MACRO:500</c>).
/// </summary>
/// <remarks>
///     PMC letters: <c>X/Y</c> (IO), <c>F/G</c> (signals between PMC + CNC), <c>R</c> (internal
///     relay), <c>D</c> (data table), <c>C</c> (counter), <c>K</c> (keep relay), <c>A</c>
///     (message display), <c>E</c> (extended relay), <c>T</c> (timer). Byte numbering is 0-based;
///     bit index when present is 0–7 and uses <c>.N</c> for PMC or <c>/N</c> for parameters.
/// </remarks>
public sealed record FocasAddress(
    FocasAreaKind Kind,
    string? PmcLetter,
    int Number,
    int? BitIndex)
{
    /// <summary>Gets the canonical string representation of this address.</summary>
    public string Canonical => Kind switch
    {
        FocasAreaKind.Pmc => BitIndex is null
            ? $"{PmcLetter}{Number}"
            : $"{PmcLetter}{Number}.{BitIndex}",
        FocasAreaKind.Parameter => BitIndex is null
            ? $"PARAM:{Number}"
            : $"PARAM:{Number}/{BitIndex}",
        FocasAreaKind.Macro => $"MACRO:{Number}",
        _ => $"?{Number}",
    };

    /// <summary>Attempts to parse a FOCAS address from the given string.</summary>
    /// <param name="value">The address string to parse.</param>
    /// <returns>A FocasAddress if parsing succeeds; otherwise null.</returns>
    public static FocasAddress? TryParse(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return null;
        var src = value.Trim();

        if (src.StartsWith("PARAM:", StringComparison.OrdinalIgnoreCase))
            return ParseScoped(src["PARAM:".Length..], FocasAreaKind.Parameter, bitSeparator: '/');

        if (src.StartsWith("MACRO:", StringComparison.OrdinalIgnoreCase))
            return ParseScoped(src["MACRO:".Length..], FocasAreaKind.Macro, bitSeparator: null);

        // PMC path: letter + digits + optional .bit
        if (src.Length < 2 || !char.IsLetter(src[0])) return null;
        var letter = src[0..1].ToUpperInvariant();
        if (!IsValidPmcLetter(letter)) return null;

        var remainder = src[1..];
        int? bit = null;
        var dotIdx = remainder.IndexOf('.');
        if (dotIdx >= 0)
        {
            if (!int.TryParse(remainder[(dotIdx + 1)..], out var bitValue) || bitValue is < 0 or > 7)
                return null;
            bit = bitValue;
            remainder = remainder[..dotIdx];
        }
        if (!int.TryParse(remainder, out var number) || number < 0) return null;
        return new FocasAddress(FocasAreaKind.Pmc, letter, number, bit);
    }

    private static FocasAddress? ParseScoped(string body, FocasAreaKind kind, char? bitSeparator)
    {
        int? bit = null;
        if (bitSeparator is char sep)
        {
            var slashIdx = body.IndexOf(sep);
            if (slashIdx >= 0)
            {
                if (!int.TryParse(body[(slashIdx + 1)..], out var bitValue) || bitValue is < 0 or > 31)
                    return null;
                bit = bitValue;
                body = body[..slashIdx];
            }
        }
        if (!int.TryParse(body, out var number) || number < 0) return null;
        return new FocasAddress(kind, PmcLetter: null, number, bit);
    }

    private static bool IsValidPmcLetter(string letter) => letter switch
    {
        "X" or "Y" or "F" or "G" or "R" or "D" or "C" or "K" or "A" or "E" or "T" => true,
        _ => false,
    };
}

/// <summary>Addressing-space kinds the driver understands.</summary>
public enum FocasAreaKind
{
    Pmc,
    Parameter,
    Macro,
}
