"""
textinput4ja.py

Last modified: 2025/09/28

TextInput class for Japanese
Tested on macOS Monterey 12.6 and Android emulator api=33 using Kivy 2.1.0

MIT License

Copyright ©︎ 2022 bu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import copy
import re

# from operator import is_
from kivy.base import EventLoop
from kivy.clock import Clock

# from kivy.core.window import Window
from kivy.properties import (
    BooleanProperty,
    ListProperty,
    NumericProperty,
    StringProperty,
)
from kivy.uix.textinput import FL_IS_LINEBREAK, FL_IS_WORDBREAK, TextInput

# from kivy.utils import platform


# override TextInput
class TextInput_JA(TextInput):
    # __events__ = ("on_text_validate", "on_double_tap", "on_triple_tap", "on_quad_touch")

    _resolved_base_dir = None  # よくわからん
    is_JP_ime_on = BooleanProperty()  # 日本語IMEの起動状態のフラグ
    is_lang_en = BooleanProperty()  # 言語設定がEnglishかどうかのフラグ
    is_lang_ja = BooleanProperty()  # 言語設定が日本語かどうかのフラグ
    is_android_ime_on = BooleanProperty()  # androidでのIMEの起動状態のフラグ
    lines = ListProperty()  # TextInputに入力されているテキストの行ごとのリスト
    alltext = StringProperty()  # TextInputに入力済みの全テキスト
    add_to_end = (
        BooleanProperty()
    )  # テキストの追加が入力済みテキストの末端への追加かどうかのフラグ
    del_by_bkspc = (
        BooleanProperty()
    )  # backspaceを押してテキストを消去したかどうかのフラグ
    _text_confirmed = BooleanProperty()  # テキスト入力を確定したかどうかのフラグ
    _justbreaklined = (
        BooleanProperty()
    )  # テキスト入力して直後に改行されたかどうかのフラグ
    _row_start_of_typing = NumericProperty()  # テキスト入力を始めた行を格納
    _ci_start_of_typing = (
        NumericProperty()
    )  # テキスト入力を始めたときのcursor indexを格納

    def __init__(self, **kwargs):
        super(TextInput_JA, self).__init__(**kwargs)
        self.is_JP_ime_on = False  # 初期設定はFalseとする
        self.is_android_ime_on = False
        self.gcursor_index = self.cursor_index()
        self.lines = ["", ""]
        self.alltext = ""
        self.add_to_end = True
        self.del_by_bkspc = False
        self._text_confirmed = False
        self._justbreaklined = False
        self._row_start_of_typing = 0
        self._ci_start_of_typing = 0

        self._pressed_key = ""
        self.text_validate_unfocus = False

    def keyboard_on_key_down(self, window, keycode, text, modifiers):
        def ime_JP_off(dt):
            self.is_JP_ime_on = False

        def refresh_text(row, new_text):
            (start, finish, lines, lines_flags, len_lines) = self._get_line_from_cursor(
                row, new_text
            )
            self._refresh_text_from_property(
                "insert", start, finish, lines, lines_flags, len_lines
            )
            self.cursor = self.get_cursor_from_index(cindex + len(new_text))
            Clock.schedule_once(ime_JP_off, 0.1)

        self._pressed_key = keycode[1]
        cindex = self.cursor_index()

        if self._ime_composition and keycode[1] == "tab":
            return
        elif self._ime_composition and keycode[1] == "escape":
            col, row = self.cursor
            text = self._lines[row]
            refresh_text(row, text)
            return
        elif self._ime_composition and keycode[1] == "enter":
            col, row = self.cursor
            text = self._lines[row]
            new_text = text[:col] + self._ime_composition + text[col:]
            refresh_text(row, new_text)
            return

        super().keyboard_on_key_down(window, keycode, text, modifiers)

    def do_backspace(self, from_undo=False, mode="bkspc"):
        """Do backspace operation from the current cursor position.
        This action might do several things:

            - removing the current selection if available.
            - removing the previous char and move the cursor back.
            - do nothing, if we are at the start.
        """

        # IME system handles its own backspaces
        # 元のコードは if self._ime_composition:  となっていたが、これだとbackspaceが効かなくなってしまっていたので改良。
        # androidの場合はreadonlyの時だけreturn, android以外の時はis_JP_ime_onのときもreturn
        if self.readonly or self.is_JP_ime_on:
            return

        self.del_by_bkspc = True

        # 以下、print文以外はオリジナルのコードのまま

        col, row = self.cursor
        _lines = self._lines
        _lines_flags = self._lines_flags
        text = _lines[row]
        cursor_index = self.cursor_index()

        if col == 0 and row == 0:
            return

        start = row
        if col == 0:
            if _lines_flags[row] == FL_IS_LINEBREAK:
                substring = "\n"
                new_text = _lines[row - 1] + text
            else:
                substring = _lines[row - 1][-1] if len(_lines[row - 1]) > 0 else ""
                new_text = _lines[row - 1][:-1] + text

            self._set_line_text(row - 1, new_text)
            self._delete_line(row)
            start = row - 1
        else:
            # ch = text[col-1]
            substring = text[col - 1]
            new_text = text[: col - 1] + text[col:]
            self._set_line_text(row, new_text)

        # refresh just the current line instead of the whole text
        start, finish, lines, lineflags, len_lines = self._get_line_from_cursor(
            start, new_text
        )

        self._refresh_text_from_property(
            "insert" if col == 0 else "del", start, finish, lines, lineflags, len_lines
        )

        self.cursor = self.get_cursor_from_index(cursor_index - 1)
        # handle undo and redo
        self._set_unredo_bkspc(
            cursor_index, cursor_index - 1, substring, from_undo, mode
        )

    # current IME composition in progress by the IME system, or '' if nothing
    _ime_composition = StringProperty("")
    # cursor position of last IME event
    _ime_cursor = ListProperty(None, allownone=True)

    def window_on_textedit(self, window, ime_input):
        self.gcursor_index = self.cursor_index()
        self.gcursor = self.cursor
        col, row = self.cursor
        _lines = self._lines
        text_lines = self._lines or [""]

        # if self._ime_composition and not ime_input:
        if self._ime_composition:
            pcc, pcr = self._ime_cursor

            # 入力開始した行よりも下に行があるかないかでtextに採用する行を分ける
            if len(text_lines) > pcr:
                text = text_lines[pcr]
            else:
                text = text_lines[pcr - 1]

            # _ime_compositionの文字数を取得しておく
            len_ime = len(self._ime_composition)

            # 改行がない時
            if text[pcc - len_ime : pcc] == self._ime_composition:  # always?
                # 入力行のテキストを更新する
                remove_old_ime_text = text[: pcc - len_ime] + text[pcc:]
                ci = self.cursor_index()

                # ここ検討してスッキリさせる余地あり
                # 入力開始した行が現在のカーソルがある行と一致しているかどうかで処理を分ける
                if pcr == self.cursor[1]:
                    self._refresh_text_from_property(
                        "insert",
                        *self._get_line_from_cursor(pcr, remove_old_ime_text),
                    )
                else:
                    self._refresh_text_from_property(
                        "insert",
                        *self._get_line_from_cursor(
                            self.cursor[1], remove_old_ime_text
                        ),
                    )

                # カーソル位置は入力文字数分を戻して更新
                self.cursor = self.get_cursor_from_index(ci - len_ime)

                # IME開始直後はself.is_JP_ime_onがFalseなのでその時に入力されているテキストを取得し反映する
                # 保持しているカーソル位置はime_inputで取得しているccだとここでは定義されてないので使えないため、self. gcursorから取ってくる

                if not self.is_JP_ime_on:
                    gcc, gcr = self.gcursor
                    new_text = text[:gcc] + text[gcc:]

                    # テキストをリフレッシュ
                    self._refresh_text_from_property(
                        "insert", *self._get_line_from_cursor(gcr, new_text)
                    )

                    # new_textの長さを加味して修正したカーソル位置を更新
                    self.cursor = self.get_cursor_from_index(
                        self.cursor_index() + len(new_text)
                    )

            # 改行があるとき
            else:
                if len(text_lines) > pcr:
                    text = text_lines[pcr]
                else:
                    text = text_lines[pcr - 1]

                cc, cr = self.cursor
                # remove_old_ime_text = ''としたら改行時に余計な文字が付かなくなって正常に動く。なぜかよくわからん。
                remove_old_ime_text = ""
                ci = self.cursor_index()

                if len(text) > len(ime_input):
                    # テキスト全体の取得と差分の取得
                    alltext = copy.deepcopy(self.text)
                    # diff_ime_input = ime_input[len(alltext[ci:]) :]
                    # diff_alltext = alltext[ci:]
                    self.add_to_end = False
                    # テキストの挿入の時はime_input >= self._ime_compositionかつself.del_by_bkspc==Falseになる。
                    # そうでなければ削除操作中。それぞれで処理を分ける
                    if not self.del_by_bkspc:
                        self.text = (
                            alltext[: ci - len(self._ime_composition)]
                            + ime_input
                            + alltext[ci:]
                        )
                        self.cursor = self.get_cursor_from_index(
                            ci - len(self._ime_composition)
                        )
                    else:
                        self.text = self.text[: ci - 1] + self.text[ci:]
                        self.cursor = self.get_cursor_from_index(
                            ci - len(self._ime_composition)
                        )
                else:
                    self.add_to_end = True  # フラグを更新
                    # 改行があるかどうかを入力開始行と現在の行の比較で検出し、改行の有無で処理を分ける
                    if pcr == self.cursor[1]:
                        self._refresh_text_from_property(
                            "insert",
                            *self._get_line_from_cursor(pcr, remove_old_ime_text),
                        )
                    else:
                        self._refresh_text_from_property(
                            "insert",
                            *self._get_line_from_cursor(
                                self.cursor[1], remove_old_ime_text
                            ),
                        )
                    self.cursor = self.get_cursor_from_index(ci - len_ime)

                # IME開始直後はself.is_JP_ime_onがFalseなのでその時に入力されているテキストを取得し反映する
                # カーソル位置はime_inputで取得しているccは定義されてないので使えないためself. gcursorを使う

                if not self.is_JP_ime_on:
                    gcc, gcr = self.gcursor
                    new_text = text[:gcc] + text[gcc:]  # これでうまくいった。
                    if gcr == self.cursor[1]:
                        self._refresh_text_from_property(
                            "insert", *self._get_line_from_cursor(gcr, new_text)
                        )
                    else:
                        self._refresh_text_from_property(
                            "insert",
                            *self._get_line_from_cursor(self.cursor[1], new_text),
                        )

                    # new_textの長さを加味してカーソル位置を更新
                    self.cursor = self.get_cursor_from_index(
                        self.cursor_index() + len(new_text)
                    )

        if ime_input:
            if self._selection:
                self.delete_selection()

            # _ime_compositionがあるかどうかで処理を分ける
            if self._ime_composition:
                # 改行しないとき
                if text[pcc - len_ime : pcc] == self._ime_composition:  # type: ignore
                    cc, cr = self.cursor
                    text = text_lines[cr]

                    # original
                    new_text = text[:cc] + ime_input + text[cc:]

                # 改行するとき new_textをtext(カーソルより前),ime_input, text(カーソルより後ろ)を元にして生成
                else:
                    # いろいろ取得しておく
                    cc, cr = self.cursor
                    pcc, pcr = self._ime_cursor
                    text = text_lines[cr]
                    alltext = copy.deepcopy(self.text)
                    ci = self.cursor_index()
                    # diff_ime_input = ime_input[len(alltext[ci:]) :]
                    # diff_alltext = alltext[ci:]

                    # 末尾への挿入か途中挿入かで切り分け
                    if self.add_to_end:
                        # テキストの挿入の時はlen(ime_input) >= len(self._ime_composition)になる。
                        # そうでなければ削除操作中
                        if len(ime_input) >= len(self._ime_composition):
                            new_text = (
                                text[:cc]
                                + ime_input
                                + alltext[ci:][len(ime_input) - 1 :]
                            )
                        else:
                            new_text = text[:cc] + ime_input

                    # 途中挿入の時
                    else:
                        # テキストの挿入の時はself.del_by_bkspc==Falseになる。
                        # そうでなければ削除操作中
                        if not self.del_by_bkspc:
                            new_text = text[:cc] + text[cc:]
                        else:
                            new_text = text[:cc] + text[cc:]

            else:
                cc, cr = self.cursor
                text = text_lines[cr]

                # new_textを作成する
                new_text = text[:cc] + ime_input + text[cc:]

            # リフレッシュする
            self._refresh_text_from_property(
                "insert", *self._get_line_from_cursor(cr, new_text)
            )

            # カーソル位置の更新
            self.cursor = self.get_cursor_from_index(
                self.cursor_index() + len(ime_input)
            )

        # _ime_compositionをime_inputで更新
        self._ime_composition = ime_input
        # _ime_cursorを現在のカーソル位置で更新
        self._ime_cursor = self.cursor

        # is_JP_ime_onをTrueに戻す
        self.is_JP_ime_on = True

        # del_by_bkspcをFalseに戻す
        self.del_by_bkspc = False

    def insert_text(self, substring, from_undo=False):
        """Insert new text at the current cursor position. Override this
        function in order to pre-process text for input validation.
        """

        _lines = self._lines
        _lines_flags = self._lines_flags
        self.gcursor = self.cursor

        if self.readonly or not substring or not self._lines:
            return

        if isinstance(substring, bytes):
            substring = substring.decode("utf8")

        if self.replace_crlf:
            substring = substring.replace("\r\n", "\n")

        self._hide_handles(EventLoop.window)

        if not from_undo and self.multiline and self.auto_indent and substring == "\n":
            substring = self._auto_indent(substring)

        mode = self.input_filter
        if mode not in (None, "int", "float"):
            substring = mode(substring, from_undo)
            if not substring:
                return

        col, row = self.cursor
        cindex = self.cursor_index()
        text = _lines[row]
        len_str = len(substring)

        # 改変ここから

        # ここで末端追加か途中挿入かのフラグを立てておく 必要ないかも
        if len(text[col:]) == 0:
            self.add_to_end = True
        else:
            self.add_to_end = False

        # 改行コードの入力の有無により条件分岐
        if substring == "\n":
            # IMEがONの時は改行コードを入れずに改行しないようにする。IMEがOFFの時は改行コードを入れて改行する。
            if self.is_JP_ime_on:
                # is_JP_ime_onをFalseに設定
                self.is_JP_ime_on = False
                new_text = text[:col] + text[col:]
            else:
                if self.multiline:
                    new_text = text[:col] + "\n" + text[col:]
                    ##カーソルの表示が一行下になるように更新する
                    Clock.schedule_once(lambda x: self.cursor_update(self.cursor), 0.1)
                else:
                    new_text = text[:col] + text[col:]
        else:
            # if self.is_JP_ime_on:
            #     # is_JP_ime_onをFalseに設定
            #     self.is_JP_ime_on = False
            #     new_text = text[:col] + text[col:]
            # else:
            # ime_compositionとsubstringが同じだったらIMEがOFF(？)に切り替わった直後なので、
            # 生成テキストの重複を避けるためsubstringを使わずにnew_textを生成し、substringと_ime_compositionの内容を消去
            # if self._ime_composition == substring:
            #     substring = ""
            #     self._ime_composition = ""
            #     new_text = text[:col] + text[col:]
            if self._pressed_key in ["enter"]:
                new_text = text
                # new_text = text[:col] + text[col:]
            else:
                new_text = text[:col] + substring + text[col:]

        if mode is not None:
            if mode == "int":
                if not re.match(self._insert_int_pat, new_text):
                    return
            elif mode == "float":
                if not re.match(self._insert_float_pat, new_text):
                    return

        self._set_line_text(row, new_text)

        # len_strはsubstringの長さ
        if (
            len_str > 1
            or substring == "\n"
            or (substring == " " and _lines_flags[row] != FL_IS_LINEBREAK)
            or (row + 1 < len(_lines) and _lines_flags[row + 1] != FL_IS_LINEBREAK)
            or (
                self._get_text_width(new_text, self.tab_width, self._label_cached)
                > (self.width - self.padding[0] - self.padding[2])
            )
        ):
            # Avoid refreshing text on every keystroke.
            # Allows for faster typing of text when the amount of text in
            # TextInput gets large.

            (start, finish, lines, lines_flags, len_lines) = self._get_line_from_cursor(
                row, new_text
            )

            # calling trigger here could lead to wrong cursor positioning
            # and repeating of text when keys are added rapidly in a automated
            # fashion. From Android Keyboard for example.
            self._refresh_text_from_property(
                "insert", start, finish, lines, lines_flags, len_lines
            )

        # IMEがONで入力した時は
        # cindexの位置(insert_textに入った時のself.cursor.index()の戻り値)よりも
        # 後ろに何もついていなければ末尾追加なので、len_str分カーソル位置を移動
        # 後ろに文字がついていれば挿入追加なので、カーソル位置はそのまま
        # isalnum()を使って、
        # substringが半角英数字のみ、またはlen(self.text[ciindex:])==0のときはlen_strを追加する。
        # それ以外（つまりIME ONで挿入追加を行った時）はカーソル位置はそのまま
        # if (
        #     substring.isascii()
        #     or substring in [" ", "　"]
        #     or len(self.text[cindex:]) == 0
        # ):
        #     self.cursor = self.get_cursor_from_index(cindex + len_str)
        # print(f"cursor moved to {self.cursor}")

        # print(f"self.cursor: {self.cursor}, cindex: {cindex}, len_str: {len_str}")
        self.cursor = self.get_cursor_from_index(cindex + len_str)
        # print(f"cindex: {cindex}, cursor moved to {self.cursor}")

        # handle undo and redo
        self._set_unredo_insert(cindex, cindex + len_str, substring, from_undo)

        # is_JP_ime_onをFalseに設定
        self.is_JP_ime_on = False

    # androidでない場合は、IMEがONの時にテキストボックス中でのカーソル移動を無効にする
    def do_cursor_movement(self, action, control=False, alt=False):
        # print(f"self._ime_composition: {self._ime_composition}")
        # if self.is_JP_ime_on:
        if self._ime_composition:
            return
        else:
            TextInput.do_cursor_movement(self, action, control=False, alt=False)

    #  on_focusについて #
    # - androidでないとき
    # textinputをdefocusしたときにis_JP_ime_onをFalseに変更する。
    # これによりIME入力中にdefocusになってもfocusが復帰した時にbackspaceが効く
    # また、defocusしたときはself._ime_compositionをリセットしておき、次にfocusしたときの入力に影響がないようにする
    # - androidのとき
    # input_typeを有するインスタンス(つまりTextInput)をfocusした場合、
    # 言語設定が日本語のときはinput_typeを'text'にする。=> 日本語入力を受け付けるようになる。
    # 言語設定が日本語以外ではinput_typeを'null'にする。=> English入力のみを受け付けるようになる（ただしGboardの入力予測などが使えない）

    def on_focus(self, instance, value):
        if value:
            if hasattr(instance, "input_type"):
                if self.is_lang_ja:
                    self.input_type = "text"
                else:
                    self.input_type = "null"
        else:
            self.is_JP_ime_on = False
            self._ime_composition = ""

    # 画面でのカーソル表示位置のupdate用
    def cursor_update(self, cursornow):
        self.cursor = cursornow[0], cursornow[1] + 1
        return self.cursor


# if __name__ == "__main__":
#     print("TextInput for Japanese")
