import { assert } from "../../../../core/assert.js";
import {
    FRAGMENT_HEADER_BYTES,
    MAX_CHANNEL_PAYLOAD_BYTES,
    MAX_CHUNKS_PER_MESSAGE,
    MAX_FRAGMENT_CHUNK_BYTES,
} from "./packet_size.js";

/**
 * Send a logical payload through a callback, splitting into fragment
 * packets if it exceeds the channel's single-packet capacity.
 *
 * Layout of each emitted fragment packet (before the Channel adds its
 * 8-byte header):
 *
 *   uint8   FRAGMENT_PACKET_TYPE
 *   uint16  message_id   (little-endian)
 *   uint8   chunk_index
 *   uint8   total_chunks
 *   bytes   chunk_payload
 *
 * The chunk_payload bytes are slices of the original logical payload,
 * concatenated in chunk_index order they reconstruct the original
 * payload byte-for-byte. The logical payload's own packet-type byte is
 * the first byte of chunk 0 and is preserved by the reassembly.
 *
 * Single-packet messages (payload <= MAX_CHANNEL_PAYLOAD_BYTES) are sent
 * as-is with NO fragmentation header — the receiver dispatches them
 * normally via the FIRST byte. Fragmentation overhead only applies when
 * the payload genuinely doesn't fit.
 *
 * @param {Uint8Array} payload
 * @param {number} payload_length
 * @param {number} message_id  sender-assigned, unique per logical message
 * @param {number} fragment_packet_type  packet-type byte for fragment packets
 * @param {Uint8Array} scratch  reusable scratch buffer; must be >= MAX_CHANNEL_PAYLOAD_BYTES
 * @param {function(Uint8Array, number): void} send_fn
 *   invoked once per emitted packet — single send for non-fragmented case,
 *   N sends for fragmented case
 */
export function send_fragmented(
    payload,
    payload_length,
    message_id,
    fragment_packet_type,
    scratch,
    send_fn,
) {
    assert.isNonNegativeInteger(payload_length, 'payload_length');
    assert.isNonNegativeInteger(message_id, 'message_id');
    assert.ok(message_id <= 0xFFFF, 'message_id must fit in uint16');
    assert.ok(scratch.length >= MAX_CHANNEL_PAYLOAD_BYTES,
        'scratch buffer must be at least MAX_CHANNEL_PAYLOAD_BYTES');

    if (payload_length <= MAX_CHANNEL_PAYLOAD_BYTES) {
        // Fits in one packet; send as-is.
        send_fn(payload, payload_length);
        return;
    }

    const total_chunks = Math.ceil(payload_length / MAX_FRAGMENT_CHUNK_BYTES);
    if (total_chunks > MAX_CHUNKS_PER_MESSAGE) {
        throw new Error(
            `send_fragmented: payload of ${payload_length} bytes would require ` +
            `${total_chunks} chunks, exceeding the ${MAX_CHUNKS_PER_MESSAGE}-chunk wire limit`
        );
    }

    for (let i = 0; i < total_chunks; i++) {
        const chunk_offset = i * MAX_FRAGMENT_CHUNK_BYTES;
        const remaining = payload_length - chunk_offset;
        const chunk_size = remaining < MAX_FRAGMENT_CHUNK_BYTES ? remaining : MAX_FRAGMENT_CHUNK_BYTES;

        // Build the fragment packet inside scratch.
        scratch[0] = fragment_packet_type;
        scratch[1] = message_id & 0xFF;
        scratch[2] = (message_id >> 8) & 0xFF;
        scratch[3] = i;
        scratch[4] = total_chunks;
        scratch.set(
            payload.subarray(chunk_offset, chunk_offset + chunk_size),
            FRAGMENT_HEADER_BYTES,
        );

        send_fn(scratch, FRAGMENT_HEADER_BYTES + chunk_size);
    }
}

/**
 * Re-emit a single fragment chunk for a previously-sent fragmented
 * message. Used by the NACK retransmit path: the sender's
 * {@link FragmentRetention} keeps the source bytes so any chunk can be
 * deterministically rebuilt.
 *
 * The wire layout matches what {@link send_fragmented} would have
 * produced for the same `(message_id, chunk_index, total_chunks)` —
 * receivers can't tell a retransmit from an original.
 *
 * @param {Uint8Array} payload  retained source bytes
 * @param {number} payload_length
 * @param {number} message_id
 * @param {number} chunk_index  must be < derived total_chunks; silently ignored otherwise
 * @param {number} fragment_packet_type
 * @param {Uint8Array} scratch  reusable scratch; must be >= MAX_CHANNEL_PAYLOAD_BYTES
 * @param {function(Uint8Array, number): void} send_fn
 */
export function resend_fragment_chunk(
    payload,
    payload_length,
    message_id,
    chunk_index,
    fragment_packet_type,
    scratch,
    send_fn,
) {
    assert.isNonNegativeInteger(payload_length, 'payload_length');
    assert.isNonNegativeInteger(message_id, 'message_id');
    assert.ok(message_id <= 0xFFFF, 'message_id must fit in uint16');
    assert.isNonNegativeInteger(chunk_index, 'chunk_index');
    assert.ok(scratch.length >= MAX_CHANNEL_PAYLOAD_BYTES,
        'scratch buffer must be at least MAX_CHANNEL_PAYLOAD_BYTES');

    // Recover total_chunks from the retained payload length. Same formula
    // as send_fragmented; payloads short enough to not fragment in the
    // first place would never have been retained, so we still take the
    // ceil here as defensive consistency.
    const total_chunks = payload_length <= MAX_FRAGMENT_CHUNK_BYTES
        ? 1
        : Math.ceil(payload_length / MAX_FRAGMENT_CHUNK_BYTES);
    if (chunk_index >= total_chunks) return;

    const chunk_offset = chunk_index * MAX_FRAGMENT_CHUNK_BYTES;
    const remaining = payload_length - chunk_offset;
    const chunk_size = remaining < MAX_FRAGMENT_CHUNK_BYTES ? remaining : MAX_FRAGMENT_CHUNK_BYTES;

    scratch[0] = fragment_packet_type;
    scratch[1] = message_id & 0xFF;
    scratch[2] = (message_id >> 8) & 0xFF;
    scratch[3] = chunk_index;
    scratch[4] = total_chunks;
    scratch.set(
        payload.subarray(chunk_offset, chunk_offset + chunk_size),
        FRAGMENT_HEADER_BYTES,
    );

    send_fn(scratch, FRAGMENT_HEADER_BYTES + chunk_size);
}
