import os
import sys
import traceback
import re
import subprocess
import tkinter as tk
import tkinter.messagebox as messagebox


def show_startup_error(title, text):
    try:
        messagebox.showerror(title, text)
    except Exception:
        sys.stderr.write(f"{title}: {text}\n")


missing = []
err = ""
for lib in ["chardet", "configparser", "tkinter", "ttkbootstrap"]:
    try:
        __import__(lib)
    except ImportError:
        missing.append(lib)
        err += "\n" + traceback.format_exc()

if missing:
    show_startup_error(
        "ライブラリエラー",
        f"以下のライブラリが不足しています:\n{', '.join(missing)}\n\n詳細:\n{err}",
    )
    sys.exit(1)

import chardet
import configparser
from tkinter import filedialog, simpledialog
import ttkbootstrap
from ttkbootstrap import Window
from ttkbootstrap.widgets import Treeview


APP_NAME = "IniView"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SETTINGS_FILE = os.path.join(SCRIPT_DIR, "iniview.ini")
DEFAULT_GEOMETRY = "1000x700"
DEFAULT_SASH = 320


def detect_text_encoding(filepath):
    detected_encoding = "utf-8"
    try:
        with open(filepath, "rb") as f_raw:
            raw_data = f_raw.read()
        result = chardet.detect(raw_data)
        if result.get("encoding") and (result.get("confidence") or 0) > 0.8:
            detected_encoding = result["encoding"]
    except Exception:
        pass
    return detected_encoding



def parse_multiline_ini(filepath):
    # 独自の簡易INIパーサ
    # - key=value を読む
    # - # / ; から始まる行はコメント
    # - value が三連引用符で始まると複数行値として扱う
    # - .ini 以外は全文を all キーとして返す
    settings = {}
    if not filepath or not os.path.exists(filepath):
        return settings

    encoding = detect_text_encoding(filepath)
    _, ext = os.path.splitext(filepath)

    try:
        with open(filepath, "r", encoding=encoding, errors="ignore") as f:
            content = f.read()
    except Exception as e:
        print(f"読み込みエラー: {filepath}: {e}")
        return {}

    if ext.lower() != ".ini":
        return {"all": content}

    in_multiline = False
    current_key = None
    current_value = []

    for line in content.splitlines():
        stripped = line.strip()

        if in_multiline:
            if stripped.endswith('"""'):
                tail = line
                end_pos = tail.rfind('"""')
                current_value.append(tail[:end_pos])
                settings[current_key] = "\n".join(current_value)
                in_multiline = False
                current_key = None
                current_value = []
            else:
                current_value.append(line)
            continue

        if not stripped or stripped.startswith("#") or stripped.startswith(";"):
            continue

        if "=" not in line:
            continue

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

        if not key:
            continue

        if value_part.startswith('"""'):
            rest = value_part[3:]
            if rest.endswith('"""'):
                settings[key] = rest[:-3]
            else:
                in_multiline = True
                current_key = key
                current_value = [rest]
        else:
            settings[key] = value_part

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

    return dict(sorted(settings.items(), key=lambda kv: kv[0].lower()))


class IniViewer:
    def __init__(self, root, ini_path=None):
        self.root = root
        self.root.title(APP_NAME)
        self.script_dir = SCRIPT_DIR
        self.settings_file = SETTINGS_FILE

        self.settings = {}
        self.paths = {}
        self.item_map = {}
        self.item_to_key = {}
        self.last_query = None
        self.search_results = []
        self.search_index = -1
        self.last_opened_file = None

        self.wrap_var = tk.BooleanVar(value=True)

        self._build_ui()
        self.create_menus()
        self.create_context_menu()
        self.load_window_settings()
        self._add_external_commands_to_menu(self.tools_menu)

        if ini_path:
            self.load_ini(ini_path)
            self.last_opened_file = ini_path

        self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        self.tree.bind("<<TreeviewSelect>>", self.on_select)
        self.tree.bind("<Double-1>", lambda _e: self.copy_value())
        self.editor.bind("<Control-f>", lambda _e: self.search_item())
        self.tree.bind("<Control-f>", lambda _e: self.search_item())
        self.root.bind("<F3>", lambda _e: self.navigate_search(1))
        self.root.bind("<Shift-F3>", lambda _e: self.navigate_search(-1))

    def _build_ui(self):
        self.pane = tk.PanedWindow(self.root, orient="horizontal")
        self.pane.pack(fill="both", expand=True)

        tree_frame = tk.Frame(self.pane)
        self.tree = Treeview(tree_frame, show="tree")
        yscroll_tree = tk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
        xscroll_tree = tk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=yscroll_tree.set, xscrollcommand=xscroll_tree.set)
        self.tree.grid(row=0, column=0, sticky="nsew")
        yscroll_tree.grid(row=0, column=1, sticky="ns")
        xscroll_tree.grid(row=1, column=0, sticky="ew")
        tree_frame.rowconfigure(0, weight=1)
        tree_frame.columnconfigure(0, weight=1)
        self.pane.add(tree_frame)

        editor_frame = tk.Frame(self.pane)
        self.editor = tk.Text(editor_frame, wrap="word", undo=False, font=("Consolas", 10))
        yscroll_editor = tk.Scrollbar(editor_frame, orient="vertical", command=self.editor.yview)
        xscroll_editor = tk.Scrollbar(editor_frame, orient="horizontal", command=self.editor.xview)
        self.editor.configure(yscrollcommand=yscroll_editor.set, xscrollcommand=xscroll_editor.set)
        self.editor.grid(row=0, column=0, sticky="nsew")
        yscroll_editor.grid(row=0, column=1, sticky="ns")
        xscroll_editor.grid(row=1, column=0, sticky="ew")
        editor_frame.rowconfigure(0, weight=1)
        editor_frame.columnconfigure(0, weight=1)
        self.pane.add(editor_frame)

        self.editor_menu = tk.Menu(self.root, tearoff=0)
        self.editor_menu.add_command(label="コピー", command=self.copy_from_editor)
        self.editor.bind("<Button-3>", self.show_editor_menu)

        self.status = tk.Label(self.root, text="", anchor="w")
        self.status.pack(fill="x", side="bottom")

    def create_menus(self):
        menubar = tk.Menu(self.root)

        op_menu = tk.Menu(menubar, tearoff=0)
        op_menu.add_command(label="ファイル読み込み", command=self.select_file)
        op_menu.add_separator()
        op_menu.add_command(label="検索", command=self.search_item, accelerator="Ctrl+F")
        op_menu.add_command(label="次を検索", command=lambda: self.navigate_search(1), accelerator="F3")
        op_menu.add_command(label="前を検索", command=lambda: self.navigate_search(-1), accelerator="Shift+F3")
        op_menu.add_separator()
        op_menu.add_command(label="コピー", command=self.copy_value)
        menubar.add_cascade(label="操作", menu=op_menu)

        view_menu = tk.Menu(menubar, tearoff=0)
        view_menu.add_checkbutton(label="右端で折り返す", variable=self.wrap_var, command=self.toggle_wrap)
        menubar.add_cascade(label="表示", menu=view_menu)

        tools_menu = tk.Menu(menubar, tearoff=0)
        tools_menu.add_command(label="ファイル場所を開く", command=self._open_file_location)
        tools_menu.add_command(label="ターミナルを開く", command=self._open_terminal)
        menubar.add_cascade(label="ツール", menu=tools_menu)
        self.tools_menu = tools_menu

        self.root.config(menu=menubar)

    def create_context_menu(self):
        self.context_menu = tk.Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="検索", command=self.search_item)
        self.context_menu.add_command(label="コピー", command=self.copy_value)
        self.tree.bind("<Button-3>", self.show_context_menu)

    def toggle_wrap(self):
        self.editor.config(wrap="word" if self.wrap_var.get() else "none")

    def set_status(self, msg):
        self.status.config(text=msg)

    def show_context_menu(self, event):
        self.context_menu.tk_popup(event.x_root, event.y_root)

    def show_editor_menu(self, event):
        self.editor_menu.tk_popup(event.x_root, event.y_root)

    def _get_working_directory(self):
        if self.last_opened_file and os.path.exists(self.last_opened_file):
            return os.path.dirname(os.path.abspath(self.last_opened_file))
        return self.script_dir

    def _add_external_commands_to_menu(self, menu):
        added_any = False
        i = 1
        while True:
            cmd_name = self.paths.get(f"external_cmd{i}_name")
            cmd_path = self.paths.get(f"external_cmd{i}_path")
            if not cmd_name or not cmd_path:
                break
            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))
            i += 1
        if added_any:
            menu.add_separator()

    def _substitute_variables(self, command_template):
        command = command_template
        if self.last_opened_file:
            command = command.replace("{{file_path}}", f'"{self.last_opened_file}"')
            command = command.replace("{{file_dir}}", f'"{os.path.dirname(self.last_opened_file)}"')

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

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

    def _run_external_command(self, command_template):
        command = self._substitute_variables(command_template)
        try:
            subprocess.Popen(command, shell=True, cwd=self._get_working_directory())
            self.set_status(f"外部コマンド実行: {command}")
        except Exception as e:
            messagebox.showerror("コマンド実行エラー", f"コマンドの実行に失敗しました:\n{command}\n\n{e}")

    def _open_file_location(self):
        directory = os.path.normpath(os.path.abspath(self._get_working_directory()))
        try:
            file_manager_path = self.paths.get("file_manager_path")
            if file_manager_path:
                subprocess.Popen([file_manager_path, directory])
            elif os.name == "nt":
                subprocess.Popen(["explorer", directory])
            elif sys.platform == "darwin":
                subprocess.Popen(["open", directory])
            else:
                subprocess.Popen(["xdg-open", directory])
        except Exception as e:
            messagebox.showerror("エラー", f"ファイル場所を開けませんでした。\n{directory}\n\n{e}")

    def _open_terminal(self):
        directory = self._get_working_directory()
        try:
            terminal_path = self.paths.get("terminal_path")
            if terminal_path:
                subprocess.Popen([terminal_path], cwd=directory)
            elif 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 e:
            messagebox.showerror("エラー", f"ターミナルを開けませんでした。\n\n{e}")

    def select_file(self):
        if self.last_opened_file and os.path.exists(self.last_opened_file):
            initial_dir = os.path.dirname(os.path.abspath(self.last_opened_file))
        else:
            initial_dir = self.script_dir

        filetypes = [
            ("Readable files", "*.ini;*.txt;*.md;*.py;*.pyw;*.c;*.cpp;*.h;*.f;*.f90;*.toml;*.json"),
            ("INI files", "*.ini"),
            ("Text/Code files", "*.txt;*.md;*.py;*.pyw;*.c;*.cpp;*.h;*.f;*.f90;*.toml;*.json"),
            ("All files", "*.*"),
        ]
        filepath = filedialog.askopenfilename(filetypes=filetypes, initialdir=initial_dir)
        if filepath:
            self.load_ini(filepath)
            self.last_opened_file = filepath
            self.set_status(f"{filepath} を読み込みました")

    def load_ini(self, filepath):
        self.settings = parse_multiline_ini(filepath)
        self.last_opened_file = filepath
        filename = os.path.basename(filepath)
        self.root.title(f"{APP_NAME}: {filename}")

        self.tree.delete(*self.tree.get_children())
        self.item_map.clear()
        self.item_to_key.clear()
        self.editor.delete("1.0", "end")

        for key, value in self.settings.items():
            parts = key.split("\\") if key else ["(empty)"]
            parent = ""
            for i, part in enumerate(parts):
                path = "\\".join(parts[: i + 1])
                if path not in self.item_map:
                    item_id = self.tree.insert(parent, "end", text=part or "(empty)", open=True)
                    self.item_map[path] = item_id
                parent = self.item_map[path]

            leaf_item_id = self.item_map["\\".join(parts)]
            self.item_to_key[leaf_item_id] = key

        if self.settings:
            first = next(iter(self.item_to_key.keys()), None)
            if first:
                self.tree.selection_set(first)
                self.tree.see(first)
                self.on_select(None)
        else:
            self.set_status("読み込める項目がありません")

    def search_item(self):
        query = simpledialog.askstring("検索", "検索する文字列を入力してください", initialvalue=self.last_query or "")
        if not query:
            return

        q = query.lower()
        self.last_query = query
        self.search_results = []
        for key, item_id in self.item_map.items():
            value = self.settings.get(key, "")
            if q in key.lower() or q in value.lower():
                self.search_results.append((key, item_id))
        self.search_index = -1
        self.navigate_search(1)

    def navigate_search(self, direction):
        if not self.search_results:
            self.set_status("検索結果なし")
            return
        self.search_index = (self.search_index + direction) % len(self.search_results)
        key, item_id = self.search_results[self.search_index]
        self.tree.selection_set(item_id)
        self.tree.focus(item_id)
        self.tree.see(item_id)
        self.on_select(None)
        self.set_status(f"{self.search_index + 1}/{len(self.search_results)} 件目: {key}")

    def copy_value(self):
        selected = self.tree.selection()
        if not selected:
            return
        item_id = selected[0]
        key = self.item_to_key.get(item_id)
        if key is None:
            key = self._find_key_from_selected(item_id)
        if key is None:
            self.set_status("値を持つ項目を選択してください")
            return
        value = self.settings.get(key, "")
        self.root.clipboard_clear()
        self.root.clipboard_append(value)
        self.set_status(f"コピーしました: {key}")

    def _find_key_from_selected(self, item_id):
        for key, mapped_id in self.item_map.items():
            if mapped_id == item_id and key in self.settings:
                self.item_to_key[item_id] = key
                return key
        return None

    def copy_from_editor(self):
        try:
            text = self.editor.get("sel.first", "sel.last")
        except tk.TclError:
            text = self.editor.get("1.0", "end-1c")
        self.root.clipboard_clear()
        self.root.clipboard_append(text)
        self.set_status("コピーしました")

    def on_select(self, _event):
        selected = self.tree.selection()
        if not selected:
            return
        item_id = selected[0]
        key = self.item_to_key.get(item_id)
        if key is None:
            key = self._find_key_from_selected(item_id)
        value = self.settings.get(key, "") if key is not None else ""
        self.editor.delete("1.0", "end")
        self.editor.insert("1.0", value)

    def load_window_settings(self):
        cfg = configparser.ConfigParser()
        cfg.read_dict(
            {
                "Window": {
                    "geometry": DEFAULT_GEOMETRY,
                    "sash": str(DEFAULT_SASH),
                    "last_file": "",
                    "wrap": "1",
                },
                "paths": {
                    "file_manager_path": "",
                    "terminal_path": "",
                },
            }
        )

        if os.path.exists(self.settings_file):
            try:
                cfg.read(self.settings_file, encoding="utf-8")
            except Exception as e:
                messagebox.showwarning("警告", f"設定ファイルの読み込みに失敗しました。既定値で起動します。\n\n{e}")

        geom = cfg.get("Window", "geometry", fallback=DEFAULT_GEOMETRY)
        if geom:
            self.root.geometry(geom)

        sash = cfg.get("Window", "sash", fallback=str(DEFAULT_SASH))
        try:
            pos = int(sash)
            self.root.after(100, lambda: self.pane.sash_place(0, pos, 0))
        except Exception:
            pass

        wrap_flag = cfg.getboolean("Window", "wrap", fallback=True)
        self.wrap_var.set(wrap_flag)
        self.toggle_wrap()

        last_file = cfg.get("Window", "last_file", fallback="").strip()
        if last_file and os.path.exists(last_file):
            self.last_opened_file = last_file
            self.load_ini(last_file)

        self.paths = {
            "file_manager_path": cfg.get("paths", "file_manager_path", fallback="").strip(),
            "terminal_path": cfg.get("paths", "terminal_path", fallback="").strip(),
        }
        i = 1
        while True:
            name_key = f"external_cmd{i}_name"
            path_key = f"external_cmd{i}_path"
            name_val = cfg.get("paths", name_key, fallback="").strip()
            path_val = cfg.get("paths", path_key, fallback="").strip()
            if not name_val or not path_val:
                break
            self.paths[name_key] = name_val
            self.paths[path_key] = path_val
            i += 1

    def on_close(self):
        cfg = configparser.ConfigParser()
        try:
            sash = self.pane.sash_coord(0)[0]
        except Exception:
            sash = DEFAULT_SASH

        cfg["Window"] = {
            "geometry": self.root.geometry() or DEFAULT_GEOMETRY,
            "last_file": self.last_opened_file or "",
            "sash": str(sash),
            "wrap": "1" if self.wrap_var.get() else "0",
        }

        cfg["paths"] = {
            "file_manager_path": self.paths.get("file_manager_path", ""),
            "terminal_path": self.paths.get("terminal_path", ""),
        }

        i = 1
        while True:
            name_key = f"external_cmd{i}_name"
            path_key = f"external_cmd{i}_path"
            name_val = self.paths.get(name_key, "")
            path_val = self.paths.get(path_key, "")
            if not name_val or not path_val:
                break
            cfg["paths"][name_key] = name_val
            cfg["paths"][path_key] = path_val
            i += 1

        try:
            with open(self.settings_file, "w", encoding="utf-8") as f:
                cfg.write(f)
        except Exception as e:
            messagebox.showwarning("警告", f"設定ファイルを保存できませんでした。\n\n{e}")
        finally:
            self.root.destroy()


if __name__ == "__main__":
    ini_path = sys.argv[1] if len(sys.argv) > 1 else None
    app = Window(themename="flatly")
    viewer = IniViewer(app, ini_path)
    app.mainloop()
