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

/// <summary>
///     Siemens S7 memory area. The driver's tag-address parser maps every S7 tag string into
///     exactly one of these + an offset. Values match the on-wire S7 area codes only
///     incidentally — S7.Net uses its own <c>DataType</c> enum (<c>DataBlock</c>, <c>Memory</c>,
///     <c>Input</c>, <c>Output</c>, <c>Timer</c>, <c>Counter</c>) so the adapter layer translates.
/// </summary>
public enum S7Area
{
    DataBlock,
    Memory,  // M (Merker / marker byte)
    Input,   // I (process-image input)
    Output,  // Q (process-image output)
    Timer,
    Counter,
}

/// <summary>
///     Access width for a DB / M / I / Q address. Timers and counters are always 16-bit
///     opaque (not user-addressable via size suffixes).
/// </summary>
public enum S7Size
{
    Bit,     // X
    Byte,    // B
    Word,    // W — 16-bit
    DWord,   // D — 32-bit
}

/// <summary>
///     Parsed form of an S7 tag-address string. Produced by <see cref="S7AddressParser.Parse"/>.
/// </summary>
/// <param name="Area">Memory area (DB, M, I, Q, T, C).</param>
/// <param name="DbNumber">Data block number; only meaningful when <paramref name="Area"/> is <see cref="S7Area.DataBlock"/>.</param>
/// <param name="Size">Access width. Always <see cref="S7Size.Word"/> for Timer and Counter.</param>
/// <param name="ByteOffset">Byte offset into the area (for DB/M/I/Q) or the timer/counter number.</param>
/// <param name="BitOffset">Bit position 0-7 when <paramref name="Size"/> is <see cref="S7Size.Bit"/>; 0 otherwise.</param>
public readonly record struct S7ParsedAddress(
    S7Area Area,
    int DbNumber,
    S7Size Size,
    int ByteOffset,
    int BitOffset);

/// <summary>
///     Parses Siemens S7 address strings into <see cref="S7ParsedAddress"/>. Accepts the
///     Siemens TIA-Portal / STEP 7 Classic syntax documented in <c>docs/v2/driver-specs.md</c> §5:
///     <list type="bullet">
///         <item><c>DB{n}.DB{X|B|W|D}{offset}[.bit]</c> — e.g. <c>DB1.DBX0.0</c>, <c>DB1.DBW0</c>, <c>DB1.DBD4</c></item>
///         <item><c>M{B|W|D}{offset}</c> or <c>M{offset}.{bit}</c> — e.g. <c>MB0</c>, <c>MW0</c>, <c>MD4</c>, <c>M0.0</c></item>
///         <item><c>I{B|W|D}{offset}</c> or <c>I{offset}.{bit}</c> — e.g. <c>IB0</c>, <c>IW0</c>, <c>ID0</c>, <c>I0.0</c></item>
///         <item><c>Q{B|W|D}{offset}</c> or <c>Q{offset}.{bit}</c> — e.g. <c>QB0</c>, <c>QW0</c>, <c>QD0</c>, <c>Q0.0</c></item>
///         <item><c>T{n}</c> — e.g. <c>T0</c>, <c>T15</c></item>
///         <item><c>C{n}</c> — e.g. <c>C0</c>, <c>C10</c></item>
///     </list>
///     Grammar is case-insensitive. Leading/trailing whitespace tolerated. Bit specifiers
///     must be 0-7; byte offsets must be non-negative; DB numbers must be &gt;= 1.
/// </summary>
/// <remarks>
///     Parse is deliberately strict — the parser rejects syntactic garbage up-front so a bad
///     tag config fails at driver init time instead of surfacing as a misleading
///     <c>BadInternalError</c> on every Read against that tag.
/// </remarks>
public static class S7AddressParser
{
    /// <summary>
    ///     Parse an S7 address. Throws <see cref="FormatException"/> on any syntax error with
    ///     the offending input echoed in the message so operators can correlate to the tag
    ///     config that produced the fault.
    /// </summary>
    /// <param name="address">The S7 address string to parse.</param>
    /// <returns>The parsed address structure.</returns>
    /// <exception cref="FormatException">Thrown if the address syntax is invalid.</exception>
    public static S7ParsedAddress Parse(string address)
    {
        if (string.IsNullOrWhiteSpace(address))
            throw new FormatException("S7 address must not be empty");
        var s = address.Trim().ToUpperInvariant();

        // --- DB{n}.DB{X|B|W|D}{offset}[.bit] ---
        if (s.StartsWith("DB") && TryParseDataBlock(s, out var dbResult))
            return dbResult;

        if (s.Length < 2)
            throw new FormatException($"S7 address '{address}' is too short to parse");

        var areaChar = s[0];
        var rest = s.Substring(1);

        switch (areaChar)
        {
            case 'M': return ParseMIQ(S7Area.Memory, rest, address);
            case 'I': return ParseMIQ(S7Area.Input, rest, address);
            case 'Q': return ParseMIQ(S7Area.Output, rest, address);
            case 'T': return ParseTimerOrCounter(S7Area.Timer, rest, address);
            case 'C': return ParseTimerOrCounter(S7Area.Counter, rest, address);
            default:
                throw new FormatException($"S7 address '{address}' starts with unknown area '{areaChar}' (expected DB/M/I/Q/T/C)");
        }
    }

    /// <summary>
    ///     Try-parse variant for callers that can't afford an exception on bad input (e.g.
    ///     config validation pages in the Admin UI). Returns <c>false</c> for any input that
    ///     would throw from <see cref="Parse"/>.
    /// </summary>
    /// <param name="address">The S7 address string to parse.</param>
    /// <param name="result">The parsed address structure (valid only if method returns true).</param>
    /// <returns>True if parsing succeeded; false otherwise.</returns>
    public static bool TryParse(string address, out S7ParsedAddress result)
    {
        try
        {
            result = Parse(address);
            return true;
        }
        catch (FormatException)
        {
            result = default;
            return false;
        }
    }

    private static bool TryParseDataBlock(string s, out S7ParsedAddress result)
    {
        result = default;
        // Split on first '.': left side must be DB{n}, right side DB{X|B|W|D}{offset}[.bit]
        var dot = s.IndexOf('.');
        if (dot < 0) return false;
        var head = s.Substring(0, dot);    // DB{n}
        var tail = s.Substring(dot + 1);   // DB{X|B|W|D}{offset}[.bit]

        if (head.Length < 3) return false;
        if (!int.TryParse(head.AsSpan(2), out var dbNumber) || dbNumber < 1)
            throw new FormatException($"S7 DB number in '{s}' must be a positive integer");

        if (!tail.StartsWith("DB") || tail.Length < 4)
            throw new FormatException($"S7 DB address tail '{tail}' must start with DB{{X|B|W|D}}");

        var sizeChar = tail[2];
        var offsetStart = 3;
        var size = sizeChar switch
        {
            'X' => S7Size.Bit,
            'B' => S7Size.Byte,
            'W' => S7Size.Word,
            'D' => S7Size.DWord,
            _ => throw new FormatException($"S7 DB size '{sizeChar}' in '{s}' must be X/B/W/D"),
        };

        var (byteOffset, bitOffset) = ParseOffsetAndOptionalBit(tail, offsetStart, size, s);
        result = new S7ParsedAddress(S7Area.DataBlock, dbNumber, size, byteOffset, bitOffset);
        return true;
    }

    private static S7ParsedAddress ParseMIQ(S7Area area, string rest, string original)
    {
        if (rest.Length == 0)
            throw new FormatException($"S7 address '{original}' has no offset");

        var first = rest[0];
        S7Size size;
        int offsetStart;
        switch (first)
        {
            case 'B': size = S7Size.Byte;  offsetStart = 1; break;
            case 'W': size = S7Size.Word;  offsetStart = 1; break;
            case 'D': size = S7Size.DWord; offsetStart = 1; break;
            default:
                // No size prefix => bit-level address requires explicit .bit. Size stays Bit;
                // ParseOffsetAndOptionalBit will demand the dot.
                size = S7Size.Bit;
                offsetStart = 0;
                break;
        }

        var (byteOffset, bitOffset) = ParseOffsetAndOptionalBit(rest, offsetStart, size, original);
        return new S7ParsedAddress(area, DbNumber: 0, size, byteOffset, bitOffset);
    }

    private static S7ParsedAddress ParseTimerOrCounter(S7Area area, string rest, string original)
    {
        if (rest.Length == 0)
            throw new FormatException($"S7 address '{original}' has no {area} number");
        if (!int.TryParse(rest, out var number) || number < 0)
            throw new FormatException($"S7 {area} number in '{original}' must be a non-negative integer");
        return new S7ParsedAddress(area, DbNumber: 0, S7Size.Word, number, BitOffset: 0);
    }

    private static (int byteOffset, int bitOffset) ParseOffsetAndOptionalBit(
        string s, int start, S7Size size, string original)
    {
        var offsetEnd = start;
        while (offsetEnd < s.Length && s[offsetEnd] >= '0' && s[offsetEnd] <= '9')
            offsetEnd++;
        if (offsetEnd == start)
            throw new FormatException($"S7 address '{original}' has no byte-offset digits");

        if (!int.TryParse(s.AsSpan(start, offsetEnd - start), out var byteOffset) || byteOffset < 0)
            throw new FormatException($"S7 byte offset in '{original}' must be non-negative");

        // No bit-suffix: done unless size is Bit with no prefix, which requires one.
        if (offsetEnd == s.Length)
        {
            if (size == S7Size.Bit)
                throw new FormatException($"S7 address '{original}' needs a .{{bit}} suffix for bit access");
            return (byteOffset, 0);
        }

        if (s[offsetEnd] != '.')
            throw new FormatException($"S7 address '{original}' has unexpected character after offset");

        if (size != S7Size.Bit)
            throw new FormatException($"S7 address '{original}' has a bit suffix but the size is {size} — bit access needs X (DB) or no size prefix (M/I/Q)");

        if (!int.TryParse(s.AsSpan(offsetEnd + 1), out var bitOffset) || bitOffset is < 0 or > 7)
            throw new FormatException($"S7 bit offset in '{original}' must be 0-7");

        return (byteOffset, bitOffset);
    }
}
