using libplctag;

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

/// <summary>
///     Default libplctag-backed <see cref="IAbCipTagRuntime"/>. Wraps a <see cref="Tag"/>
///     instance + translates our <see cref="AbCipDataType"/> enum into the
///     <c>GetInt32</c> / <c>GetFloat32</c> / <c>GetString</c> / <c>GetBit</c> calls libplctag
///     exposes. One runtime instance per <c>(device, tag path)</c>; lifetime is owned by the
///     driver's per-device state dict.
/// </summary>
internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
{
    private readonly Tag _tag;

    /// <summary>Initializes a new instance of the LibplctagTagRuntime class.</summary>
    /// <param name="p">The tag creation parameters.</param>
    public LibplctagTagRuntime(AbCipTagCreateParams p)
    {
        _tag = new Tag
        {
            Gateway = p.Gateway,
            Path = p.CipPath,
            PlcType = MapPlcType(p.LibplctagPlcAttribute),
            Protocol = Protocol.ab_eip,
            Name = p.TagName,
            Timeout = p.Timeout,
            AllowPacking = p.AllowPacking,
            // Phase 4c — libplctag elem_count. For a 1-D array tag the driver passes the element
            // count so libplctag pulls every element in one CIP read; scalar tags pass 1.
            ElementCount = p.ElementCount > 1 ? p.ElementCount : 1,
        };
    }

    /// <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(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);

    /// <inheritdoc />
    public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => type switch
    {
        AbCipDataType.Bool => bitIndex is int bit
            ? _tag.GetBit(bit)
            : _tag.GetInt8(offset) != 0,
        AbCipDataType.SInt => (int)(sbyte)_tag.GetInt8(offset),
        AbCipDataType.USInt => (int)_tag.GetUInt8(offset),
        AbCipDataType.Int => (int)_tag.GetInt16(offset),
        AbCipDataType.UInt => (int)_tag.GetUInt16(offset),
        AbCipDataType.DInt => _tag.GetInt32(offset),
        AbCipDataType.UDInt => _tag.GetUInt32(offset),
        AbCipDataType.LInt => _tag.GetInt64(offset),
        AbCipDataType.ULInt => _tag.GetUInt64(offset),
        AbCipDataType.Real => _tag.GetFloat32(offset),
        AbCipDataType.LReal => _tag.GetFloat64(offset),
        AbCipDataType.String => _tag.GetString(offset),
        AbCipDataType.Dt => _tag.GetInt32(offset),
        AbCipDataType.Structure => null,
        _ => null,
    };

    /// <inheritdoc />
    public object? DecodeArray(AbCipDataType type, int count)
    {
        if (count < 1) count = 1;
        // libplctag reports the per-element byte size once the tag has been read; it correctly
        // accounts for STRING capacity + atomic widths. ElementSize is int? on the wrapper — fall
        // back to a static size table when it is null/<=0 (e.g. a zero-length read) so the stride
        // is never 0.
        var elementSize = _tag.ElementSize ?? 0;
        var stride = elementSize > 0 ? elementSize : ElementByteSize(type);

        return type switch
        {
            // A Logix BOOL array is bit-packed on the wire; libplctag exposes each element via
            // GetBit(elementIndex) rather than a byte stride, so decode bit-by-bit.
            AbCipDataType.Bool => BuildBoolArray(count),
            AbCipDataType.SInt or AbCipDataType.USInt or AbCipDataType.Int or AbCipDataType.UInt
                or AbCipDataType.DInt or AbCipDataType.Dt => BuildArray<int>(count, stride, type),
            AbCipDataType.UDInt => BuildArray<uint>(count, stride, type),
            AbCipDataType.LInt => BuildArray<long>(count, stride, type),
            AbCipDataType.ULInt => BuildArray<ulong>(count, stride, type),
            AbCipDataType.Real => BuildArray<float>(count, stride, type),
            AbCipDataType.LReal => BuildArray<double>(count, stride, type),
            AbCipDataType.String => BuildArray<string>(count, stride, type),
            _ => null,
        };
    }

    private bool[] BuildBoolArray(int count)
    {
        var arr = new bool[count];
        for (var i = 0; i < count; i++) arr[i] = _tag.GetBit(i);
        return arr;
    }

    private T[] BuildArray<T>(int count, int stride, AbCipDataType type)
    {
        var arr = new T[count];
        for (var i = 0; i < count; i++)
        {
            var decoded = DecodeValueAt(type, i * stride, null);
            // DecodeValueAt boxes to the element CLR type that ToDriverDataType maps to, which is
            // exactly T for every branch above; an explicit cast keeps the array strongly typed.
            arr[i] = decoded is null ? default! : (T)decoded;
        }
        return arr;
    }

    /// <summary>Static fallback byte stride per atomic element, used only when libplctag has not
    /// yet populated <see cref="Tag.ElementSize"/>. STRING falls back to the Logix STRING wire
    /// size (4-byte LEN + 82-byte DATA, 88 with alignment) — but libplctag.NET's
    /// <c>ElementSize</c> is the real source of truth at runtime.</summary>
    private static int ElementByteSize(AbCipDataType type) => type switch
    {
        AbCipDataType.Bool or AbCipDataType.SInt or AbCipDataType.USInt => 1,
        AbCipDataType.Int or AbCipDataType.UInt => 2,
        AbCipDataType.DInt or AbCipDataType.UDInt or AbCipDataType.Real or AbCipDataType.Dt => 4,
        AbCipDataType.LInt or AbCipDataType.ULInt or AbCipDataType.LReal => 8,
        AbCipDataType.String => 88,
        _ => 4,
    };

    /// <inheritdoc />
    public void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
    {
        switch (type)
        {
            case AbCipDataType.Bool:
                if (bitIndex is int)
                {
                    // BOOL-within-DINT writes are routed at the driver level (AbCipDriver.
                    // WriteBitInDIntAsync) via a parallel parent-DINT runtime so the RMW stays
                    // serialised. If one reaches here it means the driver dispatch was bypassed —
                    // throw so the error surfaces loudly rather than clobbering the whole DINT.
                    throw new NotSupportedException(
                        "BOOL-with-bitIndex writes must go through AbCipDriver.WriteBitInDIntAsync, not LibplctagTagRuntime.");
                }
                _tag.SetInt8(0, Convert.ToBoolean(value) ? (sbyte)1 : (sbyte)0);
                break;
            case AbCipDataType.SInt:
                _tag.SetInt8(0, Convert.ToSByte(value));
                break;
            case AbCipDataType.USInt:
                _tag.SetUInt8(0, Convert.ToByte(value));
                break;
            case AbCipDataType.Int:
                _tag.SetInt16(0, Convert.ToInt16(value));
                break;
            case AbCipDataType.UInt:
                _tag.SetUInt16(0, Convert.ToUInt16(value));
                break;
            case AbCipDataType.DInt:
                _tag.SetInt32(0, Convert.ToInt32(value));
                break;
            case AbCipDataType.UDInt:
                _tag.SetUInt32(0, Convert.ToUInt32(value));
                break;
            case AbCipDataType.LInt:
                _tag.SetInt64(0, Convert.ToInt64(value));
                break;
            case AbCipDataType.ULInt:
                _tag.SetUInt64(0, Convert.ToUInt64(value));
                break;
            case AbCipDataType.Real:
                _tag.SetFloat32(0, Convert.ToSingle(value));
                break;
            case AbCipDataType.LReal:
                _tag.SetFloat64(0, Convert.ToDouble(value));
                break;
            case AbCipDataType.String:
                _tag.SetString(0, Convert.ToString(value) ?? string.Empty);
                break;
            case AbCipDataType.Dt:
                _tag.SetInt32(0, Convert.ToInt32(value));
                break;
            case AbCipDataType.Structure:
                // Whole-UDT writes are not implemented — operators address individual UDT
                // members via dotted tag paths, which route per-member through the atomic
                // encode cases above. A whole-UDT writer is a deferred follow-up.
                throw new NotSupportedException(
                    "Whole-UDT writes are not supported — address individual member paths instead.");
            default:
                throw new NotSupportedException($"AbCipDataType {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
    {
        "controllogix" => PlcType.ControlLogix,
        "compactlogix" => PlcType.ControlLogix, // libplctag treats CompactLogix under ControlLogix family
        "micro800" => PlcType.Micro800,
        "micrologix" => PlcType.MicroLogix,
        "slc500" => PlcType.Slc500,
        "plc5" => PlcType.Plc5,
        "omron-njnx" => PlcType.Omron,
        _ => PlcType.ControlLogix,
    };
}

/// <summary>
///     Default <see cref="IAbCipTagFactory"/> — creates a fresh <see cref="LibplctagTagRuntime"/>
///     per call. Stateless; safe to share across devices.
/// </summary>
/// <summary>Default implementation of IAbCipTagFactory that creates LibplctagTagRuntime instances.</summary>
internal sealed class LibplctagTagFactory : IAbCipTagFactory
{
    /// <inheritdoc />
    public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) =>
        new LibplctagTagRuntime(createParams);
}
