use {
    crate::{
        async_engine::AsyncEngine,
        cmm::cmm_render_intent::RenderIntent,
        fixed::Fixed,
        format::ARGB8888,
        gfx_api::{AcquireSync, AlphaMode, GfxContext, GfxError, GfxTexture, ReleaseSync},
        rect::Rect,
        renderer::Renderer,
        scale::Scale,
        state::State,
        time::Time,
        tree::OutputNode,
        utils::{errorfmt::ErrorFmt, numcell::NumCell, smallmap::SmallMapMut},
    },
    ahash::{AHashMap, AHashSet},
    bstr::{BStr, BString, ByteSlice, ByteVec},
    byteorder::{LittleEndian, ReadBytesExt},
    isnt::std_1::primitive::IsntSliceExt,
    num_derive::FromPrimitive,
    std::{
        cell::Cell,
        convert::TryInto,
        env,
        fmt::{Debug, Formatter},
        fs::File,
        io::{self, BufRead, BufReader, Seek, SeekFrom},
        mem::MaybeUninit,
        rc::Rc,
        slice, str,
        sync::LazyLock,
        time::Duration,
    },
    thiserror::Error,
    uapi::Bytes,
};

const XCURSOR_MAGIC: u32 = 0x72756358;
const XCURSOR_IMAGE_TYPE: u32 = 0xfffd0002;
const XCURSOR_PATH_DEFAULT: &[u8] =
    b"~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons";
const XCURSOR_PATH: &str = "XCURSOR_PATH";
const XCURSOR_THEME: &str = "XCURSOR_THEME";
const XCURSOR_SIZE: &str = "XCURSOR_SIZE";
const HOME: &str = "HOME";

const HEADER_SIZE: u32 = 16;

pub static DEFAULT_CURSOR_SIZE: LazyLock<u32> = LazyLock::new(|| {
    if let Ok(size) = env::var(XCURSOR_SIZE)
        && let Ok(val) = size.parse()
    {
        return val;
    }
    24
});

pub trait Cursor {
    fn render(&self, renderer: &mut Renderer, x: Fixed, y: Fixed);
    fn render_hardware_cursor(&self, renderer: &mut Renderer);
    fn extents_at_scale(&self, scale: Scale) -> Rect;
    fn set_output(&self, output: &Rc<OutputNode>) {
        let _ = output;
    }
    fn handle_set(self: Rc<Self>) {}
    fn handle_unset(&self) {}
    fn tick(&self) {}
    fn needs_tick(&self) -> bool {
        false
    }
    fn time_until_tick(&self) -> Duration {
        Duration::new(0, 0)
    }

    fn set_visible(&self, visible: bool) {
        let _ = visible;
    }
}

pub struct ServerCursors {
    pub default: ServerCursorTemplate,
    pub context_menu: ServerCursorTemplate,
    pub help: ServerCursorTemplate,
    pub pointer: ServerCursorTemplate,
    pub progress: ServerCursorTemplate,
    pub wait: ServerCursorTemplate,
    pub cell: ServerCursorTemplate,
    pub crosshair: ServerCursorTemplate,
    pub text: ServerCursorTemplate,
    pub vertical_text: ServerCursorTemplate,
    pub alias: ServerCursorTemplate,
    pub copy: ServerCursorTemplate,
    pub r#move: ServerCursorTemplate,
    pub no_drop: ServerCursorTemplate,
    pub not_allowed: ServerCursorTemplate,
    pub grab: ServerCursorTemplate,
    pub grabbing: ServerCursorTemplate,
    pub e_resize: ServerCursorTemplate,
    pub n_resize: ServerCursorTemplate,
    pub ne_resize: ServerCursorTemplate,
    pub nw_resize: ServerCursorTemplate,
    pub s_resize: ServerCursorTemplate,
    pub se_resize: ServerCursorTemplate,
    pub sw_resize: ServerCursorTemplate,
    pub w_resize: ServerCursorTemplate,
    pub ew_resize: ServerCursorTemplate,
    pub ns_resize: ServerCursorTemplate,
    pub nesw_resize: ServerCursorTemplate,
    pub nwse_resize: ServerCursorTemplate,
    pub col_resize: ServerCursorTemplate,
    pub row_resize: ServerCursorTemplate,
    pub all_scroll: ServerCursorTemplate,
    pub zoom_in: ServerCursorTemplate,
    pub zoom_out: ServerCursorTemplate,
    pub dnd_ask: ServerCursorTemplate,
    pub all_resize: ServerCursorTemplate,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive)]
pub enum KnownCursor {
    Default,
    ContextMenu,
    Help,
    Pointer,
    Progress,
    Wait,
    Cell,
    Crosshair,
    Text,
    VerticalText,
    Alias,
    Copy,
    Move,
    NoDrop,
    NotAllowed,
    Grab,
    Grabbing,
    EResize,
    NResize,
    NeResize,
    NwResize,
    SResize,
    SeResize,
    SwResize,
    WResize,
    EwResize,
    NsResize,
    NeswResize,
    NwseResize,
    ColResize,
    RowResize,
    AllScroll,
    ZoomIn,
    ZoomOut,
    DndAsk,
    AllResize,
}

impl ServerCursors {
    pub fn load(ctx: &Rc<dyn GfxContext>, state: &State) -> Result<Option<Self>, CursorError> {
        let paths = find_cursor_paths();
        log::debug!("Trying to load cursors from paths {:?}", paths);
        let sizes = state.cursor_sizes.to_vec();
        let scales = state.scales.to_vec();
        if sizes.is_empty() || scales.is_empty() {
            return Ok(None);
        }
        let xcursor_theme = env::var_os(XCURSOR_THEME);
        let theme = xcursor_theme.as_ref().map(|theme| BStr::new(theme.bytes()));

        let load =
            |names: &[&str]| ServerCursorTemplate::load(names, theme, &scales, &sizes, &paths, ctx);
        Ok(Some(Self {
            // default: load(&["wait", "watch"])?,
            default: load(&["default", "left_ptr"])?,
            context_menu: load(&["context-menu"])?,
            help: load(&["help"])?,
            pointer: load(&["pointer", "hand2", "hand1"])?,
            progress: load(&["progress"])?,
            wait: load(&["wait", "watch"])?,
            cell: load(&["cell"])?,
            crosshair: load(&["crosshair"])?,
            text: load(&["text", "xterm"])?,
            vertical_text: load(&["vertical-text"])?,
            alias: load(&["alias"])?,
            copy: load(&["copy"])?,
            r#move: load(&["move"])?,
            no_drop: load(&["no-drop"])?,
            not_allowed: load(&["not-allowed"])?,
            grab: load(&["grab"])?,
            grabbing: load(&["grabbing"])?,
            e_resize: load(&["e-resize", "right_side"])?,
            w_resize: load(&["w-resize", "left_side"])?,
            n_resize: load(&["n-resize", "top_side"])?,
            s_resize: load(&["s-resize", "bottom_side"])?,
            ns_resize: load(&["ns-resize", "v_double_arrow"])?,
            ew_resize: load(&["ew-resize", "h_double_arrow"])?,
            nw_resize: load(&["nw-resize", "top_left_corner"])?,
            ne_resize: load(&["ne-resize", "top_right_corner"])?,
            sw_resize: load(&["sw-resize", "bottom_left_corner"])?,
            se_resize: load(&["se-resize", "bottom_right_corner"])?,
            nesw_resize: load(&["nesw-resize"])?,
            nwse_resize: load(&["nwse-resize"])?,
            col_resize: load(&["col-resize"])?,
            row_resize: load(&["row-resize"])?,
            all_scroll: load(&["all-scroll", "grabbing"])?,
            zoom_in: load(&["zoom-in"])?,
            zoom_out: load(&["zoom-out"])?,
            dnd_ask: load(&["dnd-ask", "dnd-copy", "copy"])?,
            all_resize: load(&["all-resize", "move"])?,
        }))
    }
}

pub struct ServerCursorTemplate {
    var: ServerCursorTemplateVariant,
    pub xcursor: Vec<AHashMap<(Scale, u32), Rc<XCursorImage>>>,
}

enum ServerCursorTemplateVariant {
    Static(Rc<CursorImage>),
    Animated(Rc<Vec<CursorImage>>),
}

impl ServerCursorTemplate {
    fn load(
        names: &[&str],
        theme: Option<&BStr>,
        scales: &[Scale],
        sizes: &[u32],
        paths: &[BString],
        ctx: &Rc<dyn GfxContext>,
    ) -> Result<Self, CursorError> {
        match open_cursor(names, theme, scales, sizes, paths) {
            Ok(cs) => {
                if cs.images.len() == 1 {
                    let mut sizes = SmallMapMut::new();
                    for (k, c) in &cs.images[0] {
                        sizes.insert(
                            *k,
                            CursorImageScaled::from_bytes(
                                ctx, &c.pixels, c.width, c.height, c.xhot, c.yhot,
                            )?,
                        );
                    }
                    let cursor = CursorImage::from_sizes(0, sizes)?;
                    Ok(ServerCursorTemplate {
                        var: ServerCursorTemplateVariant::Static(Rc::new(cursor)),
                        xcursor: cs.images,
                    })
                } else {
                    let mut images = vec![];
                    for image in &cs.images {
                        let mut sizes = SmallMapMut::new();
                        let mut delay_ms = 0;
                        for (k, c) in image {
                            delay_ms = c.delay;
                            sizes.insert(
                                *k,
                                CursorImageScaled::from_bytes(
                                    ctx, &c.pixels, c.width, c.height, c.xhot, c.yhot,
                                )?,
                            );
                        }
                        let img = CursorImage::from_sizes(delay_ms as _, sizes)?;
                        images.push(img);
                    }
                    Ok(ServerCursorTemplate {
                        var: ServerCursorTemplateVariant::Animated(Rc::new(images)),
                        xcursor: cs.images,
                    })
                }
            }
            Err(e) => {
                log::warn!("Could not load cursor {:?}: {}", names, ErrorFmt(e));
                let empty: [Cell<u8>; 4] = unsafe { MaybeUninit::zeroed().assume_init() };
                let mut img_sizes = SmallMapMut::new();
                for scale in scales {
                    for size in sizes {
                        img_sizes.insert(
                            (*scale, *size),
                            CursorImageScaled::from_bytes(ctx, &empty, 1, 1, 0, 0)?,
                        );
                    }
                }
                let cursor = CursorImage::from_sizes(0, img_sizes)?;
                Ok(ServerCursorTemplate {
                    var: ServerCursorTemplateVariant::Static(Rc::new(cursor)),
                    xcursor: Default::default(),
                })
            }
        }
    }

    pub fn instantiate(&self, state: &State, size: u32) -> Rc<dyn Cursor> {
        match &self.var {
            ServerCursorTemplateVariant::Static(s) => Rc::new(StaticCursor {
                image: s.for_size(size),
            }),
            ServerCursorTemplateVariant::Animated(a) => Rc::new(AnimatedCursor {
                start: state.now(),
                eng: state.eng.clone(),
                next: NumCell::new(a[0].delay_ns),
                idx: Cell::new(0),
                images: a.iter().map(|c| c.for_size(size)).collect(),
            }),
        }
    }
}

struct CursorImageScaled {
    extents: Rect,
    tex: Rc<dyn GfxTexture>,
}

struct CursorImage {
    delay_ns: u64,
    sizes: SmallMapMut<(Scale, u32), Rc<CursorImageScaled>, 2>,
}

struct InstantiatedCursorImage {
    delay_ns: u64,
    scales: SmallMapMut<Scale, Rc<CursorImageScaled>, 2>,
}

impl CursorImageScaled {
    fn from_bytes(
        ctx: &Rc<dyn GfxContext>,
        data: &[Cell<u8>],
        width: i32,
        height: i32,
        xhot: i32,
        yhot: i32,
    ) -> Result<Rc<Self>, CursorError> {
        Ok(Rc::new(Self {
            extents: Rect::new_sized_saturating(-xhot, -yhot, width, height),
            tex: ctx
                .clone()
                .shmem_texture(None, data, ARGB8888, width, height, width * 4, None)?,
        }))
    }
}

impl CursorImage {
    fn from_sizes(
        delay_ms: u64,
        sizes: SmallMapMut<(Scale, u32), Rc<CursorImageScaled>, 2>,
    ) -> Result<Self, CursorError> {
        Ok(Self {
            delay_ns: delay_ms.max(1) * 1_000_000,
            sizes,
        })
    }

    fn for_size(&self, size: u32) -> InstantiatedCursorImage {
        let mut sizes = SmallMapMut::new();
        for ((scale, isize), v) in &self.sizes {
            if *isize == size {
                sizes.insert(*scale, v.clone());
            }
        }
        InstantiatedCursorImage {
            delay_ns: self.delay_ns,
            scales: sizes,
        }
    }
}

struct StaticCursor {
    image: InstantiatedCursorImage,
}

fn render_img(image: &InstantiatedCursorImage, renderer: &mut Renderer, x: Fixed, y: Fixed) {
    let scale = renderer.scale();
    let img = match image.scales.get(&scale) {
        Some(img) => img,
        _ => return,
    };
    let extents = if scale != 1 {
        let scalef = scale.to_f64();
        let x = (x.to_f64() * scalef).round() as i32;
        let y = (y.to_f64() * scalef).round() as i32;
        img.extents.move_(x, y)
    } else {
        img.extents.move_(x.round_down(), y.round_down())
    };
    if extents.intersects(&renderer.pixel_extents()) {
        renderer.base.render_texture(
            &img.tex,
            None,
            extents.x1(),
            extents.y1(),
            None,
            None,
            scale,
            None,
            None,
            AcquireSync::None,
            ReleaseSync::None,
            false,
            renderer.state.color_manager.srgb_gamma22(),
            RenderIntent::Perceptual,
            AlphaMode::PremultipliedElectrical,
        );
    }
}

impl Cursor for StaticCursor {
    fn render(&self, renderer: &mut Renderer, x: Fixed, y: Fixed) {
        render_img(&self.image, renderer, x, y);
    }

    fn render_hardware_cursor(&self, renderer: &mut Renderer) {
        if let Some(img) = self.image.scales.get(&renderer.scale()) {
            renderer.base.render_texture(
                &img.tex,
                None,
                0,
                0,
                None,
                None,
                renderer.scale(),
                None,
                None,
                AcquireSync::None,
                ReleaseSync::None,
                false,
                renderer.state.color_manager.srgb_gamma22(),
                RenderIntent::Perceptual,
                AlphaMode::PremultipliedElectrical,
            );
        }
    }

    fn extents_at_scale(&self, scale: Scale) -> Rect {
        match self.image.scales.get(&scale) {
            None => Rect::new_empty(0, 0),
            Some(i) => i.extents,
        }
    }
}

struct AnimatedCursor {
    start: Time,
    eng: Rc<AsyncEngine>,
    next: NumCell<u64>,
    idx: Cell<usize>,
    images: Vec<InstantiatedCursorImage>,
}

impl Cursor for AnimatedCursor {
    fn render(&self, renderer: &mut Renderer, x: Fixed, y: Fixed) {
        let img = &self.images[self.idx.get()];
        render_img(img, renderer, x, y);
    }

    fn render_hardware_cursor(&self, renderer: &mut Renderer) {
        let img = &self.images[self.idx.get()];
        if let Some(img) = img.scales.get(&renderer.scale()) {
            renderer.base.render_texture(
                &img.tex,
                None,
                0,
                0,
                None,
                None,
                renderer.scale(),
                None,
                None,
                AcquireSync::None,
                ReleaseSync::None,
                false,
                renderer.state.color_manager.srgb_gamma22(),
                RenderIntent::Perceptual,
                AlphaMode::PremultipliedElectrical,
            );
        }
    }

    fn extents_at_scale(&self, scale: Scale) -> Rect {
        let img = &self.images[self.idx.get()];
        match img.scales.get(&scale) {
            None => Rect::new_empty(0, 0),
            Some(i) => i.extents,
        }
    }

    fn tick(&self) {
        let dist = self.eng.now() - self.start;
        if (dist.as_nanos() as u64) < self.next.get() {
            return;
        }
        let idx = (self.idx.get() + 1) % self.images.len();
        self.idx.set(idx);
        let image = &self.images[idx];
        self.next.fetch_add(image.delay_ns);
    }

    fn needs_tick(&self) -> bool {
        true
    }

    fn time_until_tick(&self) -> Duration {
        let dist = self.eng.now() - self.start;
        let dist = dist.as_nanos() as u64;
        let nanos = self.next.get().saturating_sub(dist);
        Duration::from_nanos(nanos)
    }
}

struct OpenCursorResult {
    images: Vec<AHashMap<(Scale, u32), Rc<XCursorImage>>>,
}

fn open_cursor(
    names: &[&str],
    theme: Option<&BStr>,
    scales: &[Scale],
    sizes: &[u32],
    paths: &[BString],
) -> Result<OpenCursorResult, CursorError> {
    let mut file = None;
    let mut pairs_tested = AHashSet::new();
    if let Some(theme) = theme {
        for name in names {
            let name = name.as_bytes().as_bstr();
            file = open_cursor_file(&mut pairs_tested, paths, theme, name);
            if file.is_some() {
                break;
            }
        }
    }
    if file.is_none() {
        for name in names {
            let name = name.as_bytes().as_bstr();
            file = open_cursor_file(&mut pairs_tested, paths, b"default".as_bstr(), name);
            if file.is_some() {
                break;
            }
        }
    }
    let file = match file {
        Some(f) => f,
        _ => return Err(CursorError::NotFound),
    };
    let mut file = BufReader::new(file);
    parser_cursor_file(&mut file, scales, sizes)
}

fn open_cursor_file<'a>(
    pairs_tested: &mut AHashSet<(BString, &'a BStr)>,
    paths: &[BString],
    theme: &BStr,
    name: &'a BStr,
) -> Option<File> {
    if !pairs_tested.insert((theme.to_owned(), name)) {
        return None;
    }
    if paths.is_empty() {
        return None;
    }
    let mut parents = None;
    for cursor_path in paths {
        let mut theme_dir = cursor_path.to_vec();
        theme_dir.push(b'/');
        theme_dir.extend_from_slice(theme.as_bytes());
        let mut cursor_file = theme_dir.clone();
        cursor_file.extend_from_slice(b"/cursors/");
        cursor_file.extend_from_slice(name.as_bytes());
        if let Ok(f) = File::open(cursor_file.to_os_str().unwrap()) {
            return Some(f);
        }
        if parents.is_none() {
            let mut index_file = theme_dir.clone();
            index_file.extend_from_slice(b"/index.theme");
            parents = find_parent_themes(&index_file);
        }
    }
    if let Some(parents) = parents {
        for parent in parents {
            if let Some(file) = open_cursor_file(pairs_tested, paths, parent.as_bstr(), name) {
                return Some(file);
            }
        }
    }
    None
}

fn find_cursor_paths() -> Vec<BString> {
    let home = env::var_os(HOME).map(|h| Vec::from_os_string(h).unwrap());
    let cursor_paths = env::var_os(XCURSOR_PATH);
    let cursor_paths = cursor_paths
        .as_ref()
        .map(|c| <[u8]>::from_os_str(c).unwrap())
        .unwrap_or(XCURSOR_PATH_DEFAULT);
    let mut paths = vec![];
    for path in <[u8]>::split(cursor_paths, |b| *b == b':') {
        if path.first() == Some(&b'~') {
            if let Some(home) = home.as_ref() {
                let mut full_path = home.clone();
                full_path.extend_from_slice(&path[1..]);
                paths.push(full_path.into());
            } else {
                log::warn!(
                    "`HOME` is not set. Cannot expand {}. Ignoring.",
                    path.as_bstr()
                );
            }
        } else {
            paths.push(path.as_bstr().to_owned());
        }
    }
    paths
}

fn find_parent_themes(path: &[u8]) -> Option<Vec<BString>> {
    // NOTE: The files we're reading here are really INI files with a hierarchy. This
    // algorithm treats it as a flat list and is inherited from libxcursor.
    let file = match File::open(path.to_os_str().unwrap()) {
        Ok(f) => f,
        _ => return None,
    };
    let mut buf_reader = BufReader::new(file);
    let mut buf = vec![];
    loop {
        buf.clear();
        match buf_reader.read_until(b'\n', &mut buf) {
            Ok(n) if n > 0 => {}
            _ => return None,
        }
        let mut suffix = match buf.strip_prefix(b"Inherits") {
            Some(s) => s,
            _ => continue,
        };
        while suffix.first() == Some(&b' ') {
            suffix = &suffix[1..];
        }
        if suffix.first() != Some(&b'=') {
            continue;
        }
        suffix = &suffix[1..];
        let parents = suffix
            .split(|b| matches!(*b, b' ' | b'\t' | b'\n' | b';' | b','))
            .filter(|v| v.is_not_empty())
            .map(|v| v.as_bstr().to_owned())
            .collect();
        return Some(parents);
    }
}

#[derive(Debug, Error)]
pub enum CursorError {
    #[error("An IO error occurred: {0}")]
    Io(#[from] io::Error),
    #[error("The file is not an Xcursor file")]
    NotAnXcursorFile,
    #[error("The Xcursor file contains more than 0x10000 images")]
    OversizedXcursorFile,
    #[error("The Xcursor file is empty")]
    EmptyXcursorFile,
    #[error("The Xcursor file is corrupt")]
    CorruptXcursorFile,
    #[error("The requested cursor could not be found")]
    NotFound,
    #[error("Could not import the cursor as a texture")]
    ImportError(#[from] GfxError),
}

#[derive(Default, Clone)]
pub struct XCursorImage {
    pub width: i32,
    pub height: i32,
    pub xhot: i32,
    pub yhot: i32,
    pub delay: u32,
    pub pixels: Vec<Cell<u8>>,
}

impl Debug for XCursorImage {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("XcbCursorImage")
            .field("width", &self.width)
            .field("height", &self.height)
            .field("xhot", &self.xhot)
            .field("yhot", &self.yhot)
            .field("delay", &self.delay)
            .finish_non_exhaustive()
    }
}

fn parser_cursor_file<R: BufRead + Seek>(
    r: &mut R,
    scales: &[Scale],
    sizes: &[u32],
) -> Result<OpenCursorResult, CursorError> {
    let [magic, header] = read_u32_n(r)?;
    if magic != XCURSOR_MAGIC || header < HEADER_SIZE {
        return Err(CursorError::NotAnXcursorFile);
    }
    let [_version, ntoc] = read_u32_n(r)?;
    r.seek(SeekFrom::Current((HEADER_SIZE - header) as i64))?;
    if ntoc > 0x10000 {
        return Err(CursorError::OversizedXcursorFile);
    }
    struct Target {
        positions: Vec<u32>,
        effective_size: u32,
        size: u32,
        scale: Scale,
        best_fit: i64,
        best_fit_size: u32,
    }
    let mut targets = Vec::new();
    for scale in scales {
        let scalef = scale.to_f64();
        for size in sizes {
            let effective_size = (*size as f64 * scalef).round() as _;
            targets.push(Target {
                positions: vec![],
                effective_size,
                size: *size,
                scale: *scale,
                best_fit: i64::MAX,
                best_fit_size: 0,
            });
        }
    }
    let mut sizes = AHashSet::new();
    for _ in 0..ntoc {
        let [type_, size, position] = read_u32_n(r)?;
        if type_ != XCURSOR_IMAGE_TYPE {
            continue;
        }
        sizes.insert(size);
        for target in &mut targets {
            let fit = (size as i64 - target.effective_size as i64).abs();
            if fit < target.best_fit {
                target.best_fit = fit;
                target.best_fit_size = size;
                target.positions.clear();
            }
            if size == target.best_fit_size {
                target.positions.push(position);
            }
        }
    }
    let positions: AHashSet<_> = targets
        .iter()
        .flat_map(|t| t.positions.iter().copied())
        .collect();
    if positions.is_empty() {
        return Err(CursorError::EmptyXcursorFile);
    }
    let mut images = AHashMap::new();
    for position in positions {
        r.seek(SeekFrom::Start(position as u64))?;
        let [
            _chunk_header,
            _type_,
            _size,
            _version,
            width,
            height,
            xhot,
            yhot,
            delay,
        ] = read_u32_n(r)?;
        let [width, height, xhot, yhot] = u32_to_i32([width, height, xhot, yhot])?;
        let mut image = XCursorImage {
            width,
            height,
            xhot,
            yhot,
            delay,
            pixels: vec![],
        };
        let num_bytes = width as usize * height as usize * 4;
        unsafe {
            image.pixels.reserve_exact(num_bytes);
            image.pixels.set_len(num_bytes);
            r.read_exact(slice::from_raw_parts_mut(
                image.pixels.as_mut_ptr() as _,
                num_bytes,
            ))?;
        }
        images.insert(position, Rc::new(image));
    }
    let mut num = targets[0].positions.len();
    if num > 1 && targets.iter().any(|t| t.positions.len() != num) {
        log::warn!(
            "Cursor file contains animated cursor but not all scales have the same number of images"
        );
        num = 1;
    }
    let mut res = vec![];
    for i in 0..num {
        let mut idx_images = AHashMap::new();
        for target in &targets {
            let image = images.get(&target.positions[i]).unwrap();
            idx_images.insert((target.scale, target.size), image.clone());
        }
        res.push(idx_images);
    }
    Ok(OpenCursorResult { images: res })
}

fn read_u32_n<R: BufRead, const N: usize>(r: &mut R) -> Result<[u32; N], io::Error> {
    let mut res = [0; N];
    r.read_u32_into::<LittleEndian>(&mut res)?;
    Ok(res)
}

fn u32_to_i32<const N: usize>(n: [u32; N]) -> Result<[i32; N], CursorError> {
    let mut res = [0; N];
    for i in 0..N {
        res[i] = n[i]
            .try_into()
            .map_err(|_| CursorError::CorruptXcursorFile)?;
    }
    Ok(res)
}
