use std::{collections::HashMap, error::Error, rc::Rc};

use fontdue::{FontSettings, Metrics};
use rustybuzz::{Direction, GlyphBuffer, Language, Script, ShapePlan, UnicodeBuffer};

use font_loader::system_fonts::{self, FontPropertyBuilder};

use crate::{
    RcLine, Ui,
    json_ui::{self, Config, InfoShowStyle, MenuShowStyle},
};

type GlyphIndex = u16;
type FontIndex = usize;

type Plans = HashMap<(FontIndex, Direction, Script, Option<Language>), ShapePlan>;
type GlyphCache = HashMap<(FontIndex, GlyphIndex), (Metrics, Vec<u8>)>;
type ShapeCache = HashMap<RcLine, (bool, Rc<[Run]>)>;

#[derive(Debug)]
struct Run {
    fg: [u8; 4],
    bg: Option<[u8; 4]>,
    font: FontIndex,
    glyph_buffer: GlyphBuffer,
    font_size: u32,
    upe: i32,
}

impl Run {
    fn length(&self) -> usize {
        self.glyph_buffer
            .glyph_positions()
            .iter()
            .map(|i| i.x_advance * self.font_size as i32 / self.upe)
            .sum::<i32>() as usize
    }
}

#[derive(Default)]
pub struct Shaper {
    fonts: Vec<(Vec<u8>, rustybuzz::Face<'static>)>,
    plans: Plans,
}

fn shape(plans: &mut Plans, index: FontIndex, font: &rustybuzz::Face, text: &str) -> GlyphBuffer {
    let mut ubuf = UnicodeBuffer::new();
    ubuf.push_str(text);
    ubuf.guess_segment_properties();
    let direction = match ubuf.direction() {
        Direction::Invalid => Direction::LeftToRight,
        dir => dir,
    };
    let script = ubuf.script();
    let language = ubuf.language();
    let plan = plans
        .entry((index, direction, script, language.clone()))
        .or_insert_with(|| ShapePlan::new(font, direction, Some(script), language.as_ref(), &[]));
    rustybuzz::shape_with_plan(font, plan, ubuf)
}

impl Shaper {
    fn make_runs(&mut self, line: RcLine, config: &Config, default_fg: [u8; 4]) -> Rc<[Run]> {
        let font_size = config.font_size;
        let mut res = Vec::new();
        let mut font = 0;
        let slice: &[json_ui::Atom] = &line;

        for atom in slice {
            let mut text = String::new();
            for c in atom.contents.chars() {
                let Some(curr_font) = (if self.fonts[font].1.unicode_ranges().contains_char(c) {
                    Some(font)
                } else {
                    self.fonts
                        .iter()
                        .position(|i| i.1.unicode_ranges().contains_char(c))
                }) else {
                    // TODO: Render fallback character
                    // For new we'll just use a standin
                    text.push('');
                    continue;
                };
                if font != curr_font {
                    let rb_font = &self.fonts[font].1;
                    let glyph_buffer = shape(&mut self.plans, font, rb_font, &text);
                    let run = Run {
                        fg: atom.face.fg.to_rgba(config).unwrap_or(default_fg),
                        bg: atom.face.bg.to_rgba(config),
                        font_size,
                        font,
                        glyph_buffer,
                        upe: rb_font.units_per_em(),
                    };
                    text.clear();
                    res.push(run);
                    font = curr_font;
                }
                if c == '\n' {
                    text.push(' ');
                } else {
                    text.push(c);
                }
            }
            let rb_font = &self.fonts[font].1;
            let glyph_buffer = shape(&mut self.plans, font, rb_font, &text);
            let run = Run {
                fg: atom.face.fg.to_rgba(config).unwrap_or(default_fg),
                bg: atom.face.bg.to_rgba(config),
                font_size,
                font,
                glyph_buffer,
                upe: rb_font.units_per_em(),
            };
            res.push(run);
        }
        res.into()
    }
}

#[derive(Debug, Default)]
pub struct Rasterizer {
    pub fonts: Vec<fontdue::Font>,
    pub cache: GlyphCache,
}

impl Rasterizer {
    fn rasterize_glyph(
        &mut self,
        font: FontIndex,
        glyph: GlyphIndex,
        font_size: u32,
    ) -> &(Metrics, Vec<u8>) {
        self.cache
            .entry((font, glyph))
            .or_insert_with(|| self.fonts[font].rasterize_indexed(glyph, font_size as f32))
    }
}

/// A wrapper around a buffer
pub struct Target<'a> {
    pub size: (usize, usize),
    pub buf: &'a mut [u32],
}
impl<'a> Target<'a> {
    pub fn new(buf: &'a mut [u32], width: usize, height: usize) -> Self {
        Self {
            buf,
            size: (width, height),
        }
    }

    #[inline]
    fn draw_rect(&mut self, x: usize, y: usize, w: usize, h: usize, color: [u8; 4]) {
        for i in y..(y + h) {
            for j in x..(x + w) {
                if j >= self.size.0 || i >= self.size.1 {
                    break;
                }
                let bidx = j + i * self.size.0;
                self.buf[bidx] = u32::from_be_bytes(color);
            }
        }
    }
}

pub fn load_fonts(
    fonts: &[String],
    shaper: &mut Shaper,
    rasterizer: &mut Rasterizer,
) -> Result<(), Box<dyn Error>> {
    shaper.fonts.clear();
    rasterizer.fonts.clear();
    rasterizer.cache.clear();
    for font in fonts {
        let property = if font == "monospace" {
            FontPropertyBuilder::new().monospace().build()
        } else {
            FontPropertyBuilder::new().family(font).build()
        };
        let (data, _) = system_fonts::get(&property).expect("Failed to load font");
        let rb_face = rustybuzz::Face::from_slice(unsafe { &*(data.as_slice() as *const [u8]) }, 0)
            .ok_or("Failed to load rustybuzz font")?;
        let fd_face = fontdue::Font::from_bytes(data.as_slice(), FontSettings::default())?;
        shaper.fonts.push((data, rb_face));
        rasterizer.fonts.push(fd_face);
    }
    Ok(())
}

pub fn line_height(font_size: f32) -> f32 {
    font_size * 1.5
}

fn rasterize_segment(
    buf: &mut Target,
    pos: &mut (usize, usize),
    run: &Run,
    rasterizer: &mut Rasterizer,
) {
    let Run {
        upe,
        glyph_buffer,
        font,
        font_size,
        ..
    } = run;
    let line_height = line_height(*font_size as f32) as usize;
    if let Some(bg) = run.bg {
        buf.draw_rect(
            pos.0,
            pos.1 - line_height + 4,
            run.length(),
            line_height,
            bg,
        );
    }
    let fs = *font_size as i32;

    for i in 0..glyph_buffer.len() {
        let ginfo = glyph_buffer.glyph_infos()[i];
        let gpos = glyph_buffer.glyph_positions()[i];
        let (metrics, data) = rasterizer.rasterize_glyph(*font, ginfo.glyph_id as u16, *font_size);

        if gpos.x_offset < 0 {
            continue;
        }
        let x_off = gpos.x_offset * fs / upe + metrics.xmin;
        let y_off = gpos.y_offset * fs / upe - metrics.ymin - metrics.height as i32;

        for row in 0..metrics.height {
            for column in 0..metrics.width {
                let x = (pos.0 as i32 + x_off) as usize + column;
                let y = (pos.1 as i32 + y_off) as usize + row;
                let bidx = x + y * buf.size.0;
                let sidx = column + row * metrics.width;
                if bidx >= buf.buf.len() {
                    break;
                }
                if x > buf.size.0 {
                    break;
                }
                let mut pixel = u32::to_be_bytes(buf.buf[bidx]);
                for i in 0..4 {
                    let m = data[sidx] as u32;
                    let d = pixel[i] as u32;
                    let s = run.fg[i] as u32;
                    pixel[i] = ((d * (255 - m) + s * m) / 255) as u8;
                }
                buf.buf[bidx] = u32::from_be_bytes(pixel);
            }
        }

        pos.0 += (gpos.x_advance * fs / upe) as usize;
        pos.1 += (gpos.y_advance * fs / upe) as usize;
    }
}

pub fn render(
    buf: &mut Target,
    shaper: &mut Shaper,
    rs: &mut Rasterizer,
    ui: &Ui,
    config: &Config,
) -> Result<(), Box<dyn Error>> {
    let default_bg = ui.default_face.bg.to_rgba(config).unwrap_or(config.black.0);
    let default_fg = ui.default_face.fg.to_rgba(config).unwrap_or(config.white.0);
    buf.draw_rect(0, 0, buf.size.0, buf.size.1, default_bg);
    let line_height = line_height(config.font_size as f32) as usize;

    let mut pos = (0, line_height);
    for line in &ui.lines {
        for run in shaper.make_runs(line.clone(), config, default_fg).iter() {
            rasterize_segment(buf, &mut pos, run, rs);
        }
        pos.0 = 0;
        pos.1 += line_height;
    }
    let status_padding: usize = 10;

    pos.1 = buf.size.1 - status_padding;
    pos.0 = 0;

    if let Some(bg) = ui.line_face.bg.to_rgba(config) {
        buf.draw_rect(0, pos.1 - line_height + 4, buf.size.0, line_height, bg);
    }
    let line_fg = ui.line_face.fg.to_rgba(config).unwrap_or(default_fg);

    for run in shaper
        .make_runs(ui.prompt_line.clone(), config, line_fg)
        .iter()
    {
        rasterize_segment(buf, &mut pos, run, rs);
    }

    let mode_fg = ui.line_face.fg.to_rgba(config).unwrap_or(default_fg);
    let mode_line = shaper.make_runs(ui.mode_line.clone(), config, mode_fg);
    let text_width: i32 = mode_line.iter().map(|run| run.length() as i32).sum();

    pos.0 = buf.size.0.saturating_sub(text_width as usize);
    for run in mode_line.iter() {
        rasterize_segment(buf, &mut pos, run, rs);
    }

    let menu_padding_x: usize = 2;
    let menu_padding_y: usize = 2;
    let mut menu_space: usize = 0;
    for menu in &ui.menus {
        let menu_bg = menu.menu_face.bg.to_rgba(config).unwrap_or(default_bg);
        let menu_fg = menu.menu_face.fg.to_rgba(config).unwrap_or(default_fg);
        let selected_fg = menu.selected_face.fg.to_rgba(config).unwrap_or(default_fg);
        let selected_bg = menu.selected_face.bg.to_rgba(config).unwrap_or(default_bg);
        let lines = menu.content.iter().enumerate().map(|(i, line)| {
            if menu.selected == i {
                (
                    i,
                    selected_bg,
                    shaper.make_runs(line.clone(), config, selected_fg),
                )
            } else {
                (i, menu_bg, shaper.make_runs(line.clone(), config, menu_fg))
            }
        });

        match menu.style {
            MenuShowStyle::Prompt => {
                let width = config.font_size * 15;
                let num_width = buf.size.0 / width as usize;
                let num_height = usize::min(menu.content.len() / num_width, 10);

                let width = buf.size.0 / num_width;
                menu_space = line_height * num_height + menu_padding_y * 2;

                for (i, bg, line) in lines {
                    let x = i % num_width;
                    let y = i / num_width;
                    if y >= num_height {
                        break;
                    }
                    pos.0 = x * width + menu_padding_x;
                    pos.1 = (buf.size.1)
                        .saturating_sub(line_height * (y + 1) + menu_padding_y + status_padding);

                    buf.draw_rect(pos.0, pos.1 - line_height + 4, width, line_height, bg);
                    for run in line.iter() {
                        rasterize_segment(buf, &mut pos, run, rs);
                    }
                }
            }
            _ => {}
        }
    }

    let info_padding_x: usize = 5;
    let info_padding_y: usize = 5;
    for info in &ui.infos {
        let info_fg = info.face.fg.to_rgba(config).unwrap_or(default_fg);

        let lines: Vec<_> = info
            .content
            .iter()
            .map(|line| shaper.make_runs(line.clone(), config, info_fg))
            .collect();
        let width = lines
            .iter()
            .map(|run| run.iter().map(|run| run.length() as i32).sum::<i32>())
            .max()
            .unwrap_or(0) as usize
            + info_padding_x * 2;
        let height = line_height * info.content.len() + info_padding_y * 2;
        let info_bg = info.face.bg.to_rgba(config).unwrap_or(default_bg);

        match info.style {
            InfoShowStyle::Prompt => {
                pos.0 = buf.size.0.saturating_sub(width);
                pos.1 = buf
                    .size
                    .1
                    .saturating_sub(height + line_height + status_padding + menu_space);
            }
            InfoShowStyle::Modal => {
                pos.0 = (buf.size.0.saturating_sub(width)) / 2;
                pos.1 = (buf.size.1.saturating_sub(height)) / 2;
            }
            _ => {}
        }
        buf.draw_rect(pos.0, pos.1, width, height, info_bg);

        let start = pos.0 + info_padding_x;
        pos.1 += info_padding_y;
        for runs in lines {
            pos.0 = start;
            pos.1 += line_height;
            for run in runs.iter() {
                rasterize_segment(buf, &mut pos, run, rs);
            }
        }
    }

    Ok(())
}