//! Codec layer — format detection, decoding, and the explicit anti-list.
//!
//! Architecture:
//! - [`FormatRegistry`] holds the list of supported formats and answers
//!   "is this file supported?" queries.
//! - [`Codec`] trait — every decoder implements this.
//! - [`anti_list`] module owns the list of formats we deliberately reject,
//!   with clear user-facing reasons.
//!
//! Decoding flow:
//! 1. Caller passes a path to `FormatRegistry::decode(path)`.
//! 2. Registry checks the extension against the anti-list first. If it matches,
//!    returns [`DecodeError::AntiListed`] immediately — no I/O, no decode attempt.
//! 3. Registry checks the extension against the supported-formats list. If it
//!    matches, hands off to the appropriate codec.
//! 4. Codec reads the file, decodes, returns a [`DecodedImage`].
//! 5. If the extension is unknown (not in anti-list, not in supported list),
//!    returns [`DecodeError::Unsupported`].
//!
//! This layer is intentionally synchronous. Async wrapping happens at the
//! `app` layer via `iced::Task::perform`, which keeps the codec code simple
//! and testable.

pub mod anti_list;
pub mod exr;
pub mod image_crate;
pub mod jxl;
pub mod svg;
pub mod tiff;

use std::path::{Path, PathBuf};

use thiserror::Error;

/// A decoded image ready to hand off to the UI layer.
///
/// Pixels are always RGBA8 (4 bytes per pixel, row-major, top-to-bottom).
/// Any format-specific quirks (palette indices, 16-bit depth, HDR float,
/// premultiplied alpha) are normalized to RGBA8 by the codec.
#[derive(Debug, Clone)]
pub struct DecodedImage {
    pub width: u32,
    pub height: u32,
    pub pixels: Vec<u8>,
    /// Original format name, e.g. "PNG", "JPEG", "AVIF".
    pub format: &'static str,
}

/// Why a decode failed.
///
/// The UI layer renders each variant differently:
/// - `AntiListed` → modal overlay explaining the project's stance.
/// - `Unsupported` → "this format isn't supported" message + link to docs.
/// - `Io` / `Decode` → generic error toast.
#[derive(Debug, Clone, Error)]
pub enum DecodeError {
    #[error("File is on the project's anti-list: {format_name}")]
    AntiListed {
        path: PathBuf,
        format_name: &'static str,
        reason: &'static str,
        alternative: &'static str,
    },

    #[error("Unsupported format: {extension}")]
    Unsupported {
        path: PathBuf,
        extension: String,
    },

    #[error("Could not read file: {0}")]
    Io(String),

    #[error("Decoder failed: {0}")]
    Decode(String),
}

impl From<std::io::Error> for DecodeError {
    fn from(e: std::io::Error) -> Self {
        DecodeError::Io(e.to_string())
    }
}

/// Trait every decoder implements.
///
/// Implementations live in submodules (`image_crate`, future `jxl`, `tiff`, etc.).
/// Each codec is registered in [`FormatRegistry::new`] with the extensions it handles.
pub trait Codec: Send + Sync {
    /// Human-readable name, e.g. "PNG", "JPEG XL".
    /// Reserved for diagnostics and a future "supported formats" dialog.
    #[allow(dead_code)]
    fn name(&self) -> &'static str;

    /// Extensions this codec handles, lowercase, no leading dot.
    /// e.g. `["png", "apng"]`.
    fn extensions(&self) -> &'static [&'static str];

    /// Decode the given bytes. The codec does NOT need to verify the format —
    /// the registry already matched by extension before calling this.
    fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, DecodeError>;
}

/// The central format registry. Owns the codec list and the anti-list.
pub struct FormatRegistry {
    codecs: Vec<Box<dyn Codec>>,
}

impl FormatRegistry {
    /// Construct a registry pre-populated with all Tier 1 codecs. Tier 2
    /// codecs (jxl, tiff, svg, exr) will be added here when their modules
    /// land — see `image_crate` for the implementation template.
    pub fn new() -> Self {
        let mut codecs: Vec<Box<dyn Codec>> = Vec::new();

        // Tier 1: all handled by the `image` crate in one codec.
        codecs.push(Box::new(image_crate::ImageCrateCodec));

        // Tier 2 codecs:
        //   - JXL: pure-Rust JPEG XL (jxl-oxide)
        //   - TIFF: direct `tiff` crate for multi-page / 16-bit
        //   - SVG: vector rasterization (usvg + resvg + tiny-skia)
        //   - EXR: pure-Rust OpenEXR (exr) with Reinhard tone-mapping
        codecs.push(Box::new(jxl::JxlCodec));
        codecs.push(Box::new(tiff::TiffCodec));
        codecs.push(Box::new(svg::SvgCodec));
        codecs.push(Box::new(exr::ExrCodec));

        Self { codecs }
    }

    /// Is this file's extension in the anti-list?
    /// Returns the format info if so.
    pub fn check_anti_list(&self, path: &Path) -> Option<anti_list::AntiListedFormat> {
        let ext = path.extension()?.to_str()?.to_ascii_lowercase();
        anti_list::lookup(&ext)
    }

    /// Is this file's extension supported (in any registered codec)?
    pub fn is_supported(&self, path: &Path) -> bool {
        let ext = match path.extension().and_then(|e| e.to_str()) {
            Some(e) => e.to_ascii_lowercase(),
            None => return false,
        };
        self.codecs.iter().any(|c| c.extensions().contains(&ext.as_str()))
    }

    /// Decode a file. Anti-list is checked first; then supported codecs; then
    /// returns `Unsupported` for everything else.
    pub fn decode(&self, path: &Path) -> Result<DecodedImage, DecodeError> {
        // 1. Anti-list check — fail fast, no I/O.
        if let Some(info) = self.check_anti_list(path) {
            return Err(DecodeError::AntiListed {
                path: path.to_path_buf(),
                format_name: info.name,
                reason: info.reason,
                alternative: info.alternative,
            });
        }

        // 2. Find a codec by extension.
        let ext = path
            .extension()
            .and_then(|e| e.to_str())
            .map(|e| e.to_ascii_lowercase())
            .unwrap_or_default();

        let codec = self
            .codecs
            .iter()
            .find(|c| c.extensions().contains(&ext.as_str()));

        let codec = match codec {
            Some(c) => c,
            None => {
                return Err(DecodeError::Unsupported {
                    path: path.to_path_buf(),
                    extension: ext,
                });
            }
        };

        // 3. Read + decode.
        let bytes = std::fs::read(path)?;
        codec.decode(&bytes)
    }

    /// Iterate all supported extensions (used by the folder walker to filter).
    /// Kept public even though the folder walker uses `is_supported` directly —
    /// future callers (e.g. an "Open File" filter dialog) will need it.
    #[allow(dead_code)]
    pub fn supported_extensions(&self) -> Vec<&'static str> {
        self.codecs.iter().flat_map(|c| c.extensions().iter().copied()).collect()
    }
}

impl Default for FormatRegistry {
    fn default() -> Self {
        Self::new()
    }
}

// ── Pixel rotation ──────────────────────────────────────────────────────

/// Rotate an RGBA pixel buffer by the given degrees (must be 0, 90, 180, or 270).
/// Returns (new_pixels, new_width, new_height).
///
/// 90° = clockwise, 270° = counter-clockwise.
pub fn rotate_rgba(pixels: &[u8], width: u32, height: u32, degrees: i32) -> (Vec<u8>, u32, u32) {
    // Invariant: callers pass one of {0, 90, 180, 270, -90, …}. The modulo
    // normalizes any integer to [0, 360); only the four cardinal values map
    // to a real rotation. Any other value (e.g. 45°) returns the input
    // unchanged — callers in this codebase only ever pass 90-degree steps.
    match ((degrees % 360) + 360) % 360 {
        0 => (pixels.to_vec(), width, height),
        90 => rotate_rgba_cw(pixels, width, height),
        180 => rotate_rgba_180(pixels, width, height),
        270 => rotate_rgba_ccw(pixels, width, height),
        _ => (pixels.to_vec(), width, height),
    }
}

fn rotate_rgba_cw(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // 90° clockwise: new[x][y] = old[h-1-y][x]
    // new dimensions: (h, w) — width and height swap
    // Performance: for loops chosen over iterators for pixel-level hot path.
    // SEI CERT/overflow note: the `as usize` casts are safe on 64-bit because
    // pixel count (w*h*4) is bounded by the original allocation, which already
    // fit in usize. On 32-bit, a single image would need to exceed both the
    // address space and the `image` crate's own limits.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = h - 1 - y;
            let ny = x;
            let dst_idx = ((ny * h + nx) * 4) as usize; // new width = old height
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, h, w)
}

fn rotate_rgba_ccw(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // 90° counter-clockwise: new[x][y] = old[y][w-1-x]
    // Performance: for loops chosen over iterators for pixel-level hot path.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = y;
            let ny = w - 1 - x;
            let dst_idx = ((ny * h + nx) * 4) as usize; // new width = old height
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, h, w)
}

fn rotate_rgba_180(src: &[u8], w: u32, h: u32) -> (Vec<u8>, u32, u32) {
    // 180°: new[x][y] = old[h-1-y][w-1-x]
    // Performance: for loops chosen over iterators for pixel-level hot path.
    let mut dst = vec![0u8; src.len()];
    for y in 0..h {
        for x in 0..w {
            let src_idx = ((y * w + x) * 4) as usize;
            let nx = w - 1 - x;
            let ny = h - 1 - y;
            let dst_idx = ((ny * w + nx) * 4) as usize;
            dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
        }
    }
    (dst, w, h)
}

#[cfg(test)]
mod rotation_tests {
    use super::*;

    fn make_pixels(w: u32, h: u32) -> Vec<u8> {
        let mut v = Vec::with_capacity((w * h * 4) as usize);
        for y in 0..h {
            for x in 0..w {
                v.push(x as u8);
                v.push(y as u8);
                v.push(0);
                v.push(255);
            }
        }
        v
    }

    #[test]
    fn rotate_0_is_identity() {
        let px = make_pixels(3, 2);
        let (out, w, h) = rotate_rgba(&px, 3, 2, 0);
        assert_eq!(w, 3);
        assert_eq!(h, 2);
        assert_eq!(out, px);
    }

    #[test]
    fn rotate_360_is_identity() {
        let px = make_pixels(3, 2);
        let (out, w, h) = rotate_rgba(&px, 3, 2, 360);
        assert_eq!(w, 3);
        assert_eq!(h, 2);
        assert_eq!(out, px);
    }

    #[test]
    fn rotate_90_swaps_dimensions() {
        let px = make_pixels(3, 2);
        let (_out, w, h) = rotate_rgba(&px, 3, 2, 90);
        assert_eq!(w, 2);
        assert_eq!(h, 3);
    }

    #[test]
    fn rotate_90_four_times_is_identity() {
        let px = make_pixels(5, 3);
        let (p1, w1, h1) = rotate_rgba(&px, 5, 3, 90);
        let (p2, w2, h2) = rotate_rgba(&p1, w1, h1, 90);
        let (p3, w3, h3) = rotate_rgba(&p2, w2, h2, 90);
        let (p4, w4, h4) = rotate_rgba(&p3, w3, h3, 90);
        assert_eq!(w4, 5);
        assert_eq!(h4, 3);
        assert_eq!(p4, px);
    }

    #[test]
    fn rotate_180_keeps_dimensions() {
        let px = make_pixels(3, 2);
        let (out, w, h) = rotate_rgba(&px, 3, 2, 180);
        assert_eq!(w, 3);
        assert_eq!(h, 2);
        // Top-left pixel should become bottom-right
        assert_eq!(out[0..4], px[(3 * 2 - 1) * 4..(3 * 2) * 4]);
    }
}
