import os
import sys
import toml
import xml.etree.ElementTree as ET
import tkinter as tk
from tkinter import ttk

"""
XMLファイルを読み込み、TkinterのGUIツリービューで表示するスクリプト。

概要:
    このスクリプトは、指定されたXMLファイルの内容を読み込み、
    それをグラフィカルユーザーインターフェース（GUI）のツリービュー形式で表示します。
    ユーザーはXMLファイルの構造を視覚的に探索できます。

詳細説明:
    - コマンドライン引数またはTOML形式のINIファイルから、XMLファイルのパスとウィンドウサイズを読み込みます。
    - `xml.etree.ElementTree` モジュールを使用してXMLファイルを解析し、その構造を再帰的に走査します。
    - Tkinterとttk（themed Tkinter）を使用して、解析されたXML要素を階層的なTreeviewウィジェットに表示します。
    - ウィンドウを閉じる際に、現在のウィンドウのジオメトリをINIファイルに保存する機能を提供し、
      次回の起動時に前回の設定を復元できるようにします。

使用方法:
    python xml_viewer.py [XMLファイルパス] [ウィンドウサイズ (例: 800x600)]

関連リンク:
    :doc:`XMLView_usage`
"""


def usage():
    print("Usage: python {} infile window_size".format(sys.argv[0]))


class XMLViewer:
    """
    XMLデータをTkinterのTreeviewウィジェットに表示するためのビューアクラス。

    概要:
        XMLファイルを読み込み、その内容をTkinterのTreeviewウィジェットに階層的に表示します。

    詳細説明:
        このクラスは、Tkinterのルートウィンドウを受け取り、XMLファイルのパスと初期ウィンドウサイズに基づいて、
        Treeviewウィジェットを作成し、XML構造を解析して表示します。
        XML要素はタグ、テキスト、属性に基づいてTreeviewノードとして追加されます。
    """
    def __init__(self, root: tk.Tk, infile: str, geometry: str = '800x600', tree_open: bool = True):
        """
        XMLViewerクラスのコンストラクタ。GUIの初期設定とXMLファイルの読み込みを行います。

        概要:
            Tkinterのルートウィンドウを設定し、Treeviewウィジェットを作成。
            指定されたXMLファイルを読み込み、その内容をTreeviewに表示します。

        :param root: tk.Tk - Tkinterのルートウィンドウオブジェクト。
        :param infile: str - 読み込むXMLファイルのパス。
        :param geometry: str - ウィンドウの初期サイズと位置を指定する文字列（例: '800x600'）。デフォルトは '800x600'。
        :param tree_open: bool - Treeviewの初期状態でノードを開くかどうか。Trueの場合、特定のレベルまで開く。
        """
        self.tree_open = tree_open

        self.root = root
        self.root.title(f"XML Viewer - {infile}")
        self.root.geometry(geometry)

        self.treeview = ttk.Treeview(self.root)
        self.treeview.heading('#0', text = infile)
        self.treeview.pack(fill = 'both', expand = True)

        self.read_xml(infile)

    def read_xml(self, infile: str):
        """
        指定されたXMLファイルを解析し、Treeviewに表示するための準備を行う。

        概要:
            `xml.etree.ElementTree` を使用してXMLファイルを解析し、ルート要素を取得。
            その後、`display_element` メソッドを呼び出してTreeviewに内容を再帰的に表示します。

        詳細説明:
            ファイルが存在しない場合やXML解析エラーが発生した場合は、
            エラーメッセージをコンソールとTreeviewに表示し、アプリケーションを終了します。
            正常に解析された場合、XMLのルート要素から表示を開始します。

        :param infile: str - 読み込むXMLファイルのパス。
        :returns: None
        """
        if not os.path.isfile(infile):
            print(f"\nError: Can not find [{infile}]")
            input("Press ENTER to terminate>>\n")
            exit()

        try:
            self.etree = ET.parse(infile)
            self.root_element = self.etree.getroot()
            self.display_element(self.root_element, '')
        except ET.ParseError as e:
            self.treeview.insert("", "end", text="Error", values = (str(e)))

    def display_element(self, element: ET.Element, item_id: str = '', level: int = 0):
        """
        XML要素を再帰的にTkinterのTreeviewウィジェットに表示する。

        概要:
            指定されたXML要素のタグ、テキスト、属性をTreeviewのノードとして追加し、
            子要素があればそれらも再帰的に表示します。

        詳細説明:
            ノードの表示テキストは、`name` 属性が存在すればそれを優先し、次に要素のテキスト、
            最後にタグ名を使用します。
            レベル1までのノードは初期状態で開き、それ以降のレベルは閉じられた状態で挿入されます。
            各要素に対して、そのタグ、テキスト、属性が詳細として子ノードに追加されます。
            属性はさらに個別のキーと値のペアとして表示されます。

        :param element: ET.Element - 表示するXML要素。
        :param item_id: str - 親となるTreeviewアイテムのID。ルート要素の場合は空文字列。
        :param level: int - 現在の要素の深さ（ネストレベル）。
        :returns: None
        """
        if level <= 1:
            tree_open = self.tree_open
        else:
            tree_open = False

        text = None
        if element.attrib.get('name', None) is not None:
            text = element.attrib["name"]
        if element.text is not None:
            if text is None:
                text = element.text.strip()
            else:
                text += f":{element.text.strip()}"
        if text is None or text == '':
            text = element.tag.strip()
        if text is None or text == '':
            text = 'root'

        child_item = self.treeview.insert(item_id, "end", text = text, open = tree_open)
        self.treeview.insert(child_item, "end", text = f"tag: {element.tag.strip()}", open = tree_open)
        if element.text is not None:
            self.treeview.insert(child_item, "end", text = f"text: {element.text.strip()}")
        self.treeview.insert(child_item, "end", text = f"attrib: {element.attrib}", open = tree_open)
        for key, value in element.attrib.items():
            self.treeview.insert(child_item, "end", text = f"{key}: {value}", open = tree_open)

        for child in element.findall('*'):
            self.display_element(child, child_item, level = level + 1)


def window_close(root: tk.Tk, inifile: str):
    """
    ウィンドウが閉じられたときに呼び出され、現在のウィンドウのジオメトリをINIファイルに保存する。

    概要:
        Tkinterウィンドウが閉じられる際に実行され、現在のウィンドウのサイズと位置をTOML形式のINIファイルに保存します。

    詳細説明:
        この関数は、`WM_DELETE_WINDOW` プロトコルハンドラとして設定されます。
        ウィンドウの現在のジオメトリ情報を取得し、`inifile` で指定されたパスにあるINIファイルに
        `geometry` キーとして書き込みます。その後、アプリケーションを終了します。

    :param root: tk.Tk - Tkinterのルートウィンドウオブジェクト。
    :returns: None
    """
    with open(inifile, "w") as file:
        toml.dump({"geometry": root.geometry()}, file)    
    root.quit()


def main():
    """
    アプリケーションのエントリポイント。TkinterウィンドウとXMLViewerインスタンスを作成し、GUIイベントループを開始する。

    概要:
        XMLビューアアプリケーションを初期化し、実行します。

    詳細説明:
        Tkinterのルートウィンドウを初期化し、コマンドライン引数やINIファイルから取得した設定（XMLファイルパス、ウィンドウサイズ）に基づいて
        `XMLViewer` インスタンスを作成します。
        ウィンドウが閉じられる際のプロトコルハンドラとして `window_close` 関数を設定し、
        GUIイベントループを開始してアプリケーションをユーザー操作可能にします。

    :returns: None
    """
    argv = sys.argv
    nargs = len(argv)

    if nargs <= 1:
        usage()
        sys.exit(1)

    infile = '../XML/vasprun.xml'
    geometry = '800x600'

    inifile = os.path.splitext(argv[0])[0] + ".ini"
    if os.path.isfile(inifile): 
        geometry = toml.load(inifile).get("geometry", geometry)

    if nargs >= 2:
        infile = argv[1]
    if nargs >= 3:
        geometry = argv[2]

    root = tk.Tk()
    app = XMLViewer(root, infile, geometry, tree_open = True)
    root.protocol("WM_DELETE_WINDOW", lambda: window_close(root, inifile))

    root.mainloop()


if __name__ == "__main__":
    main()