/**
 * PNG text chunk utilities for embedding/extracting workflow metadata.
 *
 * PNG files are composed of chunks: [4-byte length][4-byte type][data][4-byte CRC].
 * We add an iTXt chunk with key "workflow" containing the JSON-serialised graph,
 * inserted just before the IEND chunk.
 */

// ── CRC32 (PNG uses CRC-32/ISO 3309) ────────────────────────────────

const crcTable = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
  let c = i;
  for (let j = 0; j < 8; j++) {
    c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
  }
  crcTable[i] = c;
}

function crc32(bytes) {
  let crc = 0xFFFFFFFF;
  for (let i = 0; i < bytes.length; i++) {
    crc = crcTable[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
  }
  return (crc ^ 0xFFFFFFFF) >>> 0;
}

// ── Helpers ──────────────────────────────────────────────────────────

const PNG_SIG = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];

function isPng(data) {
  if (data.length < 8) return false;
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIG[i]) return false;
  }
  return true;
}

function chunkType(data, offset) {
  return String.fromCharCode(
    data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7],
  );
}

function readUint32(data, offset) {
  return new DataView(data.buffer, data.byteOffset + offset, 4).getUint32(0);
}

function buildChunk(type, payload) {
  const encoder = new TextEncoder();
  const typeBytes = encoder.encode(type);
  const forCrc = new Uint8Array(4 + payload.length);
  forCrc.set(typeBytes, 0);
  forCrc.set(payload, 4);

  const chunk = new Uint8Array(12 + payload.length);
  const view = new DataView(chunk.buffer);
  view.setUint32(0, payload.length);
  chunk.set(typeBytes, 4);
  chunk.set(payload, 8);
  view.setUint32(8 + payload.length, crc32(forCrc));
  return chunk;
}

function parseTextChunk(type, chunkData) {
  const decoder = new TextDecoder();
  const keywordEnd = chunkData.indexOf(0);
  if (keywordEnd === -1) return null;

  const keyword = decoder.decode(chunkData.subarray(0, keywordEnd));
  if (keyword !== 'workflow') return null;

  if (type !== 'iTXt') return null;

  const compressionFlagIdx = keywordEnd + 1;
  const compressionMethodIdx = keywordEnd + 2;
  if (compressionMethodIdx >= chunkData.length) return null;

  const compressionFlag = chunkData[compressionFlagIdx];
  if (compressionFlag !== 0) {
    throw new Error('Compressed PNG workflow metadata is not supported');
  }

  let offset = compressionMethodIdx + 1;
  const languageEnd = chunkData.indexOf(0, offset);
  if (languageEnd === -1) return null;

  offset = languageEnd + 1;
  const translatedEnd = chunkData.indexOf(0, offset);
  if (translatedEnd === -1) return null;

  return JSON.parse(decoder.decode(chunkData.subarray(translatedEnd + 1)));
}

// ── Public API ───────────────────────────────────────────────────────

/**
 * Embed a workflow object into a PNG blob as an iTXt chunk.
 * Returns a new Blob with the metadata inserted before IEND.
 */
export async function embedWorkflow(pngBlob, workflow) {
  const data = new Uint8Array(await pngBlob.arrayBuffer());
  if (!isPng(data)) throw new Error('Not a valid PNG file');

  const encoder = new TextEncoder();

  // Build iTXt payload:
  // keyword \0 compression-flag compression-method language-tag \0 translated-keyword \0 text
  const key = encoder.encode('workflow');
  const val = encoder.encode(JSON.stringify(workflow));
  const payload = new Uint8Array(key.length + 5 + val.length);
  payload.set(key, 0);
  payload.set(val, key.length + 5);
  const chunk = buildChunk('iTXt', payload);

  // Locate IEND
  let pos = 8;
  let iendPos = data.length;
  while (pos < data.length) {
    if (pos + 8 > data.length) break;
    const len = readUint32(data, pos);
    if (pos + 12 + len > data.length) break;
    if (chunkType(data, pos) === 'IEND') { iendPos = pos; break; }
    pos += 12 + len;
  }

  // Splice: [before IEND] + [tEXt chunk] + [IEND]
  const result = new Uint8Array(data.length + chunk.length);
  result.set(data.subarray(0, iendPos), 0);
  result.set(chunk, iendPos);
  result.set(data.subarray(iendPos), iendPos + chunk.length);

  return new Blob([result], { type: 'image/png' });
}

/**
 * Extract the workflow object from a PNG blob's iTXt chunks.
 * Returns the parsed object, or null if no "workflow" key is found.
 */
export async function extractWorkflow(pngBlob) {
  const data = new Uint8Array(await pngBlob.arrayBuffer());
  if (!isPng(data)) return null;

  let pos = 8;
  let found = null;

  while (pos + 8 <= data.length) {
    const len = readUint32(data, pos);
    if (pos + 12 + len > data.length) break;
    const type = chunkType(data, pos);

    if (type === 'iTXt') {
      const chunkData = data.subarray(pos + 8, pos + 8 + len);
      const parsed = parseTextChunk(type, chunkData);
      if (parsed) found = parsed;
    }

    if (type === 'IEND') break;
    pos += 12 + len;
  }

  return found;
}
