//! PNG, JPEG and GIF (media-identify §5.11 to §5.13). No pixels are read.

use super::{be16, be32, clean, get, le16, Candidate, Source, Strength, MAX_STEPS};
use crate::codecs::*;
use serde_json::json;
use std::io;

/// PNG's checksum: the reflected polynomial 0xEDB88320, from all ones, inverted at the end.
fn crc32_png(b: &[u8]) -> u32 {
    let mut c = 0xffff_ffffu32;
    for &x in b {
        c ^= x as u32;
        for _ in 0..8 {
            c = if c & 1 != 0 { (c >> 1) ^ 0xedb8_8320 } else { c >> 1 };
        }
    }
    !c
}

pub fn png(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    if head.get(..8) != Some(b"\x89PNG\r\n\x1a\n") {
        return Ok(None);
    }
    let size = src.size();
    let mut c = Candidate::new("png", "image");
    c.cap(CAP_IMAGE_PNG);
    c.see(0, 8, "signature");
    let (mut pos, mut chunks) = (8u64, 0u64);
    loop {
        if chunks == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        if pos == size {
            c.stop(pos, "overrun");
            break;
        }
        let h = get(src, pos, 8)?;
        if h.len() < 8 {
            c.stop(pos, "overrun");
            break;
        }
        let len = be32(&h, 0).unwrap_or(0);
        let ty = &h[4..8];
        if !ty.iter().all(u8::is_ascii_alphabetic) {
            c.stop(pos, "invalid");
            break;
        }
        if chunks == 0 {
            if ty != b"IHDR" || len != 13 {
                c.stop(pos, "invalid");
                break;
            }
            let b = get(src, pos + 4, 21)?;
            if b.len() < 21 {
                c.stop(pos, "overrun");
                break;
            }
            if crc32_png(&b[..17]) as u64 != be32(&b, 17).unwrap_or(0) {
                c.stop(pos, "crc");
                break;
            }
            c.see(pos, 25, "ihdr");
            c.fact("width", be32(&b, 4));
            c.fact("height", be32(&b, 8));
            c.fact("bit_depth", b[12]);
            c.fact("colour_type", b[13]);
            c.fact("interlace", b[16]);
            c.strength = Strength::Structure;
        }
        if ty == b"acTL" {
            c.fact("animated", true);
        }
        if ty == b"cICP" {
            let b = get(src, pos + 8, 4)?;
            if b.len() == 4 {
                c.fact("cicp", json!(b));
            }
        }
        if pos + 12 + len > size {
            c.stop(pos, "overrun");
            break;
        }
        pos += 12 + len;
        chunks += 1;
        if ty == b"IEND" {
            c.see(pos - 12 - len, 12, "iend");
            break;
        }
    }
    c.walked_to = Some(pos);
    c.fact("chunks_read", chunks);
    Ok(Some(c))
}

pub fn jpeg(src: &dyn Source, head: &[u8]) -> io::Result<Option<Candidate>> {
    if head.get(..3) != Some(b"\xff\xd8\xff") {
        return Ok(None);
    }
    let size = src.size();
    let mut c = Candidate::new("jpeg", "image");
    c.cap(CAP_IMAGE_JPEG);
    c.see(0, 2, "soi");
    let (mut pos, mut markers) = (2u64, 0u64);
    let mut apps: Vec<String> = Vec::new();
    loop {
        if markers == MAX_STEPS {
            c.stop(pos, "limit");
            break;
        }
        let h = get(src, pos, 4)?;
        if h.len() < 2 {
            c.stop(pos, "overrun");
            break;
        }
        if h[0] != 0xff {
            c.stop(pos, "invalid");
            break;
        }
        let m = h[1];
        // A fill byte before a marker.
        if m == 0xff {
            pos += 1;
            markers += 1;
            continue;
        }
        if m == 0xda {
            c.see(pos, 2, "sos");
            break;
        }
        if h.len() < 4 {
            c.stop(pos, "overrun");
            break;
        }
        let len = be16(&h, 2).unwrap_or(0);
        if len < 2 {
            c.stop(pos, "invalid");
            break;
        }
        if (0xe0..=0xef).contains(&m) {
            let b = get(src, pos + 4, (len - 2).min(32) as usize)?;
            let name = match b.as_slice() {
                x if x.starts_with(b"JFIF\0") => "jfif",
                x if x.starts_with(b"Exif\0\0") => "exif",
                x if x.starts_with(b"ICC_PROFILE\0") => "icc",
                x if x.starts_with(b"MPF\0") => "mpf",
                x if x.starts_with(b"http://ns.adobe.com/xap/1.0/\0") => "xmp",
                x if x.starts_with(b"Adobe") => "adobe", // allow-name: the APP14 segment's identifier, a byte signature a JPEG carries
                _ => "other",
            };
            if !apps.iter().any(|a| a == name) && apps.len() < 8 {
                apps.push(name.to_string());
            }
        }
        if matches!(m, 0xc0..=0xc3 | 0xc5..=0xc7 | 0xc9..=0xcb | 0xcd..=0xcf) {
            let b = get(src, pos + 4, 6)?;
            if b.len() < 6 {
                c.stop(pos, "overrun");
                break;
            }
            c.see(pos, 2 + len, "sof");
            c.fact("process", match m {
                0xc0 => "baseline",
                0xc1 => "extended",
                0xc2 => "progressive",
                0xc3 => "lossless",
                0xc9..=0xcb => "arithmetic",
                _ => "hierarchical",
            });
            c.fact("precision", b[0]);
            c.fact("height", be16(&b, 1));
            c.fact("width", be16(&b, 3));
            c.fact("components", b[5]);
            c.strength = Strength::Structure;
        }
        if pos + 2 + len > size {
            c.stop(pos, "overrun");
            break;
        }
        pos += 2 + len;
        markers += 1;
    }
    c.walked_to = Some(pos);
    c.fact("applications", json!(apps));
    Ok(Some(c))
}

pub fn gif(head: &[u8], size: u64) -> Option<Candidate> {
    let version = head.get(..6)?;
    if version != b"GIF87a" && version != b"GIF89a" {
        return None;
    }
    let mut c = Candidate::new("gif", "image");
    c.see(0, 6, format!("signature:{}", clean(version)));
    let cut = |c: &mut Candidate, pos: usize| c.stop(pos as u64, if (head.len() as u64) < size { "limit" } else { "overrun" });
    let Some(lsd) = head.get(6..13) else {
        c.walked_to = Some(6);
        cut(&mut c, 6);
        return Some(c);
    };
    c.fact("width", le16(lsd, 0));
    c.fact("height", le16(lsd, 2));
    let mut pos = 13 + if lsd[4] & 0x80 != 0 { 3usize << ((lsd[4] & 7) + 1) } else { 0 };
    let mut frames = 0u64;
    let mut steps = 0u64;
    let mut ended = false;
    // Sub-blocks from `p`, to just after their terminator.
    let blocks = |mut p: usize| -> Option<usize> {
        loop {
            let n = *head.get(p)? as usize;
            p += 1 + n;
            if n == 0 {
                return Some(p);
            }
        }
    };
    loop {
        if steps == MAX_STEPS {
            c.stop(pos as u64, "limit");
            break;
        }
        let Some(&b) = head.get(pos) else {
            cut(&mut c, pos);
            break;
        };
        match b {
            0x3b => {
                c.see(pos as u64, 1, "trailer");
                pos += 1;
                ended = true;
                break;
            }
            0x21 => {
                let Some(p) = blocks(pos + 2) else {
                    cut(&mut c, pos);
                    break;
                };
                pos = p;
            }
            0x2c => {
                let Some(id) = head.get(pos + 1..pos + 10) else {
                    cut(&mut c, pos);
                    break;
                };
                let table = if id[8] & 0x80 != 0 { 3usize << ((id[8] & 7) + 1) } else { 0 };
                let Some(p) = blocks(pos + 10 + table + 1) else {
                    cut(&mut c, pos);
                    break;
                };
                if frames == 0 {
                    c.see(pos as u64, 10, "image");
                }
                frames += 1;
                pos = p;
            }
            _ => {
                c.stop(pos as u64, "invalid");
                break;
            }
        }
        steps += 1;
    }
    c.walked_to = Some(pos as u64);
    c.fact("frames_read", frames);
    if frames >= 1 || ended {
        c.strength = Strength::Structure;
    }
    Some(c)
}
