namespace Mbproxy.Bcd;

/// <summary>
/// Pure, allocation-free codec for DirectLOGIC BCD register encoding/decoding.
///
/// 16-bit BCD: one register holds 4 BCD digits (0–9999).
///   Wire value 0x1234 decodes to decimal 1234.
///
/// 32-bit BCD (CDAB word order, low-word-first):
///   Register at Address   = low 4 BCD digits (least-significant).
///   Register at Address+1 = high 4 BCD digits (most-significant).
///   Decoded decimal = Decode16(high) * 10_000 + Decode16(low).
///   Example: 12_345_678 → low=0x5678, high=0x1234.
///
/// Bad-nibble policy: Decode16/Decode32 throw <see cref="FormatException"/>
/// (not a sentinel). The rewrite pipeline catches and surfaces the exception as an
/// mbproxy.rewrite.invalid_bcd warning event.
/// </summary>
internal static class BcdCodec
{
    private const int Max16 = 9_999;
    private const int Max32 = 99_999_999;

    // ── Encode ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Encodes a non-negative integer in [0, 9999] to a 16-bit BCD register.
    /// E.g. 1234 → 0x1234.
    /// </summary>
    /// <exception cref="ArgumentOutOfRangeException">value &lt; 0 or value &gt; 9999.</exception>
    public static ushort Encode16(int value)
    {
        if ((uint)value > Max16)
            throw new ArgumentOutOfRangeException(nameof(value),
                value, $"BCD-16 value must be in [0, {Max16}]; got {value}.");

        // Pack four decimal digits into four BCD nibbles.
        int d3 = value / 1000;
        int d2 = (value / 100) % 10;
        int d1 = (value / 10) % 10;
        int d0 = value % 10;
        return (ushort)((d3 << 12) | (d2 << 8) | (d1 << 4) | d0);
    }

    /// <summary>
    /// Encodes a non-negative integer in [0, 99_999_999] to a CDAB BCD register pair.
    /// Returns (low, high) where low holds the 4 least-significant BCD digits and
    /// high holds the 4 most-significant BCD digits.
    /// E.g. 12_345_678 → (low: 0x5678, high: 0x1234).
    /// </summary>
    /// <exception cref="ArgumentOutOfRangeException">value &lt; 0 or value &gt; 99_999_999.</exception>
    public static (ushort low, ushort high) Encode32(int value)
    {
        if ((uint)value > Max32)
            throw new ArgumentOutOfRangeException(nameof(value),
                value, $"BCD-32 value must be in [0, {Max32}]; got {value}.");

        int lo = value % 10_000;   // low 4 decimal digits
        int hi = value / 10_000;   // high 4 decimal digits
        return (Encode16(lo), Encode16(hi));
    }

    // ── Decode ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Decodes a 16-bit BCD register to a non-negative integer.
    /// E.g. 0x1234 → 1234.
    /// </summary>
    /// <exception cref="FormatException">Any nibble is &gt;= 0xA (not a valid BCD digit).</exception>
    public static int Decode16(ushort raw)
    {
        // Validate all four nibbles first (fail fast with the raw value in the message).
        if (HasBadNibble(raw))
            throw new FormatException(
                $"Register value 0x{raw:X4} is not valid BCD: one or more nibbles are >= 0xA.");

        int d3 = (raw >> 12) & 0xF;
        int d2 = (raw >> 8) & 0xF;
        int d1 = (raw >> 4) & 0xF;
        int d0 = raw & 0xF;
        return d3 * 1000 + d2 * 100 + d1 * 10 + d0;
    }

    /// <summary>
    /// Decodes a CDAB BCD register pair to a non-negative integer.
    /// <paramref name="low"/> = low 4 BCD digits; <paramref name="high"/> = high 4 BCD digits.
    /// E.g. (low: 0x5678, high: 0x1234) → 12_345_678.
    /// </summary>
    /// <exception cref="FormatException">Either word has a bad nibble.</exception>
    public static int Decode32(ushort low, ushort high)
    {
        // Decode high first: if it throws, we skip decoding low unnecessarily.
        // But the spec says "throws once with the raw value" per word, so we decode
        // in natural order. Decode16 throws on the first bad word it encounters.
        int hiVal = Decode16(high);
        int loVal = Decode16(low);
        return hiVal * 10_000 + loVal;
    }

    // ── Helpers ─────────────────────────────────────────────────────────────

    /// <summary>
    /// Returns true if any nibble in <paramref name="raw"/> is &gt;= 0xA (i.e. a non-BCD
    /// digit). Internal so <see cref="Mbproxy.Proxy.BcdPduPipeline"/> can call it from
    /// the response-rewrite path's per-word check without re-implementing the same logic.
    /// </summary>
    internal static bool HasBadNibble(ushort raw)
    {
        return ((raw >> 12) & 0xF) >= 0xA
            || ((raw >> 8)  & 0xF) >= 0xA
            || ((raw >> 4)  & 0xF) >= 0xA
            || (raw & 0xF)         >= 0xA;
    }
}
