import argparse
import os
import re
import subprocess
import tkinter as tk
import tkinter.font as tkFont
from tkinter import filedialog, messagebox, simpledialog, scrolledtext

import chardet
from PIL import Image, ImageTk
from tkinterdnd2 import DND_FILES, TkinterDnD

# --- Constants ---
CONFIG_FILE = "iniedit.ini"
TEXT_INI_FILE = "text.ini"
COLOR_INI_FILE = "color.ini"

DEFAULT_GEOMETRY = "800x600+100+100"
DEFAULT_FIND_GEOMETRY = "350x150"
DEFAULT_REPLACE_GEOMETRY = "400x180"
DEFAULT_SECTION_GEOMETRY = "300x400"
DEFAULT_FONT_FAMILY = "Consolas"
DEFAULT_FONT_SIZE = 11
ENCODINGS = ["utf-8", "shift_jis", "cp932", "euc_jp", "iso2022_jp"]


def detect_text_encoding(raw_data, fallback="utf-8"):
    """Detect text encoding with chardet, returning a safe fallback when unsure."""
    try:
        result = chardet.detect(raw_data)
        encoding = result.get("encoding")
        confidence = result.get("confidence") or 0.0
        if encoding and confidence > 0.8:
            return encoding
    except Exception as exc:
        print(f"Warning: encoding detection failed: {exc}")
    return fallback


# --- INI File Reading/Writing Functions ---
def read_ini(filepath):
    """
    Read a simple key=value file without sections.
    Lines beginning with # and blank lines are ignored.
    """
    settings = {}
    if not os.path.exists(filepath):
        return settings

    try:
        with open(filepath, "rb") as f_raw:
            raw_data = f_raw.read()
        detected_encoding = detect_text_encoding(raw_data)
        text = raw_data.decode(detected_encoding, errors="ignore")
    except Exception as exc:
        print(f"Error reading INI file {filepath}: {exc}")
        messagebox.showerror(
            "INIファイル読み込みエラー",
            f"'{os.path.basename(filepath)}' の読み込み中にエラーが発生しました:\n{exc}",
        )
        return settings

    for line in text.splitlines():
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        if "=" not in line:
            continue
        key, value = line.split("=", 1)
        settings[key.strip()] = value.strip()
    return settings


def write_ini(filepath, settings):
    """Write settings to a simple key=value file."""
    try:
        with open(filepath, "w", encoding="utf-8") as f:
            for key, value in settings.items():
                f.write(f"{key}={value}\n")
    except Exception as exc:
        print(f"Error writing INI file {filepath}: {exc}")
        messagebox.showerror(
            "INIファイル書き込みエラー",
            f"'{os.path.basename(filepath)}' の書き込み中にエラーが発生しました:\n{exc}",
        )


def parse_multiline_ini(filepath):
    """
    Parse a simple key=value file supporting triple-quoted multiline values.

    Example:
        key=<triple-quoted multiline text>
    """
    settings = {}
    if not os.path.exists(filepath):
        return settings

    try:
        with open(filepath, "rb") as f_raw:
            raw_data = f_raw.read()
        detected_encoding = detect_text_encoding(raw_data)
        lines = raw_data.decode(detected_encoding, errors="ignore").splitlines()
    except Exception as exc:
        print(f"Error reading multiline INI file {filepath}: {exc}")
        messagebox.showerror(
            "INIファイル読み込みエラー",
            f"'{os.path.basename(filepath)}' の読み込み中にエラーが発生しました:\n{exc}",
        )
        return settings

    in_multiline = False
    current_key = None
    current_value = []

    for line in lines:
        stripped_line = line.strip()

        if in_multiline:
            end_index = line.find('"""')
            if end_index != -1:
                current_value.append(line[:end_index])
                settings[current_key] = "\n".join(current_value)
                in_multiline = False
                current_key = None
                current_value = []
            else:
                current_value.append(line)
            continue

        if not stripped_line or stripped_line.startswith("#") or "=" not in line:
            continue

        key, value_part = line.split("=", 1)
        key = key.strip()
        value_part = value_part.strip()

        if value_part.startswith('"""'):
            remainder = value_part[3:]
            end_index = remainder.find('"""')
            if end_index != -1:
                settings[key] = remainder[:end_index]
            else:
                in_multiline = True
                current_key = key
                current_value = [remainder]
        else:
            settings[key] = value_part

    if in_multiline and current_key is not None:
        settings[current_key] = "\n".join(current_value)

    return settings


# --- Simple TOML/INI Parser Class ---
class SimpleTomlParser:
    """
    A simplified TOML/INI parser.
    Handles section-like headers and key=value pairs.
    """

    def __init__(self):
        self.data = {}
        self.sections = []

    def parse(self, content):
        self.data = {}
        self.sections = []
        current_section = ""
        lines = content.splitlines()
        section_start_pattern = re.compile(r"^\s*\[")

        for i, line in enumerate(lines):
            stripped_line = line.strip()
            if stripped_line.startswith("#") or not stripped_line:
                continue

            if section_start_pattern.match(stripped_line) and "=" not in stripped_line:
                extracted_section_name = stripped_line
                self.sections.append((extracted_section_name, i + 1, line))

                match_inner_section = re.match(r"^\[([^\]]+)\].*", stripped_line)
                parsed_section_key = match_inner_section.group(1).strip() if match_inner_section else ""

                if parsed_section_key not in self.data:
                    self.data[parsed_section_key] = {}
                current_section = parsed_section_key

            elif "=" in stripped_line:
                key, value = stripped_line.split("=", 1)
                target_section_data = self.data.setdefault(current_section, {})
                target_section_data[key.strip()] = value.strip()

        return self.data, self.sections


# --- ToolTip Class ---
class ToolTip:
    """Provides a tooltip for a given widget."""

    def __init__(self, widget, text):
        self.widget = widget
        self.text = text
        self.tip_window = None
        self.id = None
        self.widget.bind("<Enter>", self.enter)
        self.widget.bind("<Leave>", self.leave)
        self.widget.bind("<ButtonPress>", self.leave)

    def enter(self, _event=None):
        self.schedule()

    def leave(self, _event=None):
        self.unschedule()
        self.hide()

    def schedule(self):
        self.unschedule()
        self.id = self.widget.after(500, self.show)

    def unschedule(self):
        if self.id:
            self.widget.after_cancel(self.id)
        self.id = None

    def show(self):
        if self.tip_window or not self.text:
            return
        x = self.widget.winfo_rootx() + 20
        y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
        self.tip_window = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry(f"+{x}+{y}")
        label = tk.Label(
            tw,
            text=self.text,
            background="#ffffe0",
            relief=tk.SOLID,
            borderwidth=1,
            font=("tahoma", "8", "normal"),
        )
        label.pack(ipadx=1)

    def hide(self):
        if self.tip_window:
            self.tip_window.destroy()
        self.tip_window = None


# --- Main Editor Class ---
class TomlEditor:
    def __init__(self, master, initial_file=None, initial_line=None, initial_col=None):
        self.master = master
        self.loading = True
        self.current_file = None
        self.file_encoding = "auto"
        self.last_search_term = ""
        self.last_search_is_regex = False
        self.last_search_case_sensitive = False
        self.text_ini_data = {}
        self.highlight_rules = []
        self.highlight_fonts = {}
        self.parser = SimpleTomlParser()
        self.found_sections_list = []

        self.script_dir = os.path.dirname(os.path.abspath(__file__))
        self.config_filepath = os.path.join(self.script_dir, CONFIG_FILE)
        self.text_ini_filepath = os.path.join(self.script_dir, TEXT_INI_FILE)
        self.color_ini_filepath = os.path.join(self.script_dir, COLOR_INI_FILE)
        self.settings = read_ini(self.config_filepath)

        self.master.title("Inifile Editor")
        self._apply_saved_settings()

        font_family = self.settings.get("font_family", DEFAULT_FONT_FAMILY)
        try:
            font_size = int(self.settings.get("font_size", DEFAULT_FONT_SIZE))
        except ValueError:
            font_size = DEFAULT_FONT_SIZE
        self.editor_font = tkFont.Font(family=font_family, size=font_size)

        self.toolbar = tk.Frame(master, bd=1, relief=tk.RAISED)
        self.text_area = scrolledtext.ScrolledText(master, wrap=tk.WORD, undo=True, font=self.editor_font)
        self.status_bar = tk.Label(
            master,
            text="行: 1, 桁: 1 | 文字コード: 自動判別",
            bd=1,
            relief=tk.SUNKEN,
            anchor=tk.W,
        )
        self.context_menu = tk.Menu(self.text_area, tearoff=0)
        self.selected_encoding = tk.StringVar(value="auto")
        self.word_wrap_var = tk.BooleanVar(value=True)

        self.text_area.bind("<<Modified>>", self.on_text_modified)
        self.text_area.bind("<KeyRelease>", self.update_status_bar)
        self.text_area.bind("<ButtonRelease-1>", self.update_status_bar)
        self.text_area.bind("<Button-3>", self._show_context_menu)
        self.text_area.drop_target_register(DND_FILES)
        self.text_area.dnd_bind("<<Drop>>", self.handle_drop)

        self._add_toolbar_buttons()
        self._create_menus()

        self.toolbar.pack(side=tk.TOP, fill=tk.X)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        self.text_area.pack(expand=True, fill="both")

        self._bind_shortcuts()
        self._load_text_ini_data()
        self._setup_highlighting()
        self.master.protocol("WM_DELETE_WINDOW", self._on_closing)

        if initial_file:
            self.load_file(initial_file, requested_encoding="auto")
            if initial_line:
                self.master.after(100, lambda: self._jump_to_line_col(initial_line, initial_col or 0))
        else:
            self._set_new_document_state()

        self.master.after(0, self._finish_initialization)

    # --- State helpers ---
    def _finish_initialization(self):
        self.loading = False
        self._mark_clean()
        self._update_window_title()
        self.update_status_bar()

    def _mark_clean(self):
        self.text_area.edit_separator()
        self.text_area.edit_reset()
        self.text_area.edit_modified(False)

    def _set_new_document_state(self):
        self.loading = True
        self.current_file = None
        self.file_encoding = "auto"
        self.selected_encoding.set("auto")
        self.text_area.delete("1.0", tk.END)
        self._clear_transient_tags()
        self._mark_clean()
        self.loading = False
        self._update_window_title()
        self.update_status_bar()

    def _clear_transient_tags(self):
        for tag_name in ("found", "found_section"):
            self.text_area.tag_remove(tag_name, "1.0", tk.END)
        self.text_area.tag_remove(tk.SEL, "1.0", tk.END)

    def _update_window_title(self):
        filename = os.path.basename(self.current_file) if self.current_file else "無題"
        dirty = self.text_area.edit_modified()
        suffix = "*" if dirty else ""
        self.master.title(f"Inifile Editor - {filename}{suffix}")

    def _set_dialog_geometry_from_settings(self, dialog, width_key, height_key, fallback_geometry):
        try:
            width = int(self.settings.get(width_key))
            height = int(self.settings.get(height_key))
            dialog.geometry(f"{width}x{height}")
        except Exception:
            dialog.geometry(fallback_geometry)

    def _remember_dialog_size(self, dialog, width_key, height_key):
        self.settings[width_key] = dialog.winfo_width()
        self.settings[height_key] = dialog.winfo_height()
        write_ini(self.config_filepath, self.settings)

    # --- File lifecycle ---
    def _prompt_save_changes_if_needed(self):
        if not self.text_area.edit_modified():
            return True

        response = messagebox.askyesnocancel("未保存の変更", "変更が保存されていません。保存しますか？")
        if response is None:
            return False
        if response is True:
            return self.save_file()
        return True

    def _load_file_text(self, file_path, requested_encoding="auto"):
        with open(file_path, "rb") as f:
            raw_data = f.read()

        if requested_encoding == "auto":
            encoding = detect_text_encoding(raw_data)
        else:
            encoding = requested_encoding

        content = raw_data.decode(encoding, errors="ignore")
        return content, encoding

    def load_file(self, file_path, requested_encoding="auto"):
        if not file_path:
            return False

        try:
            content, encoding = self._load_file_text(file_path, requested_encoding=requested_encoding)
        except Exception as exc:
            messagebox.showerror("エラー", f"ファイルの読み込み中にエラーが発生しました:\n{exc}")
            return False

        self.loading = True
        self.current_file = file_path
        self.file_encoding = encoding
        self.selected_encoding.set("auto" if requested_encoding == "auto" else encoding)
        self.text_area.delete("1.0", tk.END)
        self.text_area.insert("1.0", content)
        self._clear_transient_tags()
        self._mark_clean()
        self.loading = False
        self._update_window_title()
        self.update_status_bar()
        return True

    def _save_content(self, file_path):
        try:
            encoding_to_save = self.file_encoding if self.file_encoding != "auto" else "utf-8"
            content = self.text_area.get("1.0", tk.END).rstrip("\n")
            with open(file_path, "w", encoding=encoding_to_save, newline="") as f:
                f.write(content)
        except Exception as exc:
            messagebox.showerror("エラー", f"ファイルの保存中にエラーが発生しました:\n{exc}")
            return False

        self.current_file = file_path
        self._mark_clean()
        self._update_window_title()
        self.update_status_bar()
        return True

    def new_file(self):
        if not self._prompt_save_changes_if_needed():
            return False
        self._set_new_document_state()
        return True

    def open_file(self):
        if not self._prompt_save_changes_if_needed():
            return False
        file_types = [
            ("INI files", "*.ini"),
            ("TOML files", "*.toml"),
            ("Text files", "*.txt"),
            ("All files", "*.*"),
        ]
        file_path = filedialog.askopenfilename(filetypes=file_types)
        if not file_path:
            return False
        return self.load_file(file_path, requested_encoding="auto")

    def save_file(self):
        if self.current_file:
            return self._save_content(self.current_file)
        return self.save_file_as()

    def save_file_as(self):
        file_types = [
            ("INI files", "*.ini"),
            ("TOML files", "*.toml"),
            ("Text files", "*.txt"),
            ("All files", "*.*"),
        ]
        file_path = filedialog.asksaveasfilename(defaultextension=".toml", filetypes=file_types)
        if not file_path:
            return False
        return self._save_content(file_path)

    def _on_closing(self):
        if not self._prompt_save_changes_if_needed():
            return

        self.settings["main_width"] = self.master.winfo_width()
        self.settings["main_height"] = self.master.winfo_height()
        self.settings["main_pos_x"] = self.master.winfo_x()
        self.settings["main_pos_y"] = self.master.winfo_y()
        write_ini(self.config_filepath, self.settings)
        self.master.destroy()

    # --- UI construction ---
    def _apply_saved_settings(self):
        try:
            width = int(self.settings.get("main_width", 800))
            height = int(self.settings.get("main_height", 600))
            pos_x = int(self.settings.get("main_pos_x", 100))
            pos_y = int(self.settings.get("main_pos_y", 100))
            self.master.geometry(f"{width}x{height}+{pos_x}+{pos_y}")
        except ValueError:
            self.master.geometry(DEFAULT_GEOMETRY)

    def _add_toolbar_buttons(self):
        self.icons = {}
        icon_size = (18, 18)
        resampling = getattr(getattr(Image, "Resampling", Image), "LANCZOS")
        icon_paths = {
            "new": "icons/new.png",
            "open": "icons/open.png",
            "save": "icons/save.png",
            "find": "icons/find.png",
            "replace": "icons/replace.png",
            "undo": "icons/undo.png",
            "redo": "icons/redo.png",
            "cut": "icons/cut.png",
            "copy": "icons/copy.png",
            "paste": "icons/paste.png",
            "sections": "icons/sections.png",
        }
        for name, path in icon_paths.items():
            full_path = os.path.join(self.script_dir, path)
            try:
                img = Image.open(full_path).resize(icon_size, resampling)
                self.icons[name] = ImageTk.PhotoImage(img)
            except Exception as exc:
                print(f"Error loading icon {full_path}: {exc}")
                self.icons[name] = None

        buttons_info = [
            ("new", self.new_file, "新規ファイル (Ctrl+N)"),
            ("open", self.open_file, "ファイルを開く (Ctrl+O)"),
            ("save", self.save_file, "ファイルを保存 (Ctrl+S)"),
            ("separator",),
            ("find", self.find_text, "テキストを検索 (Ctrl+F)"),
            ("replace", self.replace_text, "テキストを置換 (Ctrl+H)"),
            ("separator",),
            ("undo", self.text_area.edit_undo, "元に戻す (Ctrl+Z)"),
            ("redo", self.text_area.edit_redo, "やり直し (Ctrl+Y)"),
            ("separator",),
            ("cut", lambda: self.text_area.event_generate("<<Cut>>"), "切り取り (Ctrl+X)"),
            ("copy", lambda: self.text_area.event_generate("<<Copy>>"), "コピー (Ctrl+C)"),
            ("paste", lambda: self.text_area.event_generate("<<Paste>>"), "貼り付け (Ctrl+V)"),
            ("separator",),
            ("sections", self.show_section_dialog, "セクションダイアログを表示"),
        ]
        for info in buttons_info:
            if info[0] == "separator":
                tk.Frame(self.toolbar, width=1, bg="gray").pack(side=tk.LEFT, fill=tk.Y, padx=5)
                continue
            name, command, tooltip = info
            btn = tk.Button(self.toolbar, command=command)
            if self.icons.get(name):
                btn.config(image=self.icons[name])
            else:
                btn.config(text=name.capitalize())
            btn.pack(side=tk.LEFT, padx=2, pady=2)
            ToolTip(btn, tooltip)

    def _create_menus(self):
        self.menubar = tk.Menu(self.master)
        self.master.config(menu=self.menubar)

        file_menu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規", command=self.new_file, accelerator="Ctrl+N")
        file_menu.add_command(label="開く", command=self.open_file, accelerator="Ctrl+O")
        file_menu.add_command(label="保存", command=self.save_file, accelerator="Ctrl+S")
        file_menu.add_command(label="名前を付けて保存...", command=self.save_file_as, accelerator="Ctrl+Shift+S")
        file_menu.add_separator()
        file_menu.add_command(label="ファイル場所を開く", command=self._open_file_location)
        file_menu.add_command(label="ターミナルを開く", command=self._open_terminal)
        file_menu.add_separator()
        self.encoding_menu = tk.Menu(file_menu, tearoff=0)
        file_menu.add_cascade(label="文字コード", menu=self.encoding_menu)
        self._add_encoding_options()
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self._on_closing, accelerator="Ctrl+Q")

        edit_menu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="検索...", command=self.find_text, accelerator="Ctrl+F")
        edit_menu.add_command(label="次を検索", command=self.find_next_menu, accelerator="F3")
        edit_menu.add_command(label="前を検索", command=self.find_previous_menu, accelerator="Shift+F3")
        edit_menu.add_command(label="置換...", command=self.replace_text, accelerator="Ctrl+H")
        edit_menu.add_separator()
        edit_menu.add_command(label="元に戻す", command=self.text_area.edit_undo, accelerator="Ctrl+Z")
        edit_menu.add_command(label="やり直し", command=self.text_area.edit_redo, accelerator="Ctrl+Y")
        edit_menu.add_separator()
        edit_menu.add_command(label="文書先頭へジャンプ", command=self._jump_to_start)
        edit_menu.add_command(label="文書末へジャンプ", command=self._jump_to_end)
        edit_menu.add_command(label="行番号を指定してジャンプ", command=self._jump_to_line)
        edit_menu.add_separator()
        edit_menu.add_command(label="切り取り", command=lambda: self.text_area.event_generate("<<Cut>>"), accelerator="Ctrl+X")
        edit_menu.add_command(label="コピー", command=lambda: self.text_area.event_generate("<<Copy>>"), accelerator="Ctrl+C")
        edit_menu.add_command(label="貼り付け", command=lambda: self.text_area.event_generate("<<Paste>>"), accelerator="Ctrl+V")

        view_menu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="表示", menu=view_menu)
        view_menu.add_checkbutton(
            label="折り返し",
            onvalue=True,
            offvalue=False,
            variable=self.word_wrap_var,
            command=self.toggle_word_wrap,
        )

        tools_menu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label="ツール", menu=tools_menu)
        tools_menu.add_command(label="セクションダイアログ", command=self.show_section_dialog)
        tools_menu.add_command(label="ハイライト適用", command=self.manual_apply_highlighting)
        self._add_external_commands_to_menu(tools_menu)

    def _bind_shortcuts(self):
        self.master.bind("<Control-n>", lambda _event: self.new_file())
        self.master.bind("<Control-o>", lambda _event: self.open_file())
        self.master.bind("<Control-s>", lambda _event: self.save_file())
        self.master.bind("<Control-Shift-S>", lambda _event: self.save_file_as())
        self.master.bind("<Control-q>", lambda _event: self._on_closing())
        self.master.bind("<Control-f>", lambda _event: self.find_text())
        self.master.bind("<F3>", lambda _event: self.find_next_menu())
        self.master.bind("<Shift-F3>", lambda _event: self.find_previous_menu())
        self.master.bind("<Control-h>", lambda _event: self.replace_text())
        self.master.bind("<Control-z>", lambda _event: self.text_area.edit_undo())
        self.master.bind("<Control-y>", lambda _event: self.text_area.edit_redo())

    # --- Highlighting Methods ---
    def manual_apply_highlighting(self):
        print("--- Applying Syntax Highlighting ---")
        self._setup_highlighting()
        self._apply_highlighting()
        print("--- Highlighting complete. ---")

    def _setup_highlighting(self):
        self.highlight_rules = []
        self.highlight_fonts = {}
        highlight_data = read_ini(self.color_ini_filepath)

        print("Reading color.ini for highlighting rules...")
        for i, (pattern_str, format_str) in enumerate(highlight_data.items()):
            try:
                style, color = format_str.split(":", 1)
                style = style.lower().strip()
                color = color.strip()
                tag_name = f"highlight_{i}"

                family = self.editor_font.cget("family")
                size = self.editor_font.cget("size")
                weight = "bold" if style == "bold" else "normal"
                slant = "italic" if style == "italic" else "roman"
                new_font = tkFont.Font(family=family, size=size, weight=weight, slant=slant)

                self.highlight_fonts[tag_name] = new_font
                self.text_area.tag_configure(tag_name, foreground=color, font=new_font)
                self.highlight_rules.append((re.compile(pattern_str), tag_name))

                print(
                    f"  Rule {i}: Pattern='{pattern_str}', Style='{style}', Color='{color}', Tag='{tag_name}'"
                )
            except Exception as exc:
                print(f"  Invalid highlight rule for '{pattern_str}': {exc}")

    def _apply_highlighting(self):
        for _, tag_name in self.highlight_rules:
            self.text_area.tag_remove(tag_name, "1.0", tk.END)

        content = self.text_area.get("1.0", tk.END)
        for pattern, tag_name in self.highlight_rules:
            for match in pattern.finditer(content):
                start, end = match.span()
                start_index = self.text_area.index(f"1.0 + {start} chars")
                end_index = self.text_area.index(f"1.0 + {end} chars")
                self.text_area.tag_add(tag_name, start_index, end_index)

    # --- Other methods ---
    def on_text_modified(self, _event=None):
        if self.loading:
            return
        self._update_window_title()
        self.update_status_bar()

    def update_status_bar(self, _event=None):
        line, char = self.text_area.index(tk.INSERT).split(".")
        encoding_display = self.file_encoding.upper() if self.file_encoding != "auto" else "自動判別"
        self.status_bar.config(text=f"行: {line}, 桁: {int(char) + 1} | 文字コード: {encoding_display}")

    def _load_text_ini_data(self):
        self.text_ini_data = parse_multiline_ini(self.text_ini_filepath)

    def _show_context_menu(self, event):
        self.context_menu.delete(0, tk.END)
        self.context_menu.add_command(label="切り取り", command=lambda: self.text_area.event_generate("<<Cut>>"))
        self.context_menu.add_command(label="コピー", command=lambda: self.text_area.event_generate("<<Copy>>"))
        self.context_menu.add_command(label="貼り付け", command=lambda: self.text_area.event_generate("<<Paste>>"))
        self.context_menu.add_separator()

        insert_ini_menu = tk.Menu(self.context_menu, tearoff=0)
        self.context_menu.add_cascade(label=f"{TEXT_INI_FILE}から挿入", menu=insert_ini_menu)

        if self.text_ini_data:
            nested_data = {}
            for key, value in self.text_ini_data.items():
                parts = key.split("\\")
                d = nested_data
                for part in parts[:-1]:
                    d = d.setdefault(part, {})
                d[parts[-1]] = value
            self._build_dynamic_menu(insert_ini_menu, nested_data)
        else:
            insert_ini_menu.add_command(label=f"({TEXT_INI_FILE}が空か見つかりません)", state=tk.DISABLED)

        self.context_menu.add_separator()
        self.context_menu.add_command(label=f"{TEXT_INI_FILE}再読み込み", command=self._load_text_ini_data)

        try:
            self.context_menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.context_menu.grab_release()

    def _build_dynamic_menu(self, parent_menu, data_dict):
        for key, value in sorted(data_dict.items()):
            if isinstance(value, dict):
                submenu = tk.Menu(parent_menu, tearoff=0)
                parent_menu.add_cascade(label=key, menu=submenu)
                self._build_dynamic_menu(submenu, value)
            else:
                parent_menu.add_command(label=key, command=lambda v=value: self._insert_value_from_ini(v))

    def _insert_value_from_ini(self, value):
        self.text_area.insert(tk.INSERT, str(value) + "\n")
        self.text_area.edit_modified(True)
        self._update_window_title()

    def _jump_to_start(self):
        self.text_area.mark_set(tk.INSERT, "1.0")
        self.text_area.see(tk.INSERT)
        self.update_status_bar()

    def _jump_to_end(self):
        self.text_area.mark_set(tk.INSERT, "end-1c")
        self.text_area.see(tk.INSERT)
        self.update_status_bar()

    def _jump_to_line(self):
        line_str = simpledialog.askstring("行へジャンプ", "ジャンプする行番号を入力してください:", parent=self.master)
        if not line_str:
            return
        try:
            self._jump_to_line_col(int(line_str), 0)
        except ValueError:
            messagebox.showerror("入力エラー", "有効な行番号を入力してください。")

    def _jump_to_line_col(self, line, col):
        if line is None:
            return
        if line < 1:
            line = 1
        last_line = int(self.text_area.index("end-1c").split(".")[0])
        if line > last_line:
            line = last_line
        if col < 0:
            col = 0
        self.text_area.mark_set(tk.INSERT, f"{line}.{col}")
        self.text_area.see(tk.INSERT)
        self.update_status_bar()

    def _add_encoding_options(self):
        self.encoding_menu.add_radiobutton(
            label="自動判別",
            value="auto",
            variable=self.selected_encoding,
            command=lambda: self._on_encoding_selected(is_auto=True),
        )
        self.encoding_menu.add_separator()
        for enc in ENCODINGS:
            self.encoding_menu.add_radiobutton(
                label=enc.upper(),
                value=enc,
                variable=self.selected_encoding,
                command=self._on_encoding_selected,
            )

    def _on_encoding_selected(self, is_auto=False):
        requested = "auto" if is_auto else self.selected_encoding.get()
        if not self.current_file:
            self.file_encoding = requested
            self.update_status_bar()
            return

        if self.text_area.edit_modified():
            messagebox.showinfo("文字コード", "未保存の変更があるため、再読み込みしません。保存または破棄してから再試行してください。")
            if self.file_encoding in ENCODINGS:
                self.selected_encoding.set(self.file_encoding)
            else:
                self.selected_encoding.set("auto")
            return

        self.load_file(self.current_file, requested_encoding=requested)

    def _parse_drop_files(self, data):
        files = []
        current = []
        brace_level = 0
        for ch in data:
            if ch == "{":
                brace_level += 1
                if brace_level == 1:
                    continue
            elif ch == "}":
                brace_level = max(0, brace_level - 1)
                if brace_level == 0:
                    files.append("".join(current))
                    current = []
                    continue
            if ch == " " and brace_level == 0:
                if current:
                    files.append("".join(current))
                    current = []
                continue
            current.append(ch)
        if current:
            files.append("".join(current))
        return [f for f in files if f]

    def handle_drop(self, event):
        paths = self._parse_drop_files(event.data)
        if not paths:
            messagebox.showwarning("D&Dエラー", "ドロップされたファイルが取得できませんでした。")
            return False

        file_path = paths[0]
        if not os.path.isfile(file_path):
            messagebox.showwarning("D&Dエラー", "ドロップされたファイルが無効です。")
            return False

        if not self._prompt_save_changes_if_needed():
            return False
        return self.load_file(file_path, requested_encoding="auto")

    def find_next_menu(self):
        if self.last_search_term:
            self._perform_find(
                self.last_search_term,
                self.last_search_is_regex,
                self.last_search_case_sensitive,
                backwards=False,
            )
        else:
            self.find_text()

    def find_previous_menu(self):
        if self.last_search_term:
            self._perform_find(
                self.last_search_term,
                self.last_search_is_regex,
                self.last_search_case_sensitive,
                backwards=True,
            )
        else:
            self.find_text()

    def find_text(self):
        if hasattr(self, "find_dialog") and self.find_dialog.winfo_exists():
            self.find_dialog.lift()
            self.find_dialog.focus_set()
            return

        self.find_dialog = tk.Toplevel(self.master)
        self.find_dialog.title("検索")
        self._set_dialog_geometry_from_settings(
            self.find_dialog,
            "find_dialog_width",
            "find_dialog_height",
            DEFAULT_FIND_GEOMETRY,
        )
        self.find_dialog.protocol("WM_DELETE_WINDOW", self._on_find_dialog_closing)
        self.find_dialog.grid_columnconfigure(1, weight=1)

        tk.Label(self.find_dialog, text="検索:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.find_entry = tk.Entry(self.find_dialog)
        self.find_entry.grid(row=0, column=1, columnspan=2, padx=5, pady=5, sticky="we")
        self.find_entry.focus_set()
        if self.last_search_term:
            self.find_entry.insert(0, self.last_search_term)

        check_frame = tk.Frame(self.find_dialog)
        check_frame.grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=5)
        self.find_regex_var = tk.BooleanVar(value=self.last_search_is_regex)
        tk.Checkbutton(check_frame, text="正規表現", variable=self.find_regex_var).pack(side=tk.LEFT)
        self.find_case_var = tk.BooleanVar(value=self.last_search_case_sensitive)
        tk.Checkbutton(check_frame, text="大文字小文字を区別する", variable=self.find_case_var).pack(side=tk.LEFT)

        button_frame = tk.Frame(self.find_dialog)
        button_frame.grid(row=2, column=0, columnspan=3, pady=5)
        tk.Button(button_frame, text="前を検索", command=self._find_previous).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="次を検索", command=self._find_next).pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="閉じる", command=self._on_find_dialog_closing).pack(side=tk.LEFT, padx=5)
        self.find_dialog.bind("<Return>", lambda _e: self._find_next())
        self.find_dialog.bind("<Shift-Return>", lambda _e: self._find_previous())
        self.find_dialog.bind("<Escape>", lambda _e: self._on_find_dialog_closing())

    def _on_find_dialog_closing(self):
        self._remember_dialog_size(self.find_dialog, "find_dialog_width", "find_dialog_height")
        self.find_dialog.destroy()

    def _find_next(self):
        self.last_search_term = self.find_entry.get()
        self.last_search_is_regex = self.find_regex_var.get()
        self.last_search_case_sensitive = self.find_case_var.get()
        self._perform_find(self.last_search_term, self.last_search_is_regex, self.last_search_case_sensitive, backwards=False)

    def _find_previous(self):
        self.last_search_term = self.find_entry.get()
        self.last_search_is_regex = self.find_regex_var.get()
        self.last_search_case_sensitive = self.find_case_var.get()
        self._perform_find(self.last_search_term, self.last_search_is_regex, self.last_search_case_sensitive, backwards=True)

    def _set_found_selection(self, pos, end_pos, backwards=False):
        self.text_area.tag_remove("found", "1.0", tk.END)
        self.text_area.tag_add("found", pos, end_pos)
        self.text_area.tag_config("found", background="yellow")
        self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
        self.text_area.tag_add(tk.SEL, pos, end_pos)
        self.text_area.mark_set(tk.INSERT, pos if backwards else end_pos)
        self.text_area.see(pos)

    def _perform_find(self, search_text, is_regex, case_sensitive, backwards=False):
        if not search_text:
            return

        start_index = self.text_area.index(tk.INSERT)
        self.text_area.tag_remove("found", "1.0", tk.END)
        self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
        try:
            nocase = not case_sensitive
            pos = self.text_area.search(
                search_text,
                start_index,
                stopindex="1.0" if backwards else tk.END,
                backwards=backwards,
                regexp=is_regex,
                nocase=nocase,
            )
            if not pos:
                pos = self.text_area.search(
                    search_text,
                    tk.END if backwards else "1.0",
                    stopindex="1.0" if backwards else tk.END,
                    backwards=backwards,
                    regexp=is_regex,
                    nocase=nocase,
                )
                if not pos:
                    messagebox.showinfo("検索", "テキストが見つかりませんでした。")
                    return

            if is_regex:
                flags = 0 if case_sensitive else re.IGNORECASE
                content_after = self.text_area.get(pos, f"{pos} lineend")
                match = re.match(search_text, content_after, flags)
                match_len = len(match.group(0)) if match else 0
            else:
                match_len = len(search_text)

            if match_len <= 0:
                messagebox.showinfo("検索", "一致長が 0 のため検索できませんでした。")
                return

            end_pos = f"{pos}+{match_len}c"
            self._set_found_selection(pos, end_pos, backwards=backwards)
        except re.error as exc:
            messagebox.showerror("正規表現エラー", f"無効な正規表現です:\n{exc}")

    def replace_text(self):
        if hasattr(self, "replace_dialog") and self.replace_dialog.winfo_exists():
            self.replace_dialog.lift()
            self.replace_dialog.focus_set()
            return

        self.replace_dialog = tk.Toplevel(self.master)
        self.replace_dialog.title("置換")
        self._set_dialog_geometry_from_settings(
            self.replace_dialog,
            "replace_dialog_width",
            "replace_dialog_height",
            DEFAULT_REPLACE_GEOMETRY,
        )
        self.replace_dialog.protocol("WM_DELETE_WINDOW", self._on_replace_dialog_closing)
        self.replace_dialog.grid_columnconfigure(1, weight=1)

        tk.Label(self.replace_dialog, text="検索:").grid(row=0, column=0, padx=5, pady=2, sticky=tk.W)
        self.replace_find_entry = tk.Entry(self.replace_dialog)
        self.replace_find_entry.grid(row=0, column=1, padx=5, pady=2, sticky="we")
        self.replace_find_entry.focus_set()
        if self.last_search_term:
            self.replace_find_entry.insert(0, self.last_search_term)

        tk.Label(self.replace_dialog, text="置換:").grid(row=1, column=0, padx=5, pady=2, sticky=tk.W)
        self.replace_entry = tk.Entry(self.replace_dialog)
        self.replace_entry.grid(row=1, column=1, padx=5, pady=2, sticky="we")

        check_frame = tk.Frame(self.replace_dialog)
        check_frame.grid(row=2, column=1, sticky=tk.W, padx=5)
        self.replace_regex_var = tk.BooleanVar(value=self.last_search_is_regex)
        tk.Checkbutton(check_frame, text="正規表現", variable=self.replace_regex_var).pack(side=tk.LEFT)
        self.replace_case_var = tk.BooleanVar(value=self.last_search_case_sensitive)
        tk.Checkbutton(check_frame, text="大文字小文字を区別する", variable=self.replace_case_var).pack(side=tk.LEFT)

        button_frame = tk.Frame(self.replace_dialog)
        button_frame.grid(row=0, column=2, rowspan=4, padx=5)
        tk.Button(button_frame, text="次を検索", command=self._find_next_for_replace).pack(fill=tk.X, pady=2)
        tk.Button(button_frame, text="置換", command=self._do_replace).pack(fill=tk.X, pady=2)
        tk.Button(button_frame, text="すべて置換", command=self._do_replace_all).pack(fill=tk.X, pady=2)
        tk.Button(button_frame, text="閉じる", command=self._on_replace_dialog_closing).pack(fill=tk.X, pady=2)
        self.replace_dialog.bind("<Escape>", lambda _e: self._on_replace_dialog_closing())

    def _on_replace_dialog_closing(self):
        self._remember_dialog_size(self.replace_dialog, "replace_dialog_width", "replace_dialog_height")
        self.replace_dialog.destroy()

    def _find_next_for_replace(self):
        search_text = self.replace_find_entry.get()
        is_regex = self.replace_regex_var.get()
        case_sensitive = self.replace_case_var.get()
        self._perform_find(search_text, is_regex, case_sensitive, backwards=False)

    def _get_current_match_range(self):
        ranges = self.text_area.tag_ranges("found")
        if len(ranges) >= 2:
            return ranges[0], ranges[1]
        return None, None

    def _do_replace(self):
        search_text = self.replace_find_entry.get()
        replace_with = self.replace_entry.get()
        is_regex = self.replace_regex_var.get()
        case_sensitive = self.replace_case_var.get()

        start, end = self._get_current_match_range()
        if not start or not end:
            self._find_next_for_replace()
            start, end = self._get_current_match_range()
            if not start or not end:
                return

        try:
            selected_text = self.text_area.get(start, end)
            if is_regex:
                flags = 0 if case_sensitive else re.IGNORECASE
                replaced_text, count = re.subn(search_text, replace_with, selected_text, count=1, flags=flags)
                if count == 0:
                    self._find_next_for_replace()
                    return
            else:
                if case_sensitive:
                    if selected_text != search_text:
                        self._find_next_for_replace()
                        return
                else:
                    if selected_text.lower() != search_text.lower():
                        self._find_next_for_replace()
                        return
                replaced_text = replace_with

            self.text_area.delete(start, end)
            self.text_area.insert(start, replaced_text)
            self.text_area.edit_modified(True)
            self.text_area.mark_set(tk.INSERT, f"{start}+{len(replaced_text)}c")
            self._find_next_for_replace()
        except re.error as exc:
            messagebox.showerror("正規表現エラー", f"無効な正規表現です:\n{exc}", parent=self.replace_dialog)

    def _do_replace_all(self):
        search_text = self.replace_find_entry.get()
        replace_with = self.replace_entry.get()
        if not search_text:
            return

        is_regex = self.replace_regex_var.get()
        case_sensitive = self.replace_case_var.get()
        content = self.text_area.get("1.0", tk.END)
        try:
            flags = 0 if case_sensitive else re.IGNORECASE
            if is_regex:
                new_content, count = re.subn(search_text, replace_with, content, flags=flags)
            else:
                new_content, count = re.subn(re.escape(search_text), replace_with, content, flags=flags)

            if count > 0:
                self.text_area.delete("1.0", tk.END)
                self.text_area.insert("1.0", new_content)
                self.text_area.edit_modified(True)
                self._clear_transient_tags()
            messagebox.showinfo("置換", f"{count} 件を置換しました。", parent=self.replace_dialog)
        except re.error as exc:
            messagebox.showerror("正規表現エラー", f"無効な正規表現です:\n{exc}", parent=self.replace_dialog)

    def toggle_word_wrap(self):
        self.text_area.config(wrap=tk.WORD if self.word_wrap_var.get() else tk.NONE)

    def show_section_dialog(self):
        if hasattr(self, "section_dialog") and self.section_dialog.winfo_exists():
            self.section_dialog.lift()
            return

        self.section_dialog = tk.Toplevel(self.master)
        self.section_dialog.title("セクション")
        self._set_dialog_geometry_from_settings(
            self.section_dialog,
            "section_dialog_width",
            "section_dialog_height",
            DEFAULT_SECTION_GEOMETRY,
        )

        top_frame = tk.Frame(self.section_dialog)
        top_frame.pack(fill=tk.X, padx=5, pady=5)

        self.section_regex_var = tk.StringVar(value=r"^\s*\[.*\]")
        regex_entry = tk.Entry(top_frame, textvariable=self.section_regex_var)
        regex_entry.pack(side=tk.LEFT, expand=True, fill=tk.X)

        search_button = tk.Button(top_frame, text="検索", command=self._search_and_populate_sections)
        search_button.pack(side=tk.LEFT, padx=(5, 0))

        self.section_case_var = tk.BooleanVar(value=False)
        case_check = tk.Checkbutton(
            self.section_dialog,
            text="大文字小文字を区別する",
            variable=self.section_case_var,
            command=self._search_and_populate_sections,
        )
        case_check.pack(anchor=tk.W, padx=5)

        self.section_listbox = tk.Listbox(self.section_dialog)
        self.section_listbox.pack(expand=True, fill="both", padx=5, pady=(0, 5))
        self.section_listbox.bind("<<ListboxSelect>>", self._on_section_select)
        self.section_dialog.protocol("WM_DELETE_WINDOW", self._on_section_dialog_closing)
        self._search_and_populate_sections()

    def _on_section_dialog_closing(self):
        self._remember_dialog_size(self.section_dialog, "section_dialog_width", "section_dialog_height")
        self.section_dialog.destroy()

    def _search_and_populate_sections(self):
        self.section_listbox.delete(0, tk.END)
        self.found_sections_list = []
        regex_str = self.section_regex_var.get()
        if not regex_str:
            return

        try:
            flags = 0 if self.section_case_var.get() else re.IGNORECASE
            pattern = re.compile(regex_str, flags)
        except re.error as exc:
            messagebox.showerror("正規表現エラー", f"無効な正規表現です:\n{exc}", parent=self.section_dialog)
            return

        lines = self.text_area.get("1.0", tk.END).splitlines()
        nlines = len(lines)
        for i, line in enumerate(lines):
            if pattern.search(line):
                caption = None
                if nlines > i + 1:
                    next_line = lines[i + 1].split("=", 1)
                    if len(next_line) > 1 and next_line[0].strip().lower() == "caption":
                        caption = next_line[1].strip()

                self.found_sections_list.append((line.strip(), i + 1))
                if caption is not None:
                    self.section_listbox.insert(tk.END, f"{line.strip()} ({caption})")
                else:
                    self.section_listbox.insert(tk.END, line.strip())

    def _on_section_select(self, _event):
        selected_indices = self.section_listbox.curselection()
        if not selected_indices:
            return
        index = selected_indices[0]
        _, line_number = self.found_sections_list[index]
        start = f"{line_number}.0"
        end = f"{line_number}.end"
        self.text_area.mark_set(tk.INSERT, start)
        self.text_area.see(start)
        self.text_area.tag_remove("found_section", "1.0", tk.END)
        self.text_area.tag_add("found_section", start, end)
        self.text_area.tag_config("found_section", background="lightblue")
        self.update_status_bar()

    def _substitute_variables(self, command_template):
        if self.current_file:
            command_template = command_template.replace("{{file_path}}", f'"{self.current_file}"')

        def replacer(match):
            var_name = match.group(1)
            if var_name in self.settings:
                return str(self.settings[var_name])
            return os.environ.get(var_name, "")

        return re.sub(r"\{\{(.*?)\}\}", replacer, command_template)

    def _add_external_commands_to_menu(self, menu):
        added_any = False
        for i in range(1, 10):
            cmd_name = self.settings.get(f"external_cmd{i}_name")
            cmd_path = self.settings.get(f"external_cmd{i}_path")
            if cmd_name and cmd_path:
                if not added_any:
                    menu.add_separator()
                    added_any = True
                menu.add_command(label=cmd_name, command=lambda cmd=cmd_path: self._run_external_command(cmd))
        if added_any:
            menu.add_separator()

    def _run_external_command(self, command_template):
        command = self._substitute_variables(command_template)
        try:
            cwd = os.path.dirname(self.current_file) if self.current_file and os.path.exists(self.current_file) else self.script_dir
            subprocess.Popen(command, shell=True, cwd=cwd)
        except Exception as exc:
            messagebox.showerror("コマンド実行エラー", f"コマンドの実行に失敗しました:\n{command}\nエラー: {exc}")

    def _open_file_location(self):
        directory = os.path.dirname(self.current_file) if self.current_file and os.path.exists(self.current_file) else self.script_dir
        directory = os.path.normpath(os.path.abspath(directory))
        try:
            if os.name == "nt":
                subprocess.Popen(["explorer", directory])
            elif os.uname().sysname == "Darwin":
                subprocess.Popen(["open", directory])
            else:
                subprocess.Popen(["xdg-open", directory])
        except Exception as exc:
            messagebox.showerror("エラー", f"フォルダを開けませんでした:\n{exc}")

    def _open_terminal(self):
        directory = os.path.dirname(self.current_file) if self.current_file and os.path.exists(self.current_file) else self.script_dir
        try:
            if os.name == "nt":
                subprocess.Popen(["cmd.exe"], cwd=directory)
            else:
                terminal_cmd = os.environ.get("TERMINAL", "x-terminal-emulator")
                subprocess.Popen([terminal_cmd], cwd=directory)
        except FileNotFoundError:
            messagebox.showerror("エラー", "ターミナルが見つかりません。")
        except Exception as exc:
            messagebox.showerror("エラー", f"ターミナルを開けませんでした:\n{exc}")


# --- Main Application Entry Point ---
def main():
    parser = argparse.ArgumentParser(description="簡易Iniファイルエディタ")
    parser.add_argument("input_file", nargs="?", default=None, help="編集するINI/TOMLファイルのパス (オプション)")
    parser.add_argument("--line", type=int, default=None, help="開始行番号 (オプション)")
    parser.add_argument("--col", type=int, default=0, help="開始桁番号 (オプション, デフォルトは0)")
    args = parser.parse_args()

    root = TkinterDnD.Tk()
    TomlEditor(root, initial_file=args.input_file, initial_line=args.line, initial_col=args.col)
    root.mainloop()


if __name__ == "__main__":
    main()
