//! Top-level window containing a font selector.

use crate::{
    WINGsP::*,
    font::{Font, FontName},
    widget::Widget,
};

use std::{
    collections::{BTreeMap, btree_map::Entry},
    ffi::{CStr, CString, c_int, c_void},
    pin::Pin,
    ptr::{self, NonNull},
    rc::Rc,
};

/// Provides quick lookup of fonts that are known to exist.
pub struct FontLibrary {
    families: BTreeMap<Rc<str>, Family>,
}

/// Tracks a set of known [`Typeface`] objects and a quick mapping from typeface names to `Typeface`s.
#[derive(Clone)]
pub struct Family {
    name: Rc<str>,
    typefaces: BTreeMap<Rc<str>, Rc<Typeface>>,
}

/// Full name information for a typeface that is known to exist on this sytem.
#[derive(Clone)]
pub struct Typeface {
    family: Rc<str>,
    name: Rc<str>,
    sizes: Vec<u32>,
}

impl FontLibrary {
    /// Creates a `FontLibrary` with all fonts known by the system.
    ///
    /// Small errors in listing fonts (like encountering a font name which is
    /// not a valid UTF-8 byte sequence) are silently ignored (and those fonts
    /// are skipped).
    ///
    /// If an unrecoverable error is encountered while listing system fonts,
    /// returns `None`.
    pub fn for_all_families() -> Option<Self> {
        let pattern = unsafe { fontconfig_sys::FcPatternCreate() };
        if pattern.is_null() {
            // TODO: complain.
            return None;
        }
        let object_set = unsafe {
            fontconfig_sys::FcObjectSetBuild(
                fontconfig_sys::constants::FC_FAMILY.as_ptr() as *mut u8,
                fontconfig_sys::constants::FC_STYLE.as_ptr(),
                ptr::null_mut::<c_void>(),
            )
        };
        if object_set.is_null() {
            // TODO: complain.
            unsafe {
                fontconfig_sys::FcPatternDestroy(pattern);
            }
            return None;
        }
        let fonts = unsafe { fontconfig_sys::FcFontList(ptr::null_mut(), pattern, object_set) };
        unsafe {
            fontconfig_sys::FcPatternDestroy(pattern);
        }
        if fonts.is_null() {
            // TODO: complain.
            return None;
        }

        let mut library = FontLibrary {
            families: BTreeMap::new(),
        };
        if unsafe { (*fonts).nfont } < 0 {
            // TODO: complain.
            return None;
        }
        for font in
            unsafe { std::slice::from_raw_parts_mut((*fonts).fonts, (*fonts).nfont as usize) }
        {
            let mut family = ptr::null_mut();
            let mut style = ptr::null_mut();
            unsafe {
                if fontconfig_sys::FcPatternGetString(
                    *font,
                    fontconfig_sys::constants::FC_FAMILY.as_ptr(),
                    0,
                    &mut family as *mut _,
                ) == fontconfig_sys::FcResultMatch
                {
                    let Ok(family) = CStr::from_ptr(family).to_str().map(String::from) else {
                        // TODO: complain.
                        continue;
                    };
                    if fontconfig_sys::FcPatternGetString(
                        *font,
                        fontconfig_sys::constants::FC_STYLE.as_ptr(),
                        0,
                        &mut style as *mut _,
                    ) == fontconfig_sys::FcResultMatch
                    {
                        let Ok(style) = CStr::from_ptr(style).to_str().map(String::from) else {
                            // TODO: complain.
                            continue;
                        };
                        library.register(family, style);
                    }
                }
            }
        }
        unsafe {
            fontconfig_sys::FcFontSetDestroy(fonts);
        }

        Some(library)
    }

    /// Adds a font with the given family and typeface names to the fonts tracked by `self`.
    pub fn register(&mut self, family: String, typeface: String) {
        let family_name: Rc<str> = family.into();
        let typeface_name: Rc<str> = typeface.into();
        match self.families.entry(family_name.clone()) {
            Entry::Occupied(mut o) => {
                if let Entry::Vacant(v) = o.get_mut().typefaces.entry(typeface_name.clone()) {
                    v.insert(Rc::new(Typeface {
                        family: family_name,
                        name: typeface_name,
                        sizes: SCALABLE_FONT_SIZES.iter().copied().collect(),
                    }));
                }
            }
            Entry::Vacant(v) => {
                let name = v.key().clone();
                v.insert(Family {
                    name,
                    typefaces: [(
                        typeface_name.clone(),
                        Rc::new(Typeface {
                            family: family_name,
                            name: typeface_name,
                            sizes: SCALABLE_FONT_SIZES.iter().copied().collect(),
                        }),
                    )]
                    .into_iter()
                    .collect(),
                });
            }
        }
    }

    /// Searches for a font family named `family`, returning `None` if it cannot
    /// be found.
    pub fn get_family(&self, family: &str) -> Option<Family> {
        self.families.get(family).cloned()
    }

    /// Searches for a typeface with the given `family` and `style`, returning
    /// `None` if it cannot be found.
    pub fn get_typeface(&self, family: &str, style: &str) -> Option<Rc<Typeface>> {
        self.families
            .get(family)
            .and_then(|f| f.typefaces.get(style))
            .cloned()
    }
}

pub struct FontPanel {
    fonts: FontLibrary,

    window: NonNull<W_Window>,

    split: NonNull<W_SplitView>,

    upper_frame: NonNull<W_Frame>,
    sample_text: NonNull<W_TextField>,

    lower_frame: NonNull<W_Frame>,
    family_label: NonNull<W_Label>,
    family_list: NonNull<W_List>,
    type_label: NonNull<W_Label>,
    type_list: NonNull<W_List>,
    size_label: NonNull<W_Label>,
    size_text: NonNull<W_TextField>,
    size_list: NonNull<W_List>,

    action: Option<Box<dyn FnMut()>>,

    set_button: NonNull<W_Button>,
    revert_button: NonNull<W_Button>,
}

impl FontPanel {
    /// Constructs a font panel that shows all fonts known to the system.
    ///
    /// If there are errors in listing fonts or creating widgets, returns `None`.
    ///
    /// Call [`FontPanel::show`] to make the panel visible.
    pub fn new(screen: *mut W_Screen) -> Option<Pin<Box<Self>>> {
        extern "C" fn size_click_callback(_widget: *mut WMWidget, data: *mut c_void) {
            if data.is_null() {
                return;
            }
            let panel = unsafe { &mut *data.cast::<FontPanel>() };
            panel.update_size_label();
            panel.update_preview();
        }

        extern "C" fn typeface_click_callback(_widget: *mut WMWidget, data: *mut c_void) {
            if data.is_null() {
                return;
            }

            let panel = unsafe { &mut *data.cast::<FontPanel>() };
            panel.update_typeface_label();
            panel.update_preview();
        }

        extern "C" fn family_click(_widget: *mut WMWidget, data: *mut c_void) {
            if data.is_null() {
                return;
            }
            let panel = unsafe { &mut *data.cast::<FontPanel>() };
            panel.handle_family_click();
            panel.update_preview();
        }

        extern "C" fn close_window_callback(_widget: *mut WMWidget, data: *mut c_void) {
            unsafe {
                ffi::WMHideFontPanel(data.cast());
            }
        }

        extern "C" fn split_view_constrain_callback(
            _split_view: *mut W_SplitView,
            view_index: c_int,
            min: *mut c_int,
            _max: *mut c_int,
        ) {
            unsafe {
                *min = if view_index == 0 {
                    MIN_UPPER_HEIGHT as i32
                } else {
                    MIN_LOWER_HEIGHT as i32
                };
            }
        }

        extern "C" fn set_clicked_action(_widget: *mut WMWidget, data: *mut c_void) {
            let panel = unsafe { &mut *data.cast::<FontPanel>() };
            if let Some(ref mut f) = panel.action {
                (*f)()
            }
        }

        extern "C" fn revert_clicked_action(_widget: *mut WMWidget, _data: *mut c_void) {}

        let screen = NonNull::new(screen).map(|s| unsafe { &mut *s.as_ptr() })?;
        let fonts = FontLibrary::for_all_families()?;
        let window = NonNull::new(unsafe { WMCreateWindow(screen, c"fontPanel".as_ptr()) })?;
        let split = NonNull::new(unsafe { WMCreateSplitView(window.as_ptr().cast()) })?;
        let upper_frame = NonNull::new(unsafe { WMCreateFrame(window.as_ptr().cast()) })?;
        let sample_text = NonNull::new(unsafe { WMCreateTextField(upper_frame.as_ptr().cast()) })?;
        let lower_frame = NonNull::new(unsafe { WMCreateFrame(window.as_ptr().cast()) })?;
        let family_label = NonNull::new(unsafe { WMCreateLabel(lower_frame.as_ptr().cast()) })?;
        let family_list = NonNull::new(unsafe { WMCreateList(lower_frame.as_ptr().cast()) })?;
        let type_label = NonNull::new(unsafe { WMCreateLabel(lower_frame.as_ptr().cast()) })?;
        let type_list = NonNull::new(unsafe { WMCreateList(lower_frame.as_ptr().cast()) })?;
        let size_label = NonNull::new(unsafe { WMCreateLabel(lower_frame.as_ptr().cast()) })?;
        let size_text = NonNull::new(unsafe { WMCreateTextField(lower_frame.as_ptr().cast()) })?;
        let size_list = NonNull::new(unsafe { WMCreateList(lower_frame.as_ptr().cast()) })?;
        let set_button = NonNull::new(unsafe {
            crate::button::ffi::WMCreateCommandButton(window.as_ptr().cast())
        })?;
        let revert_button = NonNull::new(unsafe {
            crate::button::ffi::WMCreateCommandButton(window.as_ptr().cast())
        })?;

        let mut panel = Box::new(FontPanel {
            fonts,
            window,
            split,
            upper_frame,
            sample_text,
            lower_frame,
            family_label,
            family_list,
            type_label,
            type_list,
            size_label,
            size_text,
            size_list,

            action: None,
            set_button,
            revert_button,
        });

        unsafe {
            // TODO: i18n.
            WMSetWindowTitle(window.as_ptr(), c"Font Panel".as_ptr());
            WMResizeWidget(window.as_ptr().cast(), DEF_WIDTH, DEF_HEIGHT);
            WMSetWindowMinSize(window.as_ptr().cast(), MIN_WIDTH, MIN_HEIGHT);
            WMSetViewNotifySizeChanges((*window.as_ptr()).view().as_ptr(), 1);
            WMSetWindowCloseAction(
                window.as_ptr(),
                Some(close_window_callback),
                panel.as_mut() as *mut _ as *mut c_void,
            );
        }

        unsafe {
            WMResizeWidget(
                split.as_ptr().cast(),
                DEF_WIDTH,
                DEF_HEIGHT - BUTTON_SPACE_HEIGHT,
            );
            WMSetSplitViewConstrainProc(split.as_ptr(), Some(split_view_constrain_callback));
        }

        let div_width = unsafe { WMGetSplitViewDividerThickness(split.as_ptr()) };

        unsafe {
            WMSetFrameRelief(upper_frame.as_ptr(), WMReliefType_WRFlat);
            WMSetViewNotifySizeChanges((*upper_frame.as_ptr()).view().as_ptr(), 1);
        }

        unsafe {
            WMSetFrameRelief(lower_frame.as_ptr(), WMReliefType_WRFlat);
            WMSetViewNotifySizeChanges((*lower_frame.as_ptr()).view().as_ptr(), 1);

            WMAddSplitViewSubview(split.as_ptr(), (*upper_frame.as_ptr()).view().as_ptr());
            WMAddSplitViewSubview(split.as_ptr(), (*lower_frame.as_ptr()).view().as_ptr());

            WMResizeWidget(upper_frame.as_ptr().cast(), DEF_WIDTH, DEF_UPPER_HEIGHT);
            WMResizeWidget(lower_frame.as_ptr().cast(), DEF_WIDTH, DEF_LOWER_HEIGHT);

            WMMoveWidget(lower_frame.as_ptr().cast(), 0, 60 + div_width);
        }

        let white = unsafe { WMWhiteColor(screen) };
        let dark = unsafe { WMDarkGrayColor(screen) };

        unsafe {
            WMResizeWidget(sample_text.as_ptr().cast(), DEF_WIDTH - 20, 50);
            WMMoveWidget(sample_text.as_ptr().cast(), 10, 10);
            // TODO: i18n.
            WMSetTextFieldText(
                sample_text.as_ptr(),
                c"The quick brown fox jumps over the lazy dog".as_ptr(),
            );
        }

        let font = unsafe { crate::font::ffi::WMBoldSystemFontOfSize(screen, 12) };

        unsafe {
            WMSetWidgetBackgroundColor(family_label.as_ptr().cast(), dark);
            // TODO: i18n.
            WMSetLabelText(family_label.as_ptr(), c"Family".as_ptr());
            WMSetLabelFont(family_label.as_ptr(), font);
            WMSetLabelTextColor(family_label.as_ptr(), white);
            WMSetLabelRelief(family_label.as_ptr(), WMReliefType_WRSunken);
            WMSetLabelTextAlignment(family_label.as_ptr(), WMAlignment_WACenter);
        }

        unsafe {
            WMSetListAction(
                family_list.as_ptr(),
                Some(family_click),
                panel.as_mut() as *mut FontPanel as *mut _,
            );
        }

        unsafe {
            WMSetWidgetBackgroundColor(type_label.as_ptr().cast(), dark);
            // TODO: i18n.
            WMSetLabelText(type_label.as_ptr(), c"Typeface".as_ptr());
            WMSetLabelFont(type_label.as_ptr(), font);
            WMSetLabelTextColor(type_label.as_ptr(), white);
            WMSetLabelRelief(type_label.as_ptr(), WMReliefType_WRSunken);
            WMSetLabelTextAlignment(type_label.as_ptr(), WMAlignment_WACenter);
        }

        unsafe {
            WMSetListAction(
                type_list.as_ptr(),
                Some(typeface_click_callback),
                panel.as_mut() as *mut FontPanel as *mut _,
            );
        }

        unsafe {
            WMSetWidgetBackgroundColor(size_label.as_ptr().cast(), dark);
            // TODO: i18n.
            WMSetLabelText(size_label.as_ptr().cast(), c"Size".as_ptr());
            WMSetLabelFont(size_label.as_ptr().cast(), font);
            WMSetLabelTextColor(size_label.as_ptr().cast(), white);
            WMSetLabelRelief(size_label.as_ptr().cast(), WMReliefType_WRSunken);
            WMSetLabelTextAlignment(size_label.as_ptr().cast(), WMAlignment_WACenter);
        }

        unsafe {
            WMSetListAction(
                size_list.as_ptr(),
                Some(size_click_callback),
                panel.as_mut() as *mut FontPanel as *mut _,
            );
        }

        unsafe {
            crate::font::ffi::WMReleaseFont(font);
            WMReleaseColor(white);
            WMReleaseColor(dark);
        }

        unsafe {
            WMResizeWidget(set_button.as_ptr().cast(), 70, 24);
            WMMoveWidget(
                set_button.as_ptr().cast(),
                240,
                (DEF_HEIGHT - (BUTTON_SPACE_HEIGHT - 5)) as i32,
            );
            // TODO: i18n.
            WMSetButtonText(set_button.as_ptr().cast(), c"Set".as_ptr());
            WMSetButtonAction(
                set_button.as_ptr().cast(),
                Some(set_clicked_action),
                panel.as_mut() as *mut FontPanel as *mut _,
            );
        }

        unsafe {
            WMResizeWidget(revert_button.as_ptr().cast(), 70, 24);
            WMMoveWidget(
                revert_button.as_ptr().cast(),
                80,
                (DEF_HEIGHT - (BUTTON_SPACE_HEIGHT - 5)) as i32,
            );
            // TODO: i18n.
            WMSetButtonText(revert_button.as_ptr().cast(), c"Revert".as_ptr());
            WMSetButtonAction(
                revert_button.as_ptr().cast(),
                Some(revert_clicked_action),
                panel.as_mut() as *mut FontPanel as *mut _,
            );
        }

        unsafe {
            WMRealizeWidget(window.as_ptr().cast());

            WMMapSubwidgets(upper_frame.as_ptr().cast());
            WMMapSubwidgets(lower_frame.as_ptr().cast());
            WMMapSubwidgets(split.as_ptr().cast());
            WMMapSubwidgets(window.as_ptr().cast());

            WMUnmapWidget(revert_button.as_ptr().cast());
        }

        panel.arrange_lower_frame();

        extern "C" fn notification_observer(this: *mut c_void, notification: *mut WMNotification) {
            if this.is_null() || notification.is_null() {
                return;
            }
            let panel = unsafe { &mut *this.cast::<FontPanel>() };
            let object = unsafe { WMGetNotificationObject(notification) };
            let window = (*panel).window;

            let name = unsafe { CStr::from_ptr(WMGetNotificationName(notification)) };
            if name == unsafe { CStr::from_ptr(WMViewSizeDidChangeNotification) } {
                if ptr::addr_eq(object, window.as_ptr()) {
                    unsafe {
                        let h = WMWidgetHeight(window.as_ptr().cast());
                        let w = WMWidgetWidth(window.as_ptr().cast());
                        WMResizeWidget(panel.split.as_ptr().cast(), w, h - BUTTON_SPACE_HEIGHT);
                        WMMoveWidget(
                            panel.set_button.as_ptr().cast(),
                            (w - 80) as i32,
                            (h - (BUTTON_SPACE_HEIGHT - 5)) as i32,
                        );
                        WMMoveWidget(
                            panel.revert_button.as_ptr().cast(),
                            (w - 240) as i32,
                            (h - (BUTTON_SPACE_HEIGHT - 5)) as i32,
                        );
                    }
                } else if ptr::addr_eq(object, unsafe { (*panel.upper_frame.as_ptr()).view().as_ptr() })
                {
                    unsafe {
                        if WMWidgetHeight(panel.upper_frame.as_ptr().cast()) < MIN_UPPER_HEIGHT {
                            WMResizeWidget(
                                panel.upper_frame.as_ptr().cast(),
                                WMWidgetWidth(panel.upper_frame.as_ptr().cast()),
                                MIN_UPPER_HEIGHT,
                            );
                        } else {
                            WMResizeWidget(
                                panel.sample_text.as_ptr().cast(),
                                WMWidgetWidth(panel.upper_frame.as_ptr().cast()) - 20,
                                WMWidgetHeight(panel.upper_frame.as_ptr().cast()) - 10,
                            );
                        }
                    }
                } else if ptr::addr_eq(object, unsafe { (*panel.lower_frame.as_ptr()).view().as_ptr() })
                {
                    unsafe {
                        if WMWidgetHeight(panel.lower_frame.as_ptr().cast()) < MIN_LOWER_HEIGHT {
                            WMResizeWidget(
                                panel.upper_frame.as_ptr().cast(),
                                WMWidgetWidth(panel.upper_frame.as_ptr().cast()),
                                MIN_UPPER_HEIGHT,
                            );
                            WMMoveWidget(
                                panel.lower_frame.as_ptr().cast(),
                                0,
                                WMWidgetHeight(panel.upper_frame.as_ptr().cast()) as i32
                                    + WMGetSplitViewDividerThickness(panel.split.as_ptr()) as i32,
                            );
                            WMResizeWidget(
                                panel.lower_frame.as_ptr().cast(),
                                WMWidgetWidth(panel.lower_frame.as_ptr().cast()) as u32,
                                WMWidgetWidth(panel.split.as_ptr().cast())
                                    - MIN_UPPER_HEIGHT as u32
                                    - WMGetSplitViewDividerThickness(panel.split.as_ptr().cast())
                                    as u32,
                            );
                        } else {
                            panel.arrange_lower_frame();
                        }
                    }
                }
            }
        }

        unsafe {
            WMAddNotificationObserver(
                Some(notification_observer),
                panel.as_mut() as *mut FontPanel as *mut _,
                WMViewSizeDidChangeNotification,
                (*window.as_ptr()).view().as_ptr().cast(),
            );
            WMAddNotificationObserver(
                Some(notification_observer),
                panel.as_mut() as *mut FontPanel as *mut _,
                WMViewSizeDidChangeNotification,
                (*upper_frame.as_ptr()).view().as_ptr().cast(),
            );
            WMAddNotificationObserver(
                Some(notification_observer),
                panel.as_mut() as *mut FontPanel as *mut _,
                WMViewSizeDidChangeNotification,
                (*lower_frame.as_ptr()).view().as_ptr().cast(),
            );
        }

        // Add font families to the list.
        for family in panel.fonts.families.values() {
            let Ok(name) = CString::new(&*family.name) else {
                continue;
            };
            unsafe {
                let item = crate::list::ffi::WMAddListItem(panel.family_list.as_ptr(), name.as_ptr());
                if !item.is_null() {
                    (*item).clientData = family as *const Family as *mut _;
                }
            }
        }
        unsafe {
            WMSortListItems(panel.family_list.as_ptr());
        }

        Some(Pin::new(panel))
    }

    fn arrange_lower_frame(&mut self) {
        const LABEL_HEIGHT: u32 = 20;

        let width = unsafe { WMWidgetWidth(self.lower_frame.as_ptr().cast()) } - 55 - 30;
        let height = unsafe {
            WMWidgetHeight(self.split.as_ptr().cast())
                - WMWidgetHeight(self.upper_frame.as_ptr().cast())
                - WMGetSplitViewDividerThickness(self.split.as_ptr().cast()) as u32
                - LABEL_HEIGHT
                - 8
        };

        let fw: u32 = (125 * width as u32) / 235;
        let tw: u32 = (110 * width as u32) / 235;
        let sw: u32 = 55;

        unsafe {
            WMMoveWidget(self.family_label.as_ptr().cast(), 10, 0);
            WMResizeWidget(self.family_label.as_ptr().cast(), fw, LABEL_HEIGHT);
            WMMoveWidget(self.family_list.as_ptr().cast(), 10, 23);
            WMResizeWidget(self.family_list.as_ptr().cast(), fw, height);

            WMMoveWidget(self.type_label.as_ptr().cast(), 10 + (fw as i32) + 3, 0);
            WMResizeWidget(self.type_label.as_ptr().cast(), tw, LABEL_HEIGHT);

            WMMoveWidget(self.type_list.as_ptr().cast(), 10 + (fw as i32) + 3, 23);
            WMResizeWidget(self.type_list.as_ptr().cast(), tw, height);

            WMMoveWidget(
                self.size_label.as_ptr().cast(),
                10 + (fw as i32) + 3 + (tw as i32) + 3,
                0,
            );
            WMResizeWidget(self.size_label.as_ptr().cast(), sw + 4, LABEL_HEIGHT);

            WMMoveWidget(
                self.size_text.as_ptr().cast(),
                10 + (fw as i32) + 3 + (tw as i32) + 3,
                23,
            );
            WMResizeWidget(self.size_text.as_ptr().cast(), sw + 4, 20);

            WMMoveWidget(
                self.size_list.as_ptr().cast(),
                10 + (fw as i32) + 3 + (tw as i32) + 3,
                46,
            );
            WMResizeWidget(self.size_list.as_ptr().cast(), sw + 4, height - 23);
        }
    }

    fn handle_family_click(&mut self) {
        let Some(family) = (unsafe {
            NonNull::new(WMGetListSelectedItem(self.family_list.as_ptr()))
                .and_then(|item| NonNull::new((*item.as_ptr()).clientData.cast::<Family>()))
                .map(|family| (*family.as_ptr()).clone())
        }) else {
            return;
        };
        // Hang onto the selected typeface and size so we can try to keep them
        // selected for the new family.
        let selected_type = unsafe {
            NonNull::new(WMGetListSelectedItem(self.type_list.as_ptr()))
                .and_then(|item| NonNull::new((*item.as_ptr()).clientData.cast::<Typeface>()))
                .map(|typeface| (*typeface.as_ptr()).clone())
        };
        let selected_size = unsafe {
            NonNull::new(WMGetListSelectedItem(self.size_list.as_ptr()))
                .and_then(|item| NonNull::new((*item.as_ptr()).text))
                .map(|text| CString::from(CStr::from_ptr(text.as_ptr())))
        };

        unsafe {
            WMClearList(self.type_list.as_ptr());
        }
        for face in family.typefaces.values() {
            let Ok(label) = CString::new(&*face.name) else {
                continue;
            };
            unsafe {
                let item = if face.name.eq_ignore_ascii_case("Roman")
                    || face.name.eq_ignore_ascii_case("Regular")
                {
                    WMInsertListItem(self.type_list.as_ptr(), 0, label.as_ptr())
                } else {
                    crate::list::ffi::WMAddListItem(self.type_list.as_ptr(), label.as_ptr())
                };
                (*item).clientData = Rc::<Typeface>::as_ptr(face) as *mut _
            }
        }

        let face_index = selected_type
            .map(|x| unsafe {
                WMFindRowOfListItemWithTitle(self.type_list.as_ptr(), x.name.as_ptr())
            })
            .unwrap_or(0);

        unsafe {
            WMSelectListItem(self.type_list.as_ptr(), face_index);
        }
        let size_index = selected_size
            .map(|x| unsafe {
                WMFindRowOfListItemWithTitle(self.size_list.as_ptr(), x.as_ptr())
            })
            .unwrap_or(0);
        unsafe {
            WMSelectListItem(self.size_list.as_ptr(), size_index);
        }
        self.update_typeface_label();
    }

    fn update_typeface_label(&mut self) {
        let selected_size_text = unsafe {
            NonNull::new(WMGetTextFieldText(self.size_text.as_ptr()))
                .map(|text| CString::from(CStr::from_ptr(text.as_ptr())))
        };

        unsafe {
            WMClearList(self.size_list.as_ptr() as *mut _);
        }

        if let Some(selected_typeface_sizes) =
            NonNull::new(unsafe { WMGetListSelectedItem(self.type_list.as_ptr()) })
                .and_then(|item| {
                    NonNull::new(unsafe { (*item.as_ptr()).clientData.cast::<Typeface>() })
                })
                .map(|typeface| unsafe { &(*typeface.as_ptr()).sizes })
        {
            for size in selected_typeface_sizes {
                let Ok(item_text) = CString::new(format!("{}", size)) else {
                    continue;
                };
                unsafe {
                    crate::list::ffi::WMAddListItem(self.size_list.as_ptr(), item_text.as_ptr());
                }
            }
        }

        let mut new_size_index = 0;
        if let Some(size) = selected_size_text {
            new_size_index =
                unsafe { WMFindRowOfListItemWithTitle(self.size_list.as_ptr(), size.as_ptr()) };
        }
        if new_size_index < 0 {
            new_size_index =
                unsafe { WMFindRowOfListItemWithTitle(self.size_list.as_ptr(), c"12".as_ptr()) };
        }
        if new_size_index < 0 {
            new_size_index = 0;
        }

        unsafe {
            WMSelectListItem(self.size_list.as_ptr(), new_size_index);
            WMSetListPosition(self.size_list.as_ptr(), new_size_index);
        }

        self.update_size_label();
    }

    fn update_size_label(&mut self) {
        unsafe {
            let item = WMGetListSelectedItem(self.size_list.as_ptr());
            if item.is_null() {
                return;
            }
            WMSetTextFieldText(self.size_text.as_ptr(), (*item).text);
            let Ok(len) = TryInto::<c_int>::try_into(CStr::from_ptr((*item).text).count_bytes()) else {
                return;
            };
            WMSelectTextFieldRange(self.size_text.as_ptr(), wutil_rs::range::Range::from(0..len));
        }
    }

    pub fn selected_font_name(&self) -> Option<FontName> {
        let typeface = unsafe {
            NonNull::new(WMGetListSelectedItem(self.type_list.as_ptr()))
                .and_then(|item| NonNull::new((*item.as_ptr()).clientData.cast::<Typeface>()))
                .map(|typeface| (*typeface.as_ptr()).clone())?
        };

        let size = unsafe {
            NonNull::new(WMGetTextFieldText(self.size_text.as_ptr()))
                .and_then(|text| CStr::from_ptr(text.as_ptr()).to_str().ok())?
        };

        FontName::new(&format!(
            "{}:style={}:pixelsize={}",
            typeface.family, typeface.name, size
        ))
    }

    fn update_preview(&mut self) {
        let Some(name) = self.selected_font_name() else {
            return;
        };
        let screen = unsafe { &mut *WMWidgetScreen(self.window.as_ptr().cast()) };
        let x_display = unsafe { &mut *(screen.display) };
        let x_screen = screen.screen;
        let Some(mut font) =
            screen.font_cache_get_or_else(name, |name| Font::load(x_display, x_screen, name))
        else {
            return;
        };
        unsafe {
            WMSetTextFieldFont(self.sample_text.as_ptr(), &mut font as *mut _);
        }
    }

    pub fn set_font_name(&mut self, family: &CStr, style: &CStr, size: f64) {
        let family_row =
            unsafe { WMFindRowOfListItemWithTitle(self.family_list.as_ptr(), family.as_ptr()) };
        if family_row < 0 {
            return;
        }
        unsafe {
            WMSelectListItem(self.family_list.as_ptr(), family_row);
            WMSetListPosition(self.family_list.as_ptr(), family_row);
            WMClearList(self.type_list.as_ptr());
        }

        let item = unsafe { WMGetListSelectedItem(self.family_list.as_ptr()) };
        if let Some(family) = NonNull::new(item)
            .and_then(|item| NonNull::new(unsafe { *item.as_ptr() }.clientData.cast::<Family>()))
        {
            for face in unsafe { (*family.as_ptr()).typefaces.values() } {
                let Ok(face_name) = CString::new(&*face.name) else {
                    continue;
                };
                let face_item = unsafe {
                    if face.name.eq_ignore_ascii_case("Roman") {
                        WMInsertListItem(self.type_list.as_ptr(), 0, face_name.as_ptr())
                    } else {
                        crate::list::ffi::WMAddListItem(self.type_list.as_ptr(), face_name.as_ptr())
                    }
                };
                unsafe {
                    (*face_item).clientData = Rc::<Typeface>::as_ptr(face) as *mut _;
                }
            }
        }

        let style_row =
            unsafe { WMFindRowOfListItemWithTitle(self.type_list.as_ptr(), style.as_ptr()) };

        if style_row < 0 {
            return;
        }

        unsafe {
            WMSelectListItem(self.type_list.as_ptr(), style_row);
        }
        let item = unsafe { WMGetListSelectedItem(self.type_list.as_ptr()) };
        let face = unsafe { (*item).clientData.cast::<Typeface>() };

        unsafe {
            WMClearList(self.size_list.as_ptr());
        }
        if !face.is_null() {
            for size in unsafe { &(*face).sizes } {
                if let Ok(size) = CString::new(format!("{}", size)) {
                    unsafe {
                        crate::list::ffi::WMAddListItem(self.size_list.as_ptr(), size.as_ptr());
                    }
                }
            }
        }

        if let Ok(asize) = CString::new(format!("{}", (size + 0.5) as u32)) {
            let sz =
                unsafe { WMFindRowOfListItemWithTitle(self.size_list.as_ptr(), asize.as_ptr()) };
            if sz < 0 {
                return;
            }
            unsafe {
                WMSelectListItem(self.size_list.as_ptr(), sz);
            }
        }

        self.update_size_label();
        self.update_preview();
    }

    pub fn show(&mut self) {
        unsafe {
            WMMapWidget(self.window.as_ptr().cast());
        }
    }
}

impl Drop for FontPanel {
    fn drop(&mut self) {
        unsafe {
            WMRemoveNotificationObserver(self as *mut FontPanel as *mut _);
            WMUnmapWidget(self.window.as_ptr().cast());
            WMDestroyWidget(self.window.as_ptr().cast());
        }
    }
}

const MIN_UPPER_HEIGHT: u32 = 20;
const MIN_LOWER_HEIGHT: u32 = 140;

const BUTTON_SPACE_HEIGHT: u32 = 40;

const MIN_WIDTH: u32 = 250;
const MIN_HEIGHT: u32 = MIN_UPPER_HEIGHT + MIN_LOWER_HEIGHT + BUTTON_SPACE_HEIGHT;

const DEF_UPPER_HEIGHT: u32 = 60;
const DEF_LOWER_HEIGHT: u32 = 310;

const DEF_WIDTH: u32 = 320;
const DEF_HEIGHT: u32 = DEF_UPPER_HEIGHT + DEF_LOWER_HEIGHT;

const SCALABLE_FONT_SIZES: [u32; 12] = [8, 10, 11, 12, 14, 16, 18, 20, 24, 36, 48, 64];

pub mod ffi {
    use super::FontPanel;
    use crate::{
        WINGsP::*,
        font::{FontName, ffi::WMFont},
    };

    use std::{
        ffi::{CStr, c_char, c_void},
        pin::Pin,
        ptr,
    };

    pub type WMFontPanel = Pin<Box<FontPanel>>;

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

        let screen = unsafe { &mut *screen };
        if let Some(panel) = FontPanel::new(screen) {
            Box::leak(Box::new(panel)) as *mut WMFontPanel
        } else {
            ptr::null_mut()
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMShowFontPanel(panel: *mut FontPanel) {
        if panel.is_null() {
            return;
        }
        let panel = unsafe { &mut *panel };
        panel.show();
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMHideFontPanel(panel: *mut FontPanel) {
        unsafe { WMUnmapWidget((*panel).window.as_ptr().cast()) }
    }

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

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetFontPanelAction(
        panel: *mut FontPanel,
        action: Option<unsafe extern "C" fn(this: *mut c_void, client_data: *mut c_void)>,
        data: *mut c_void,
    ) {
        if panel.is_null() {
            return;
        }
        let panel = unsafe { &mut *panel };
        if let Some(f) = action {
            let panel_ptr = panel as *mut _ as *mut c_void;
            panel.action = Some(Box::new(move || unsafe { (f)(panel_ptr, data) }));
        } else {
            panel.action = None;
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetFontPanelFont(panel: *mut FontPanel, font_name: *const c_char) {
        if panel.is_null() || font_name.is_null() {
            return;
        }
        let panel = unsafe { &mut *panel };
        let Some(name) = (unsafe { CStr::from_ptr(font_name).to_str() })
            .ok()
            .and_then(|n| FontName::new(n))
        else {
            return;
        };

        unsafe {
            let pattern = fontconfig_sys::FcNameParse(name.as_xft_name());
            if pattern.is_null() {
                return;
            }
            let mut family = ptr::null_mut();
            let mut style = ptr::null_mut();
            let mut size = 0.0;
            if fontconfig_sys::FcPatternGetString(
                pattern,
                fontconfig_sys::constants::FC_FAMILY.as_ptr(),
                0,
                &mut family,
            ) == fontconfig_sys::FcResultMatch
            {
                if fontconfig_sys::FcPatternGetString(
                    pattern,
                    fontconfig_sys::constants::FC_STYLE.as_ptr(),
                    0,
                    &mut style,
                ) == fontconfig_sys::FcResultMatch
                {
                    if fontconfig_sys::FcPatternGetDouble(
                        pattern,
                        c"pixelsize".as_ptr(),
                        0,
                        &mut size,
                    ) == fontconfig_sys::FcResultMatch
                    {
                        panel.set_font_name(CStr::from_ptr(family), CStr::from_ptr(style), size);
                    }
                }
            }
            fontconfig_sys::FcPatternDestroy(pattern);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFontPanelFont(panel: *mut FontPanel) -> *mut WMFont {
        if panel.is_null() {
            return ptr::null_mut();
        }
        return unsafe { WMGetTextFieldFont((*panel).sample_text.as_ptr()) };
    }
}
