using libplctag;

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

/// <summary>
///     Default libplctag-backed <see cref="IAbLegacyTagRuntime"/>. Uses <c>ab_pccc</c> protocol
///     on top of EtherNet/IP — libplctag's PCCC layer handles the file-letter + word + bit +
///     sub-element decoding internally, so our wrapper just has to forward the atomic type to
///     the right Get/Set call.
/// </summary>
internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
{
    private readonly Tag _tag;

    /// <summary>Initializes a new instance of the <see cref="LibplctagLegacyTagRuntime"/> class.</summary>
    /// <param name="p">The parameters for tag creation.</param>
    public LibplctagLegacyTagRuntime(AbLegacyTagCreateParams p)
    {
        _tag = new Tag
        {
            Gateway = p.Gateway,
            Path = p.CipPath,
            PlcType = MapPlcType(p.LibplctagPlcAttribute),
            Protocol = Protocol.ab_eip, // PCCC-over-EIP; libplctag routes via the PlcType-specific PCCC layer
            Name = p.TagName,
            Timeout = p.Timeout,
            ElementCount = p.ElementCount > 1 ? p.ElementCount : null,
        };
    }

    /// <inheritdoc />
    public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);

    /// <inheritdoc />
    public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);

    /// <inheritdoc />
    public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);

    /// <inheritdoc />
    public int GetStatus() => (int)_tag.GetStatus();

    /// <inheritdoc />
    public object? DecodeValue(AbLegacyDataType type, int? bitIndex) => type switch
    {
        // When a bit suffix is present (e.g. B3:0/5) libplctag resolves the individual bit and
        // GetBit returns it directly. When there is no suffix the caller addressed a Bit-typed
        // tag without an explicit bit index; read the full 16-bit word and test bit 0 — GetInt8
        // only covers the low byte and silently misses any bit set in bits 8..15.
        AbLegacyDataType.Bit => bitIndex is int bit
            ? _tag.GetBit(bit)
            : (_tag.GetInt16(0) & 1) != 0,
        AbLegacyDataType.Int or AbLegacyDataType.AnalogInt => (int)_tag.GetInt16(0),
        AbLegacyDataType.Long => _tag.GetInt32(0),
        AbLegacyDataType.Float => _tag.GetFloat32(0),
        AbLegacyDataType.String => _tag.GetString(0),
        AbLegacyDataType.TimerElement or AbLegacyDataType.CounterElement
            or AbLegacyDataType.ControlElement => _tag.GetInt32(0),
        _ => null,
    };

    /// <inheritdoc />
    public object? DecodeArray(AbLegacyDataType type, int count)
    {
        // The 5 libplctag-PCCC array-read ASSUMPTIONS this decode relies on (none live-proven —
        // no PCCC fixture on this build host; they match the AbCip sibling's offset-decode pattern
        // and the libplctag.NET API surface):
        //
        //   1. ELEMENT ADDRESSING — each of the `count` elements is read from its own byte offset
        //      `i * elementSize` within the single contiguous tag buffer that the prior ReadAsync
        //      filled (the tag was created with ElementCount = count, so one PCCC transaction
        //      fetched the whole span from the base address, e.g. N7:0 reading N7:0..N7:count-1).
        //   2. FILE/ELEMENT vs SUB-ELEMENT — these are whole-element (file/element) reads, NOT
        //      sub-element reads. The array tag addresses a span of file elements (N7:0, N7:1, …),
        //      so there is no /N bit suffix or T/C/R sub-element field in play on this path; the
        //      bit-within-word and structured-element paths are handled elsewhere as scalars.
        //   3. ELEMENT WIDTHS — the canonical PCCC element widths drive the offset stride:
        //        • N / A (integer word files → Int)      → 2 bytes/element (GetInt16 at i*2)
        //        • L (long) and F (float)                → 4 bytes/element (GetInt32/GetFloat32 at i*4)
        //        • B (bit file)                          → bit-packed: element i is the i-th bit,
        //          read by index via libplctag's GetBit(i) (NOT a byte offset).
        //   4. CONTIGUOUS LAYOUT — libplctag packs the multi-element read contiguously with no
        //      per-element padding, so the `i * elementSize` stride (and GetBit(i) for B-files)
        //      lands exactly on element i.
        //   5. UNSUPPORTED ELEMENT TYPES — String (ST-file) and Timer / Counter / Control
        //      sub-element structures do NOT lay out as a flat scalar array (variable-width /
        //      structured), so array reads of them are rejected in the `default` arm below; the
        //      driver also filters them before reaching here.
        switch (type)
        {
            case AbLegacyDataType.Int:
            case AbLegacyDataType.AnalogInt:
            {
                var arr = new short[count];
                for (var i = 0; i < count; i++) arr[i] = _tag.GetInt16(i * 2);
                return arr;
            }
            case AbLegacyDataType.Long:
            {
                var arr = new int[count];
                for (var i = 0; i < count; i++) arr[i] = _tag.GetInt32(i * 4);
                return arr;
            }
            case AbLegacyDataType.Float:
            {
                var arr = new float[count];
                for (var i = 0; i < count; i++) arr[i] = _tag.GetFloat32(i * 4);
                return arr;
            }
            case AbLegacyDataType.Bit:
            {
                var arr = new bool[count];
                for (var i = 0; i < count; i++) arr[i] = _tag.GetBit(i);
                return arr;
            }
            default:
                // String / Timer / Counter / Control element arrays are not supported — these
                // are structured or variable-width PCCC element types that don't lay out as a
                // flat scalar array. The driver rejects them before reaching here.
                throw new NotSupportedException(
                    $"AbLegacyDataType {type} is not supported as an array element.");
        }
    }

    /// <inheritdoc />
    public void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value)
    {
        switch (type)
        {
            case AbLegacyDataType.Bit:
                if (bitIndex is int)
                    // Bit-within-word writes are routed at the driver level
                    // (AbLegacyDriver.WriteBitInWordAsync) via a parallel parent-word runtime —
                    // this branch only fires if dispatch was bypassed. Throw loudly rather than
                    // silently clobbering the whole word.
                    throw new NotSupportedException(
                        "Bit-with-bitIndex writes must go through AbLegacyDriver.WriteBitInWordAsync.");
                _tag.SetInt16(0, Convert.ToBoolean(value) ? (short)1 : (short)0);
                break;
            case AbLegacyDataType.Int:
            case AbLegacyDataType.AnalogInt:
                _tag.SetInt16(0, Convert.ToInt16(value));
                break;
            case AbLegacyDataType.Long:
                _tag.SetInt32(0, Convert.ToInt32(value));
                break;
            case AbLegacyDataType.Float:
                _tag.SetFloat32(0, Convert.ToSingle(value));
                break;
            case AbLegacyDataType.String:
                _tag.SetString(0, Convert.ToString(value) ?? string.Empty);
                break;
            case AbLegacyDataType.TimerElement:
            case AbLegacyDataType.CounterElement:
            case AbLegacyDataType.ControlElement:
                _tag.SetInt32(0, Convert.ToInt32(value));
                break;
            default:
                throw new NotSupportedException($"AbLegacyDataType {type} not writable.");
        }
    }

    /// <summary>Disposes the tag and releases its resources.</summary>
    public void Dispose() => _tag.Dispose();

    private static PlcType MapPlcType(string attribute) => attribute switch
    {
        "slc500" => PlcType.Slc500,
        "micrologix" => PlcType.MicroLogix,
        "plc5" => PlcType.Plc5,
        "logixpccc" => PlcType.LogixPccc,
        _ => PlcType.Slc500,
    };
}

internal sealed class LibplctagLegacyTagFactory : IAbLegacyTagFactory
{
    /// <inheritdoc />
    public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams createParams) =>
        new LibplctagLegacyTagRuntime(createParams);
}
