use std::{
    any::Any as _,
    fmt::{Debug, Display},
    num::NonZero,
    ops::{Deref, DerefMut},
    sync::Arc,
};

use itertools::Either;

use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, fontdb::Database};
use image::{
    DynamicImage, GenericImage as _, GenericImageView as _, ImageFormat, ImageReader, Pixel as _,
    Rgba, RgbaImage, imageops::FilterType,
};
use mdfrier::{MarkdownLink, SourceContent};
use ratatui::{layout::Size, text::Line};

use ratatui_image::{FontSize, Resize, picker::Picker, protocol::Protocol, sliced::SlicedProtocol};
use regex::{Match, Regex};
use reqwest::{
    Client,
    header::{ACCEPT, CONTENT_TYPE, HeaderMap, HeaderValue},
};

#[cfg(feature = "svg")]
use resvg::usvg::Tree;

use tokio::sync::RwLock;
use unicode_width::UnicodeWidthStr as _;

use crate::{
    Error,
    cursor::CursorPointer,
    setup::FontRenderer,
    sources::{DocumentSource, SharedDocumentSource, extend_url, github_usercontent_url},
    worker::ImageCache,
};

#[derive(Default)]
pub struct Document {
    sections: Vec<Section>,
    total_lines: u16,
}

impl Document {
    pub fn push(&mut self, section: Section) {
        debug_assert!(
            !self.sections.iter().any(|s| s.id == section.id),
            "Document::push expects unique ids"
        );
        self.total_lines += section.height;
        self.sections.push(section);
    }

    // Update widgets with a list by id
    pub fn update(&mut self, updates: Vec<Section>) {
        let Some(first_id) = updates.first().map(|s| s.id) else {
            log::error!("ineffective Document::update with empty list");
            return;
        };
        debug_assert!(
            updates[1..].iter().all(|s| s.id == first_id),
            "Document::update must be called with same id for in the one updates list"
        );

        let mut range = None;

        for (i, section) in self.sections.iter().enumerate() {
            if section.id == first_id {
                range = match range {
                    None => Some((i, i + 1)),
                    Some((start, _)) => Some((start, i + 1)),
                };
            } else if range.is_some() {
                break; // Found the end of consecutive ID sections
            }
        }

        if let Some((start, end)) = range {
            let old_height: u16 = self.sections[start..end].iter().map(|s| s.height).sum();
            let new_height: u16 = updates.iter().map(|s| s.height).sum();
            self.sections.splice(start..end, updates);
            self.total_lines = self.total_lines.saturating_sub(old_height) + new_height;
        } else if let Some(last) = self.sections.last()
            && last.id < first_id
        {
            log::debug!("Update section #{first_id} not found but id is higher than last section");
            for section in updates {
                self.total_lines += section.height;
                self.sections.push(section);
            }
        } else {
            log::error!(
                "Update section #{first_id} not found anymore for {} updates",
                updates.len()
            );
        }
    }

    /// Update a specific image line within a section with loaded image data.
    pub fn update_image(
        &mut self,
        section_id: SectionID,
        link: MarkdownLink,
        (proto, size, max_size): (SlicedProtocol, Size, Size),
        trailing_blank: bool,
    ) {
        let Some(section) = self.sections.iter_mut().find(|s| s.id == section_id) else {
            log::error!("update_image: section #{section_id} not found");
            return;
        };

        // Add an extra space if the source had one.
        let old_height = section.height;
        let trailing_blank: u16 = trailing_blank.into();
        let new_height = size.height + trailing_blank;
        *section = Section {
            id: section_id,
            height: new_height,
            content: SectionContent::Image(link, proto, size, max_size),
        };
        self.total_lines = self.total_lines.saturating_sub(old_height) + new_height;
    }

    pub fn update_header(&mut self, section_id: SectionID, rows: Vec<(String, u8, Protocol)>) {
        if rows.is_empty() {
            log::error!("update_header: empty rows for section #{section_id}");
            return;
        }

        let new_sections: Vec<Section> = rows
            .into_iter()
            .map(|(text, tier, proto)| {
                log::debug!("update_header: {text}");
                let height = proto.size().height;
                Section {
                    id: section_id,
                    height,
                    content: SectionContent::Header(text, tier, Some(proto)),
                }
            })
            .collect();

        self.update(new_sections);
    }

    /// Extract all image protocols from the document for caching before reparse.
    /// Returns Vec<(url, protocol)>. Protocols are moved out, lines revert to height 1.
    pub fn take_image_protocols(&mut self, width: u16) -> ImageCache {
        let mut cache = ImageCache::default();
        for section in &mut self.sections {
            match &mut section.content {
                SectionContent::Image(url, _, _, _) => {
                    let url = url.clone();
                    let old_height = section.height;
                    let SectionContent::Image(link, proto, size, max_size) = std::mem::replace(
                        &mut section.content,
                        SectionContent::ImagePlaceholder(url, vec![]),
                    ) else {
                        unreachable!();
                    };
                    cache
                        .images
                        .insert(link.url.clone(), (proto, size, max_size));
                    section.height = 1;
                    self.total_lines = self.total_lines.saturating_sub(old_height) + section.height;
                }
                SectionContent::Header(text, tier, Some(_)) => {
                    let text = text.clone();
                    let tier = *tier;
                    let SectionContent::Header(text, tier, Some(proto)) = std::mem::replace(
                        &mut section.content,
                        SectionContent::HeaderPlaceholder(text, tier, vec![]),
                    ) else {
                        unreachable!();
                    };
                    let key = (text, tier);
                    if let Some(existing) = cache.headers(width).and_then(|hc| hc.get_mut(&key)) {
                        log::debug!("cache push into existing: {key:?}");
                        existing.push(proto);
                    } else {
                        log::debug!("cache insert: {key:?}");
                        if let Some(hc) = cache.headers(width) {
                            hc.insert(key, vec![proto]);
                        }
                    }
                }
                _ => {}
            }
        }
        cache
    }

    pub fn trim(&mut self, last_section_id: Option<usize>) {
        let Some(last_section_id) = last_section_id else {
            log::warn!("Document::trim without last_section_id, nothing parsed");
            return;
        };
        if let Some(last) = self.sections.last()
            && last.id == last_section_id
        {
            return;
        }
        if let Some(idx) = self
            .sections
            .iter()
            .position(|section| section.id == last_section_id)
        {
            log::debug!("trim: {idx} + 1");
            let removed: u16 = self.sections[idx + 1..].iter().map(|s| s.height).sum();
            self.sections.truncate(idx + 1);
            self.total_lines = self.total_lines.saturating_sub(removed);
        }
    }

    pub fn get_y(&self, CursorPointer { id, index }: &CursorPointer) -> Option<i16> {
        let mut y = 0;
        for section in &self.sections {
            match &section.content {
                SectionContent::Lines(lines) => {
                    if section.id != *id {
                        y += section.height as i16;
                        continue;
                    }

                    // Flatten extras, mirrors what we do when building cursor in
                    // find_nth_next_cursor and friends. Not pretty.
                    let mut i = 0;
                    for (line_y, (_line, extras)) in lines.iter().enumerate() {
                        for _extra in extras {
                            if i == *index {
                                return Some(y + (line_y as i16));
                            }
                            i += 1;
                        }
                    }
                    // Probably some test, didn't have LineExtras.
                    log::warn!("get_y did not match index {index} in LineExtras: {y}");
                    return Some(y);
                }
                _ => {
                    if section.id == *id {
                        return Some(y);
                    }
                    y += section.height as i16;
                }
            }
        }
        log::warn!("get_y did not find {id},{index}");
        None
    }

    /// Find first cursor in visible lines
    ///
    /// Find the first cursor in visible lines, or the first after, or the earliest before, in
    /// that order.
    pub fn find_first_cursor<'b, Iter: Iterator<Item = &'b Section>>(
        sections: Iter,
        target: FindTarget,
        scroll: u16,
    ) -> Option<CursorPointer> {
        let locate = move |section: &Section| -> Option<(u16, CursorPointer)> {
            if let SectionContent::Lines(lines) = &section.content {
                let mut flat_index = 0;
                for (line_y, (_, extras)) in lines.iter().enumerate() {
                    if let Some(i) = extras.iter().position(|extra| target.matches(extra)) {
                        return Some((
                            line_y as u16,
                            CursorPointer {
                                id: section.id,
                                index: flat_index + i,
                            },
                        ));
                    }
                    flat_index += extras.len();
                }
            }
            None
        };

        let mut last = None;
        let mut y = 0;
        for section in sections {
            let next = locate(section);
            if let Some((line_y, _)) = &next
                && y + line_y >= scroll
            {
                return next.map(|f| f.1);
            }
            if next.is_some() {
                last = next.map(|f| f.1);
            }
            y += section.height;
        }
        last
    }

    // Find nth (nonzero) next cursor.
    pub fn find_nth_next_cursor<'b, Iter>(
        iter: Iter,
        current: &CursorPointer,
        mode: FindMode,
        target: FindTarget,
        steps: NonZero<u16>,
    ) -> Option<CursorPointer>
    where
        Iter: DoubleEndedIterator<Item = &'b Section> + Clone,
    {
        let mut iter = Document::flatten_sections(iter, &mode, &target);
        let mut iter2 = iter.clone();
        let Some(curr_pos) = iter2.position(|x| x == *current) else {
            // TODO: This probably won't happen after #52 and #53 are fixed
            return iter.next();
        };
        let total = curr_pos + 1 + iter2.count();
        let index = (curr_pos + steps.get() as usize) % total;
        if index == curr_pos {
            return Some(current.clone());
        }
        iter.nth(index)
    }

    fn flatten_sections<'a, Iter>(
        iter: Iter,
        mode: &FindMode,
        target: &FindTarget,
    ) -> Either<
        impl Iterator<Item = CursorPointer> + Clone,
        impl Iterator<Item = CursorPointer> + Clone,
    >
    where
        Iter: DoubleEndedIterator<Item = &'a Section> + Clone,
    {
        match mode {
            FindMode::Next => Either::Left(iter.flat_map(move |section| {
                Document::line_extras_to_cursor_pointers(section, mode, target)
            })),
            FindMode::Prev => Either::Right(iter.rev().flat_map(move |section| {
                Document::line_extras_to_cursor_pointers(section, mode, target)
            })),
        }
    }

    fn line_extras_to_cursor_pointers(
        section: &Section,
        mode: &FindMode,
        target: &FindTarget,
    ) -> Either<
        Either<
            impl Iterator<Item = CursorPointer> + Clone,
            impl Iterator<Item = CursorPointer> + Clone,
        >,
        impl Iterator<Item = CursorPointer> + Clone,
    > {
        match mode {
            FindMode::Next => {
                if let SectionContent::Lines(lines) = &section.content {
                    let id = section.id;
                    let mut flat_index = 0;
                    let flattened: Vec<_> = lines
                        .iter()
                        .flat_map(|(_, extras)| {
                            let start = flat_index;
                            flat_index += extras.len();
                            extras
                                .iter()
                                .enumerate()
                                .map(move |(i, extra)| (start + i, extra))
                        })
                        .filter(|(_, extra)| target.matches(extra))
                        .map(move |(index, _)| CursorPointer { id, index })
                        .collect();
                    Either::Left(Either::Left(flattened.into_iter()))
                } else {
                    Either::Right(std::iter::empty())
                }
            }
            FindMode::Prev => {
                if let SectionContent::Lines(lines) = &section.content {
                    let id = section.id;
                    let mut flat_index = 0;
                    let mut flattened: Vec<_> = lines
                        .iter()
                        .flat_map(|(_, extras)| {
                            let start = flat_index;
                            flat_index += extras.len();
                            extras
                                .iter()
                                .enumerate()
                                .map(move |(i, extra)| (start + i, extra))
                        })
                        .filter(|(_, extra)| target.matches(extra))
                        .map(move |(index, _)| CursorPointer { id, index })
                        .collect();
                    flattened.reverse();
                    Either::Left(Either::Right(flattened.into_iter()))
                } else {
                    Either::Right(std::iter::empty())
                }
            }
        }
    }

    #[cfg(test)]
    pub fn find_extra_by_cursor(&self, pointer: &CursorPointer) -> Option<&LineExtra> {
        for section in self.iter() {
            if section.id != pointer.id {
                continue;
            }
            let SectionContent::Lines(lines) = &section.content else {
                continue;
            };
            let mut remaining = pointer.index;
            for (_, extras) in lines {
                if remaining < extras.len() {
                    return Some(&extras[remaining]);
                }
                remaining -= extras.len();
            }
        }
        None
    }

    #[cfg(test)]
    pub fn has_pending_images(&self) -> bool {
        self.sections
            .iter()
            .any(|section| matches!(&section.content, SectionContent::ImagePlaceholder(..)))
    }

    // Update all link URLs that point to the link reference definition.
    pub fn update_link_references(&mut self, definition_id: String, url: &str) {
        for section in &mut self.sections {
            let SectionContent::Lines(lines) = &mut section.content else {
                continue;
            };
            for (_, extras) in lines {
                for extra in extras {
                    let LineExtra::Link {
                        source, reference, ..
                    } = extra
                    else {
                        continue;
                    };
                    let LinkReference::Reference { id } = reference else {
                        continue;
                    };
                    if *id == definition_id {
                        *source = SourceContent::from(url);
                    }
                }
            }
        }
    }

    pub fn total_lines(&self) -> u16 {
        self.total_lines
    }
}

impl Deref for Document {
    type Target = Vec<Section>;
    fn deref(&self) -> &Vec<Section> {
        &self.sections
    }
}

impl DerefMut for Document {
    fn deref_mut(&mut self) -> &mut Vec<Section> {
        &mut self.sections
    }
}

#[derive(Debug, Clone, Copy)]
pub enum FindMode {
    Prev,
    Next,
}

#[derive(Debug, Clone, Copy)]
pub enum FindTarget {
    Link,
    Search,
}
impl FindTarget {
    fn matches(&self, extra: &LineExtra) -> bool {
        match self {
            FindTarget::Link => matches!(extra, LineExtra::Link { .. }),
            FindTarget::Search => matches!(extra, LineExtra::SearchMatch(..)),
        }
    }
}

pub type SectionID = usize;

#[derive(Debug)]
pub struct Section {
    pub id: SectionID,
    pub height: u16,
    pub content: SectionContent,
}

pub enum SectionContent {
    Image(MarkdownLink, SlicedProtocol, Size, Size),
    ImagePlaceholder(MarkdownLink, Vec<(Line<'static>, Vec<LineExtra>)>),
    Header(String, u8, Option<Protocol>),
    HeaderPlaceholder(String, u8, Vec<(Line<'static>, Vec<LineExtra>)>),
    Lines(Vec<(Line<'static>, Vec<LineExtra>)>),
    Code(String, Vec<(Line<'static>, Vec<LineExtra>)>),
}

impl SectionContent {
    pub fn add_search(&mut self, re: Option<&Regex>) {
        if let SectionContent::Lines(lines) = self {
            for (line, extras) in lines {
                let line_string = line.to_string();
                extras.retain(|extra| !matches!(extra, LineExtra::SearchMatch(_, _, _)));
                if let Some(re) = re {
                    extras.extend(
                        re.find_iter(&line_string)
                            .map(SectionContent::regex_to_searchmatch(&line_string)),
                    );
                }
            }
        }
        // TODO: search in headers
    }

    #[expect(clippy::string_slice)] // Regex byte ranges are guaranteed to fall between characters.
    fn regex_to_searchmatch(line_string: &str) -> impl Fn(Match<'_>) -> LineExtra {
        |m: Match| {
            // Convert from byte positions to character positions, with unicode_width.
            let start = line_string[..m.start()].width();
            let end = line_string[..m.end()].width();
            LineExtra::SearchMatch(start, end, m.as_str().to_owned())
        }
    }
}

#[cfg(test)]
impl PartialEq for SectionContent {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Image(..), _) | (_, Self::Image(..)) => {
                panic!("PartialEq not supported for SectionContent::Image")
            }
            (Self::Lines(l), Self::Lines(r)) => l == r,
            (Self::Header(l0, l1, l2), Self::Header(r0, r1, r2)) => {
                l0 == r0 && l1 == r1 && l2.is_some() == r2.is_some()
            }
            _ => false,
        }
    }
}

impl Debug for SectionContent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Image(url, _, _, _) => f.debug_tuple(format!("Image({url:?})").as_str()).finish(),
            Self::ImagePlaceholder(url, _) => f
                .debug_tuple(format!("ImagePlaceholder({url:?})").as_str())
                .finish(),
            Self::Lines(lines) => {
                let mut tuple = &mut f.debug_tuple("Line");
                for (line, extra) in lines {
                    tuple = tuple.field(line);
                    if !extra.is_empty() {
                        tuple = tuple.field(extra);
                    }
                }
                tuple.finish()
            }
            Self::Header(text, tier, _) => f.debug_tuple("Header").field(text).field(tier).finish(),
            Self::HeaderPlaceholder(_, _, lines) => {
                let mut tuple = &mut f.debug_tuple("HeaderPlaceholder");
                for (line, extra) in lines {
                    tuple = tuple.field(line);
                    if !extra.is_empty() {
                        tuple = tuple.field(extra);
                    }
                }
                tuple.finish()
            }
            Self::Code(language, lines) => {
                f.debug_tuple("Code").field(language).field(lines).finish()
            }
        }
    }
}

impl Display for SectionContent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Image(url, protocol, _, _) => {
                write!(f, "Image({url:?}, {:?})", protocol.type_id())
            }
            Self::ImagePlaceholder(url, _) => {
                write!(f, "ImagePlaceholder({url:?})")
            }
            Self::Lines(lines) => write!(f, "Line({lines:?})"),
            Self::Header(text, tier, _) => write!(f, "Header({text}, {tier})"),
            Self::HeaderPlaceholder(_, _, lines) => write!(f, "HeaderPlaceholder({lines:?})"),
            Self::Code(language, lines) => write!(f, "Code({language}, {lines:?})"),
        }
    }
}

impl Section {
    pub fn add_search(&mut self, re: Option<&Regex>) {
        self.content.add_search(re);
    }
}

#[cfg(test)]
impl Display for Section {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.content {
            SectionContent::Image(_, _, _, _) => write!(f, "<image>"),
            SectionContent::ImagePlaceholder(_, _) => write!(f, "<image-placeholder>"),
            SectionContent::Lines(lines) => {
                for (i, (line, _)) in lines.iter().enumerate() {
                    if i > 0 {
                        writeln!(f)?;
                    }
                    write!(f, "{}", line)?;
                }
                Ok(())
            }
            SectionContent::Header(text, tier, _) => {
                write!(f, "{} {}", "#".repeat(*tier as usize), text)
            }
            SectionContent::HeaderPlaceholder(_, _, lines) => {
                for (i, (line, _)) in lines.iter().enumerate() {
                    if i > 0 {
                        writeln!(f)?;
                    }
                    write!(f, "{}", line)?;
                }
                Ok(())
            }
            SectionContent::Code(language, lines) => {
                write!(f, "```{language}")?;
                for (i, (line, _)) in lines.iter().enumerate() {
                    if i > 0 {
                        writeln!(f)?;
                    }
                    write!(f, "{}", line)?;
                }
                Ok(())
            }
        }
    }
}

pub enum LineExtra {
    /// URL, start, end, multiline_count
    ///
    /// The "multiline_count" means "how many lines up does the link actually start".
    Link {
        source: SourceContent,
        start: u16,
        end: u16,
        lines: Option<usize>,
        reference: LinkReference,
    },
    /// start, end, text
    SearchMatch(usize, usize, String),
}

impl Debug for LineExtra {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LineExtra::Link {
                source: url,
                start,
                end,
                lines,
                reference,
            } => {
                write!(
                    f,
                    "Link({:?}, {}, {}, {}, {:?})",
                    url,
                    start,
                    end,
                    lines.unwrap_or_default(),
                    reference,
                )
            }
            LineExtra::SearchMatch(start, end, text) => {
                write!(f, "SearchMatch({}, {}, {:?})", start, end, text)
            }
        }
    }
}

#[cfg(test)]
impl PartialEq for LineExtra {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (
                LineExtra::Link {
                    source: l0,
                    start: l1,
                    end: l2,
                    lines: l3,
                    reference: l4,
                },
                LineExtra::Link {
                    source: r0,
                    start: r1,
                    end: r2,
                    lines: r3,
                    reference: r4,
                },
            ) => l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 && l4 == r4,
            (LineExtra::SearchMatch(l0, l1, l2), LineExtra::SearchMatch(r0, r1, r2)) => {
                l0 == r0 && l1 == r1 && l2 == r2
            }
            _ => false,
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum LinkReference {
    None,
    Reference { id: String },
    ReferenceDefinition { id: String, url: String },
}

/// Layout/shape and render `text` into a list of [`DynamicImage`] with a given terminal width.
pub fn header_images(
    font_renderer: &mut FontRenderer,
    width: u16,
    text: String,
    tier: u8,
    deep_fry_meme: bool,
) -> Result<Vec<(String, u8, DynamicImage)>, Error> {
    const HEADER_ROW_COUNT: u16 = 2;
    let FontSize {
        width: font_width,
        height: font_height,
    } = font_renderer.font_size;

    let tier_scale = f32::from(12 - tier) / 12.0_f32;

    let line_height = f32::from(font_renderer.font_size.height * HEADER_ROW_COUNT);
    let font_size = line_height * tier_scale;
    let metrics = Metrics::new(font_size, line_height);

    let mut buffer = Buffer::new(&mut font_renderer.font_system, metrics);

    let mut attrs = Attrs::new();
    attrs = attrs.family(Family::Name(&font_renderer.font_name));

    let max_width = width * font_width;
    buffer.set_size(
        &mut font_renderer.font_system,
        Some(f32::from(max_width)),
        None,
    );
    buffer.set_text(
        &mut font_renderer.font_system,
        &(if deep_fry_meme {
            text.replace('a', "🤣")
        } else {
            text
        }),
        &attrs,
        Shaping::Advanced,
        None,
    );
    buffer.shape_until_scroll(&mut font_renderer.font_system, false);

    // Make one image per shaped line.
    let layout_runs = buffer.layout_runs().collect::<Vec<_>>();
    let run_count = layout_runs.len();
    let mut dyn_imgs = Vec::with_capacity(run_count);
    let img_height = u32::from(font_height * 2);
    let img_width = u32::from(width * font_width);
    const DEFAULT_RGBA_BG: [u8; 4] = [0, 0, 0, 0];
    let background_color = font_renderer
        .background_color
        .unwrap_or_else(|| Rgba::<u8>::from(DEFAULT_RGBA_BG));

    for layout_run in layout_runs {
        let img: RgbaImage = RgbaImage::from_pixel(img_width, img_height, background_color);
        let dyn_img = DynamicImage::ImageRgba8(img);
        dyn_imgs.push((layout_run.text.into(), tier, dyn_img));
    }

    let fg = font_renderer.font_color;

    // Render shaped text, picking the image off the Vec by the Y coord.
    buffer.draw(
        &mut font_renderer.font_system,
        &mut font_renderer.swash_cache,
        fg,
        |x, y, w, h, color| {
            let a = color.a();
            if a == 0
                || x < 0
                || x >= i32::from(max_width)
                || y < 0
                // || y >= ... // Just pick relevant dyn_img
                || w != 1
                || h != 1
            {
                // Ignore alphas of 0, or invalid x, y coordinates, or unimplemented sizes
                return;
            }

            // Pick image-index by Y coord.
            let index = (y / img_height as i32) as usize;

            if index >= dyn_imgs.len() {
                return;
            }

            let dyn_img = &mut dyn_imgs[index].2;

            // Adjust picked image's Y coord offset.
            let y_offset: u32 = index as u32 * img_height;

            if font_renderer.background_color.is_some() {
                // Blend the pixels in with background color.
                let px = x as u32;
                let py = y as u32 - y_offset;
                let fg: Rgba<u8> = color.as_rgba().into();
                let mut bg = dyn_img.get_pixel(px, py);
                bg.blend(&fg);
                dyn_img.put_pixel(px, py, bg);
            } else {
                // Just write on the fully transparent image.
                dyn_img.put_pixel(x as u32, y as u32 - y_offset, color.as_rgba().into());
            }
        },
    );

    Ok(dyn_imgs)
}

const HEADER_ROW_COUNT: u16 = 2;

/// Render a list of images to [`Section`]s.
pub fn header_sections(
    picker: &Picker,
    width: u16,
    dyn_imgs: Vec<(String, u8, DynamicImage)>,
    deep_fry_meme: bool,
) -> Result<Vec<(String, u8, Protocol)>, Error> {
    let mut protos = vec![];
    for (text, tier, mut dyn_img) in dyn_imgs {
        if deep_fry_meme {
            dyn_img = deep_fry(dyn_img);
        }
        let proto = picker.new_protocol(
            dyn_img,
            Size::new(width, HEADER_ROW_COUNT),
            Resize::Fit(None),
        )?;
        protos.push((text, tier, proto));
    }
    Ok(protos)
}

#[derive(Debug)]
enum ImageSource {
    Bytes(Vec<u8>, ImageFormat),
    Path(String),
    #[cfg_attr(not(feature = "svg"), allow(dead_code))]
    DynamicImage(DynamicImage),
}

#[expect(clippy::too_many_arguments)]
pub async fn image_section(
    picker: &Arc<Picker>,
    max_height: u16,
    width: u16,
    document_source: SharedDocumentSource,
    client: Arc<RwLock<Client>>,
    id: SectionID,
    link: MarkdownLink,
    deep_fry_meme: bool,
    fontdb: Option<Arc<Database>>,
) -> Result<Section, Error> {
    let link_url = &link.url;
    let image_source = if link_url.starts_with("https://") || link_url.starts_with("http://") {
        download_image(client, fontdb, link_url).await?
    } else {
        let image_source: Option<ImageSource> = match document_source.read() {
            Ok(DocumentSource::File {
                basepath: Some(basepath),
                ..
            }) => {
                let path = basepath.join(link_url).to_str().map(String::from);
                path.map(ImageSource::Path)
            }
            Ok(DocumentSource::Github { repo, branch }) => {
                if let Ok(repo_url) = github_usercontent_url(&repo, &branch, link_url) {
                    Some(download_image(client, fontdb, repo_url.as_str()).await?)
                } else {
                    None
                }
            }
            Ok(DocumentSource::HyperText { url }) => {
                if let Ok(extended_url) = extend_url(url.clone(), link_url) {
                    Some(download_image(client, fontdb, extended_url.as_str()).await?)
                } else {
                    None
                }
            }
            _ => None,
        };
        image_source.unwrap_or_else(|| ImageSource::Path(link_url.to_owned()))
    };

    // Now do all the blocking stuff
    let picker = picker.clone();
    let section = tokio::task::spawn_blocking(move || {
        let mut dyn_img = match image_source {
            ImageSource::Bytes(bytes, format) => {
                ImageReader::with_format(std::io::Cursor::new(bytes), format).decode()?
            }
            ImageSource::Path(path) => ImageReader::open(path)?.decode()?,
            ImageSource::DynamicImage(dyn_img) => dyn_img,
        };

        if deep_fry_meme {
            dyn_img = deep_fry(dyn_img);
        }

        let max_width: u16 = (max_height * 3 / 2).min(width);
        let max_resize_size = Size::new(max_width, max_height);
        let size = Resize::Fit(None).size_for(&dyn_img, picker.font_size(), max_resize_size);

        let max_size = Size::new(width, max_height);
        let sliced = SlicedProtocol::new_with_resize(
            &picker,
            dyn_img,
            size,
            Resize::Fit(Some(FilterType::Lanczos3)),
        )?;
        Ok::<Section, Error>(Section {
            id,
            height: size.height,
            content: SectionContent::Image(link, sliced, size, max_size),
        })
    })
    .await??;
    Ok(section)
}

async fn download_image(
    client: Arc<RwLock<Client>>,
    #[cfg_attr(not(feature = "svg"), allow(unused_variables))] fontdb: Option<Arc<Database>>,
    url: &str,
) -> Result<ImageSource, Error> {
    let mut headers = HeaderMap::new();
    headers.insert(ACCEPT, HeaderValue::from_static("image/png,image/jpg")); // or "image/jpeg"
    let client = client.read().await;
    let response = client.get(url).headers(headers).send().await?;
    drop(client);
    if !response.status().is_success() {
        return Err(Error::ImageLoad(
            url.to_owned(),
            format!("status {}", response.status()),
        ));
    }
    let ct = response
        .headers()
        .get(CONTENT_TYPE)
        .and_then(|h| h.to_str().ok());

    let Some(ct) = ct else {
        return Err(Error::ImageLoad(
            url.to_owned(),
            "no content-type".to_owned(),
        ));
    };

    // e.g. "image/svg+xml;charest=utf-8", but varies.
    if ct.starts_with("image/svg+xml") {
        #[cfg(feature = "svg")]
        {
            let Some(fontdb) = fontdb else {
                return Err(Error::ImageLoad(
                    url.to_owned(),
                    "svg feature enabled but no fontdb at runtime".to_owned(),
                ));
            };
            let bytes = response.bytes().await?.to_vec();
            let dyn_img = svg_to_rgba(&bytes, fontdb)?;
            Ok(ImageSource::DynamicImage(dyn_img))
        }
        #[cfg(not(feature = "svg"))]
        return Err(Error::ImageLoad(
            url.to_owned(),
            "svg feature not enabled".to_owned(),
        ));
    } else {
        let format = match ct {
            "image/jpeg" => Ok(ImageFormat::Jpeg),
            "image/png" => Ok(ImageFormat::Png),
            "image/webp" => Ok(ImageFormat::WebP),
            "image/gif" => Ok(ImageFormat::Gif),
            ct => Err(Error::ImageLoad(
                url.to_owned(),
                format!("unhandled content-type {ct}"),
            )),
        }?;

        let bytes = response.bytes().await?.to_vec();
        Ok(ImageSource::Bytes(bytes, format))
    }
}

#[cfg(feature = "svg")]
fn svg_to_rgba(bytes: &[u8], fontdb: Arc<Database>) -> Result<DynamicImage, Error> {
    use resvg::usvg;
    let options = usvg::Options {
        fontdb,
        ..Default::default()
    };
    let tree = Tree::from_data(bytes, &options)
        .map_err(|err| Error::ImageLoad("(svg)".to_owned(), format!("{err}")))?;
    svg_tree_to_rgba(tree)
}

#[cfg(feature = "svg")]
pub fn svg_tree_to_rgba(tree: Tree) -> Result<DynamicImage, Error> {
    use resvg::tiny_skia;

    let size = tree.size().to_int_size();

    let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).ok_or(
        Error::ImageLoad("(svg)".to_owned(), "could not allocate pixmap".to_owned()),
    )?;
    resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
    let rgba =
        RgbaImage::from_raw(size.width(), size.height(), pixmap.take()).ok_or_else(|| {
            Error::ImageLoad(
                "(svg)".to_owned(),
                "could not create RBGA image from pixmap".to_owned(),
            )
        })?;
    Ok(DynamicImage::ImageRgba8(rgba))
}

fn deep_fry(mut dyn_img: DynamicImage) -> DynamicImage {
    let width = dyn_img.width();
    let height = dyn_img.height();
    dyn_img = dyn_img.adjust_contrast(50.0);
    dyn_img = dyn_img.huerotate(45);

    let down_width = (width as f32 * 0.9) as u32;
    let down_height = (height as f32 * 0.8) as u32;
    dyn_img = dyn_img.resize(down_width, down_height, FilterType::Gaussian);
    dyn_img = dyn_img.resize(width, height, FilterType::Nearest);

    let mut deep_fried = dyn_img.to_rgba8();
    let mut seed: i32 = 42;

    #[expect(clippy::cast_possible_truncation)]
    for pixel in deep_fried.pixels_mut() {
        // Boost color intensities and add artifacts
        let mut r = f32::from(pixel[0]);
        let mut g = f32::from(pixel[1]);
        let mut b = f32::from(pixel[2]);

        // Exaggerate color values
        r = (r * 1.5).min(255.0);
        g = (g * 1.5).min(255.0);
        b = (b * 1.5).min(255.0);

        // Add "random" noise for "deep fried" effect
        seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
        let noise = (seed % 30) as f32;

        r = (r + noise).min(255.0);
        g = (g + noise).min(255.0);
        b = (b + noise).min(255.0);

        *pixel = Rgba([r as u8, g as u8, b as u8, pixel[3]]);
    }

    DynamicImage::ImageRgba8(deep_fried)
}

#[cfg(test)]
mod tests {

    use ratatui::{
        style::Stylize as _,
        text::{Line, Span},
    };
    use regex::Regex;

    use crate::{
        cursor::CursorPointer,
        document::{Document, LineExtra, SectionContent},
        *,
    };

    #[ctor::ctor]
    fn init_logger() {
        debug::init_test_logger();
    }

    #[test]
    fn widgestsources_update() {
        let mut ws = Document::default();
        ws.push(Section {
            id: 0,
            height: 2,
            content: SectionContent::Lines(vec![(Line::from("line #0"), Vec::new())]),
        });
        ws.push(Section {
            id: 1,
            height: 2,
            content: SectionContent::Lines(vec![(
                Line::from("headerline1 headerline2"),
                Vec::new(),
            )]),
        });
        ws.push(Section {
            id: 2,
            height: 2,
            content: SectionContent::Lines(vec![(Line::from("line #2"), Vec::new())]),
        });

        ws.update(vec![
            Section {
                id: 1,
                height: 2,
                content: SectionContent::Header(String::from("headerline1"), 1, None),
            },
            Section {
                id: 1,
                height: 2,
                content: SectionContent::Header(String::from("headerline2"), 1, None),
            },
        ]);
        assert_eq!(ws.sections.len(), 4);
        assert_eq!(0, ws.sections[0].id,);
        assert_eq!(1, ws.sections[1].id,);
        assert_eq!(
            SectionContent::Header(String::from("headerline1"), 1, None),
            ws.sections[1].content
        );
        assert_eq!(1, ws.sections[2].id,);
        assert_eq!(
            SectionContent::Header(String::from("headerline2"), 1, None),
            ws.sections[2].content
        );
        assert_eq!(2, ws.sections[3].id,);

        ws.update(vec![
            Section {
                id: 1,
                height: 2,
                content: SectionContent::Header(String::from("headerline3"), 1, None),
            },
            Section {
                id: 1,
                height: 2,
                content: SectionContent::Header(String::from("headerline4"), 1, None),
            },
        ]);
        assert_eq!(ws.sections.len(), 4);
        assert_eq!(0, ws.sections[0].id,);
        assert_eq!(1, ws.sections[1].id,);
        assert_eq!(
            SectionContent::Header(String::from("headerline3"), 1, None),
            ws.sections[1].content
        );
        assert_eq!(1, ws.sections[2].id,);
        assert_eq!(
            SectionContent::Header(String::from("headerline4"), 1, None),
            ws.sections[2].content
        );
        assert_eq!(2, ws.sections[3].id,);
    }

    #[test]
    #[expect(clippy::unwrap_used)]
    fn get_y() {
        let mut doc = Document::default();
        doc.push(Section {
            id: 1,
            height: 2,
            content: SectionContent::Header(String::from("one"), 1, None),
        });
        doc.push(Section {
            id: 2,
            height: 1,
            content: SectionContent::Lines(vec![(Line::from("line"), Vec::new())]),
        });
        doc.push(Section {
            id: 3,
            height: 1,
            content: SectionContent::Lines(vec![(Line::from("line"), Vec::new())]),
        });
        doc.push(Section {
            id: 4,
            height: 2,
            content: SectionContent::Header(String::from("one"), 1, None),
        });
        doc.push(Section {
            id: 5,
            height: 1,
            content: SectionContent::Lines(vec![(Line::from("line"), Vec::new())]),
        });
        assert_eq!(doc.get_y(&CursorPointer { id: 1, index: 0 }).unwrap(), 0);
        assert_eq!(doc.get_y(&CursorPointer { id: 2, index: 0 }).unwrap(), 2);
        assert_eq!(doc.get_y(&CursorPointer { id: 3, index: 0 }).unwrap(), 3);
        assert_eq!(doc.get_y(&CursorPointer { id: 4, index: 0 }).unwrap(), 4);
        assert_eq!(doc.get_y(&CursorPointer { id: 5, index: 0 }).unwrap(), 6);
    }

    #[test]
    fn add_search_offset() {
        let line = Line::from(vec![Span::from("▐").magenta(), Span::from(" hi")]);
        let mut wsd = SectionContent::Lines(vec![(line, Vec::new())]);
        wsd.add_search(Regex::new("hi").ok().as_ref());
        let SectionContent::Lines(lines) = wsd else {
            panic!("Line");
        };
        assert_eq!(
            lines[0].1[0],
            LineExtra::SearchMatch(2, 4, String::from("hi"))
        );
    }
}
