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

/// <summary>
///     Mitsubishi MELSEC PLC family selector for address-translation helpers. The Q/L/iQ-R
///     families write bit-device addresses (X, Y) in <b>hexadecimal</b> in GX Works and the
///     CPU manuals; the FX and iQ-F families write them in <b>octal</b> (same convention as
///     AutomationDirect DirectLOGIC). Mixing the two up is the #1 MELSEC driver bug source —
///     an operator typing <c>X20</c> into a Q-series tag config means decimal 32, but the
///     same string on an FX3U means decimal 16, so the helper must know the family to route
///     correctly.
/// </summary>
public enum MelsecFamily
{
    /// <summary>
    ///     MELSEC-Q / MELSEC-L / MELSEC iQ-R. X and Y device numbers are interpreted as
    ///     <b>hexadecimal</b>; <c>X20</c> means decimal 32.
    /// </summary>
    Q_L_iQR,

    /// <summary>
    ///     MELSEC-F (FX3U / FX3GE / FX3G) and MELSEC iQ-F (FX5U). X and Y device numbers
    ///     are interpreted as <b>octal</b> (same as DirectLOGIC); <c>X20</c> means decimal 16.
    ///     iQ-F has a GX Works3 project toggle that can flip to decimal — if a site uses
    ///     that, configure the tag's Address directly as a decimal PDU address and do not
    ///     route through this helper.
    /// </summary>
    F_iQF,
}

/// <summary>
///     Mitsubishi MELSEC address-translation helpers for the QJ71MT91 / LJ71MT91 / RJ71EN71 /
///     iQ-R built-in / iQ-F / FX3U-ENET-P502 Modbus modules. MELSEC does NOT hard-wire
///     Modbus-to-device mappings like DL260 does — every site configures its own "Modbus
///     Device Assignment Parameter" block of up to 16 entries. The helpers here cover only
///     the <b>address-notation</b> portion of the translation (hex X20 vs octal X20 + adding
///     the bank base); the caller is still responsible for knowing the assignment-block
///     offset for their site.
/// </summary>
/// <remarks>
///     See <c>docs/v2/mitsubishi.md</c> §device-assignment + §X-Y-hex-trap for the full
///     matrix and primary-source citations.
/// </remarks>
public static class MelsecAddress
{
    /// <summary>
    ///     Translate a MELSEC X-input address (e.g. <c>"X0"</c>, <c>"X10"</c>) to a 0-based
    ///     Modbus discrete-input address, given the PLC family's address notation (hex or
    ///     octal) and the Modbus Device Assignment block's X-range base.
    /// </summary>
    /// <param name="xAddress">MELSEC X address. <c>X</c> prefix optional, case-insensitive.</param>
    /// <param name="family">The PLC family — determines whether the trailing digits are hex or octal.</param>
    /// <param name="xBankBase">
    ///     0-based Modbus DI address the assignment-block has configured X0 to land at.
    ///     Typical default on QJ71MT91 sample projects: 0. Pass the site-specific value.
    /// </param>
    /// <returns>The 0-based Modbus discrete-input address.</returns>
    public static ushort XInputToDiscrete(string xAddress, MelsecFamily family, ushort xBankBase = 0) =>
        AddFamilyOffset(xBankBase, StripPrefix(xAddress, 'X'), family);

    /// <summary>
    ///     Translate a MELSEC Y-output address to a 0-based Modbus coil address. Same rules
    ///     as <see cref="XInputToDiscrete"/> for hex/octal parsing.
    /// </summary>
    /// <param name="yAddress">MELSEC Y address. <c>Y</c> prefix optional, case-insensitive.</param>
    /// <param name="family">The PLC family — determines whether the trailing digits are hex or octal.</param>
    /// <param name="yBankBase">0-based Modbus coil address the assignment-block has configured Y0 to land at.</param>
    /// <returns>The 0-based Modbus coil address.</returns>
    public static ushort YOutputToCoil(string yAddress, MelsecFamily family, ushort yBankBase = 0) =>
        AddFamilyOffset(yBankBase, StripPrefix(yAddress, 'Y'), family);

    /// <summary>
    ///     Translate a MELSEC M-relay address (internal relay) to a 0-based Modbus coil
    ///     address. M-addresses are <b>decimal</b> on every MELSEC family — unlike X/Y which
    ///     are hex on Q/L/iQ-R. Includes the bank base that the assignment-block configured.
    /// </summary>
    /// <param name="mAddress">MELSEC M address. <c>M</c> prefix optional, case-insensitive.</param>
    /// <param name="mBankBase">0-based Modbus coil address the assignment-block has configured M0 to land at.</param>
    /// <returns>The 0-based Modbus coil address.</returns>
    public static ushort MRelayToCoil(string mAddress, ushort mBankBase = 0)
    {
        var digits = StripPrefix(mAddress, 'M');
        if (!ushort.TryParse(digits, out var offset))
            throw new ArgumentException(
                $"M-relay address '{mAddress}' is not a valid decimal integer", nameof(mAddress));
        var result = mBankBase + offset;
        if (result > ushort.MaxValue)
            throw new OverflowException($"M-relay {mAddress} + base {mBankBase} exceeds 0xFFFF");
        return (ushort)result;
    }

    /// <summary>
    ///     Translate a MELSEC D-register address (data register) to a 0-based Modbus holding
    ///     register address. D-addresses are <b>decimal</b>. Default assignment convention is
    ///     D0 → HR 0 (pass <paramref name="dBankBase"/> = 0); sites with shifted layouts pass
    ///     their configured base.
    /// </summary>
    /// <param name="dAddress">MELSEC D address. <c>D</c> prefix optional, case-insensitive.</param>
    /// <param name="dBankBase">0-based Modbus holding register address the assignment-block has configured D0 to land at.</param>
    /// <returns>The 0-based Modbus holding register address.</returns>
    public static ushort DRegisterToHolding(string dAddress, ushort dBankBase = 0)
    {
        var digits = StripPrefix(dAddress, 'D');
        if (!ushort.TryParse(digits, out var offset))
            throw new ArgumentException(
                $"D-register address '{dAddress}' is not a valid decimal integer", nameof(dAddress));
        var result = dBankBase + offset;
        if (result > ushort.MaxValue)
            throw new OverflowException($"D-register {dAddress} + base {dBankBase} exceeds 0xFFFF");
        return (ushort)result;
    }

    private static string StripPrefix(string address, char expectedPrefix)
    {
        if (string.IsNullOrWhiteSpace(address))
            throw new ArgumentException("Address must not be empty", nameof(address));
        var s = address.Trim();
        if (s.Length > 0 && char.ToUpperInvariant(s[0]) == char.ToUpperInvariant(expectedPrefix))
            s = s.Substring(1);
        if (s.Length == 0)
            throw new ArgumentException($"Address '{address}' has no digits after prefix", nameof(address));
        return s;
    }

    private static ushort AddFamilyOffset(ushort baseAddr, string digits, MelsecFamily family)
    {
        uint offset = family switch
        {
            MelsecFamily.Q_L_iQR => ParseHex(digits),
            MelsecFamily.F_iQF => ParseOctal(digits),
            _ => throw new ArgumentOutOfRangeException(nameof(family), family, "Unknown MELSEC family"),
        };
        var result = baseAddr + offset;
        if (result > ushort.MaxValue)
            throw new OverflowException($"Address {baseAddr}+{offset} exceeds 0xFFFF");
        return (ushort)result;
    }

    private static uint ParseHex(string digits)
    {
        uint result = 0;
        foreach (var ch in digits)
        {
            uint nibble;
            if (ch >= '0' && ch <= '9') nibble = (uint)(ch - '0');
            else if (ch >= 'A' && ch <= 'F') nibble = (uint)(ch - 'A' + 10);
            else if (ch >= 'a' && ch <= 'f') nibble = (uint)(ch - 'a' + 10);
            else throw new ArgumentException(
                $"Address contains non-hex digit '{ch}' — Q/L/iQ-R X/Y addresses are hexadecimal",
                nameof(digits));
            result = result * 16 + nibble;
            if (result > ushort.MaxValue)
                throw new OverflowException($"Hex address exceeds 0xFFFF");
        }
        return result;
    }

    private static uint ParseOctal(string digits)
    {
        uint result = 0;
        foreach (var ch in digits)
        {
            if (ch < '0' || ch > '7')
                throw new ArgumentException(
                    $"Address contains non-octal digit '{ch}' — FX/iQ-F X/Y addresses are octal (0-7)",
                    nameof(digits));
            result = result * 8 + (uint)(ch - '0');
            if (result > ushort.MaxValue)
                throw new OverflowException($"Octal address exceeds 0xFFFF");
        }
        return result;
    }
}
