package com.foxx.androidcast.network.transport.fec;

/*********************************************************************
 * FecShardWire.java
 * Created at: Mon 18 May 2026 14:25:02 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class FecShardWire
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (3 commits, 142 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	e43675e8fa136c178c00c95e7ad0a569400a6582342223eef64e369e1aa4b719
**********************************************************************/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
 * Per-datagram FEC shard. Each shard is its own ACUD logical message so loss of one
 * fragment does not invalidate the whole Reed–Solomon block.
 *
 * <p>v2 used a single-byte shard length (max 255), which breaks VP9 keyframes. v3 uses uint16.
 */
public final class FecShardWire {
    private static final byte[] MAGIC = {'A', 'C', 'P', 'F'};
    /** Legacy on-wire format; shard length field is 1 byte (max 255). */
    public static final byte VERSION_SHARD_V2 = 2;
    /** Current format; shard length field is uint16. */
    public static final byte VERSION_SHARD = 3;
    private static final int HEADER_AFTER_MAGIC_V2 = 19;
    private static final int HEADER_AFTER_MAGIC_V3 = 20;

    private FecShardWire() {}

    public static byte[] encodeShard(
            byte mode,
            int groupId,
            int baseMessageId,
            int shardIndex,
            int totalShards,
            int originalLen,
            int dataShards,
            int parityShards,
            byte[] shardData) throws IOException {
        if (shardData == null) {
            throw new IOException("shardData required");
        }
        if (shardData.length > 0xFFFF) {
            throw new IOException("shard too large: " + shardData.length);
        }
        ByteArrayOutputStream bos = new ByteArrayOutputStream(32 + shardData.length);
        DataOutputStream dos = new DataOutputStream(bos);
        dos.write(MAGIC);
        dos.writeByte(VERSION_SHARD);
        dos.writeByte(mode);
        dos.writeInt(groupId);
        dos.writeInt(baseMessageId);
        dos.writeByte(shardIndex);
        dos.writeByte(totalShards);
        dos.writeInt(originalLen);
        dos.writeByte(dataShards);
        dos.writeByte(parityShards);
        dos.writeShort(shardData.length);
        dos.write(shardData);
        return bos.toByteArray();
    }

    public static boolean isShardPacket(byte[] wire) {
        return tryParse(wire) != null;
    }

    public static Parsed tryParse(byte[] wire) {
        if (wire == null || wire.length < MAGIC.length + HEADER_AFTER_MAGIC_V2) {
            return null;
        }
        for (int i = 0; i < MAGIC.length; i++) {
            if (wire[i] != MAGIC[i]) {
                return null;
            }
        }
        try {
            DataInputStream dis = new DataInputStream(new ByteArrayInputStream(wire, MAGIC.length,
                    wire.length - MAGIC.length));
            byte ver = dis.readByte();
            if (ver != VERSION_SHARD_V2 && ver != VERSION_SHARD) {
                return null;
            }
            byte mode = dis.readByte();
            int groupId = dis.readInt();
            int baseMessageId = dis.readInt();
            int shardIndex = dis.readUnsignedByte();
            int totalShards = dis.readUnsignedByte();
            int originalLen = dis.readInt();
            int dataShards = dis.readUnsignedByte();
            int parityShards = dis.readUnsignedByte();
            int shardSize = ver == VERSION_SHARD ? dis.readUnsignedShort() : dis.readUnsignedByte();
            if (dataShards < 1 || parityShards < 1 || totalShards != dataShards + parityShards) {
                return null;
            }
            if (shardIndex >= totalShards || originalLen < 1 || shardSize < 1) {
                return null;
            }
            int headerAfterMagic = ver == VERSION_SHARD ? HEADER_AFTER_MAGIC_V3 : HEADER_AFTER_MAGIC_V2;
            if (wire.length < MAGIC.length + headerAfterMagic + shardSize) {
                return null;
            }
            byte[] shardData = new byte[shardSize];
            dis.readFully(shardData);
            return new Parsed(mode, groupId, baseMessageId, shardIndex, totalShards, originalLen,
                    dataShards, parityShards, shardData);
        } catch (IOException e) {
            return null;
        }
    }

    public static final class Parsed {
        public final byte mode;
        public final int groupId;
        public final int baseMessageId;
        public final int shardIndex;
        public final int totalShards;
        public final int originalLen;
        public final int dataShards;
        public final int parityShards;
        public final byte[] shardData;

        Parsed(byte mode, int groupId, int baseMessageId, int shardIndex, int totalShards,
                int originalLen, int dataShards, int parityShards, byte[] shardData) {
            this.mode = mode;
            this.groupId = groupId;
            this.baseMessageId = baseMessageId;
            this.shardIndex = shardIndex;
            this.totalShards = totalShards;
            this.originalLen = originalLen;
            this.dataShards = dataShards;
            this.parityShards = parityShards;
            this.shardData = shardData;
        }

        public int messageIdForShard() {
            return baseMessageId + shardIndex;
        }
    }
}
