"""
文字列を入力するための Kivy 用ダイアログボックス

    Last modified: 2025/09/29

    Copyright (c) 2025 toshifumi tsutsui
    Released under the MIT license
    https://wpandora8.net/the_mit_license.html
"""

import platform
from typing import Callable

from kivy.core.window import Keyboard, Window
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import ObjectProperty, StringProperty
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button
from kivy.uix.modalview import ModalView
from kivy.uix.textinput import TextInput

textinput: str = "TextInput_JA" if platform.system() == "Darwin" else "TextInput"

Builder.load_string(f"""
<_InputBoxLayout>:
    text_input: text_input
    button_ok: button_ok
    button_cancel: button_cancel

    size_hint: 0.8, 0.8
    BoxLayout:
        id: root_layout
        orientation: 'vertical'
        Label:
            size_hint_y: 1.0-(dp(48+32+32))/root_layout.height
            text_size: self.size
            valign: 'middle'
            line_height: 1.5
            text: root.message
        {textinput}:
        # TextInput:
            id: text_input
            size_hint_y: dp(32)/root_layout.height
            padding: dp(8), dp(8)
            use_bubble: True
            multiline: False
            write_tab: False
            text: root.text
            hint_text: root.hint_text
            on_text_validate: root.on_text_input_validate()
        AnchorLayout:
            size_hint_y: (button_cancel.height+dp(32))/root_layout.height
            BoxLayout:
                spacing: dp(20)
                orientation: 'horizontal'
                Button:
                    id: button_cancel
                    size_hint_y: None
                    height: dp(48)
                    text: 'キャンセル'
                    on_release: root.button_cancel_click()
                Button:
                    id: button_ok
                    size_hint_y: None
                    height: dp(48)
                    text: root.button_ok_text
                    on_release: root.button_ok_click()
""")


class InputBox:
    """文字列を入力するためのダイアログボックスを定義したクラス。"""

    def __init__(
        self,
        message: str = "",
        on_ok_callback: Callable[[str], None] | None = None,
        on_cancel_callback: Callable | None = None,
        button_ok_text: str = "OK",
        size_hint: tuple[float | None, float | None] = (0.8, 0.5),
        size: tuple[float, float] = (dp(640), dp(480)),
        text: str = "",
        hint_text: str = "",
    ) -> None:
        """文字列を入力するためのダイアログボックスのインスタンスを作成して返す。

        Args:
            message (str, optional): 表示するメッセージ。
            on_ok_callback (Callable[[str], None] | None, optional): 「OK」が選択されたときの処理。
            on_cancel_callback (Callable | None, optional): 「キャンセル」が選択されたときの処理。
            button_ok_text (str, optional): 「OK」ボタンの表示名。
            ok_key (tuple[str, Iterable[str]], optional): 「OK」ボタンのアクセスキー。
            size_hint (tuple[float | None, float | None], optional): 親ウィンドウに対するダイアログボックスのサイズの比率。
            size (tuple[float, float], optional): ダイアログボックスのサイズ（size_hint にはそれぞれ None を指定）。
            text (str, optional): 事前に入力されている文字列。
            hint_text (str, optional): 未入力状態のときに表示されるヒント文字列。
        """

        self._on_ok_callback: Callable[[str], None] | None = on_ok_callback
        self._on_cancel_callback: Callable | None = on_cancel_callback

        self._view: ModalView = ModalView(
            size_hint=size_hint, size=size, auto_dismiss=False
        )
        self._view.add_widget(
            _InputBoxLayout(
                on_ok=lambda t: self._ok(t),
                on_cancel=self._cancel,
                message=message,
                button_ok_text=button_ok_text,
                text=text,
                hint_text=hint_text,
            )
        )

    def show(self) -> None:
        """InputBox を表示する。"""

        self._view.open()

    def _ok(self, text: str) -> None:
        """「OK」ボタンがクリックされたとき。"""

        self._view.dismiss()

        if self._on_ok_callback is not None:
            self._on_ok_callback(text)

    def _cancel(self) -> None:
        """「キャンセル」ボタンがクリックされたとき。"""

        self._view.dismiss()

        if self._on_cancel_callback is not None:
            self._on_cancel_callback()


class _InputBoxLayout(AnchorLayout):
    """文字列を入力するためのダイアログボックスのレイアウト。"""

    button_ok_text: str = StringProperty("")
    hint_text: str = StringProperty("")
    message: str = StringProperty("")
    text: str = StringProperty("")

    text_input: TextInput = ObjectProperty(None)
    button_ok: Button = ObjectProperty(None)
    button_cancel: Button = ObjectProperty(None)

    def __init__(
        self, on_ok: Callable[[str], None], on_cancel: Callable | None, **kwargs
    ) -> None:
        super().__init__(**kwargs)

        self._on_ok: Callable[[str], None] = on_ok
        self._on_cancel: Callable | None = on_cancel

        self._set_access_key()
        self.text_input.bind(focus=self._on_focus)  # type: ignore
        self.text_input.focus = True

    def _set_access_key(self) -> None:
        """アクセスキーを設定する。"""

        def keyboard_closed():
            self._keyboard.unbind(on_key_down=self.on_keyboard_down)

        self._keyboard: Keyboard = Window.request_keyboard(keyboard_closed, self)
        self._keyboard.bind(on_key_down=self.on_keyboard_down)

    def on_text_input_validate(self) -> None:
        """text_input にフォーカスがある状態で Enter キーが押されたとき。"""

        self.button_ok_click()

    def on_keyboard_down(
        self,
        window: Keyboard,
        keycode: tuple[int, str],
        text: str,
        modifiers: list[str],
    ) -> None:
        """押下されたアクセスキーにより処理を行う。

        Args:
            window (Keyboard): バインドされた Keyboard のインスタンス。
            keycode (tupl[int, str]): 押されたキーのキーコードと文字の tuple。
            text (str): 押されたキーのテキスト。
            modifiers (list[str]): 同時に押された補助キー名の list。
        """

        if keycode[1] == "escape":
            self.button_cancel_click()
        elif keycode[1] == "enter":
            self.button_ok_click()

    def _on_focus(self, instance: TextInput, value: bool) -> None:
        """MyTextInput のフォーカスの状態が変わったとき。

        Args:
            instance (MyTextInput): 対象となる MyTextInput のインスタンス。
            value (bool): フォーカスが外れた場合は False。
        """

        # TextInput のフォーカスが外れたときに、再度 Keyboard にアクセスキーをバインドする。
        if not value:
            self._keyboard.bind(on_key_down=self.on_keyboard_down)

    def button_ok_click(self) -> None:
        """「OK」ボタンがクリックされたとき。"""

        self._unbind_functions()

        self._on_ok(self.text_input.text)

    def button_cancel_click(self) -> None:
        """「キャンセル」ボタンがクリックされたとき。"""

        self._unbind_functions()

        if self._on_cancel is not None:
            self._on_cancel()

    def _unbind_functions(self) -> None:
        """イベントにバインドされた関数を解除する。"""

        self.text_input.unbind(focus=self._on_focus)  # type: ignore
        self._keyboard.unbind(on_key_down=self.on_keyboard_down)
