use crate::pango_extras;

use std::{
    ffi::{CStr, CString, c_double, c_int, c_uint, c_void},
    ptr::NonNull,
    rc::Rc,
};

use crate::configuration::Configuration;

pub const DEFAULT_FONT: &'static str = "sans serif:pixelsize=12";
pub const DEFAULT_FONT_CSTR: &'static CStr = c"sans serif:pixelsize=12";

/// A loaded font, with support for basic drawing operations.
///
/// This owns Pango rendering state, and draw operations may mutate it. As a
/// result, this is not at all thread-safe.
///
/// ## Rust rewrite notes
///
/// Unlike the original C API, font caching is handled by methods on
/// [`W_Screen`].
pub struct Font {
    name: FontName,
    font: NonNull<x11::xft::XftFont>,
    height: c_uint,
    ascent: c_uint,
    layout: NonNull<pango_sys::PangoLayout>,
}

/// Attempts to convert `xlfd` to a Fontconfig pattern. Returns `None` if this
/// cannot be accomplished (e.g., `xlfd` could not be parsed).
fn xlfd_to_fc_pattern(xlfd: &str) -> Option<NonNull<x11::xft::FcPattern>> {
    if xlfd.contains('%') {
        return NonNull::new(unsafe {
            fontconfig_sys::FcNameParse(DEFAULT_FONT_CSTR.as_ptr()).cast::<x11::xft::FcPattern>()
        });
    }

    let name = xlfd.split(',').next().unwrap();
    let ignore_scalable = 0;
    let complete = 0;
    NonNull::new(unsafe {
        x11::xft::XftXlfdParse(
            name.as_ptr(),
            ignore_scalable,
            complete,
        )
    })
    .or_else(|| {
        // TODO: warn.
        NonNull::new(unsafe {
            x11::xft::XftXlfdParse(
                DEFAULT_FONT_CSTR.as_ptr(),
                ignore_scalable,
                complete,
            )
        })
    })
}

/// A font name, with support for X logical font descriptions (like
/// `-bitstream-charter-medium-r-normal--12-120-75-75-p-68-iso8859-1`, including
/// `*`-style wildcards but not old-style `%d` tokens) and Fontconfig
/// descriptions (like `sans serif:pixelsize=12`).
///
/// Data is held in a [`std::rc::Rc`], so copies are lightweight, and ownership
/// may be shared between multiple objects (e.g., keying a hashtable and
/// referenced by hashtable values).
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct FontName(Rc<CStr>);

impl FontName {
    /// Tries to interpret `name` as an XLFD or Fontconfig name. Returns an
    /// instantiated `FontName` if this succeeds, or `None`.
    pub fn new(name: &str) -> Option<Self> {
        let name = if name.starts_with('-') {
            let pattern = xlfd_to_fc_pattern(name)?;
            let name = unsafe { fontconfig_sys::FcNameUnparse(pattern.as_ptr().cast::<c_void>()) };
            let name = NonNull::new(name)?;
            let result = unsafe { CStr::from_ptr(name.as_ptr()) }.to_str().ok()?;
            unsafe {
                libc::free(name.as_ptr().cast::<c_void>());
                fontconfig_sys::FcPatternDestroy(pattern.as_ptr().cast::<c_void>());
            }
            result
        } else {
            name
        };
        if !Configuration::global()
            .map(|c| c.antialiased_text)
            .unwrap_or(false)
            && !name.contains(":antialias=")
        {
            let mut name = String::from(name);
            name += ":antialias=false";
            Some(FontName(Rc::from(CString::new(name).ok()?)))
        } else {
            Some(FontName(Rc::from(CString::new(name).ok()?)))
        }
    }

    /// Returns a C-style string owned by this object that names the font in a
    /// way that is suitable for `x11::xft::XftFontOpenName` and other Xft
    /// functions.
    pub fn as_xft_name(&self) -> *const u8 {
        self.0.as_ptr()
    }

    /// Converts `self` into a shareable name.
    pub fn into_name(self) -> Rc<CStr> {
        self.0.into()
    }
}

impl Font {
    /// Tries to load the named font for `screen` on `display`. Returns `None`
    /// if `name` can't be handled or font cannot be loaded.
    pub fn load(display: &mut x11::xlib::Display, screen: c_int, name: &FontName) -> Option<Self> {
        let font = NonNull::new(unsafe {
            x11::xft::XftFontOpenName(display, screen, name.as_xft_name())
        })?;
        let f = unsafe { font.as_ref() };
        let height = u32::try_from(f.ascent + f.descent).ok()?;
        let fontmap = unsafe { pango_extras::pango_xft_get_font_map(display, screen) };
        let context = unsafe { pango_sys::pango_font_map_create_context(fontmap) };
        let layout = NonNull::new(unsafe { pango_sys::pango_layout_new(context) })?;
        let pattern = unsafe { fontconfig_sys::FcNameParse(name.as_xft_name()) };
        let description =
            unsafe { pango_extras::pango_fc_font_description_from_pattern(pattern.cast(), 0) };

        // Pango examines FC_SIZE but not FC_PIXEL_SIZE of the pattern, but
        // font-name has only "pixelsize", so set the size manually here.
        let mut size: c_double = 0.0;
        unsafe {
            if fontconfig_sys::FcPatternGetDouble(
                pattern,
                fontconfig_sys::constants::FC_PIXEL_SIZE.as_ptr(),
                0,
                &mut size,
            ) == fontconfig_sys::FcResultMatch
            {
                pango_sys::pango_font_description_set_absolute_size(
                    description,
                    size * (pango_sys::PANGO_SCALE as c_double),
                );
            }

            pango_sys::pango_layout_set_font_description(layout.as_ptr(), description);
            pango_sys::pango_font_description_free(description);
        }

        Some(Font {
            font,
            height,
            ascent: u32::try_from(f.ascent).ok()?,
            name: name.clone(),
            layout,
        })
    }

    /// Loads the font `name` at the size `size` (in pixels).
    pub fn load_at_size(
        display: &mut x11::xlib::Display,
        screen: c_int,
        name: &FontName,
        size: u32,
    ) -> Option<Self> {
        let pattern = unsafe { fontconfig_sys::FcNameParse(name.as_xft_name()) };
        unsafe {
            fontconfig_sys::FcPatternDel(
                pattern,
                fontconfig_sys::constants::FC_PIXEL_SIZE.as_ptr(),
            );
            fontconfig_sys::FcPatternAddDouble(
                pattern,
                fontconfig_sys::constants::FC_PIXEL_SIZE.as_ptr(),
                size as f64,
            );
        }

        let name = unsafe { fontconfig_sys::FcNameUnparse(pattern) };
        unsafe {
            fontconfig_sys::FcPatternDestroy(pattern);
        }
        let name = NonNull::new(name)?;
        let font_name = FontName(Rc::from(unsafe { CStr::from_ptr(name.as_ptr()) }));
        unsafe {
            libc::free(name.as_ptr().cast::<c_void>());
        }

        Font::load(display, screen, &font_name)
    }

    pub fn name(&self) -> &FontName {
        &self.name
    }
}

impl Drop for Font {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.font.as_ptr().cast::<c_void>());
        }
    }
}

pub mod ffi {
    use super::{Font, FontName};

    use std::ffi::CStr;

    use crate::{
        WINGsP,
        configuration::Configuration,
        color::ffi::WMColor,
        pango_extras,
    };

    use std::{
        ffi::{c_char, c_int, c_uint},
        ptr,
        rc::Rc,
    };

    pub type WMFont = Rc<Font>;

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateFont(
        screen: *mut WINGsP::W_Screen,
        font_name: *const c_char,
    ) -> *mut WMFont {
        if screen.is_null() || font_name.is_null() {
            return ptr::null_mut();
        }

        let screen = unsafe { &mut *screen };

        if screen.display.is_null() {
            return ptr::null_mut();
        }

        let font_name = unsafe { CStr::from_ptr(font_name) };
        let Ok(font_name) = font_name.to_str() else {
            return ptr::null_mut();
        };
        let Some(font_name) = FontName::new(font_name) else {
            return ptr::null_mut();
        };

        let display = unsafe { &mut *screen.display };
        let screen_n = screen.screen;
        screen
            .font_cache_get_or_else(font_name, |font_name| {
                Font::load(display, screen_n, font_name)
            })
            .map(|font| Box::leak(Box::new(font)) as *mut WMFont)
            .unwrap_or(ptr::null_mut())
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRetainFont(font: *mut WMFont) -> *mut WMFont {
        if font.is_null() {
            return ptr::null_mut();
        }
        let font = unsafe { (*font).clone() };
        Box::leak(Box::new(font))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMReleaseFont(font: *mut WMFont) {
        if font.is_null() {
            return;
        }
        let _ = unsafe { Box::from_raw(font) };
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFontName(font: *const WMFont) -> *const c_char {
        if font.is_null() {
            return ptr::null_mut();
        }
        unsafe { (**font).name.as_xft_name() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMFontAscent(font: *mut WMFont) -> c_uint {
        if font.is_null() {
            return 0;
        }
        return unsafe { (**font).ascent };
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMFontXftFont(font: *mut WMFont) -> *mut x11::xft::XftFont {
        if font.is_null() {
            return ptr::null_mut();
        }
        return unsafe { (**font).font.as_ptr() };
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMFontHeight(font: *const WMFont) -> c_uint {
        if font.is_null() {
            return 0;
        }
        unsafe { (**font).height }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetScaleBaseFromSystemFont(
        screen: *mut WINGsP::W_Screen,
        alphabet_width: *mut c_int,
        font_height: *mut c_int,
    ) {
        let font = unsafe { WMDefaultSystemFont(screen) };
        unsafe {
            *alphabet_width = WMWidthOfString(font, c"abcdefghijklmnopqrstuvwxyz".as_ptr(), 26);
        }
        if let Ok(x) = unsafe { (**font).height.try_into() } {
            unsafe {
                *font_height = x;
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDefaultSystemFont(screen: *mut WINGsP::W_Screen) -> *mut WMFont {
        if screen.is_null() {
            return ptr::null_mut();
        }

        unsafe { WMRetainFont((*screen).normalFont) }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDefaultBoldSystemFont(screen: *mut WINGsP::W_Screen) -> *mut WMFont {
        if screen.is_null() {
            return ptr::null_mut();
        }

        unsafe { WMRetainFont((*screen).boldFont) }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSystemFontOfSize(
        screen: *mut WINGsP::W_Screen,
        size: c_int,
    ) -> *mut WMFont {
        if screen.is_null() {
            return ptr::null_mut();
        }
        let screen = unsafe { &*screen };
        if screen.display.is_null() {
            return ptr::null_mut();
        }
        if screen.display.is_null() {
            return ptr::null_mut();
        }
        let display = unsafe { &mut *screen.display };
        let size = if size > 0 {
            size as u32
        } else {
            match Configuration::global().map(|c| c.default_font_size as u32) {
                Some(x) => x,
                None => return ptr::null_mut(),
            }
        };
        let Some(font_name) = Configuration::global()
            .and_then(|c| c.system_font.to_str().ok())
            .and_then(|name| FontName::new(name))
        else {
            return ptr::null_mut();
        };
        if let Some(font) = Font::load_at_size(display, screen.screen, &font_name, size) {
            Box::leak(Box::new(Rc::new(font)))
        } else {
            // TODO: warn.
            ptr::null_mut()
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMBoldSystemFontOfSize(
        screen: *mut WINGsP::W_Screen,
        size: c_int,
    ) -> *mut WMFont {
        if screen.is_null() {
            return ptr::null_mut();
        }
        let screen = unsafe { &*screen };
        if screen.display.is_null() {
            return ptr::null_mut();
        }
        if screen.display.is_null() {
            return ptr::null_mut();
        }
        let display = unsafe { &mut *screen.display };
        let size = if size > 0 {
            size as u32
        } else {
            match Configuration::global().map(|c| c.default_font_size as u32) {
                Some(x) => x,
                None => return ptr::null_mut(),
            }
        };
        let Some(font_name) = Configuration::global()
            .and_then(|c| c.bold_system_font.to_str().ok())
            .and_then(|name| FontName::new(name))
        else {
            return ptr::null_mut();
        };
        if let Some(font) = Font::load_at_size(display, screen.screen, &font_name, size) {
            Box::leak(Box::new(Rc::new(font)))
        } else {
            // TODO: warn.
            ptr::null_mut()
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMWidthOfString(
        font: *mut WMFont,
        text: *const c_char,
        length: c_int,
    ) -> c_int {
        if font.is_null() || text.is_null() || length <= 0 {
            return 0;
        }
        let font = unsafe { &*font };
        let layout = font.layout.as_ptr();
        let previous_text = unsafe { pango_sys::pango_layout_get_text(layout) };

        if previous_text.is_null() {
            unsafe {
                pango_sys::pango_layout_set_text(layout, text, length);
            }
        } else {
            let length = length as usize;
            let previous_text = unsafe { CStr::from_ptr(previous_text).to_bytes() };
            let text = unsafe { std::slice::from_raw_parts(text, length) };
            if previous_text.len() < length || previous_text[0..length] != text[0..length] {
                unsafe {
                    pango_sys::pango_layout_set_text(layout, text.as_ptr(), length as i32);
                }
            }
        }

        let mut width = 0;
        unsafe {
            pango_sys::pango_layout_get_pixel_size(layout, &mut width, ptr::null_mut());
        }
        width
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDrawString(
        screen: *mut WINGsP::W_Screen,
        d: x11::xlib::Drawable,
        color: *mut WMColor,
        font: *mut WMFont,
        x: c_int,
        y: c_int,
        text: *const c_char,
        length: c_int,
    ) {
        if screen.is_null() || color.is_null() || font.is_null() || text.is_null() {
            return;
        }

        let screen = unsafe { &*screen };
        let color = unsafe { &*color };
        let layout = unsafe { &mut *(**font).layout.as_ptr() };
        let mut xftcolor = color.to_xft_color();

        let previous_text = unsafe { pango_sys::pango_layout_get_text(layout) };
        if previous_text.is_null() {
            unsafe {
                pango_sys::pango_layout_set_text(layout, text, length);
            }
        } else {
            let text = unsafe { CStr::from_ptr(text) };
            let previous_text = unsafe { CStr::from_ptr(previous_text) };
            if previous_text != text {
                unsafe {
                    pango_sys::pango_layout_set_text(layout, text.as_ptr(), length);
                }
            }
        }

        unsafe {
            x11::xft::XftDrawChange(screen.xftdraw.cast(), d);
            pango_extras::pango_xft_render_layout(
                screen.xftdraw.cast(),
                &mut xftcolor,
                layout,
                x * pango_sys::PANGO_SCALE,
                y * pango_sys::PANGO_SCALE,
            );
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDrawImageString(
        screen: *mut WINGsP::W_Screen,
        d: x11::xlib::Drawable,
        color: *mut WMColor,
        background: *mut WMColor,
        font: *mut WMFont,
        x: c_int,
        y: c_int,
        text: *const c_char,
        length: c_int,
    ) {
        if screen.is_null() || color.is_null() || font.is_null() || text.is_null() {
            return;
        }

        let screen = unsafe { &*screen };
        let color = unsafe { &*color };
        let background = unsafe { &*background };
        let layout = unsafe { &mut *(**font).layout.as_ptr() };
        let mut text_color = color.to_xft_color();
        let background = background.to_xft_color();

        unsafe {
            let Ok(width) = u32::try_from(WMWidthOfString(font, text, length)) else {
                // TODO: complain.
                return;
            };
            x11::xft::XftDrawChange(screen.xftdraw.cast(), d);
            x11::xft::XftDrawRect(
                screen.xftdraw.cast(),
                &background,
                x,
                y,
                width,
                (**font).height,
            );
        }

        let previous_text = unsafe { pango_sys::pango_layout_get_text(layout) };
        if previous_text.is_null() {
            unsafe {
                pango_sys::pango_layout_set_text(layout, text, length);
            }
        } else {
            let text = unsafe { CStr::from_ptr(text) };
            let previous_text = unsafe { CStr::from_ptr(previous_text) };
            if text != previous_text {
                unsafe {
                    pango_sys::pango_layout_set_text(layout, text.as_ptr(), length);
                }
            }
        }

        unsafe {
            pango_extras::pango_xft_render_layout(
                screen.xftdraw.cast(),
                &mut text_color,
                layout,
                x * pango_sys::PANGO_SCALE,
                y * pango_sys::PANGO_SCALE,
            );
        }
    }
}
