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

/// <summary>
///     Documented-API capability matrix — per CNC series, what ranges each
///     <see cref="FocasAreaKind"/> supports. Authoritative source for the driver's
///     pre-flight validation in <see cref="FocasDriver.InitializeAsync"/>.
/// </summary>
/// <remarks>
///     <para>Ranges come from the Fanuc FOCAS Developer Kit documentation matrix
///     (see <c>docs/v2/focas-version-matrix.md</c> for the authoritative copy with
///     per-function citations). Numbers chosen to match what the FOCAS library
///     accepts — a read against an address outside the documented range returns
///     <c>EW_NUMBER</c> or <c>EW_PARAM</c> at the wire, which this driver maps to
///     BadOutOfRange. Catching at init time surfaces the mismatch as a config
///     error before any session is opened.</para>
///     <para><see cref="FocasCncSeries.Unknown"/> is treated permissively: every
///     address passes validation. Pre-matrix configs don't break on upgrade; new
///     deployments are encouraged to declare a series in the device options.</para>
/// </remarks>
public static class FocasCapabilityMatrix
{
    /// <summary>
    ///     Check whether <paramref name="address"/> is accepted by a CNC of
    ///     <paramref name="series"/>. Returns <c>null</c> on pass + a failure reason
    ///     on reject — the driver surfaces the reason string verbatim when failing
    ///     <c>InitializeAsync</c> so operators see the specific out-of-range without
    ///     guessing.
    /// </summary>
    public static string? Validate(FocasCncSeries series, FocasAddress address)
    {
        if (series == FocasCncSeries.Unknown) return null;

        return address.Kind switch
        {
            FocasAreaKind.Macro     => ValidateMacro(series, address.Number),
            FocasAreaKind.Parameter => ValidateParameter(series, address.Number),
            FocasAreaKind.Pmc       => ValidatePmc(series, address.PmcLetter, address.Number),
            _ => null,
        };
    }

    /// <summary>Macro variable number accepted by a CNC series. Cites
    /// <c>cnc_rdmacro</c>/<c>cnc_wrmacro</c> in the Developer Kit.</summary>
    internal static (int min, int max) MacroRange(FocasCncSeries series) => series switch
    {
        // Common macros 1-33 + 100-199 + 500-999 universally; extended 10000+ only on
        // higher-end series. Using the extended ceiling per series per DevKit notes.
        FocasCncSeries.Sixteen_i    => (0, 999),
        FocasCncSeries.Zero_i_D     => (0, 999),
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF    => (0, 9999),
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i  => (0, 99999),
        FocasCncSeries.PowerMotion_i => (0, 999),
        _ => (0, int.MaxValue),
    };

    /// <summary>Parameter number accepted; from <c>cnc_rdparam</c>/<c>cnc_wrparam</c>.
    /// Ranges reflect the highest-numbered parameter documented per series.</summary>
    internal static (int min, int max) ParameterRange(FocasCncSeries series) => series switch
    {
        FocasCncSeries.Sixteen_i    => (0, 9999),
        FocasCncSeries.Zero_i_D or
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF    => (0, 14999),
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i  => (0, 29999),
        FocasCncSeries.PowerMotion_i => (0, 29999),
        _ => (0, int.MaxValue),
    };

    /// <summary>PMC letters accepted per series. Legacy controllers omit F/M/C
    /// signal groups that 30i-family ladder programs use.</summary>
    internal static IReadOnlySet<string> PmcLetters(FocasCncSeries series) => series switch
    {
        FocasCncSeries.Sixteen_i     => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
        FocasCncSeries.Zero_i_D      => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D", "E", "A" },
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF     => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C" },
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i   => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C", "K", "T" },
        FocasCncSeries.PowerMotion_i => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
        _ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
    };

    /// <summary>PMC address-number ceiling per series. Multiplied by 8 to get bit
    /// count since PMC addresses are byte-addressed on read + bit-addressed on
    /// write — FocasAddress carries the bit separately.</summary>
    internal static int PmcMaxNumber(FocasCncSeries series) => series switch
    {
        FocasCncSeries.Sixteen_i    => 999,
        FocasCncSeries.Zero_i_D     => 1999,
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF    => 9999,
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i  => 59999,
        FocasCncSeries.PowerMotion_i => 1999,
        _ => int.MaxValue,
    };

    private static string? ValidateMacro(FocasCncSeries series, int number)
    {
        var (min, max) = MacroRange(series);
        return (number < min || number > max)
            ? $"Macro variable #{number} is outside the documented range [{min}, {max}] for {series}."
            : null;
    }

    private static string? ValidateParameter(FocasCncSeries series, int number)
    {
        var (min, max) = ParameterRange(series);
        return (number < min || number > max)
            ? $"Parameter #{number} is outside the documented range [{min}, {max}] for {series}."
            : null;
    }

    private static string? ValidatePmc(FocasCncSeries series, string? letter, int number)
    {
        if (string.IsNullOrEmpty(letter)) return "PMC address is missing its letter prefix.";
        var letters = PmcLetters(series);
        if (!letters.Contains(letter))
        {
            var letterList = string.Join(", ", letters);
            return $"PMC letter '{letter}' is not supported on {series}. Accepted: {{{letterList}}}.";
        }
        var max = PmcMaxNumber(series);
        return number > max
            ? $"PMC address {letter}{number} is outside the documented range [0, {max}] for {series}."
            : null;
    }
}
