package com.foxx.androidcast.network;

/*********************************************************************
 * CastProtoHeader.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 CastProtoHeader
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 138 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	afb671c1209f1fb7414fca91bf9b5c1ccbe2cac079a4b1725ffc8daf9f96df61
**********************************************************************/
import com.foxx.androidcast.VersionInfo;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.CRC32;

/**
 * Fixed binary header prefixed to every cast/discovery packet (little-endian).
 * v1 size = 48 bytes.
 */
public final class CastProtoHeader {
  public static final int MAGIC = 0xDEADBABE;
  public static final int HEADER_SIZE_V1 = 48;

  public static final byte HS_DEFAULT = 0;
  public static final byte HS_SDP = 1;

  public int magic = MAGIC;
  public long protoVersion = VersionInfo.PROTO_VERSION;
  public int headerSize = HEADER_SIZE_V1;
  public int flags;
  public int reserved;
  public byte hsType = HS_DEFAULT;
  public int videoCodecs;
  public int audioCodecs;
  public int placeholderId;
  public int sessionId;
  public int checksum;

  public static CastProtoHeader forCast(int sessionId, int placeholderId, int videoCodecs, int audioCodecs) {
    CastProtoHeader h = new CastProtoHeader();
    h.hsType = HS_DEFAULT;
    h.sessionId = sessionId;
    h.placeholderId = placeholderId;
    h.videoCodecs = videoCodecs;
    h.audioCodecs = audioCodecs;
    h.recomputeChecksum();
    return h;
  }

  public static CastProtoHeader forDiscovery(int placeholderId, int videoCodecs, int audioCodecs) {
    CastProtoHeader h = new CastProtoHeader();
    h.hsType = HS_SDP;
    h.sessionId = 0;
    h.placeholderId = placeholderId;
    h.videoCodecs = videoCodecs;
    h.audioCodecs = audioCodecs;
    h.recomputeChecksum();
    return h;
  }

  public void recomputeChecksum() {
    checksum = 0;
    checksum = (int) crc32(encode());
  }

  public boolean verifyChecksum() {
    int saved = checksum;
    checksum = 0;
    int expected = (int) crc32(encode());
    checksum = saved;
    return saved == expected;
  }

  public byte[] encode() {
    ByteBuffer buf = ByteBuffer.allocate(HEADER_SIZE_V1).order(ByteOrder.LITTLE_ENDIAN);
    buf.putInt(magic);
    buf.putLong(protoVersion);
    buf.putInt(headerSize);
    buf.putInt(flags);
    buf.putInt(reserved);
    buf.put(hsType);
    buf.put((byte) 0);
    buf.put((byte) 0);
    buf.put((byte) 0);
    buf.putInt(videoCodecs);
    buf.putInt(audioCodecs);
    buf.putInt(placeholderId);
    buf.putInt(sessionId);
    buf.putInt(0);
    byte[] out = buf.array();
    checksum = (int) crc32(out);
    ByteBuffer.wrap(out).order(ByteOrder.LITTLE_ENDIAN).putInt(44, checksum);
    return out;
  }

  public static CastProtoHeader decode(byte[] bytes, int offset, int length) throws java.io.IOException {
    if (length < HEADER_SIZE_V1) {
      throw new java.io.IOException("Header too short: " + length);
    }
    ByteBuffer buf = ByteBuffer.wrap(bytes, offset, length).order(ByteOrder.LITTLE_ENDIAN);
    CastProtoHeader h = new CastProtoHeader();
    h.magic = buf.getInt();
    if (h.magic != MAGIC) {
      throw new java.io.IOException("Bad magic: 0x" + Integer.toHexString(h.magic));
    }
    h.protoVersion = buf.getLong();
    h.headerSize = buf.getInt();
    h.flags = buf.getInt();
    h.reserved = buf.getInt();
    h.hsType = buf.get();
    buf.get();
    buf.get();
    buf.get();
    h.videoCodecs = buf.getInt();
    h.audioCodecs = buf.getInt();
    h.placeholderId = buf.getInt();
    h.sessionId = buf.getInt();
    h.checksum = buf.getInt();
    if (h.headerSize < HEADER_SIZE_V1) {
      h.headerSize = HEADER_SIZE_V1;
    }
    return h;
  }

  private static long crc32(byte[] data) {
    CRC32 crc = new CRC32();
    int checksumOffset = 44;
    crc.update(data, 0, checksumOffset);
    crc.update((byte) 0);
    crc.update((byte) 0);
    crc.update((byte) 0);
    crc.update((byte) 0);
    if (data.length > checksumOffset + 4) {
      crc.update(data, checksumOffset + 4, data.length - checksumOffset - 4);
    }
    return crc.getValue();
  }
}
