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

/// <summary>
///     AutomationDirect DirectLOGIC address-translation helpers. DL205 / DL260 / DL350 CPUs
///     address V-memory in OCTAL while the Modbus wire uses DECIMAL PDU addresses — operators
///     see "V2000" in the PLC ladder-logic editor but the Modbus client must write PDU 0x0400.
///     The formulas differ between user V-memory (simple octal-to-decimal) and system V-memory
///     (fixed bank mappings), so the two cases are separate methods rather than one overloaded
///     "ToPdu" call.
/// </summary>
/// <remarks>
///     See <c>docs/v2/dl205.md</c> §V-memory for the full CPU-family matrix + rationale.
///     References: D2-USER-M appendix (DL205/D2-260), H2-ECOM-M §6.5 (absolute vs relative
///     addressing), AutomationDirect forum guidance on V40400 system-base.
/// </remarks>
public static class DirectLogicAddress
{
    /// <summary>
    ///     Convert a DirectLOGIC user V-memory address (octal) to a 0-based Modbus PDU address.
    ///     Accepts bare octal (<c>"2000"</c>) or <c>V</c>-prefixed (<c>"V2000"</c>). Range
    ///     depends on CPU model — DL205 D2-260 user memory is V1400-V7377 + V10000-V17777
    ///     octal, DL260 extends to V77777 octal.
    /// </summary>
    /// <param name="vAddress">The V-memory address (octal, with optional V prefix).</param>
    /// <remarks>
    ///     This handles ONLY user V-memory (octal address below the system base, V40400). For a
    ///     V-address that may fall in either bank, call <see cref="VMemoryToPdu"/>, which routes
    ///     system-bank addresses through <see cref="SystemVMemoryToPdu"/> instead.
    /// </remarks>
    /// <returns>The 0-based Modbus PDU address for the given user V-memory address.</returns>
    /// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits (8,9).</exception>
    /// <exception cref="OverflowException">Parsed value exceeds ushort.MaxValue (0xFFFF).</exception>
    public static ushort UserVMemoryToPdu(string vAddress)
        => (ushort)DecodeOctalVAddress(vAddress); // DecodeOctalVAddress guards the 0xFFFF ceiling

    /// <summary>
    ///     Decode the octal digits of a V-address (with optional <c>V</c> prefix) to a uint.
    ///     Used both as the user-memory PDU value and as the octal magnitude that decides which
    ///     bank a V-address belongs to.
    /// </summary>
    private static uint DecodeOctalVAddress(string vAddress)
    {
        if (string.IsNullOrWhiteSpace(vAddress))
            throw new ArgumentException("V-memory address must not be empty", nameof(vAddress));
        var s = vAddress.Trim();
        if (s[0] == 'V' || s[0] == 'v') s = s.Substring(1);
        if (s.Length == 0)
            throw new ArgumentException($"V-memory address '{vAddress}' has no digits", nameof(vAddress));

        // Octal conversion. Reject 8/9 digits up-front — int.Parse in the obvious base would
        // accept them silently because .NET has no built-in base-8 parser.
        uint result = 0;
        foreach (var ch in s)
        {
            if (ch < '0' || ch > '7')
                throw new ArgumentException(
                    $"V-memory address '{vAddress}' contains non-octal digit '{ch}' — DirectLOGIC V-addresses are octal (0-7)",
                    nameof(vAddress));
            result = result * 8 + (uint)(ch - '0');
            if (result > ushort.MaxValue)
                throw new OverflowException(
                    $"V-memory address '{vAddress}' exceeds the 16-bit Modbus PDU address range");
        }
        return result;
    }

    /// <summary>
    ///     DirectLOGIC system V-memory starts at octal V40400 on DL260 / H2-ECOM100 in factory
    ///     "absolute" addressing mode. Unlike user V-memory, the mapping is NOT a simple
    ///     octal-to-decimal conversion — the CPU relocates the system bank to Modbus PDU 0x2100
    ///     (decimal 8448). This helper returns the CPU-family base plus a user-supplied offset
    ///     within the system bank.
    /// </summary>
    public const ushort SystemVMemoryBasePdu = 0x2100;

    /// <summary>
    ///     Octal magnitude of the first system V-memory register (V40400). A V-address whose
    ///     octal-decoded value is at or above this belongs to the system bank and must NOT be
    ///     mapped with the plain octal-to-decimal user formula — see <see cref="VMemoryToPdu"/>.
    /// </summary>
    /// <remarks>Octal 40400 == decimal 16640 (0x4100).</remarks>
    public const ushort SystemVMemoryOctalBase = 0x4100; // octal 40400 decoded

    /// <summary>
    ///     Convert a 0-based offset within the system V-memory bank to its absolute Modbus PDU
    ///     address, relocated to the fixed <see cref="SystemVMemoryBasePdu"/> base.
    /// </summary>
    /// <param name="offsetWithinSystemBank">
    ///     0-based register offset within the system bank. Pass 0 for V40400 itself; pass 1 for
    ///     V40401 (octal), and so on. NOT an octal-decoded value — the system bank lives at
    ///     consecutive PDU addresses, so the offset is plain decimal.
    /// </param>
    /// <returns>The 0-based Modbus PDU address for the given system-bank offset.</returns>
    public static ushort SystemVMemoryToPdu(ushort offsetWithinSystemBank)
    {
        var pdu = SystemVMemoryBasePdu + offsetWithinSystemBank;
        if (pdu > ushort.MaxValue)
            throw new OverflowException(
                $"System V-memory offset {offsetWithinSystemBank} maps past 0xFFFF");
        return (ushort)pdu;
    }

    /// <summary>
    ///     Convert any DirectLOGIC V-memory address (octal, optional <c>V</c> prefix) to a 0-based
    ///     Modbus PDU address, routing user-bank and system-bank addresses through the correct
    ///     formula. User V-memory (octal &lt; V40400) is a plain octal-to-decimal decode; system
    ///     V-memory (octal &gt;= V40400) is relocated to the fixed system base at PDU 0x2100.
    ///     This is the address the parser must use — the system bank is NOT a simple octal decode.
    /// </summary>
    /// <param name="vAddress">The V-memory address (octal, with optional V prefix).</param>
    /// <remarks>
    ///     The system-bank consecutive offset is the octal-decoded distance from V40400, not an
    ///     octal-decoded value itself: V40400 → PDU 0x2100, V40401 → 0x2101, and so on.
    ///     See <c>docs/v2/dl205.md</c> §V-Memory Addressing.
    /// </remarks>
    /// <returns>The 0-based Modbus PDU address for the given V-memory address.</returns>
    /// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits.</exception>
    /// <exception cref="OverflowException">The result exceeds the 16-bit Modbus PDU range.</exception>
    public static ushort VMemoryToPdu(string vAddress)
    {
        var octalValue = DecodeOctalVAddress(vAddress);
        if (octalValue < SystemVMemoryOctalBase)
            return (ushort)octalValue;

        // System bank: the registers are contiguous from V40400, so the offset within the bank
        // is the plain decimal distance from the octal base, not another octal decode. The
        // subtraction result is provably <= 0xBEFF because DecodeOctalVAddress already caps
        // octalValue at 0xFFFF, so no overflow guard is needed here. The real overflow guard
        // (pdu > ushort.MaxValue) lives in SystemVMemoryToPdu and is reachable only when that
        // helper is called directly with a large explicit offset.
        var offsetWithinBank = (ushort)(octalValue - SystemVMemoryOctalBase);
        return SystemVMemoryToPdu(offsetWithinBank);
    }

    // Bit-memory bases per DL260 user manual §I/O-configuration.
    // Numbers after X / Y / C / SP are OCTAL in DirectLOGIC notation. The Modbus base is
    // added to the octal-decoded offset; e.g. Y017 = Modbus coil 2048 + octal(17) = 2048 + 15 = 2063.

    /// <summary>
    ///     DL260 Y-output coil base. Y0 octal → Modbus coil address 2048 (0-based).
    /// </summary>
    public const ushort YOutputBaseCoil = 2048;

    /// <summary>
    ///     DL260 C-relay coil base. C0 octal → Modbus coil address 3072 (0-based).
    /// </summary>
    public const ushort CRelayBaseCoil = 3072;

    /// <summary>
    ///     DL260 X-input discrete-input base. X0 octal → Modbus discrete input 0.
    /// </summary>
    public const ushort XInputBaseDiscrete = 0;

    /// <summary>
    ///     DL260 SP special-relay discrete-input base. SP0 octal → Modbus discrete input 1024.
    ///     Read-only; writing SP relays is rejected with Illegal Data Address.
    /// </summary>
    public const ushort SpecialBaseDiscrete = 1024;

    /// <summary>
    ///     Translate a DirectLOGIC Y-output address (e.g. <c>"Y0"</c>, <c>"Y17"</c>) to its
    ///     0-based Modbus coil address on DL260. The trailing number is OCTAL, matching the
    ///     ladder-logic editor's notation.
    /// </summary>
    /// <param name="yAddress">The Y-output address (octal, with optional Y prefix).</param>
    /// <returns>The 0-based Modbus coil address.</returns>
    public static ushort YOutputToCoil(string yAddress) =>
        AddOctalOffset(YOutputBaseCoil, StripPrefix(yAddress, 'Y'));

    /// <summary>
    ///     Translate a DirectLOGIC C-relay address (e.g. <c>"C0"</c>, <c>"C1777"</c>) to its
    ///     0-based Modbus coil address.
    /// </summary>
    /// <param name="cAddress">The C-relay address (octal, with optional C prefix).</param>
    /// <returns>The 0-based Modbus coil address.</returns>
    public static ushort CRelayToCoil(string cAddress) =>
        AddOctalOffset(CRelayBaseCoil, StripPrefix(cAddress, 'C'));

    /// <summary>
    ///     Translate a DirectLOGIC X-input address (e.g. <c>"X0"</c>, <c>"X17"</c>) to its
    ///     0-based Modbus discrete-input address. Reading an unpopulated X returns 0, not an
    ///     exception — the CPU sizes the table to configured I/O, not installed modules.
    /// </summary>
    /// <param name="xAddress">The X-input address (octal, with optional X prefix).</param>
    /// <returns>The 0-based Modbus discrete-input address.</returns>
    public static ushort XInputToDiscrete(string xAddress) =>
        AddOctalOffset(XInputBaseDiscrete, StripPrefix(xAddress, 'X'));

    /// <summary>
    ///     Translate a DirectLOGIC SP-special-relay address (e.g. <c>"SP0"</c>) to its 0-based
    ///     Modbus discrete-input address. Accepts <c>"SP"</c> prefix case-insensitively.
    /// </summary>
    /// <param name="spAddress">The SP special-relay address (octal, with optional SP prefix).</param>
    /// <returns>The 0-based Modbus discrete-input address.</returns>
    public static ushort SpecialToDiscrete(string spAddress)
    {
        if (string.IsNullOrWhiteSpace(spAddress))
            throw new ArgumentException("SP address must not be empty", nameof(spAddress));
        var s = spAddress.Trim();
        if (s.Length >= 2 && (s[0] == 'S' || s[0] == 's') && (s[1] == 'P' || s[1] == 'p'))
            s = s.Substring(2);
        return AddOctalOffset(SpecialBaseDiscrete, s);
    }

    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);
        return s;
    }

    private static ushort AddOctalOffset(ushort baseAddr, string octalDigits)
    {
        if (octalDigits.Length == 0)
            throw new ArgumentException("Address has no digits", nameof(octalDigits));
        uint offset = 0;
        foreach (var ch in octalDigits)
        {
            if (ch < '0' || ch > '7')
                throw new ArgumentException(
                    $"Address contains non-octal digit '{ch}' — DirectLOGIC I/O addresses are octal (0-7)",
                    nameof(octalDigits));
            offset = offset * 8 + (uint)(ch - '0');
        }
        var result = baseAddr + offset;
        if (result > ushort.MaxValue)
            throw new OverflowException($"Address {baseAddr}+{offset} exceeds 0xFFFF");
        return (ushort)result;
    }
}
