import os
import sys
import traceback
import re
import subprocess
import tkinter.messagebox as msgbox

import ttkbootstrap

missing = []
err = ""
for lib in ["chardet", "configparser", "tkinter", "ttkbootstrap"]:
    try:
        __import__(lib)
    except ImportError:
        missing.append(lib)
        err += "\n" + traceback.format_exc()

if missing:
    msgbox.showerror("ライブラリエラー", 
         f"以下のライブラリが不足しています:\n{', '.join(missing)}"
       + "\n\n詳細:\n{err}")
    sys.exit(1)

import chardet
import configparser
import tkinter as tk
import ttkbootstrap
from tkinter import filedialog, simpledialog
from ttkbootstrap import Window
from ttkbootstrap.widgets import Treeview


SETTINGS_FILE = "iniview.ini"

def parse_multiline_ini(filepath):
    settings = {}
    if not os.path.exists(filepath): return settings

    detected_encoding = 'utf-8'
    try:
        with open(filepath, 'rb') as f_raw:
            raw_data = f_raw.read()
            result = chardet.detect(raw_data)
            if result['encoding'] and result['confidence'] > 0.8:
                detected_encoding = result['encoding']
    except Exception as e:
        print(f"Warning: Could not detect encoding for {filepath}: {e}")

    _, ext = os.path.splitext(filepath)
    if ext.lower() != '.ini':
        try:
            with open(filepath, 'r', encoding=detected_encoding, errors='ignore') as f:
                content = f.read()
                settings['all'] = content
            return settings
        except Exception as e:
            print(f"読み込みエラー: {e}")
            return {}

    try:
        with open(filepath, 'r', encoding=detected_encoding, errors='ignore') as f:
            content = f.read()

        in_multiline = False
        current_key = None
        current_value = []

        for line in content.splitlines():
            stripped_line = line.strip()

            if in_multiline:
                if stripped_line.endswith('"""'):
                    current_value.append(stripped_line.rstrip('"""'))
                    settings[current_key] = '\n'.join(current_value)
                    in_multiline = False
                    current_key = None
                    current_value = []
                else:
                    current_value.append(line)
            elif '=' in line:
                if stripped_line.startswith('#'): 
                    continue
                key, value_part = line.split('=', 1)
                key = key.strip()
                value_part = value_part.strip()

                if value_part.startswith('"""'):
                    in_multiline = True
                    current_key = key
                    if value_part.endswith('"""') and len(value_part) > 5:
                        settings[key] = value_part[3:-3]
                        in_multiline = False
                        current_key = None
                    else:
                        current_value = [value_part[3:]]
                else:
                    settings[key] = value_part
    except Exception as e:
        print(f"INIファイル読み込みエラー: {filepath}: {e}")

    return dict(sorted(settings.items()))


class IniViewer:
    def __init__(self, root, ini_path=None):
        self.root = root
        self.root.title("INI Viewer")

        # ペイン分割
        self.pane = tk.PanedWindow(root, orient="horizontal")
        self.pane.pack(fill="both", expand=True)

        # --- 左ペイン(TreeView + Scrollbar) ---
        tree_frame = tk.Frame(self.pane)
        self.tree = Treeview(tree_frame)
        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 + Scrollbar) ---
        editor_frame = tk.Frame(self.pane)
        self.editor = tk.Text(editor_frame, wrap="word")
        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)

        # Editorコンテキストメニュー
        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(root, text="", anchor="w")
        self.status.pack(fill="x", side="bottom")

        self.settings = {}
        self.item_map = {}
        self.last_query = None
        self.search_results = []
        self.search_index = -1
        self.last_opened_file = None

        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)

    # --- メニュー生成 ---
    def create_menus(self):
        menubar = tk.Menu(self.root)
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="ファイル読み込み", command=self.select_file)
        file_menu.add_command(label="検索", command=self.search_item)
        file_menu.add_command(label="次を検索", command=lambda: self.navigate_search(1))
        file_menu.add_command(label="前を検索", command=lambda: self.navigate_search(-1))
        file_menu.add_command(label="コピー", command=self.copy_value)
        menubar.add_cascade(label="操作", menu=file_menu)

        view_menu = tk.Menu(menubar, tearoff=0)
        self.wrap_var = tk.BooleanVar(value=True)
        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)
        menubar.add_cascade(label="ツール", menu=tools_menu)
        tools_menu.add_command(label="ファイル場所を開く", command=self._open_file_location)
        tools_menu.add_command(label="ターミナルを開く", command=self._open_terminal)

        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 _add_external_commands_to_menu(self, menu):
        added_any = False
        i = 1
        while True:
            cmd_name = self.paths.get(f'external_cmd{i}_name', None)
            cmd_path = self.paths.get(f'external_cmd{i}_path', None)
            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):
        """Substitutes {{var}} placeholders in a string."""
        if self.last_opened_file:
            command_template = command_template.replace("{{file_path}}", f'"{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_template)

    def _run_external_command(self, command_template):
        command = self._substitute_variables(command_template)
        try:
#            cwd = os.path.dirname(self.last_opened_file) if self.last_opened_file else self.script_dir
            if not self.last_opened_file or not os.path.exists(self.last_opened_file):
                cwd = self.script_dir
            else:
                cwd = os.path.dirname(self.last_opened_file)
            subprocess.Popen(command, shell=True, cwd=cwd)
        except Exception as e:
            messagebox.showerror("コマンド実行エラー", f"コマンドの実行に失敗しました:\n{command}\nエラー: {e}")

    def _open_file_location(self):
        if not self.last_opened_file or not os.path.exists(self.last_opened_file):
            directory = self.script_dir
        else:
            directory = os.path.dirname(self.last_opened_file)
        directory = os.path.abspath(directory)  
        directory = os.path.normpath(directory) 
        if os.name == 'nt': subprocess.Popen(['explorer', directory])
        elif os.uname().sysname == 'Darwin': subprocess.Popen(['open', directory])
        else: subprocess.Popen(['xdg-open', directory])

    def _open_terminal(self):
        if not self.last_opened_file or not os.path.exists(self.last_opened_file):
            directory = self.script_dir
        else:
            directory = os.path.dirname(self.last_opened_file)
        
        if os.name == 'nt':
            os.startfile('cmd.exe', cwd=directory)
        else:
            terminal_cmd = os.environ.get('TERMINAL', 'x-terminal-emulator')
            try:
                subprocess.Popen([terminal_cmd], cwd=directory)
            except FileNotFoundError:
                messagebox.showerror("エラー", f"ターミナル '{terminal_cmd}' が見つかりません。")

    # --- 折り返し切替 ---
    def toggle_wrap(self):
        if self.wrap_var.get():
            self.editor.config(wrap="word")
        else:
            self.editor.config(wrap="none")

    # --- ステータスバー更新 ---
    def set_status(self, msg):
        self.status.config(text=msg)

    def show_context_menu(self, event):
        self.context_menu.post(event.x_root, event.y_root)

    def show_editor_menu(self, event):
        self.editor_menu.post(event.x_root, event.y_root)

    def select_file(self):
        # 直前に開いたファイルのディレクトリを既定にする
        script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
        if self.last_opened_file and os.path.exists(self.last_opened_file):
            initial_dir = os.path.abspath(os.path.dirname(self.last_opened_file))
        else:
            initial_dir = os.path.join(script_dir, "iniview")

        filetypes = [("Readable files", "*.ini;*.p?;*.c*;*.f*"),
                     ("INI files", "*.ini"), 
                     ("Python/Perl files", "*.p?"), 
                     ("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)
        filename = os.path.basename(filepath)
        self.root.title(f"IniView.py: {filename}")

        self.tree.delete(*self.tree.get_children())
        self.item_map.clear()
        for key, value in self.settings.items():
            parts = key.split("\\")
            parent = ""
            for i, part in enumerate(parts):
                path = "\\".join(parts[:i+1])
                if path not in self.item_map:
                    item = self.tree.insert(parent, "end", text=part, open=True)
                    self.item_map[path] = item
                parent = self.item_map[path]
            self.tree.item(self.item_map[key], values=(value,))

    def search_item(self):
        query = simpledialog.askstring("検索", "検索する文字列を入力してください")
        if not query:
            return
        self.last_query = query
        self.search_results = [
            (key, item_id) for key, item_id in self.item_map.items()
            if query in key or query in self.settings.get(key, "")
        ]
        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)
        _, item_id = self.search_results[self.search_index]
        self.tree.selection_set(item_id)
        self.tree.see(item_id)
        self.set_status(f"{self.search_index+1}/{len(self.search_results)} 件目")

    # --- コピー機能 ---
    def copy_value(self):
        selected = self.tree.selection()
        if not selected:
            return
        for item_id in selected:
            for key, id_ in self.item_map.items():
                if id_ == item_id:
                    value = self.settings.get(key, "")
                    self.root.clipboard_clear()
                    self.root.clipboard_append(value)
                    self.set_status("コピーしました")
                    break

    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("コピーしました")

    # --- Tree選択時に右ペインに表示 ---
    def on_select(self, event):
        self.set_status("")  # ステータスバーをクリア
        selected = self.tree.selection()
        if not selected:
            return
        for item_id in selected:
            for key, id_ in self.item_map.items():
                if id_ == item_id:
                    value = self.settings.get(key, "")
                    self.editor.delete("1.0", "end")
                    self.editor.insert("1.0", value)
                    break

    # --- 設定保存 ---
    def load_window_settings(self):
        cfg = configparser.ConfigParser()
        if os.path.exists(SETTINGS_FILE):
            cfg.read(SETTINGS_FILE)
            if "Window" in cfg:
                geom = cfg["Window"].get("geometry")
                if geom:
                    self.root.geometry(geom)
                sash = cfg["Window"].get("sash", 100)
                if sash:
                    try:
                        pos = int(sash)
                        self.root.after(100, lambda: self.pane.sash_place(0, pos, 0))
                    except Exception:
                        pass
            self.last_opened_file = cfg["Window"].get("last_file", None)
            if self.last_opened_file and os.path.exists(self.last_opened_file):
                self.load_ini(self.last_opened_file)

        self.paths = {}
        self.paths["file_manager_path"] = cfg["paths"].get("file_manager_path")
        self.paths["terminal_path"] = cfg["paths"].get("terminal_path")
        i = 1
        while True:
           name_key = f"external_cmd{i}_name"
           name_val = cfg["paths"].get(name_key, None)
           path_key = f"external_cmd{i}_path"
           path_val = cfg["paths"].get(path_key, None)
           if path_val is None or name_val is None: break

           self.paths[name_key] = name_val
           self.paths[path_key] = path_val

           i += 1

    def on_close(self):
        cfg = configparser.ConfigParser()
        sash = self.pane.sash_coord(0)[0] if self.pane.sash_coord(0) else 200
        cfg["Window"] = {
            "geometry": self.root.geometry(),
            "last_file": getattr(self, "last_opened_file", ""),
            "sash": str(sash)
        }
        cfg["paths"] = {
            "file_manager_path": self.paths["file_manager_path"],
            "terminal_path": self.paths["terminal_path"],
        }
        i = 1
        while True:
           name_key = f"external_cmd{i}_name"
           name_val = self.paths.get(name_key, None)
           path_key = f"external_cmd{i}_path"
           path_val = self.paths.get(path_key, None)
           if path_val is None or name_val is None: break

           cfg["paths"][name_key] = name_val
           cfg["paths"][path_key] = path_val

           i += 1

        with open(SETTINGS_FILE, "w") as f:
            cfg.write(f)
        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()
