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

use fontdue::{FontSettings, Metrics};
use pixels::{Pixels, SurfaceTexture};
use rustybuzz::{Direction, GlyphBuffer, Language, Script, ShapePlan, UnicodeBuffer};
use winit::{dpi::PhysicalSize, event_loop::ActiveEventLoop, window::Window};

use crate::{
    HEIGHT, RcLine, Ui, WIDTH,
    json_ui::{self, BuiltinColor, Color},
};

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, Vec<(json_ui::Face, FontIndex, i32, GlyphBuffer)>)>;

#[derive(Default)]
pub struct Shaper {
    pub fonts: Vec<(Vec<u8>, rustybuzz::Face<'static>)>,
    pub cache: ShapeCache,
    pub plans: Plans,
}
impl Shaper {
    fn shape(
        plans: &mut Plans,
        index: FontIndex,
        font: &rustybuzz::Face,
        mut ubuf: UnicodeBuffer,
    ) -> GlyphBuffer {
        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)
    }

    fn make_runs(&mut self, line: RcLine) -> &[(json_ui::Face, FontIndex, i32, GlyphBuffer)] {
        let res = self.cache.entry(line.clone()).or_insert_with(|| {
            let mut res = Vec::new();
            let mut curr_font = 0;
            let slice: &[json_ui::Atom] = &line;
            for atom in slice {
                let mut start = 0;
                for (i, c) in atom.contents.char_indices() {
                    let font = self
                        .fonts
                        .iter()
                        .position(|i| i.1.unicode_ranges().contains_char(c))
                        .unwrap_or(0);
                    if curr_font != font || c == '\n' {
                        let mut ubuf = UnicodeBuffer::new();
                        ubuf.push_str(&atom.contents[start..i]);
                        let rb_font = &self.fonts[curr_font].1;
                        let glyph_buffer = Self::shape(&mut self.plans, curr_font, rb_font, ubuf);
                        res.push((
                            atom.face.clone(),
                            curr_font,
                            rb_font.units_per_em(),
                            glyph_buffer,
                        ));
                        curr_font = font;
                        start = i;
                    }
                    if c == '\n' {
                        start += 1;
                    }
                }
                let mut ubuf = UnicodeBuffer::new();
                ubuf.push_str(&atom.contents[start..]);
                let rb_font = &self.fonts[curr_font].1;
                let glyph_buffer = Self::shape(&mut self.plans, curr_font, rb_font, ubuf);
                res.push((
                    atom.face.clone(),
                    curr_font,
                    rb_font.units_per_em(),
                    glyph_buffer,
                ));
            }
            (true, res)
        });
        res.0 = true;
        &res.1
    }

    fn clear_cache(&mut self) {
        self.cache.retain(|_, v| {
            let oldv = v.0;
            v.0 = false;
            oldv
        })
    }
}

#[derive(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: f32,
    ) -> &(Metrics, Vec<u8>) {
        self.cache
            .entry((font, glyph))
            .or_insert_with(|| self.fonts[font].rasterize_indexed(glyph, font_size))
    }

    #[allow(clippy::too_many_arguments)]
    fn rasterize_segment(
        &mut self,
        buf: &mut [u8],
        size: (u32, u32),

        font: FontIndex,
        glyph_buffer: &GlyphBuffer,
        upe: i32,
        font_size: f32,
        face: &json_ui::Face,
        default_face: &json_ui::Face,
        pos: &mut (i32, i32),
    ) {
        let fs = font_size as i32;
        let line_height = 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) = self.rasterize_glyph(font, ginfo.glyph_id as u16, font_size);

            if face.bg != Color::BuiltinColor(BuiltinColor::Default) {
                for row in -4..line_height {
                    for column in 0..(gpos.x_advance * fs / upe) {
                        let x = pos.0 + column;
                        let y = pos.1 - row;
                        let bidx = ((x + y * size.0 as i32) * 4) as usize;
                        let bg = face.bg.to_rgba([0, 0, 0, 255]);
                        buf[bidx..(4 + bidx)].copy_from_slice(&bg);
                    }
                }
            }

            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 + x_off + column as i32;
                    let y = pos.1 + y_off + row as i32;
                    let bidx = ((x + y * size.0 as i32) * 4) as usize;
                    let sidx = column + row * metrics.width;
                    if bidx >= buf.len() {
                        break;
                    }
                    if x > size.0 as i32 {
                        break;
                    }
                    let fg = &face
                        .fg
                        .to_rgba(default_face.fg.to_rgba([255, 255, 255, 255]));
                    for i in 0..3 {
                        let m = data[sidx] as u32;
                        let d = buf[bidx + i] as u32;
                        let s = fg[i] as u32;
                        let res = (d * (255 - m) + s * m) / 255;
                        buf[bidx + i] = res as u8;
                    }
                    buf[bidx + 3] = 255;
                }
            }

            pos.0 += gpos.x_advance * fs / upe;
            pos.1 += gpos.y_advance * fs / upe;
        }
    }
}

pub struct Renderer {
    /// pixels depends on this being alive
    #[allow(unused)]
    pub window: Window,
    pub pixels: Pixels<'static>,
    pub size: (u32, u32),

    shaper: Shaper,
    rasterizer: Rasterizer,
    font_size: f32,
}

impl Renderer {
    pub fn new(event_loop: &ActiveEventLoop) -> Result<Self, Box<dyn Error>> {
        let window = event_loop.create_window(
            Window::default_attributes()
                .with_inner_size(PhysicalSize::new(WIDTH as f64, HEIGHT as f64))
                .with_title("Kakoune Client"),
        )?;
        let st = SurfaceTexture::new(
            WIDTH,
            HEIGHT,
            // We need to use unsafe here because otherwise we can't have self-referencial structs
            unsafe { &*(&window as *const Window) },
        );
        let pixels = Pixels::new(WIDTH, HEIGHT, st)?;

        let mut fontdb = fontdb::Database::new();
        fontdb.load_system_fonts();

        let mut shaper = Shaper::default();
        let mut rasterizer = Rasterizer::default();
        for font in ["Iosevka", "Manjari"] {
            let id = fontdb
                .query(&fontdb::Query {
                    families: &[fontdb::Family::Name(font)],
                    ..Default::default()
                })
                .ok_or("Unknown font")?;
            fontdb
                .with_face_data(id, |data, _| {
                    let data = data.to_vec();
                    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(())
                })
                .ok_or("Failed to load font")
                .flatten()?;
        }
        Ok(Self {
            window,
            pixels,
            size: (WIDTH, HEIGHT),
            shaper,
            rasterizer,
            font_size: 16.0,
        })
    }

    pub fn line_height(&self) -> u32 {
        (self.font_size * 1.5) as u32
    }

    pub fn render(&mut self, ui: &Ui) -> Result<(), Box<dyn Error>> {
        let bg = ui.default_face.bg.to_rgba([0, 0, 0, 255]);
        for c in self.pixels.frame_mut().chunks_exact_mut(4) {
            c.copy_from_slice(&bg);
        }

        let line_height = self.line_height() as i32;
        let mut pos = (0, line_height);
        for line in &ui.lines {
            for (face, font, upe, glyph_buffer) in self.shaper.make_runs(line.clone()) {
                self.rasterizer.rasterize_segment(
                    self.pixels.frame_mut(),
                    self.size,
                    *font,
                    glyph_buffer,
                    *upe,
                    self.font_size,
                    face,
                    &ui.default_face,
                    &mut pos,
                );
            }
            pos.0 = 0;
            pos.1 += line_height;
        }
        let line_height = self.font_size as i32;
        let line_bg = ui.line_face.bg.to_rgba([0, 0, 0, 255]);

        pos.1 = self.size.1 as i32 - line_height;
        pos.0 = 0;

        let start = (pos.0 as usize + (pos.1 - line_height) as usize * self.size.0 as usize) * 4;
        let end = (pos.0 as usize + (pos.1 + 4) as usize * self.size.0 as usize) * 4;
        for c in self.pixels.frame_mut()[start..=end].chunks_exact_mut(4) {
            c.copy_from_slice(&line_bg);
        }

        for (face, font, upe, glyph_buffer) in self.shaper.make_runs(ui.prompt_line.clone()) {
            self.rasterizer.rasterize_segment(
                self.pixels.frame_mut(),
                self.size,
                *font,
                glyph_buffer,
                *upe,
                self.font_size,
                face,
                &ui.line_face,
                &mut pos,
            );
        }

        let mode_line = self.shaper.make_runs(ui.mode_line.clone());
        let text_width: i32 = mode_line
            .iter()
            .map(|(_, _, upe, glyph_buffer)| {
                glyph_buffer
                    .glyph_positions()
                    .iter()
                    .map(|gpos| gpos.x_advance * self.font_size as i32 / upe)
                    .sum::<i32>()
            })
            .sum();

        pos.0 = self.size.0 as i32 - text_width;
        for (face, font, upe, glyph_buffer) in mode_line {
            self.rasterizer.rasterize_segment(
                self.pixels.frame_mut(),
                self.size,
                *font,
                glyph_buffer,
                *upe,
                self.font_size,
                face,
                &ui.line_face,
                &mut pos,
            );
        }
        self.shaper.clear_cache();
        self.pixels.render().unwrap();
        Ok(())
    }
}