use crate::editor::{DOCKMANAGER, UNDOMANAGER};
use crate::prelude::*;

const REMAP_ALL_ID: &str = "actionRemapAll";
const REMAP_MODE_ID: &str = "actionRemapMode";
const REMAP_RANGE_ID: &str = "actionRemapRange";

#[derive(Clone, Copy)]
enum RemapMode {
    Nearest,
    FloydSteinberg,
    Bayer4x4,
    Exact,
}

impl RemapMode {
    fn from_index(index: i32) -> Self {
        match index {
            1 => Self::FloydSteinberg,
            2 => Self::Bayer4x4,
            3 => Self::Exact,
            _ => Self::Nearest,
        }
    }
}

pub struct RemapTile {
    id: TheId,
    nodeui: TheNodeUI,
}

fn find_exact_color_index(palette: &ThePalette, color: &TheColor) -> Option<usize> {
    palette
        .colors
        .iter()
        .position(|entry| entry.as_ref() == Some(color))
}

fn nearest_palette_color_u8(palette: &ThePalette, color: [u8; 4]) -> Option<[u8; 4]> {
    let color = TheColor::from(color);
    let index = palette.find_closest_color_index(&color)?;
    palette
        .colors
        .get(index)
        .and_then(|entry| entry.as_ref())
        .map(TheColor::to_u8_array)
}

fn parse_palette_range(text: &str, palette_len: usize) -> Option<(usize, usize)> {
    let trimmed = text.trim();
    if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("all") {
        if palette_len == 0 {
            None
        } else {
            Some((0, palette_len.saturating_sub(1)))
        }
    } else if let Some((start, end)) = trimmed.split_once('-') {
        let start = start.trim().parse::<usize>().ok()?;
        let end = end.trim().parse::<usize>().ok()?;
        if palette_len == 0 {
            return None;
        }
        let start = start.min(palette_len.saturating_sub(1));
        let end = end.min(palette_len.saturating_sub(1));
        Some((start.min(end), start.max(end)))
    } else {
        let index = trimmed.parse::<usize>().ok()?;
        if palette_len == 0 {
            None
        } else {
            let index = index.min(palette_len.saturating_sub(1));
            Some((index, index))
        }
    }
}

fn palette_for_range(palette: &ThePalette, range_text: &str) -> ThePalette {
    let Some((start, end)) = parse_palette_range(range_text, palette.colors.len()) else {
        return palette.clone();
    };

    let mut filtered = palette.clone();
    for (index, entry) in filtered.colors.iter_mut().enumerate() {
        if index < start || index > end {
            *entry = None;
        }
    }
    filtered
}

fn remap_texture_nearest(tex: &mut rusterix::Texture, palette: &ThePalette) {
    for y in 0..tex.height {
        for x in 0..tex.width {
            let mut col = tex.get_pixel(x as u32, y as u32);
            if col[3] == 0 {
                continue;
            }
            if let Some(mapped) = nearest_palette_color_u8(palette, col) {
                col = mapped;
                tex.set_pixel(x as u32, y as u32, col);
            }
        }
    }
}

fn remap_texture_exact(tex: &mut rusterix::Texture, palette: &ThePalette) {
    for y in 0..tex.height {
        for x in 0..tex.width {
            let mut col = tex.get_pixel(x as u32, y as u32);
            if col[3] == 0 {
                continue;
            }
            let color = TheColor::from(col);
            if let Some(index) = find_exact_color_index(palette, &color)
                && let Some(c) = palette.colors.get(index)
                && let Some(c) = c
            {
                col = c.to_u8_array();
                tex.set_pixel(x as u32, y as u32, col);
            }
        }
    }
}

fn remap_texture_floyd_steinberg(tex: &mut rusterix::Texture, palette: &ThePalette) {
    let width = tex.width as usize;
    let height = tex.height as usize;
    let mut work = vec![[0.0f32; 4]; width * height];

    for y in 0..height {
        for x in 0..width {
            let col = tex.get_pixel(x as u32, y as u32);
            work[y * width + x] = [col[0] as f32, col[1] as f32, col[2] as f32, col[3] as f32];
        }
    }

    let diffuse = |work: &mut [[f32; 4]],
                   x: usize,
                   y: usize,
                   dx: isize,
                   dy: isize,
                   err: [f32; 3],
                   factor: f32| {
        let nx = x as isize + dx;
        let ny = y as isize + dy;
        if nx < 0 || ny < 0 || nx >= width as isize || ny >= height as isize {
            return;
        }
        let idx = ny as usize * width + nx as usize;
        if work[idx][3] == 0.0 {
            return;
        }
        work[idx][0] = (work[idx][0] + err[0] * factor).clamp(0.0, 255.0);
        work[idx][1] = (work[idx][1] + err[1] * factor).clamp(0.0, 255.0);
        work[idx][2] = (work[idx][2] + err[2] * factor).clamp(0.0, 255.0);
    };

    for y in 0..height {
        for x in 0..width {
            let idx = y * width + x;
            if work[idx][3] == 0.0 {
                continue;
            }
            let source = [
                work[idx][0].round().clamp(0.0, 255.0) as u8,
                work[idx][1].round().clamp(0.0, 255.0) as u8,
                work[idx][2].round().clamp(0.0, 255.0) as u8,
                work[idx][3].round().clamp(0.0, 255.0) as u8,
            ];

            if let Some(mapped) = nearest_palette_color_u8(palette, source) {
                tex.set_pixel(
                    x as u32,
                    y as u32,
                    [mapped[0], mapped[1], mapped[2], source[3]],
                );
                let err = [
                    source[0] as f32 - mapped[0] as f32,
                    source[1] as f32 - mapped[1] as f32,
                    source[2] as f32 - mapped[2] as f32,
                ];
                diffuse(&mut work, x, y, 1, 0, err, 7.0 / 16.0);
                diffuse(&mut work, x, y, -1, 1, err, 3.0 / 16.0);
                diffuse(&mut work, x, y, 0, 1, err, 5.0 / 16.0);
                diffuse(&mut work, x, y, 1, 1, err, 1.0 / 16.0);
            }
        }
    }
}

fn remap_texture_bayer4x4(tex: &mut rusterix::Texture, palette: &ThePalette) {
    const BAYER_4X4: [[f32; 4]; 4] = [
        [0.0, 8.0, 2.0, 10.0],
        [12.0, 4.0, 14.0, 6.0],
        [3.0, 11.0, 1.0, 9.0],
        [15.0, 7.0, 13.0, 5.0],
    ];

    for y in 0..tex.height {
        for x in 0..tex.width {
            let mut col = tex.get_pixel(x as u32, y as u32);
            if col[3] == 0 {
                continue;
            }

            let threshold = (BAYER_4X4[(y as usize) % 4][(x as usize) % 4] / 16.0) - 0.5;
            let offset = threshold * 48.0;
            let adjusted = [
                (col[0] as f32 + offset).clamp(0.0, 255.0) as u8,
                (col[1] as f32 + offset).clamp(0.0, 255.0) as u8,
                (col[2] as f32 + offset).clamp(0.0, 255.0) as u8,
                col[3],
            ];

            if let Some(mapped) = nearest_palette_color_u8(palette, adjusted) {
                col = [mapped[0], mapped[1], mapped[2], col[3]];
                tex.set_pixel(x as u32, y as u32, col);
            }
        }
    }
}

fn remap_tile_to_palette(tile: &mut rusterix::Tile, palette: &ThePalette, mode: RemapMode) -> bool {
    let prev = tile.clone();

    for tex in &mut tile.textures {
        match mode {
            RemapMode::Nearest => remap_texture_nearest(tex, palette),
            RemapMode::FloydSteinberg => remap_texture_floyd_steinberg(tex, palette),
            RemapMode::Bayer4x4 => remap_texture_bayer4x4(tex, palette),
            RemapMode::Exact => remap_texture_exact(tex, palette),
        }
    }

    if prev.textures == tile.textures {
        return false;
    }

    for tex in &mut tile.textures {
        tex.generate_normals(true);
    }

    true
}

impl Action for RemapTile {
    fn new() -> Self
    where
        Self: Sized,
    {
        let mut nodeui: TheNodeUI = TheNodeUI::default();
        nodeui.add_item(TheNodeUIItem::Selector(
            REMAP_MODE_ID.into(),
            "".into(),
            "".into(),
            vec![
                "nearest".into(),
                "floyd-steinberg".into(),
                "bayer-4x4".into(),
                "exact".into(),
            ],
            0,
        ));
        nodeui.add_item(TheNodeUIItem::Text(
            REMAP_RANGE_ID.into(),
            "".into(),
            "".into(),
            "all".into(),
            Some("all".into()),
            false,
        ));
        nodeui.add_item(TheNodeUIItem::Checkbox(
            REMAP_ALL_ID.into(),
            "".into(),
            "".into(),
            false,
        ));

        Self {
            id: TheId::named(&fl!("action_remap_tile")),
            nodeui,
        }
    }

    fn id(&self) -> TheId {
        self.id.clone()
    }

    fn info(&self) -> String {
        fl!("action_remap_tile_desc")
    }

    fn role(&self) -> ActionRole {
        ActionRole::Dock
    }

    fn accel(&self) -> Option<TheAccelerator> {
        None
    }

    fn is_applicable(&self, _map: &Map, _ctx: &mut TheContext, server_ctx: &ServerContext) -> bool {
        DOCKMANAGER.read().unwrap().dock == "Tiles"
            && (server_ctx.curr_tile_source.is_some() || server_ctx.curr_tile_id.is_some())
    }

    fn apply_project(
        &self,
        project: &mut Project,
        _ui: &mut TheUI,
        ctx: &mut TheContext,
        server_ctx: &mut ServerContext,
    ) {
        let remap_all = self.nodeui.get_bool_value(REMAP_ALL_ID).unwrap_or(false);
        let remap_mode =
            RemapMode::from_index(self.nodeui.get_i32_value(REMAP_MODE_ID).unwrap_or(0));
        let range_text = self
            .nodeui
            .get_text_value(REMAP_RANGE_ID)
            .unwrap_or_else(|| "all".to_string());
        let palette = palette_for_range(&project.palette, &range_text);

        if remap_all {
            let mut edits = Vec::new();

            for tile in project.tiles.values_mut() {
                let prev = tile.clone();
                if remap_tile_to_palette(tile, &palette, remap_mode) {
                    edits.push((prev, tile.clone()));
                }
            }

            if !edits.is_empty() {
                UNDOMANAGER
                    .write()
                    .unwrap()
                    .add_undo(ProjectUndoAtom::TileBatchEdit(edits), ctx);
                ctx.ui.send(TheEvent::Custom(
                    TheId::named("Update Tiles"),
                    TheValue::Empty,
                ));
            }
        } else {
            let mut edits = Vec::new();

            if let Some(source) = server_ctx.curr_tile_source {
                match source {
                    TileSource::TileGroup(group_id) => {
                        if let Some(group) = project.tile_groups.get(&group_id).cloned() {
                            for member in &group.members {
                                if let Some(tile) = project.tiles.get_mut(&member.tile_id) {
                                    let prev = tile.clone();
                                    if remap_tile_to_palette(tile, &palette, remap_mode) {
                                        edits.push((prev, tile.clone()));
                                    }
                                }
                            }
                        }
                    }
                    TileSource::TileGroupMember {
                        group_id,
                        member_index,
                    } => {
                        if let Some(group) = project.tile_groups.get(&group_id)
                            && let Some(member) = group.members.get(member_index as usize)
                            && let Some(tile) = project.tiles.get_mut(&member.tile_id)
                        {
                            let prev = tile.clone();
                            if remap_tile_to_palette(tile, &palette, remap_mode) {
                                edits.push((prev, tile.clone()));
                            }
                        }
                    }
                    TileSource::SingleTile(tile_id) => {
                        if let Some(tile) = project.tiles.get_mut(&tile_id) {
                            let prev = tile.clone();
                            if remap_tile_to_palette(tile, &palette, remap_mode) {
                                edits.push((prev, tile.clone()));
                            }
                        }
                    }
                    TileSource::Procedural(_) => {}
                }
            } else if let Some(tile_id) = server_ctx.curr_tile_id
                && let Some(tile) = project.tiles.get_mut(&tile_id)
            {
                let prev = tile.clone();
                if remap_tile_to_palette(tile, &palette, remap_mode) {
                    edits.push((prev, tile.clone()));
                }
            }

            if !edits.is_empty() {
                if edits.len() == 1 {
                    let (prev, next) = edits.into_iter().next().unwrap();
                    UNDOMANAGER
                        .write()
                        .unwrap()
                        .add_undo(ProjectUndoAtom::TileEdit(prev, next), ctx);
                } else {
                    UNDOMANAGER
                        .write()
                        .unwrap()
                        .add_undo(ProjectUndoAtom::TileBatchEdit(edits), ctx);
                }
                ctx.ui.send(TheEvent::Custom(
                    TheId::named("Update Tiles"),
                    TheValue::Empty,
                ));
            }
        }
    }

    fn params(&self) -> TheNodeUI {
        self.nodeui.clone()
    }

    fn handle_event(
        &mut self,
        event: &TheEvent,
        _project: &mut Project,
        _ui: &mut TheUI,
        _ctx: &mut TheContext,
        _server_ctx: &mut ServerContext,
    ) -> bool {
        self.nodeui.handle_event(event)
    }
}
