/**
 * Utilities for reading and writing PNG metadata (tEXt chunks)
 * Used to embed user color in avatar images
 */

/* eslint-disable no-bitwise */
/* eslint-disable no-plusplus */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-console */

/** PNG signature bytes */
const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);

/** Key used to store user color in PNG metadata */
export const PAARROT_COLOR_KEY = 'paarrot:color';

/** Key used to store banner URL in PNG metadata */
export const PAARROT_BANNER_KEY = 'paarrot:banner';

/** Key used to store avatar border color in PNG metadata */
export const PAARROT_BORDER_COLOR_KEY = 'paarrot:borderColor';

/** Key used to store profile gradient (JSON) in PNG metadata */
export const PAARROT_GRADIENT_KEY = 'paarrot:gradient';

let crc32Table: Uint32Array | null = null;
function getCRC32Table(): Uint32Array {
  if (crc32Table) return crc32Table;
  
  crc32Table = 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);
    }
    crc32Table[i] = c;
  }
  return crc32Table;
}

/**
 * Calculate CRC32 for a chunk
 */
function crc32(data: Uint8Array): number {
  let crc = 0xffffffff;
  const table = getCRC32Table();
  
  for (let i = 0; i < data.length; i++) {
    crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xff];
  }
  
  return (crc ^ 0xffffffff) >>> 0;
}

/**
 * Read a 32-bit big-endian integer from a buffer
 */
function readUint32BE(data: Uint8Array, offset: number): number {
  return (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
}

/**
 * Write a 32-bit big-endian integer to a buffer
 */
function writeUint32BE(value: number): Uint8Array {
  return new Uint8Array([
    (value >>> 24) & 0xff,
    (value >>> 16) & 0xff,
    (value >>> 8) & 0xff,
    value & 0xff,
  ]);
}

/**
 * Create a tEXt chunk with the given key and value
 */
function createTextChunk(key: string, value: string): Uint8Array {
  const keyBytes = new TextEncoder().encode(key);
  const valueBytes = new TextEncoder().encode(value);
  
  // tEXt chunk data: key + null byte + value
  const chunkData = new Uint8Array(keyBytes.length + 1 + valueBytes.length);
  chunkData.set(keyBytes, 0);
  chunkData[keyBytes.length] = 0; // null separator
  chunkData.set(valueBytes, keyBytes.length + 1);
  
  const chunkType = new TextEncoder().encode('tEXt');
  
  // Calculate CRC over type + data
  const crcData = new Uint8Array(4 + chunkData.length);
  crcData.set(chunkType, 0);
  crcData.set(chunkData, 4);
  const crc = crc32(crcData);
  
  // Full chunk: length (4) + type (4) + data + crc (4)
  const chunk = new Uint8Array(4 + 4 + chunkData.length + 4);
  chunk.set(writeUint32BE(chunkData.length), 0);
  chunk.set(chunkType, 4);
  chunk.set(chunkData, 8);
  chunk.set(writeUint32BE(crc), 8 + chunkData.length);
  
  return chunk;
}

/**
 * Parse PNG chunks from image data
 */
function parseChunks(data: Uint8Array): Array<{ type: string; data: Uint8Array; offset: number; length: number }> {
  const chunks: Array<{ type: string; data: Uint8Array; offset: number; length: number }> = [];
  let offset = 8; // Skip PNG signature
  
  while (offset < data.length) {
    const length = readUint32BE(data, offset);
    const type = new TextDecoder().decode(data.slice(offset + 4, offset + 8));
    const chunkData = data.slice(offset + 8, offset + 8 + length);
    
    chunks.push({
      type,
      data: chunkData,
      offset,
      length: 12 + length, // length field + type + data + crc
    });
    
    offset += 12 + length;
    
    if (type === 'IEND') break;
  }
  
  return chunks;
}

/**
 * Read a tEXt chunk value
 */
function parseTextChunk(data: Uint8Array): { key: string; value: string } | null {
  const nullIndex = data.indexOf(0);
  if (nullIndex === -1) return null;
  
  const key = new TextDecoder().decode(data.slice(0, nullIndex));
  const value = new TextDecoder().decode(data.slice(nullIndex + 1));
  
  return { key, value };
}

/**
 * Extract paarrot color from PNG image data
 * @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
 * @returns The color string if found, or undefined
 */
export function extractColorFromPNG(imageData: ArrayBuffer | Uint8Array): string | undefined {
  const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  
  // Verify PNG signature
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIGNATURE[i]) {
      console.warn('[extractColorFromPNG] Not a valid PNG');
      return undefined; // Not a PNG
    }
  }
  
  const chunks = parseChunks(data);
  console.log('[extractColorFromPNG] Found', chunks.length, 'chunks');
  
  let textChunksFound = 0;
  for (const chunk of chunks) {
    if (chunk.type === 'tEXt') {
      textChunksFound++;
      const text = parseTextChunk(chunk.data);
      if (text) {
        console.log('[extractColorFromPNG] Found tEXt chunk:', text.key, '=', text.value);
        if (text.key === PAARROT_COLOR_KEY) {
          console.log('[extractColorFromPNG] Found paarrot:color =', text.value);
          return text.value;
        }
      }
    }
  }
  
  console.log('[extractColorFromPNG] No paarrot:color found (found', textChunksFound, 'tEXt chunks)');
  return undefined;
}

/**
 * Extract paarrot banner URL from PNG image data
 * @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
 * @returns The banner MXC URL if found, or undefined
 */
export function extractBannerFromPNG(imageData: ArrayBuffer | Uint8Array): string | undefined {
  const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  
  // Verify PNG signature
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIGNATURE[i]) {
      return undefined; // Not a PNG
    }
  }
  
  const chunks = parseChunks(data);
  
  for (const chunk of chunks) {
    if (chunk.type === 'tEXt') {
      const text = parseTextChunk(chunk.data);
      if (text && text.key === PAARROT_BANNER_KEY) {
        return text.value;
      }
    }
  }
  
  return undefined;
}

import type { ImageMetadata, ProfileGradient } from './imageMetadata';

/**
 * Extract all paarrot metadata from PNG image data
 * @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
 * @returns Object with all metadata fields if found
 */
export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
  const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  const metadata: ImageMetadata = {};
  
  // Verify PNG signature
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIGNATURE[i]) {
      console.warn('[extractMetadataFromPNG] Not a valid PNG');
      return metadata; // Not a PNG
    }
  }
  
  const chunks = parseChunks(data);
  console.log('[extractMetadataFromPNG] Found', chunks.length, 'chunks');
  
  let textChunksFound = 0;
  for (const chunk of chunks) {
    if (chunk.type === 'tEXt') {
      textChunksFound++;
      const text = parseTextChunk(chunk.data);
      if (text) {
        console.log('[extractMetadataFromPNG] Found tEXt chunk:', text.key, '=', text.value);
        if (text.key === PAARROT_COLOR_KEY) {
          metadata.color = text.value;
        } else if (text.key === PAARROT_BANNER_KEY) {
          metadata.banner = text.value;
        } else if (text.key === PAARROT_BORDER_COLOR_KEY) {
          metadata.avatarBorderColor = text.value;
        } else if (text.key === PAARROT_GRADIENT_KEY) {
          try {
            metadata.gradient = JSON.parse(text.value) as ProfileGradient;
          } catch {
            // Invalid JSON, skip
          }
        }
      }
    }
  }
  
  console.log('[extractMetadataFromPNG] Final metadata extracted:', metadata, 'found', textChunksFound, 'tEXt chunks');
  
  return metadata;
}

/**
 * Embed paarrot color into PNG image data
 * @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
 * @param color - The color string to embed (e.g., '#ff5500')
 * @returns New PNG data with embedded color, or null if not a valid PNG
 */
export function embedColorInPNG(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
  const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  
  // Verify PNG signature
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIGNATURE[i]) {
      return null; // Not a PNG
    }
  }
  
  const chunks = parseChunks(data);
  
  // Find IHDR chunk (always first) and any existing paarrot tEXt chunk
  let insertOffset = 8; // After signature
  let existingPaarrotChunk: { offset: number; length: number } | null = null;
  
  for (const chunk of chunks) {
    if (chunk.type === 'IHDR') {
      insertOffset = chunk.offset + chunk.length; // Insert after IHDR
    } else if (chunk.type === 'tEXt') {
      const text = parseTextChunk(chunk.data);
      if (text && text.key === PAARROT_COLOR_KEY) {
        existingPaarrotChunk = { offset: chunk.offset, length: chunk.length };
      }
    }
  }
  
  // Create new tEXt chunk with color
  const colorChunk = createTextChunk(PAARROT_COLOR_KEY, color);
  
  // Build new PNG data
  let newData: Uint8Array;
  
  if (existingPaarrotChunk) {
    // Replace existing chunk
    newData = new Uint8Array(data.length - existingPaarrotChunk.length + colorChunk.length);
    newData.set(data.slice(0, existingPaarrotChunk.offset), 0);
    newData.set(colorChunk, existingPaarrotChunk.offset);
    newData.set(
      data.slice(existingPaarrotChunk.offset + existingPaarrotChunk.length),
      existingPaarrotChunk.offset + colorChunk.length
    );
  } else {
    // Insert new chunk after IHDR
    newData = new Uint8Array(data.length + colorChunk.length);
    newData.set(data.slice(0, insertOffset), 0);
    newData.set(colorChunk, insertOffset);
    newData.set(data.slice(insertOffset), insertOffset + colorChunk.length);
  }
  
  return newData;
}

/** All paarrot metadata keys */
const PAARROT_KEYS = [PAARROT_COLOR_KEY, PAARROT_BANNER_KEY, PAARROT_BORDER_COLOR_KEY, PAARROT_GRADIENT_KEY];

/**
 * Embed paarrot metadata into PNG image data
 * Preserves existing metadata that is not being updated
 * @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
 * @param metadata - Object with metadata fields to embed
 * @returns New PNG data with embedded metadata, or null if not a valid PNG
 */
export function embedMetadataInPNG(
  imageData: ArrayBuffer | Uint8Array,
  metadata: ImageMetadata
): Uint8Array | null {
  const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  
  // Verify PNG signature
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIGNATURE[i]) {
      return null; // Not a PNG
    }
  }
  
  const chunks = parseChunks(data);
  
  // Get existing metadata
  const existing = extractMetadataFromPNG(data);
  
  // Merge with new metadata
  const finalMetadata: ImageMetadata = {
    color: metadata.color !== undefined ? metadata.color : existing.color,
    banner: metadata.banner !== undefined ? metadata.banner : existing.banner,
    avatarBorderColor: metadata.avatarBorderColor !== undefined ? metadata.avatarBorderColor : existing.avatarBorderColor,
    gradient: metadata.gradient !== undefined ? metadata.gradient : existing.gradient,
  };
  
  // Find IHDR chunk and existing paarrot chunks
  let insertOffset = 8;
  const existingChunks: { key: string; offset: number; length: number }[] = [];
  
  for (const chunk of chunks) {
    if (chunk.type === 'IHDR') {
      insertOffset = chunk.offset + chunk.length;
    } else if (chunk.type === 'tEXt') {
      const text = parseTextChunk(chunk.data);
      if (text && PAARROT_KEYS.includes(text.key)) {
        existingChunks.push({ key: text.key, offset: chunk.offset, length: chunk.length });
      }
    }
  }
  
  // Create new chunks
  const newChunks: Uint8Array[] = [];
  if (finalMetadata.color) {
    newChunks.push(createTextChunk(PAARROT_COLOR_KEY, finalMetadata.color));
  }
  if (finalMetadata.banner) {
    newChunks.push(createTextChunk(PAARROT_BANNER_KEY, finalMetadata.banner));
  }
  if (finalMetadata.avatarBorderColor) {
    newChunks.push(createTextChunk(PAARROT_BORDER_COLOR_KEY, finalMetadata.avatarBorderColor));
  }
  if (finalMetadata.gradient) {
    newChunks.push(createTextChunk(PAARROT_GRADIENT_KEY, JSON.stringify(finalMetadata.gradient)));
  }
  
  // Calculate new size
  const removedSize = existingChunks.reduce((sum, chunk) => sum + chunk.length, 0);
  const addedSize = newChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
  const newSize = data.length - removedSize + addedSize;
  
  // Build new PNG
  const newData = new Uint8Array(newSize);
  let writeOffset = 0;
  let readOffset = 0;
  
  // Sort existing chunks by offset
  existingChunks.sort((a, b) => a.offset - b.offset);
  
  // Copy data, skipping old chunks and inserting new ones
  for (const existingChunk of existingChunks) {
    // Copy data before this chunk
    newData.set(data.slice(readOffset, existingChunk.offset), writeOffset);
    writeOffset += existingChunk.offset - readOffset;
    
    // Skip the old chunk
    readOffset = existingChunk.offset + existingChunk.length;
  }
  
  // If no existing chunks, insert at insertOffset
  if (existingChunks.length === 0) {
    newData.set(data.slice(0, insertOffset), 0);
    writeOffset = insertOffset;
    readOffset = insertOffset;
  }
  
  // Insert new chunks
  for (const newChunk of newChunks) {
    newData.set(newChunk, writeOffset);
    writeOffset += newChunk.byteLength;
  }
  
  // Copy remaining data
  newData.set(data.slice(readOffset), writeOffset);
  
  return newData;
}

/**
 * Remove paarrot color from PNG image data
 * @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
 * @returns New PNG data without the color chunk, or null if not a valid PNG
 */
export function removeColorFromPNG(imageData: ArrayBuffer | Uint8Array): Uint8Array | null {
  const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
  
  // Verify PNG signature
  for (let i = 0; i < 8; i++) {
    if (data[i] !== PNG_SIGNATURE[i]) {
      return null; // Not a PNG
    }
  }
  
  const chunks = parseChunks(data);
  
  // Find existing paarrot tEXt chunk
  let existingPaarrotChunk: { offset: number; length: number } | null = null;
  
  for (const chunk of chunks) {
    if (chunk.type === 'tEXt') {
      const text = parseTextChunk(chunk.data);
      if (text && text.key === PAARROT_COLOR_KEY) {
        existingPaarrotChunk = { offset: chunk.offset, length: chunk.length };
        break;
      }
    }
  }
  
  if (!existingPaarrotChunk) {
    // No color to remove
    return data;
  }
  
  // Build new PNG data without the color chunk
  const newData = new Uint8Array(data.length - existingPaarrotChunk.length);
  newData.set(data.slice(0, existingPaarrotChunk.offset), 0);
  newData.set(
    data.slice(existingPaarrotChunk.offset + existingPaarrotChunk.length),
    existingPaarrotChunk.offset
  );
  
  return newData;
}

/**
 * Fetch an image and extract the paarrot color from it
 * @param url - The URL of the image to fetch
 * @returns The color string if found, or undefined
 */
export async function fetchAndExtractColor(url: string): Promise<string | undefined> {
  try {
    const response = await fetch(url);
    if (!response.ok) return undefined;
    
    const arrayBuffer = await response.arrayBuffer();
    return extractColorFromPNG(arrayBuffer);
  } catch {
    return undefined;
  }
}
