// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore dalvik jboolean jfloat jint
use super::*;
use i_slint_common::unicode_utils::{
    byte_offset_to_utf16_offset, utf16_offset_to_byte_offset_clamped,
};
use i_slint_core::SharedString;
use i_slint_core::api::{PhysicalPosition, PhysicalSize, WindowEventDispatchResult};
use i_slint_core::graphics::{Color, euclid};
use i_slint_core::input::{InternalKeyEvent, KeyEvent, KeyEventType};
use i_slint_core::item_rendering::HasFont;
use i_slint_core::items::{CapitalizationMode, ColorScheme, InputType};
use i_slint_core::lengths::{LogicalLength, PhysicalEdges};
use i_slint_core::platform::{Key, WindowAdapter, WindowEvent};
use jni::objects::{JClass, JClassLoader, JString, LoaderContext};
use jni::sys::{jfloat, jint};
use jni::{Env, JavaVM, bind_java_type};
use std::sync::OnceLock;
use std::time::Duration;

/// Maps an Android `Configuration.fontScale` to a Slint default font size.
/// 14 LP = Material Design body-text size at `fontScale = 1.0`.
pub(crate) fn font_scale_to_logical_length(font_scale: f32) -> Option<LogicalLength> {
    (font_scale.is_finite() && font_scale > 0.0).then(|| LogicalLength::new(14.0 * font_scale))
}

const DEX_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/classes.dex"));

bind_java_type! {
    SlintAndroidJavaHelper => ".SlintAndroidJavaHelper",
    type_map = {
        AndroidActivity => "android.app.Activity",
        AndroidRect => "android.graphics.Rect",
    },
    constructors {
        fn new(activity: AndroidActivity),
    },
    methods {
        fn accent_color {
            name = "accent_color",
            sig = () -> jint,
        },
        fn color_scheme {
            name = "color_scheme",
            sig = () -> jint,
        },
        fn font_scale {
            name = "font_scale",
            sig = () -> jfloat,
        },
        fn get_clipboard {
            name = "get_clipboard",
            sig = () -> JString,
        },
        fn get_safe_area {
            name = "get_safe_area",
            sig = () -> AndroidRect,
        },
        fn get_view_rect {
            name = "get_view_rect",
            sig = () -> AndroidRect,
        },
        fn finish_activity {
            name = "finish_activity",
            sig = (),
        },
        fn hide_keyboard {
            name = "hide_keyboard",
            sig = (),
        },
        fn set_clipboard {
            name = "set_clipboard",
            sig = (text: JString),
        },
        fn set_handle_color {
            name = "set_handle_color",
            sig = (color: jint),
        },
        fn set_imm_data {
            name = "set_imm_data",
            sig = (
                text: JString,
                cursor_position: jint,
                anchor_position: jint,
                preedit_start: jint,
                preedit_end: jint,
                cur_x: jint,
                cur_y: jint,
                anchor_x: jint,
                anchor_y: jint,
                cursor_height: jint,
                input_type: jint,
                show_cursor_handles: jboolean
            ),
        },
        fn show_action_menu {
            name = "show_action_menu",
            sig = (),
        },
        fn show_keyboard {
            name = "show_keyboard",
            sig = (),
        },
    },
    native_methods_export = false,
    native_methods {
        pub static fn move_cursor_handle {
            sig = (id: jint, pos_x: jint, pos_y: jint) -> (),
            fn = callback_move_cursor_handle,
        },
        pub static fn on_back_invoked {
            sig = () -> (),
            fn = callback_on_back_invoked,
        },
        pub static fn popup_menu_action {
            sig = (id: jint) -> (),
            fn = callback_popup_menu_action,
        },
        pub static fn set_insets {
            sig = (
                window_top: jint,
                window_left: jint,
                window_bottom: jint,
                window_right: jint,
                safe_area_top: jint,
                safe_area_left: jint,
                safe_area_bottom: jint,
                safe_area_right: jint,
                keyboard_top: jint,
                keyboard_left: jint,
                keyboard_bottom: jint,
                keyboard_right: jint
            ) -> (),
            fn = callback_set_insets,
        },
        pub static fn set_night_mode {
            sig = (night_mode: jint) -> (),
            fn = callback_set_night_mode,
        },
        pub static fn set_font_scale {
            sig = (font_scale: jfloat) -> (),
            fn = callback_set_font_scale,
        },
        pub static fn update_text {
            sig = (
                    text: JString,
                    cursor_position: jint,
                    anchor_position: jint,
                    preedit_start: jint,
                    preedit_offset: jint
            ) -> (),
            fn = callback_update_text,
        },
    },
}

bind_java_type! {
    AndroidActivity => "android.app.Activity",
    type_map = {
        AndroidContext => "android.content.Context",
    },
    is_instance_of = {
        AndroidContext,
    }
}

bind_java_type! {
    AndroidContext => "android.content.Context",
    type_map = {
        JFile => "java.io.File",
    },
    methods {
        fn get_files_dir() -> JFile,
        fn get_cache_dir() -> JFile,
        fn get_code_cache_dir() -> JFile, // requires API level >= 21
        fn get_class_loader() -> JClassLoader,
        fn get_package_name() -> JString,
    }
}

bind_java_type! {
    AndroidBuildVersion => "android.os.Build$VERSION",
    fields {
        #[allow(non_snake_case)]
        static SDK_INT {
            sig = jint,
            get = SDK_INT,
        },
    },
}

bind_java_type! {
    AndroidRect => "android.graphics.Rect",
    fields {
        bottom: jint,
        left: jint,
        right: jint,
        top: jint,
    },
}

bind_java_type! {
    AndroidInputType => "android.text.InputType",
    fields {
        #[allow(non_snake_case)]
        static TYPE_CLASS_NUMBER {
            sig = jint,
            get = TYPE_CLASS_NUMBER,
        },
        #[allow(non_snake_case)]
        static TYPE_CLASS_TEXT {
            sig = jint,
            get = TYPE_CLASS_TEXT,
        },
        #[allow(non_snake_case)]
        static TYPE_TEXT_VARIATION_PASSWORD {
            sig = jint,
            get = TYPE_TEXT_VARIATION_PASSWORD,
        },
        #[allow(non_snake_case)]
        static TYPE_NUMBER_FLAG_DECIMAL {
            sig = jint,
            get = TYPE_NUMBER_FLAG_DECIMAL,
        },
        #[allow(non_snake_case)]
        static TYPE_TEXT_FLAG_CAP_SENTENCES {
            sig = jint,
            get = TYPE_TEXT_FLAG_CAP_SENTENCES,
        },
        #[allow(non_snake_case)]
        static TYPE_TEXT_FLAG_CAP_WORDS {
            sig = jint,
            get = TYPE_TEXT_FLAG_CAP_WORDS,
        },
        #[allow(non_snake_case)]
        static TYPE_TEXT_FLAG_CAP_CHARACTERS {
            sig = jint,
            get = TYPE_TEXT_FLAG_CAP_CHARACTERS,
        },
        #[allow(non_snake_case)]
        static TYPE_TEXT_FLAG_AUTO_CORRECT {
            sig = jint,
            get = TYPE_TEXT_FLAG_AUTO_CORRECT,
        },
        #[allow(non_snake_case)]
        static TYPE_TEXT_FLAG_AUTO_COMPLETE {
            sig = jint,
            get = TYPE_TEXT_FLAG_AUTO_COMPLETE,
        },
    }
}

bind_java_type! {
    AndroidViewConfiguration => "android.view.ViewConfiguration",
    methods {
        static fn get_long_press_timeout() -> jint,
    }
}

bind_java_type! {
    JFile => "java.io.File",
    methods {
        fn get_absolute_path() -> JString,
    }
}

bind_java_type! {
    InMemoryDexClassLoader => "dalvik.system.InMemoryDexClassLoader",
    constructors {
        fn new(dex_buffer: JByteBuffer, parent: JClassLoader),
    },
    is_instance_of = {
        JClassLoader,
    }
}

bind_java_type! {
    DexFileClassLoader => "dalvik.system.DexClassLoader",
    constructors {
        fn new(dex_path: JString, optimized_directory: JString, library_search_path: JString, parent: JClassLoader),
    },
    is_instance_of = {
        JClassLoader,
    }
}

// See `AttachmentExceptionPolicy::PreReThrowPostCatch` in `jni` crate.
#[track_caller]
pub fn print_jni_error(_app: &AndroidApp, e: jni::errors::Error) -> ! {
    panic!("JNI error: {e:#?}")
}

#[allow(dead_code)]
pub struct JavaHelper(jni::refs::Global<SlintAndroidJavaHelper<'static>>, AndroidApp);

fn get_helper_class_loader(
    env: &mut Env,
    native_activity: &AndroidActivity<'_>,
) -> Result<&'static JClassLoader<'static>, jni::errors::Error> {
    static DEX_CLASS_LOADER: OnceLock<jni::refs::Global<JClassLoader<'static>>> = OnceLock::new();

    fn build_dex_class_loader<'local>(
        env: &mut Env<'local>,
        native_activity: &AndroidActivity<'_>,
    ) -> Result<JClassLoader<'local>, jni::errors::Error> {
        let native_activity = env.new_local_ref(native_activity)?;
        let app_context = AndroidContext::cast_local(env, native_activity)?;
        let context_class_loader = app_context.get_class_loader(env)?;

        if AndroidBuildVersion::SDK_INT(env)? >= 26 {
            // Safety: DEX_DATA is 'static and the `InMemoryDexClassLoader`` will not mutate it
            let dex_buffer =
                unsafe { env.new_direct_byte_buffer(DEX_DATA.as_ptr() as *mut _, DEX_DATA.len()) }?;
            let dex_loader = InMemoryDexClassLoader::new(env, &dex_buffer, &context_class_loader)?;
            JClassLoader::cast_local(env, dex_loader)
        } else {
            // The dex data must be written in a file; this determines the output
            // directory path inside the application code cache directory.
            let code_cache_path = app_context
                .get_code_cache_dir(env)?
                .get_absolute_path(env)
                .map(|p| std::path::PathBuf::from(p.to_string()))?;

            let dex_name = env!("CARGO_CRATE_NAME").to_string() + ".dex";
            let dex_file_path = code_cache_path.join(dex_name);
            std::fs::write(&dex_file_path, DEX_DATA).unwrap(); // Note: this panics on failure
            let dex_file_path = JString::new(env, dex_file_path.to_string_lossy())?;

            // creates the oats directory
            let oats_dir_path = code_cache_path.join("oats");
            let _ = std::fs::create_dir(&oats_dir_path);
            let oats_dir_path = JString::new(env, oats_dir_path.to_string_lossy())?;

            // loads the dex file
            let dex_loader = DexFileClassLoader::new(
                env,
                &dex_file_path,
                &oats_dir_path,
                JString::null(),
                &context_class_loader,
            )?;
            JClassLoader::cast_local(env, dex_loader)
        }
    }

    if DEX_CLASS_LOADER.get().is_none() {
        let loader = build_dex_class_loader(env, native_activity)?;
        let loader = env.new_global_ref(loader)?;
        let _ = DEX_CLASS_LOADER.set(loader);
    }
    Ok(DEX_CLASS_LOADER.get().unwrap())
}

fn load_java_helper(
    app: &AndroidApp,
) -> Result<jni::refs::Global<SlintAndroidJavaHelper<'static>>, jni::errors::Error> {
    let jvm = JavaVM::singleton().unwrap_or_else(|_| unsafe {
        // Safety: as documented in android-activity to obtain a jni::JavaVM
        JavaVM::from_raw(app.vm_as_ptr() as *mut _) // this initializes the `JavaVM::singleton()`
    });
    jvm.attach_current_thread(|env| {
        let native_activity_ptr = app.activity_as_ptr().cast();
        let native_activity =
            unsafe { env.as_cast_raw::<jni::refs::Global<AndroidActivity>>(&native_activity_ptr)? };
        let loader = LoaderContext::Loader(get_helper_class_loader(env, native_activity.as_ref())?);
        let _ = SlintAndroidJavaHelperAPI::get(env, &loader)?;
        let helper_instance = SlintAndroidJavaHelper::new(env, native_activity)?;
        env.new_global_ref(&helper_instance)
    })
}

impl JavaHelper {
    pub fn new(app: &AndroidApp) -> Result<Self, jni::errors::Error> {
        Ok(Self(load_java_helper(app)?, app.clone()))
    }

    fn with_jni_env<R>(
        &self,
        f: impl FnOnce(&mut Env, &SlintAndroidJavaHelper<'static>) -> Result<R, jni::errors::Error>,
    ) -> Result<R, jni::errors::Error> {
        JavaVM::singleton()?.attach_current_thread(|env| {
            let helper = self.0.as_ref();
            f(env, helper)
        })
    }

    /// Unfortunately, the way that the android-activity crate uses to show or hide the virtual keyboard doesn't
    /// work with native-activity. So do it manually with JNI
    pub fn show_or_hide_soft_input(&self, show: bool) -> Result<(), jni::errors::Error> {
        self.with_jni_env(
            |env, helper| {
                if show { helper.show_keyboard(env) } else { helper.hide_keyboard(env) }
            },
        )
    }

    pub fn set_imm_data(
        &self,
        data: &i_slint_core::window::InputMethodProperties,
        scale_factor: f32,
        show_cursor_handles: bool,
    ) -> Result<(), jni::errors::Error> {
        self.with_jni_env(|env, helper| {
            let mut text = data.text.to_string();
            let mut cursor_position = data.cursor_position;
            let mut anchor_position = data.anchor_position.unwrap_or(data.cursor_position);

            if !data.preedit_text.is_empty() {
                text.insert_str(data.preedit_offset, data.preedit_text.as_str());
                if cursor_position >= data.preedit_offset {
                    cursor_position += data.preedit_text.len()
                }
                if anchor_position >= data.preedit_offset {
                    anchor_position += data.preedit_text.len()
                }
            }

            let to_utf16 = |x| byte_offset_to_utf16_offset(&text, x as usize);
            let text = JString::new(env, text.as_str())?;

            let input_type = match data.input_type {
                InputType::Text | InputType::Search => {
                    let hints = &data.input_method_hints;
                    let capitalization_flag = match hints.capitalization {
                        CapitalizationMode::None => 0 as jint,
                        CapitalizationMode::Sentences => {
                            AndroidInputType::TYPE_TEXT_FLAG_CAP_SENTENCES(env)?
                        }
                        CapitalizationMode::Words => {
                            AndroidInputType::TYPE_TEXT_FLAG_CAP_WORDS(env)?
                        }
                        CapitalizationMode::Characters => {
                            AndroidInputType::TYPE_TEXT_FLAG_CAP_CHARACTERS(env)?
                        }
                        _ => 0 as jint,
                    };
                    let auto_correct_flag = if hints.auto_correct {
                        AndroidInputType::TYPE_TEXT_FLAG_AUTO_CORRECT(env)?
                    } else {
                        0 as jint
                    };
                    let auto_complete_flag = if hints.auto_complete {
                        AndroidInputType::TYPE_TEXT_FLAG_AUTO_COMPLETE(env)?
                    } else {
                        0 as jint
                    };
                    AndroidInputType::TYPE_CLASS_TEXT(env)?
                        | capitalization_flag
                        | auto_correct_flag
                        | auto_complete_flag
                }
                InputType::Password => {
                    AndroidInputType::TYPE_TEXT_VARIATION_PASSWORD(env)?
                        | AndroidInputType::TYPE_CLASS_TEXT(env)?
                }
                InputType::Number => AndroidInputType::TYPE_CLASS_NUMBER(env)?,
                InputType::Decimal => {
                    AndroidInputType::TYPE_CLASS_NUMBER(env)?
                        | AndroidInputType::TYPE_NUMBER_FLAG_DECIMAL(env)?
                }
                _ => 0 as jint,
            };

            let cur_origin = data.cursor_rect_origin.to_physical(scale_factor); // i32
            let anchor_origin = data.anchor_point.to_physical(scale_factor);
            let cur_size = data.cursor_rect_size.to_physical(scale_factor);

            // A caret at the last column or on the last line sits exactly on the clip rect's max
            // edge (e.g. every right-aligned field), where the exclusive `Rect::contains` reports
            // it invisible and the handle gets sent off-screen. Inflate the rect by one pixel so
            // the max edge counts as visible.
            let clip_rect = data.clip_rect.map(|r| r.inflate(1., 1.));

            let cur_visible = clip_rect.map_or(true, |r| {
                r.contains(i_slint_core::lengths::logical_point_from_api(data.cursor_rect_origin))
            });
            let anchor_visible = clip_rect.map_or(true, |r| {
                // anchor_point is `origin + cursor_size` for handle placement; check the anchor
                // cursor's origin instead.
                let anchor_origin =
                    i_slint_core::lengths::logical_point_from_api(data.anchor_point)
                        - i_slint_core::lengths::logical_size_from_api(data.cursor_rect_size)
                            .to_vector();
                r.contains(anchor_origin)
            });

            // Add 2*cur_size.width to the y position to be a bit under the cursor
            let cursor_height = cur_size.height as i32 + 2 * cur_size.width as i32;
            let cur_x = if cur_visible { cur_origin.x + cur_size.width as i32 / 2 } else { -1 };
            let cur_y = cur_origin.y + cursor_height;
            let anchor_x = if anchor_visible { anchor_origin.x } else { -1 };
            let anchor_y = anchor_origin.y + 2 * cur_size.width as i32;

            helper.set_imm_data(
                env,
                &text,
                to_utf16(cursor_position) as i32,
                to_utf16(anchor_position) as i32,
                to_utf16(data.preedit_offset) as i32,
                to_utf16(data.preedit_offset + data.preedit_text.len()) as i32,
                cur_x,
                cur_y,
                anchor_x,
                anchor_y,
                cursor_height,
                input_type,
                show_cursor_handles,
            )?;

            Ok(())
        })
    }

    pub fn color_scheme(&self) -> Result<i32, jni::errors::Error> {
        self.with_jni_env(|env, helper| helper.color_scheme(env))
    }

    pub fn font_scale(&self) -> Result<f32, jni::errors::Error> {
        self.with_jni_env(|env, helper| helper.font_scale(env))
    }

    pub fn accent_color(&self) -> Result<Color, jni::errors::Error> {
        self.with_jni_env(|env, helper| {
            Ok(Color::from_argb_encoded(helper.accent_color(env)? as u32))
        })
    }

    pub fn get_safe_area(&self) -> Result<PhysicalEdges, jni::errors::Error> {
        self.with_jni_env(|env, helper| {
            let rect = helper.get_safe_area(env)?;
            let left = rect.left(env)?;
            let top = rect.top(env)?;
            let right = rect.right(env)?;
            let bottom = rect.bottom(env)?;
            Ok(PhysicalEdges::new(top, bottom, left, right))
        })
    }

    pub fn set_handle_color(&self, color: Color) -> Result<(), jni::errors::Error> {
        self.with_jni_env(|env, helper| {
            helper.set_handle_color(env, color.as_argb_encoded() as i32)
        })
    }

    pub fn long_press_timeout(&self) -> Result<Duration, jni::errors::Error> {
        self.with_jni_env(|env, _helper| {
            let long_press_timeout = AndroidViewConfiguration::get_long_press_timeout(env)?;
            Ok(Duration::from_millis(long_press_timeout as _))
        })
    }

    pub fn show_action_menu(&self) -> Result<(), jni::errors::Error> {
        self.with_jni_env(|env, helper| helper.show_action_menu(env))
    }

    pub fn set_clipboard(&self, text: &str) -> Result<(), jni::errors::Error> {
        self.with_jni_env(|env, helper| {
            let text = JString::new(env, text)?;
            helper.set_clipboard(env, &text)
        })
    }

    pub fn get_clipboard(&self) -> Result<String, jni::errors::Error> {
        self.with_jni_env(|env, helper| Ok(helper.get_clipboard(env)?.to_string()))
    }

    /// Ask the Activity to finish. Used from `callback_on_back_invoked` when
    /// Slint's key dispatch reports the Back key as unhandled so we preserve
    /// the legacy Back-closes-the-activity behavior even under the
    /// OnBackInvokedCallback flow.
    pub fn finish_activity(&self) -> Result<(), jni::errors::Error> {
        self.with_jni_env(|env, helper| helper.finish_activity(env))
    }
}

fn callback_update_text<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
    text: JString<'local>,
    cursor_position: jint,
    anchor_position: jint,
    preedit_start: jint,
    preedit_end: jint,
) -> Result<(), jni::errors::Error> {
    let java_str = text.to_string();
    let decoded: std::borrow::Cow<str> = (&java_str).into();
    let text = SharedString::from(decoded.as_ref());

    let cursor_position = utf16_offset_to_byte_offset_clamped(&text, cursor_position as usize);
    let anchor_position = utf16_offset_to_byte_offset_clamped(&text, anchor_position as usize);
    let preedit_start = utf16_offset_to_byte_offset_clamped(&text, preedit_start as usize);
    let preedit_end = utf16_offset_to_byte_offset_clamped(&text, preedit_end as usize);

    i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            adaptor.show_cursor_handles.set(false);
            let runtime_window = i_slint_core::window::WindowInner::from_pub(&adaptor.window);
            let event = if preedit_start != preedit_end {
                let adjust = |pos| {
                    if pos <= preedit_start {
                        pos
                    } else if pos >= preedit_end {
                        preedit_start + (pos - preedit_end)
                    } else {
                        preedit_start
                    }
                } as i32;
                let mut key_event = KeyEvent::default();
                key_event.text =
                    i_slint_core::format!("{}{}", &text[..preedit_start], &text[preedit_end..]);

                InternalKeyEvent {
                    event_type: KeyEventType::UpdateComposition,
                    preedit_text: text[preedit_start..preedit_end].into(),
                    preedit_selection: Some(0..(preedit_end - preedit_start) as i32),
                    replacement_range: Some(i32::MIN..i32::MAX),
                    cursor_position: Some(adjust(cursor_position)),
                    anchor_position: Some(adjust(anchor_position)),
                    key_event,
                    ..Default::default()
                }
            } else {
                let mut key_event = KeyEvent::default();
                key_event.text = text;

                InternalKeyEvent {
                    event_type: KeyEventType::CommitComposition,
                    replacement_range: Some(i32::MIN..i32::MAX),
                    cursor_position: Some(cursor_position as _),
                    anchor_position: Some(anchor_position as _),
                    key_event,
                    ..Default::default()
                }
            };
            runtime_window.process_key_input(event);
        }
    })
    .unwrap();
    Ok(())
}

fn callback_set_night_mode<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
    night_mode: jint,
) -> Result<(), jni::errors::Error> {
    i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            let scheme = match night_mode {
                0x10 => ColorScheme::Light,  // UI_MODE_NIGHT_NO(0x10)
                0x20 => ColorScheme::Dark,   // UI_MODE_NIGHT_YES(0x20)
                0x0 => ColorScheme::Unknown, // UI_MODE_NIGHT_UNDEFINED
                _ => ColorScheme::Unknown,
            };
            let ctx = i_slint_core::window::WindowInner::from_pub(&w.window).context();
            ctx.set_color_scheme(scheme);
            if let Ok(accent) = w.java_helper.accent_color() {
                ctx.set_accent_color(accent);
            }
        }
    })
    .unwrap();
    Ok(())
}

fn callback_set_font_scale<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
    font_scale: jfloat,
) -> Result<(), jni::errors::Error> {
    // Skip rather than clobber: an OEM/ROM that hands us 0.0 or NaN during a
    // transient configuration change must not zero out the value `bind_context`
    // already set.
    let Some(size) = font_scale_to_logical_length(font_scale) else { return Ok(()) };
    i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            let ctx = i_slint_core::window::WindowInner::from_pub(&w.window).context();
            ctx.set_platform_default_font_size(Some(size));
        }
    })
    .unwrap();
    Ok(())
}

fn callback_move_cursor_handle<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
    id: jint,
    pos_x: jint,
    pos_y: jint,
) -> Result<(), jni::errors::Error> {
    i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            if let Some(focus_item) = i_slint_core::window::WindowInner::from_pub(&adaptor.window)
                .focus_item
                .borrow()
                .upgrade()
            {
                if let Some(text_input) = focus_item.downcast::<i_slint_core::items::TextInput>() {
                    let scale_factor = adaptor.window.scale_factor();
                    let adaptor = adaptor.clone() as Rc<dyn WindowAdapter>;
                    let size = text_input
                        .as_pin_ref()
                        .font_request(&focus_item)
                        .pixel_size
                        .unwrap_or_default()
                        .get();
                    let pos =
                        euclid::point2(
                            pos_x as f32 / scale_factor,
                            pos_y as f32 / scale_factor - size / 2.,
                        ) - focus_item.map_to_window(focus_item.geometry().origin).to_vector();
                    let text_pos = text_input.as_pin_ref().byte_offset_for_position(
                        pos,
                        &adaptor,
                        &focus_item,
                    );

                    let cur_pos = if id == 0 {
                        text_input.anchor_position_byte_offset.set(text_pos as i32);
                        text_pos as i32
                    } else {
                        let current_cursor = text_input.as_pin_ref().cursor_position_byte_offset();
                        let current_anchor = text_input.as_pin_ref().anchor_position_byte_offset();
                        if (id == 1 && current_anchor < current_cursor)
                            || (id == 2 && current_anchor > current_cursor)
                        {
                            if current_cursor == text_pos as i32 {
                                return;
                            }
                            text_input.anchor_position_byte_offset.set(text_pos as i32);
                            current_cursor
                        } else {
                            if current_anchor == text_pos as i32 {
                                return;
                            }
                            text_pos as i32
                        }
                    };

                    text_input.as_pin_ref().set_cursor_position(
                        cur_pos,
                        true,
                        i_slint_core::items::TextChangeNotify::TriggerCallbacks,
                        &adaptor,
                        &focus_item,
                    );
                }
            }
        }
    })
    .unwrap();
    Ok(())
}

fn callback_popup_menu_action<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
    id: jint,
) -> Result<(), jni::errors::Error> {
    i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            if let Some(focus_item) = i_slint_core::window::WindowInner::from_pub(&adaptor.window)
                .focus_item
                .borrow()
                .upgrade()
            {
                if let Some(text_input) = focus_item.downcast::<i_slint_core::items::TextInput>() {
                    let text_input = text_input.as_pin_ref();
                    let adaptor = adaptor.clone() as Rc<dyn WindowAdapter>;
                    match id {
                        0 => text_input.cut(&adaptor, &focus_item),
                        1 => text_input.copy(&adaptor, &focus_item),
                        2 => text_input.paste(&adaptor, &focus_item),
                        3 => text_input.select_all(&adaptor, &focus_item),
                        _ => (),
                    }
                }
            }
        }
    })
    .unwrap();
    Ok(())
}

fn callback_on_back_invoked<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
) -> Result<(), jni::errors::Error> {
    // Forward Back as a Key.Back KeyPressed + KeyReleased pair; fall back to
    // Activity.finish() if unhandled.
    let _ = i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            let text: SharedString = Key::Back.into();
            let pressed = adaptor
                .window
                .dispatch_event_with_result(WindowEvent::KeyPressed { text: text.clone() });
            let released =
                adaptor.window.dispatch_event_with_result(WindowEvent::KeyReleased { text });
            let handled = matches!(pressed, Ok(WindowEventDispatchResult::Accepted))
                || matches!(released, Ok(WindowEventDispatchResult::Accepted));
            if !handled {
                if let Err(e) = adaptor.java_helper.finish_activity() {
                    i_slint_core::debug_log!("finish_activity failed: {e:#?}");
                }
            }
        }
    });
    Ok(())
}

fn callback_set_insets<'local>(
    _env: &mut Env<'local>,
    _class: JClass<'local>,
    window_top: jint,
    window_left: jint,
    window_bottom: jint,
    window_right: jint,
    safe_area_top: jint,
    safe_area_left: jint,
    safe_area_bottom: jint,
    safe_area_right: jint,
    keyboard_top: jint,
    keyboard_left: jint,
    keyboard_bottom: jint,
    keyboard_right: jint,
) -> Result<(), jni::errors::Error> {
    i_slint_core::api::invoke_from_event_loop(move || {
        if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
            w.update_window_insets(
                PhysicalPosition::new(window_left as _, window_top as _),
                PhysicalSize::new(
                    (window_right - window_left) as _,
                    (window_bottom - window_top) as _,
                ),
                PhysicalEdges::new(
                    safe_area_top as _,
                    safe_area_bottom as _,
                    safe_area_left as _,
                    safe_area_right as _,
                ),
                PhysicalEdges::new(
                    keyboard_top as _,
                    keyboard_bottom as _,
                    keyboard_left as _,
                    keyboard_right as _,
                ),
            );
        }
    })
    .unwrap();
    Ok(())
}
