use anyhow::{anyhow, Result};
use byteorder::{LittleEndian, ReadBytesExt};
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::path::Path;

use super::mul_reader;
use super::uop_reader;

const STATIC_OFFSET: usize = 0x4000;

/// RGBA pixel data for a decoded art image.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtImage {
    pub width: u32,
    pub height: u32,
    /// Raw RGBA bytes, row-major.
    pub pixels: Vec<u8>,
}

// ── BMP artwork folder ────────────────────────────────────────────────────────

/// Find the "UO artwork" folder, checking resource dir then walking up from the exe.
/// In dev mode the exe is deep in target/debug/, so we walk up to the repo root.
pub fn find_artwork_dir(app: &tauri::AppHandle) -> Option<std::path::PathBuf> {
    use tauri::Manager;

    // 1. Try Tauri resource dir (works in production bundles)
    if let Ok(resource_dir) = app.path().resource_dir() {
        let candidate = resource_dir.join("UO artwork");
        if candidate.is_dir() {
            return Some(candidate);
        }
    }

    // 2. Walk up from the exe (works in dev mode where resource_dir points at src-tauri/)
    if let Ok(exe) = std::env::current_exe() {
        let mut dir = exe.parent().map(|p| p.to_path_buf());
        for _ in 0..8 {
            if let Some(ref d) = dir {
                let candidate = d.join("UO artwork");
                if candidate.is_dir() {
                    return Some(candidate);
                }
                dir = d.parent().map(|p| p.to_path_buf());
            } else {
                break;
            }
        }
    }

    None
}

/// Decode a BMP from the "UO artwork" folder by item_id.
/// Filename pattern: "Item 0x{id:04X}.bmp"
/// Black (0,0,0) pixels are made transparent — UO art exports use black for transparency.
pub fn decode_static_bmp(artwork_dir: &Path, item_id: usize) -> Result<ArtImage> {
    let filename = format!("Item 0x{:04X}.bmp", item_id);
    let path = artwork_dir.join(&filename);
    if !path.exists() {
        return Err(anyhow!("no art data"));
    }
    decode_bmp_file(&path)
}

fn decode_bmp_file(path: &Path) -> Result<ArtImage> {
    let img = image::open(path)
        .map_err(|e| anyhow!("BMP decode: {}", e))?;
    let mut rgba = img.to_rgba8();
    // UO art BMP exports use pure black as the transparent colour.
    for pixel in rgba.pixels_mut() {
        if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0 {
            pixel[3] = 0;
        }
    }
    Ok(ArtImage {
        width: rgba.width(),
        height: rgba.height(),
        pixels: rgba.into_raw(),
    })
}

// ── Public decode entry points ────────────────────────────────────────────────

/// Try every known art source for `item_id` in priority order.
/// Returns the first that succeeds, or Err("no art data") if none.
pub fn decode_static_any(
    uo_root: &Path,
    item_id: usize,
) -> Result<ArtImage> {
    let mul_path   = uo_root.join("art.mul");
    let idx_path   = uo_root.join("artidx.mul");
    let uop_path   = uo_root.join("artLegacyMUL.uop");
    let tileart    = uo_root.join("tileart.uop");

    // 1. Classic art.mul
    if mul_path.exists() && idx_path.exists() {
        if let Ok(img) = decode_static(&mul_path, &idx_path, item_id) {
            return Ok(img);
        }
    }

    // 2. artLegacyMUL.uop (Classic UOP packaging)
    if uop_path.exists() {
        if let Ok(img) = decode_static_uop(&uop_path, item_id) {
            return Ok(img);
        }
    }

    // 3. tileart.uop (Enhanced Client) — tries multiple hash patterns
    if tileart.exists() {
        if let Ok(img) = decode_static_tileart_uop(&tileart, item_id) {
            return Ok(img);
        }
    }

    Err(anyhow!("no art data"))
}

/// Decode from art.mul + artidx.mul (Classic Client).
pub fn decode_static(mul_path: &Path, idx_path: &Path, item_id: usize) -> Result<ArtImage> {
    let entries = mul_reader::read_index(idx_path)?;
    let index = STATIC_OFFSET + item_id;

    let entry = entries
        .get(index)
        .ok_or_else(|| anyhow!("no art data"))?;

    let raw = mul_reader::read_entry(mul_path, entry)?
        .ok_or_else(|| anyhow!("no art data"))?;

    decode_art_data(&raw)
}

/// Decode from artLegacyMUL.uop.
/// Hash pattern: "build/artlegacymul/{art_index:08}.tga"
pub fn decode_static_uop(uop_path: &Path, item_id: usize) -> Result<ArtImage> {
    let art_index = STATIC_OFFSET + item_id;
    let pattern = format!("build/artlegacymul/{:08}.tga", art_index);
    let hash = uop_reader::uop_hash(&pattern);

    let index = uop_reader::read_uop_index(uop_path)?;
    let entry = index
        .get(&hash)
        .ok_or_else(|| anyhow!("no art data"))?
        .clone();

    let raw = uop_reader::read_uop_entry(uop_path, &entry)?;
    decode_art_data(&raw)
}

/// Decode from tileart.uop (Enhanced Client).
/// Tries several hash patterns; the data can be DDS or classic 16-bit art.
pub fn decode_static_tileart_uop(uop_path: &Path, item_id: usize) -> Result<ArtImage> {
    let index = uop_reader::read_uop_index(uop_path)?;

    // Try every plausible hash pattern the EC might use.
    let candidates = [
        format!("build/tileart/{:08}.bin",   item_id + STATIC_OFFSET),
        format!("build/tileart/{:08}.bin",   item_id),
        format!("build/tileart/{:08x}.bin",  item_id + STATIC_OFFSET),
        format!("build/tileart/{:08x}.bin",  item_id),
        format!("build/tileartlegacy/{:08}.bin",  item_id + STATIC_OFFSET),
        format!("build/tileartlegacy/{:08}.bin",  item_id),
        format!("build/worldart/{:08}.dds",  item_id + STATIC_OFFSET),
        format!("build/worldart/{:08}.dds",  item_id),
    ];

    for pattern in &candidates {
        let hash = uop_reader::uop_hash(pattern);
        if let Some(entry) = index.get(&hash) {
            let raw = uop_reader::read_uop_entry(uop_path, entry)?;
            if raw.is_empty() {
                continue;
            }
            // DDS magic: "DDS " = 0x44 44 53 20
            if raw.starts_with(b"DDS ") {
                if let Ok(img) = decode_dds(&raw) {
                    return Ok(img);
                }
            }
            // Try classic art format
            if let Ok(img) = decode_art_data(&raw) {
                return Ok(img);
            }
        }
    }

    Err(anyhow!("no art data"))
}

// ── Format decoders ───────────────────────────────────────────────────────────

/// Decode a DDS file to RGBA using the `image` crate.
fn decode_dds(data: &[u8]) -> Result<ArtImage> {
    let cursor = Cursor::new(data);
    let img = image::load(cursor, image::ImageFormat::Dds)
        .map_err(|e| anyhow!("DDS decode: {}", e))?;
    let rgba = img.to_rgba8();
    Ok(ArtImage {
        width: rgba.width(),
        height: rgba.height(),
        pixels: rgba.into_raw(),
    })
}

/// Decode raw art.mul / UOP art data (16-bit 1-5-5-5 ARGB, run-length encoded).
pub fn decode_art_data(data: &[u8]) -> Result<ArtImage> {
    // Minimum: 2 unknown + 2 width + 2 height = 6 bytes
    if data.len() < 6 {
        return Err(anyhow!("no art data"));
    }

    let mut cursor = Cursor::new(data);
    let _unknown = cursor.read_u16::<LittleEndian>()?;
    let width    = cursor.read_u16::<LittleEndian>()? as u32;
    let height   = cursor.read_u16::<LittleEndian>()? as u32;

    if width == 0 || height == 0 || width > 1024 || height > 1024 {
        return Err(anyhow!("no art data"));
    }

    // Row lookup table: `height` u16 offsets.  Must all fit in the buffer.
    let row_table_bytes = height as usize * 2;
    if data.len() < 6 + row_table_bytes {
        return Err(anyhow!("no art data"));
    }

    let mut row_offsets = vec![0u32; height as usize];
    for o in row_offsets.iter_mut() {
        *o = cursor.read_u16::<LittleEndian>()? as u32;
    }
    let data_start = cursor.position() as usize;

    let mut pixels = vec![0u8; (width * height * 4) as usize];

    for y in 0..height as usize {
        let row_byte_offset = row_offsets[y] as usize * 2;
        let mut x = 0usize;
        let mut pos = data_start + row_byte_offset;

        loop {
            // Need 4 bytes: 2 for run, 2 for count
            if pos + 4 > data.len() {
                break;
            }
            let run   = u16::from_le_bytes([data[pos],     data[pos + 1]]);
            let count = u16::from_le_bytes([data[pos + 2], data[pos + 3]]);
            pos += 4;

            if run == 0 && count == 0 {
                break;
            }

            x += run as usize;

            for _ in 0..count as usize {
                if pos + 2 > data.len() || x >= width as usize {
                    break;
                }
                let color = u16::from_le_bytes([data[pos], data[pos + 1]]);
                pos += 2;
                let rgba = argb16_to_rgba32(color);
                let idx = (y * width as usize + x) * 4;
                pixels[idx..idx + 4].copy_from_slice(&rgba);
                x += 1;
            }
        }
    }

    Ok(ArtImage { width, height, pixels })
}

/// Convert 16-bit UO ARGB (1-5-5-5) to 32-bit RGBA.
pub fn argb16_to_rgba32(color: u16) -> [u8; 4] {
    if color == 0 {
        return [0, 0, 0, 0];
    }
    let r = ((color >> 10) & 0x1F) as u8;
    let g = ((color >> 5)  & 0x1F) as u8;
    let b = ( color        & 0x1F) as u8;
    [
        (r << 3) | (r >> 2),
        (g << 3) | (g >> 2),
        (b << 3) | (b >> 2),
        255,
    ]
}
