//! C ABI for the zeroth-condition codec wrappers (feature `codecs`): encode and
//! decode through PNG, AVIF, JPEG XL, Opus, FLAC, zstd, XZ and AV1. The envelope,
//! buffer and error surface they share is [`crate::ffi`] (feature `ffi`), which
//! re-exports these functions.

use std::ffi::c_int;
use std::panic;
use std::slice;

use crate::codecs;
use crate::ffi::*;

fn ffi_codec<T, F>(f: F) -> std::result::Result<Vec<u8>, (c_int, String)>
where F: FnOnce() -> std::result::Result<T, String>,
      T: Into<Vec<u8>>,
{
    f().map(Into::into).map_err(|e| (WAI_ERR_CODEC, e))
}

// ---- image: PNG -----------------------------------------------------
/// Encode RGB pixels (row-major, 3 bytes per pixel) as PNG.
///
/// # Safety
/// `rgb` must point to a valid RGB buffer of at least `len` bytes;
/// `out` must be a writable `WaiBuffer*`. Pass `(null, 0)` for empty.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_image_png_encode(
    rgb: *const u8, len: usize, h: u32, w: u32, out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(rgb, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "rgb pointer null with non-zero len".into()))?;
        let expected = (h as usize)
            .checked_mul(w as usize)
            .and_then(|n| n.checked_mul(3))
            .ok_or((WAI_ERR_INVALID_ARG, "h*w*3 overflows".into()))?;
        if s.len() != expected {
            return Err((WAI_ERR_INVALID_ARG,
                format!("rgb len {} != h*w*3 = {}", s.len(), expected)));
        }
        ffi_codec(|| codecs::image::png_encode(s, h, w)
            .map_err(|e| format!("{e:?}")))
    })
}

/// Decode a PNG byte stream to RGB. Returns `(rgb, h, w)`.
///
/// A picture past the reader limits of SPEC §2 (wider or taller than
/// [`IMAGE_MAX_SIDE`](crate::codecs::image::IMAGE_MAX_SIDE) or holding more than
/// [`IMAGE_MAX_PIXELS`](crate::codecs::image::IMAGE_MAX_PIXELS) pixels) is refused with
/// `WAI_ERR_CODEC` from the size it declares, before it decodes; the message begins
/// `image_too_large`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable; `out_h` writable; `out_w` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_image_png_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer,
    out_h: *mut u32, out_w: *mut u32,
) -> c_int {
    if out_h.is_null() || out_w.is_null() {
        set_err("out_h/out_w null");
        return WAI_ERR_INVALID_ARG;
    }
    decode_to_rgb_size(out, out_h, out_w, || {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes pointer null with non-zero len".into()))?;
        codecs::image::png_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

fn decode_to_rgb_size<F>(out: *mut WaiBuffer, out_h: *mut u32, out_w: *mut u32, f: F) -> c_int
where F: FnOnce() -> std::result::Result<(Vec<u8>, u32, u32), (c_int, String)> + panic::UnwindSafe
{
    if out.is_null() {
        set_err("out null"); return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(f) {
        Ok(Ok((v, h, w))) => unsafe {
            *out = WaiBuffer::from_vec(v); *out_h = h; *out_w = w; WAI_OK
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe { clear_buf(out); *out_h = 0; *out_w = 0; }
            code
        }
        Err(p) => {
            let msg = if let Some(s) = p.downcast_ref::<&str>() { (*s).to_string() }
                      else if let Some(s) = p.downcast_ref::<String>() { s.clone() }
                      else { "panic with non-string payload".into() };
            set_err(format!("internal panic: {msg}"));
            unsafe { clear_buf(out); *out_h = 0; *out_w = 0; }
            WAI_ERR_PANIC
        }
    }
}

// ---- image: AVIF ----------------------------------------------------
/// # Safety
/// `rgb` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_image_avif_encode(
    rgb: *const u8, len: usize, h: u32, w: u32, quality: u8,
    out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(rgb, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "rgb null + len>0".into()))?;
        let expected = (h as usize).checked_mul(w as usize)
            .and_then(|n| n.checked_mul(3))
            .ok_or((WAI_ERR_INVALID_ARG, "h*w*3 overflows".into()))?;
        if s.len() != expected {
            return Err((WAI_ERR_INVALID_ARG,
                format!("rgb len {} != h*w*3 = {}", s.len(), expected)));
        }
        codecs::image::avif_encode(s, h, w, quality)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// A picture past the reader limits of SPEC §2 (wider or taller than
/// [`IMAGE_MAX_SIDE`](crate::codecs::image::IMAGE_MAX_SIDE) or holding more than
/// [`IMAGE_MAX_PIXELS`](crate::codecs::image::IMAGE_MAX_PIXELS) pixels) is refused with
/// `WAI_ERR_CODEC` from the size it declares, before it decodes; the message begins
/// `image_too_large`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable; `out_h` writable; `out_w` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_image_avif_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer,
    out_h: *mut u32, out_w: *mut u32,
) -> c_int {
    if out_h.is_null() || out_w.is_null() {
        set_err("out_h/out_w null"); return WAI_ERR_INVALID_ARG;
    }
    decode_to_rgb_size(out, out_h, out_w, || {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        codecs::image::avif_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

// ---- image: JPEG-XL -------------------------------------------------
/// `quality = 0` ⇒ lossless. Otherwise butteraugli distance proxy
/// (higher quality = smaller distance internally).
///
/// # Safety
/// `rgb` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_image_jxl_encode(
    rgb: *const u8, len: usize, h: u32, w: u32, quality: u8,
    out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(rgb, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "rgb null + len>0".into()))?;
        let expected = (h as usize).checked_mul(w as usize)
            .and_then(|n| n.checked_mul(3))
            .ok_or((WAI_ERR_INVALID_ARG, "h*w*3 overflows".into()))?;
        if s.len() != expected {
            return Err((WAI_ERR_INVALID_ARG,
                format!("rgb len {} != h*w*3 = {}", s.len(), expected)));
        }
        let q = if quality == 0 { None } else { Some(quality as f32) };
        codecs::image::jxl_encode(s, h, w, q)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// A picture past the reader limits of SPEC §2 (wider or taller than
/// [`IMAGE_MAX_SIDE`](crate::codecs::image::IMAGE_MAX_SIDE) or holding more than
/// [`IMAGE_MAX_PIXELS`](crate::codecs::image::IMAGE_MAX_PIXELS) pixels) is refused with
/// `WAI_ERR_CODEC` from the size it declares, before it decodes; the message begins
/// `image_too_large`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable; `out_h` writable; `out_w` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_image_jxl_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer,
    out_h: *mut u32, out_w: *mut u32,
) -> c_int {
    if out_h.is_null() || out_w.is_null() {
        set_err("out_h/out_w null"); return WAI_ERR_INVALID_ARG;
    }
    decode_to_rgb_size(out, out_h, out_w, || {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        codecs::image::jxl_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

// ---- audio: Opus ----------------------------------------------------
/// # Safety
/// `samples` valid for `n` `f32`s; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_audio_opus_encode(
    samples: *const f32, n: usize, sr: u32, bitrate_bps: i32,
    out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        if samples.is_null() && n != 0 {
            return Err((WAI_ERR_INVALID_ARG, "samples null + n>0".into()));
        }
        let s = if n == 0 { &[][..] }
                else { unsafe { slice::from_raw_parts(samples, n) } };
        codecs::audio::opus_encode(s, sr, bitrate_bps)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// Output past the reader limit of SPEC §2,
/// [`AUDIO_DECODED_MAX_SAMPLES`](crate::codecs::audio::AUDIO_DECODED_MAX_SAMPLES)
/// samples, is refused with `WAI_ERR_CODEC`; the message begins
/// `audio_decoded_too_long`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable; `out_sr` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_audio_opus_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer, out_sr: *mut u32,
) -> c_int {
    if out_sr.is_null() {
        set_err("out_sr null"); return WAI_ERR_INVALID_ARG;
    }
    if out.is_null() {
        set_err("out null"); return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(|| -> std::result::Result<(Vec<f32>, u32), (c_int, String)> {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        codecs::audio::opus_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    }) {
        Ok(Ok((samples, sr))) => {
            let bytes: Vec<u8> = samples.iter().flat_map(|f| f.to_le_bytes()).collect();
            unsafe { *out = WaiBuffer::from_vec(bytes); *out_sr = sr; }
            WAI_OK
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe { clear_buf(out); *out_sr = 0; }
            code
        }
        Err(_) => { set_err("internal panic in opus_decode");
            unsafe { clear_buf(out); *out_sr = 0; } WAI_ERR_PANIC }
    }
}

// ---- audio: FLAC ----------------------------------------------------
/// # Safety
/// `samples` valid for `n` `f32`s; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_audio_flac_encode(
    samples: *const f32, n: usize, sr: u32, out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        if samples.is_null() && n != 0 {
            return Err((WAI_ERR_INVALID_ARG, "samples null + n>0".into()));
        }
        let s = if n == 0 { &[][..] }
                else { unsafe { slice::from_raw_parts(samples, n) } };
        codecs::audio::flac_encode(s, sr)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// Output past the reader limit of SPEC §2,
/// [`AUDIO_DECODED_MAX_SAMPLES`](crate::codecs::audio::AUDIO_DECODED_MAX_SAMPLES)
/// samples, is refused with `WAI_ERR_CODEC`; the message begins
/// `audio_decoded_too_long`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable; `out_sr` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_audio_flac_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer, out_sr: *mut u32,
) -> c_int {
    if out_sr.is_null() || out.is_null() {
        set_err("out/out_sr null"); return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(|| -> std::result::Result<(Vec<f32>, u32), (c_int, String)> {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        codecs::audio::flac_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    }) {
        Ok(Ok((samples, sr))) => {
            let bytes: Vec<u8> = samples.iter().flat_map(|f| f.to_le_bytes()).collect();
            unsafe { *out = WaiBuffer::from_vec(bytes); *out_sr = sr; }
            WAI_OK
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe { clear_buf(out); *out_sr = 0; }
            code
        }
        Err(_) => { set_err("internal panic in flac_decode");
            unsafe { clear_buf(out); *out_sr = 0; } WAI_ERR_PANIC }
    }
}

// ---- text: zstd / xz ------------------------------------------------
/// # Safety
/// `data` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_text_zstd_encode(
    data: *const u8, len: usize, level: i32, out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(data, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "data null + len>0".into()))?;
        codecs::text::zstd_encode(s, level)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// Decode a `wai.text.zstd` payload within the reader limits of SPEC §2: at most
/// [`TEXT_DECODED_MAX_LEN`](crate::codecs::text::TEXT_DECODED_MAX_LEN) bytes of output
/// and [`TEXT_WINDOW_MAX_LEN`](crate::codecs::text::TEXT_WINDOW_MAX_LEN) bytes of
/// window. A payload past either is refused with `WAI_ERR_CODEC`; the message begins
/// `text_decoded_too_long` or `text_window_too_large`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_text_zstd_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        codecs::text::zstd_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// # Safety
/// `data` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_text_xz_encode(
    data: *const u8, len: usize, level: u32, out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(data, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "data null + len>0".into()))?;
        codecs::text::xz_encode(s, level)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// Decode a `wai.text.xz` payload within the same limits as
/// [`wai_text_zstd_decode`], the window limit applying to the dictionary.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_text_xz_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        codecs::text::xz_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

// ---- video: AV1 -----------------------------------------------------
/// Encode a sequence of `n` RGB frames as AV1.
///
/// # Safety
/// `rgb` valid for `len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_video_av1_encode(
    rgb: *const u8, len: usize,
    n: u32, h: u32, w: u32, fps_num: u32, fps_den: u32,
    lossless: u8, quality: u8,
    out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        let s = unsafe { input_slice(rgb, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "rgb null + len>0".into()))?;
        let per_frame = (h as usize).checked_mul(w as usize)
            .and_then(|x| x.checked_mul(3))
            .ok_or((WAI_ERR_INVALID_ARG, "h*w*3 overflows".into()))?;
        let total = (n as usize).checked_mul(per_frame)
            .ok_or((WAI_ERR_INVALID_ARG, "n*h*w*3 overflows".into()))?;
        if s.len() != total {
            return Err((WAI_ERR_INVALID_ARG,
                format!("video rgb len {} != n*h*w*3 = {}", s.len(), total)));
        }
        if fps_num == 0 || fps_den == 0 {
            return Err((WAI_ERR_INVALID_ARG, "fps_num/fps_den must be non-zero".into()));
        }
        let frames: Vec<Vec<u8>> = (0..n as usize)
            .map(|i| s[i * per_frame..(i + 1) * per_frame].to_vec())
            .collect();
        crate::codecs::video::av1_encode(&frames, h, w, fps_num, fps_den,
                                          lossless != 0, quality)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    })
}

/// Decode an AV1 WAI payload to a packed RGB buffer `n*h*w*3` bytes.
///
/// A payload past the reader limits of SPEC §2 (more than
/// [`VIDEO_MAX_TEMPORAL_UNITS`](crate::codecs::video::VIDEO_MAX_TEMPORAL_UNITS)
/// temporal units, or frames past the picture limits) is refused from its header;
/// the message begins `video_too_large`. Since the buffer holds every frame, so is
/// one whose frames would take more than
/// [`AV1_DECODE_DEFAULT_MAX_LEN`](crate::codecs::video::AV1_DECODE_DEFAULT_MAX_LEN)
/// bytes together (`video_decoded_too_large`), and one whose frames differ in size.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out` writable; `out_n` writable; `out_h` writable; `out_w` writable; `out_fps_num` writable; `out_fps_den` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_video_av1_decode(
    bytes: *const u8, len: usize, out: *mut WaiBuffer,
    out_n: *mut u32, out_h: *mut u32, out_w: *mut u32,
    out_fps_num: *mut u32, out_fps_den: *mut u32,
) -> c_int {
    if out.is_null() || out_n.is_null() || out_h.is_null() || out_w.is_null()
        || out_fps_num.is_null() || out_fps_den.is_null() {
        set_err("video decode: one of the out pointers is null");
        return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(|| -> std::result::Result<(Vec<Vec<u8>>, u32, u32, u32, u32), (c_int, String)> {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        crate::codecs::video::av1_decode(s)
            .map_err(|e| (WAI_ERR_CODEC, format!("{e:?}")))
    }) {
        Ok(Ok((frames, h, w, fps_num, fps_den))) => {
            let n = frames.len() as u32;
            let mut packed = Vec::with_capacity(frames.len() * (h * w * 3) as usize);
            for f in &frames { packed.extend_from_slice(f); }
            unsafe {
                *out = WaiBuffer::from_vec(packed);
                *out_n = n; *out_h = h; *out_w = w;
                *out_fps_num = fps_num; *out_fps_den = fps_den;
            }
            WAI_OK
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe {
                clear_buf(out);
                *out_n = 0; *out_h = 0; *out_w = 0;
                *out_fps_num = 0; *out_fps_den = 0;
            }
            code
        }
        Err(_) => {
            set_err("internal panic in av1_decode");
            unsafe {
                clear_buf(out);
                *out_n = 0; *out_h = 0; *out_w = 0;
                *out_fps_num = 0; *out_fps_den = 0;
            }
            WAI_ERR_PANIC
        }
    }
}

