package com.foxx.androidcast.network;

/*********************************************************************
 * CastPacketFramer.java
 * Created at: Sat 16 May 2026 13:35:48 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CastPacketFramer
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (3 commits, 119 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	806b98464c799cdf9ccae53bcd08907959948949247045c8c6e8d6247d5e8ff0
**********************************************************************/
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/** Wraps {@link CastProtoHeader} + {@link CastProtocol} message body. */
public final class CastPacketFramer {
  private CastPacketFramer() {}

  public static byte[] frame(CastProtoHeader header, byte type, byte[] payload) throws IOException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    DataOutputStream dos = new DataOutputStream(bos);
    CastProtocol.writeMessage(dos, type, payload);
    byte[] inner = bos.toByteArray();
    header.recomputeChecksum();
    byte[] hdr = header.encode();
    byte[] out = new byte[hdr.length + inner.length];
    System.arraycopy(hdr, 0, out, 0, hdr.length);
    System.arraycopy(inner, 0, out, hdr.length, inner.length);
    return out;
  }

  public static CastProtoHeader readHeader(byte[] wire) throws IOException {
    if (wire == null || wire.length < CastProtoHeader.HEADER_SIZE_V1) {
      throw new IOException("Frame too short");
    }
    CastProtoHeader hdr = CastProtoHeader.decode(wire, 0, wire.length);
    if (!hdr.verifyChecksum()) {
      throw new IOException("Header checksum mismatch");
    }
    return hdr;
  }

  public static CastProtocol.Message unframe(byte[] wire) throws IOException {
    if (wire == null || wire.length < CastProtoHeader.HEADER_SIZE_V1 + 5) {
      throw new IOException("Frame too short");
    }
    CastProtoHeader hdr = readHeader(wire);
    int innerOff = hdr.headerSize;
    if (wire.length < innerOff + 5) {
      throw new IOException("Missing message body");
    }
    DataInputStream dis = new DataInputStream(
        new java.io.ByteArrayInputStream(wire, innerOff, wire.length - innerOff));
    return CastProtocol.readMessage(dis);
  }

  /**
   * Reads one framed message from a TCP stream (blocking).
   * Returns null if stream ends before a full frame.
   */
  public static CastProtocol.Message readFramedMessage(InputStream in) throws IOException {
    byte[] magicBuf = readFully(in, 4);
    if (magicBuf == null) {
      return null;
    }
    int magic = (magicBuf[0] & 0xff) | ((magicBuf[1] & 0xff) << 8)
        | ((magicBuf[2] & 0xff) << 16) | ((magicBuf[3] & 0xff) << 24);
    if (magic != CastProtoHeader.MAGIC) {
      throw new IOException("Bad magic on wire: 0x" + Integer.toHexString(magic));
    }
    byte[] restHdr = readFully(in, CastProtoHeader.HEADER_SIZE_V1 - 4);
    if (restHdr == null) {
      return null;
    }
    byte[] headerBytes = new byte[CastProtoHeader.HEADER_SIZE_V1];
    System.arraycopy(magicBuf, 0, headerBytes, 0, 4);
    System.arraycopy(restHdr, 0, headerBytes, 4, restHdr.length);
    CastProtoHeader hdr = CastProtoHeader.decode(headerBytes, 0, headerBytes.length);
    if (!hdr.verifyChecksum()) {
      throw new IOException("Header checksum mismatch");
    }
    byte type = (byte) in.read();
    if (type < 0) {
      return null;
    }
    byte[] lenBuf = readFully(in, 4);
    if (lenBuf == null) {
      return null;
    }
    int len = ((lenBuf[0] & 0xff) << 24) | ((lenBuf[1] & 0xff) << 16)
        | ((lenBuf[2] & 0xff) << 8) | (lenBuf[3] & 0xff);
    if (len < 0 || len > 4 * 1024 * 1024) {
      throw new IOException("Invalid payload length: " + len);
    }
    byte[] payload = len == 0 ? new byte[0] : readFully(in, len);
    if (payload == null && len > 0) {
      return null;
    }
    return new CastProtocol.Message(type, payload);
  }

  private static byte[] readFully(InputStream in, int n) throws IOException {
    byte[] buf = new byte[n];
    int off = 0;
    while (off < n) {
      int r = in.read(buf, off, n - off);
      if (r < 0) {
        if (off == 0) {
          return null;
        }
        throw new EOFException("Short read");
      }
      off += r;
    }
    return buf;
  }
}
