# -*- coding: utf-8 -*-
"""
File Format Converter v2.1 (Optimized)
统一的文件格式转换工具（性能优化版）
支持：FBX → MB | 3DMAX → FBX | Maya → FBX

核心优化：
1) Maya 相关转换改为“单进程批处理”（避免每个文件重复启动 mayapy）
2) 3ds Max 转换支持并行（可控线程数）
3) 统一临时脚本与 JSON 参数通信，减少重复开销
4) 更稳健的日志与超时控制
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinterdnd2 import DND_FILES, TkinterDnD
import os
import subprocess
import threading
import tempfile
from datetime import datetime
import queue
import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed


class FileFormatConverter:
    def __init__(self):
        self.root = TkinterDnD.Tk()
        self.root.title("文件格式转换工具 v2.1（优化版）")
        self.root.geometry("1020x880")
        self.root.resizable(False, False)

        self.files_queue = []
        self.is_processing = False
        self.clean_scene = tk.BooleanVar(value=True)
        self.max_parallel = tk.IntVar(value=2)
        self.start_ts = None

        self.max_exe = r"C:\Program Files\Autodesk\3ds Max 2021\3dsmaxbatch.exe"
        self.maya_exe = r"C:\Program Files\Autodesk\Maya2024\bin\maya.exe"

        self.log_queue = queue.Queue()

        self._find_software()
        self._apply_modern_style()
        self._build_ui()
        self._update_log_display()

        # 启动后打印实际选中的软件路径（便于排查版本问题）
        self._log(f"[{self._get_time()}] 3ds Max path: {self.max_exe}")
        self._log(f"[{self._get_time()}] Maya path: {self.maya_exe}")

    def _find_software(self):
        max_paths = [
            r"C:\Program Files\Autodesk\3ds Max 2021\3dsmaxbatch.exe",  # 优先
            r"C:\Program Files\Autodesk\3ds Max 2024\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2023\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2022\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2020\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2019\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2018\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2017\3dsmaxbatch.exe",
            r"C:\Program Files\Autodesk\3ds Max 2016\3dsmaxbatch.exe",
        ]
        for path in max_paths:
            if os.path.exists(path):
                self.max_exe = path
                break

        maya_paths = [
            r"C:\Program Files\Autodesk\Maya2025\bin\maya.exe",
            r"C:\Program Files\Autodesk\Maya2024\bin\maya.exe",
            r"C:\Program Files\Autodesk\Maya2023\bin\maya.exe",
            r"C:\Program Files\Autodesk\Maya2022\bin\maya.exe",
            r"C:\Program Files\Autodesk\Maya2021\bin\maya.exe",
            r"C:\Program Files\Autodesk\Maya2020\bin\maya.exe",
        ]
        for path in maya_paths:
            if os.path.exists(path):
                self.maya_exe = path
                break

    def _apply_modern_style(self):
        style = ttk.Style()
        try:
            style.theme_use('clam')
        except Exception:
            pass
        style.configure('TProgressbar', thickness=12)
        style.configure('TCombobox', padding=4)

    def _build_ui(self):
        left_panel = tk.Frame(self.root, width=440, bg="white")
        left_panel.pack(side=tk.LEFT, fill=tk.BOTH)
        left_panel.pack_propagate(False)

        tk.Label(left_panel, text="文件格式转换", font=("Arial", 20, "bold"), bg="white", fg="#2c3e50").pack(pady=(30, 5))
        tk.Label(left_panel, text="支持：FBX → MB | 3DMAX → FBX | Maya → FBX",
                 font=("Arial", 9), bg="white", fg="#7f8c8d").pack(pady=(0, 20))

        drop_frame = tk.Frame(left_panel, bg="white")
        drop_frame.pack(fill=tk.BOTH, padx=20, pady=(0, 20))

        self.drop_area = tk.Frame(drop_frame, bg="#ecf0f1", relief=tk.SOLID, borderwidth=2,
                                  highlightbackground="#3498db", highlightthickness=0)
        self.drop_area.pack(fill=tk.BOTH, expand=True, ipady=60)

        tk.Label(self.drop_area, text="📁", font=("Arial", 40), bg="#ecf0f1").pack(pady=(30, 10))
        tk.Label(self.drop_area, text="拖拽文件到这里", font=("Arial", 14, "bold"),
                 bg="#ecf0f1", fg="#2c3e50").pack()
        tk.Label(self.drop_area, text="支持格式：.fbx .max .ma .mb",
                 font=("Arial", 9), bg="#ecf0f1", fg="#7f8c8d").pack(pady=(5, 30))

        self.drop_area.drop_target_register(DND_FILES)
        self.drop_area.dnd_bind('<<Drop>>', self._on_drop)

        options_frame = tk.LabelFrame(left_panel, text="Options", font=("Arial", 10, "bold"),
                                      bg="white", padx=15, pady=10)
        options_frame.pack(fill=tk.X, padx=20, pady=(0, 12))

        tk.Checkbutton(options_frame,
                       text="FBX→MB：清理场景（删除灯光/相机/空组）",
                       variable=self.clean_scene,
                       font=("Arial", 9), bg="white", activebackground="white").pack(anchor=tk.W)

        max_parallel_frame = tk.Frame(options_frame, bg="white")
        max_parallel_frame.pack(anchor=tk.W, pady=(8, 0))
        tk.Label(max_parallel_frame, text="MAX 并行数：", font=("Arial", 9), bg="white").pack(side=tk.LEFT)
        max_parallel_combo = ttk.Combobox(max_parallel_frame, width=5, state="readonly",
                                          values=[1, 2, 3, 4], textvariable=self.max_parallel)
        max_parallel_combo.pack(side=tk.LEFT)

        tk.Label(options_frame,
                 text="提示：MAX 支持并行；Maya 启用单进程批处理提速",
                 font=("Arial", 8), bg="white", fg="#27ae60").pack(anchor=tk.W, pady=(8, 0))

        list_frame = tk.LabelFrame(left_panel, text="待处理文件", font=("Arial", 10, "bold"),
                                   bg="white", padx=10, pady=10)
        list_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 15))

        list_scroll = tk.Scrollbar(list_frame)
        list_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_listbox = tk.Listbox(list_frame, yscrollcommand=list_scroll.set, font=("Consolas", 9), height=8)
        self.file_listbox.pack(fill=tk.BOTH, expand=True)
        list_scroll.config(command=self.file_listbox.yview)

        btn_frame = tk.Frame(left_panel, bg="white")
        btn_frame.pack(fill=tk.X, padx=20, pady=(0, 12))

        tk.Button(btn_frame, text="添加文件", command=self._add_files, width=12, font=("Arial", 9)).pack(side=tk.LEFT, padx=(0, 5))
        tk.Button(btn_frame, text="清空", command=self._clear_files, width=12, font=("Arial", 9)).pack(side=tk.LEFT)

        self.convert_btn = tk.Button(left_panel, text="开始转换", command=self._start_conversion,
                                     font=("Arial", 12, "bold"), bg="#2ecc71", fg="white",
                                     height=2, activebackground="#27ae60")
        self.convert_btn.pack(fill=tk.X, padx=20, pady=(0, 20))

        right_panel = tk.Frame(self.root, bg="#f8f9fa")
        right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        log_header = tk.Frame(right_panel, bg="white", height=50)
        log_header.pack(fill=tk.X)
        log_header.pack_propagate(False)

        tk.Label(log_header, text="处理日志", font=("Arial", 12, "bold"),
                 bg="white", fg="#2c3e50").pack(side=tk.LEFT, padx=20, pady=15)
        tk.Button(log_header, text="清空", command=self._clear_log, font=("Arial", 9), relief=tk.FLAT,
                  bg="white", fg="#3498db", cursor="hand2").pack(side=tk.RIGHT, padx=20)

        progress_frame = tk.Frame(right_panel, bg="#f8f9fa", height=40)
        progress_frame.pack(fill=tk.X, padx=20, pady=(10, 0))
        progress_frame.pack_propagate(False)
        self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate')
        self.progress_bar.pack(fill=tk.X, pady=5)

        self.status_label = tk.Label(right_panel, text="状态：待命", font=("Arial", 9),
                                     bg="#f8f9fa", fg="#34495e")
        self.status_label.pack(anchor=tk.W, padx=20, pady=(2, 0))

        self.time_label = tk.Label(right_panel, text="用时：00:00", font=("Arial", 9),
                                   bg="#f8f9fa", fg="#34495e")
        self.time_label.pack(anchor=tk.W, padx=20, pady=(2, 8))

        log_frame = tk.Frame(right_panel, bg="#f8f9fa")
        log_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(10, 20))

        log_scroll = tk.Scrollbar(log_frame)
        log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.log_text = tk.Text(log_frame, wrap=tk.WORD, yscrollcommand=log_scroll.set,
                                font=("Consolas", 9), bg="white", state=tk.DISABLED)
        self.log_text.pack(fill=tk.BOTH, expand=True)
        log_scroll.config(command=self.log_text.yview)

        self._log(f"[{self._get_time()}] ⚙️ 准备就绪，等待转换任务...")

    def _get_time(self):
        return datetime.now().strftime("%H:%M:%S")

    def _log(self, message):
        self.log_queue.put(message)

    def _update_log_display(self):
        try:
            while True:
                msg = self.log_queue.get_nowait()
                self.log_text.config(state=tk.NORMAL)
                self.log_text.insert(tk.END, msg + "\n")
                self.log_text.see(tk.END)
                self.log_text.config(state=tk.DISABLED)
        except queue.Empty:
            pass
        self.root.after(100, self._update_log_display)

    def _clear_log(self):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete(1.0, tk.END)
        self.log_text.config(state=tk.DISABLED)
        self._log(f"[{self._get_time()}] 🔄 日志已清空")

    def _on_drop(self, event):
        files = self.root.tk.splitlist(event.data)
        for file_path in files:
            file_path = file_path.strip('{}')
            if os.path.isfile(file_path):
                ext = os.path.splitext(file_path)[1].lower()
                if ext in ['.fbx', '.max', '.ma', '.mb'] and file_path not in self.files_queue:
                    self.files_queue.append(file_path)
                    self.file_listbox.insert(tk.END, os.path.basename(file_path))
                    self._log(f"[{self._get_time()}] ➕ Added: {os.path.basename(file_path)}")

    def _add_files(self):
        files = filedialog.askopenfilenames(
            title="选择文件",
            filetypes=[
                ("全部支持格式", "*.fbx *.max *.ma *.mb"),
                ("FBX 文件", "*.fbx"),
                ("3ds Max 文件", "*.max"),
                ("Maya 文件", "*.ma *.mb"),
                ("所有文件", "*.*")
            ]
        )
        for file_path in files:
            if file_path not in self.files_queue:
                self.files_queue.append(file_path)
                self.file_listbox.insert(tk.END, os.path.basename(file_path))
                self._log(f"[{self._get_time()}] ➕ Added: {os.path.basename(file_path)}")

    def _clear_files(self):
        self.files_queue.clear()
        self.file_listbox.delete(0, tk.END)
        self._log(f"[{self._get_time()}] 🗑️ 文件队列已清空")

    def _start_conversion(self):
        if not self.files_queue:
            messagebox.showwarning("提示", "队列里还没有文件")
            return
        if self.is_processing:
            messagebox.showwarning("提示", "正在处理中，请稍候")
            return

        self.is_processing = True
        self.start_ts = time.time()
        self.convert_btn.config(state=tk.DISABLED, text="处理中...", bg="#95a5a6")
        self.progress_bar['value'] = 0
        self.status_label.config(text="状态：处理中")
        self._update_elapsed_time()

        t = threading.Thread(target=self._process_files, daemon=True)
        t.start()

    def _process_files(self):
        total = len(self.files_queue)
        success = 0
        fail = 0
        done = 0

        fbx_files = [f for f in self.files_queue if f.lower().endswith('.fbx')]
        max_files = [f for f in self.files_queue if f.lower().endswith('.max')]
        maya_files = [f for f in self.files_queue if f.lower().endswith(('.ma', '.mb'))]

        self._log(f"[{self._get_time()}] 🚀 开始批量转换（共 {total} 个文件）")
        self._log("=" * 70)

        # 1) FBX -> MB（单次 mayapy 批处理）
        if fbx_files:
            self._log(f"[{self._get_time()}] 🧠 Maya batch mode: FBX→MB x {len(fbx_files)}")
            results = self._convert_fbx_to_mb_batch(fbx_files)
            for file_path, ok, info in results:
                done += 1
                if ok:
                    success += 1
                    self._log(f"[{self._get_time()}] ✅ {os.path.basename(file_path)} -> {info}")
                else:
                    fail += 1
                    self._log(f"[{self._get_time()}] ❌ {os.path.basename(file_path)} -> {info}")
                self.root.after(0, self._update_progress, done / total * 100)

        # 2) MAX -> FBX（并行，提升吞吐）
        if max_files:
            workers = max(1, min(int(self.max_parallel.get()), 4))
            self._log(f"[{self._get_time()}] ⚡ 3ds Max 并行模式：MAX→FBX x {len(max_files)} | workers={workers}")
            with ThreadPoolExecutor(max_workers=workers) as ex:
                future_map = {ex.submit(self._convert_max_to_fbx_single, f): f for f in max_files}
                for future in as_completed(future_map):
                    file_path = future_map[future]
                    try:
                        ok, info = future.result()
                    except Exception as e:
                        ok, info = False, str(e)

                    done += 1
                    if ok:
                        success += 1
                        self._log(f"[{self._get_time()}] ✅ {os.path.basename(file_path)} -> {info}")
                    else:
                        fail += 1
                        self._log(f"[{self._get_time()}] ❌ {os.path.basename(file_path)} -> {info}")
                    self.root.after(0, self._update_progress, done / total * 100)

        # 3) Maya -> FBX（单次 mayapy 批处理）
        if maya_files:
            self._log(f"[{self._get_time()}] 🧠 Maya batch mode: Maya→FBX x {len(maya_files)}")
            results = self._convert_maya_to_fbx_batch(maya_files)
            for file_path, ok, info in results:
                done += 1
                if ok:
                    success += 1
                    self._log(f"[{self._get_time()}] ✅ {os.path.basename(file_path)} -> {info}")
                else:
                    fail += 1
                    self._log(f"[{self._get_time()}] ❌ {os.path.basename(file_path)} -> {info}")
                self.root.after(0, self._update_progress, done / total * 100)

        self._log("=" * 70)
        self._log(f"[{self._get_time()}] 🏁 转换完成")
        self._log(f"[{self._get_time()}] ✅ 成功: {success} | ❌ 失败: {fail}")
        self.root.after(0, self._conversion_complete, success, fail)

    def _format_duration(self, sec):
        sec = max(0, int(sec))
        m, s = divmod(sec, 60)
        h, m = divmod(m, 60)
        return f"{h:02d}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"

    def _update_elapsed_time(self):
        if self.is_processing and self.start_ts:
            elapsed = time.time() - self.start_ts
            self.time_label.config(text=f"用时：{self._format_duration(elapsed)}")
            self.root.after(300, self._update_elapsed_time)

    def _update_progress(self, value):
        self.progress_bar['value'] = value

    def _conversion_complete(self, success, fail):
        self.is_processing = False
        total_elapsed = time.time() - self.start_ts if self.start_ts else 0
        self.convert_btn.config(state=tk.NORMAL, text="开始转换", bg="#2ecc71")
        self.status_label.config(text="状态：已完成")
        self.time_label.config(text=f"总用时：{self._format_duration(total_elapsed)}")
        messagebox.showinfo("完成", f"批量转换完成！\n\n✅ 成功: {success}\n❌ 失败: {fail}\n⏱️ 用时: {self._format_duration(total_elapsed)}")

    # ---------- Maya 批处理 ----------
    def _get_mayapy_exe(self):
        maya_dir = os.path.dirname(self.maya_exe)
        mayapy_exe = os.path.join(maya_dir, 'mayapy.exe')
        return mayapy_exe if os.path.exists(mayapy_exe) else None

    def _run_mayapy_batch(self, payload, mode):
        if not os.path.exists(self.maya_exe):
            return [(item['input'], False, '未找到 Maya') for item in payload['items']]

        mayapy_exe = self._get_mayapy_exe()
        if not mayapy_exe:
            return [(item['input'], False, '未找到 mayapy.exe') for item in payload['items']]

        script = self._generate_maya_batch_script(mode)

        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as jf:
            json.dump(payload, jf, ensure_ascii=False, indent=2)
            json_path = jf.name

        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as sf:
            sf.write(script)
            script_path = sf.name

        env = os.environ.copy()
        env['PYTHONIOENCODING'] = 'utf-8'
        env['MAYA_DISABLE_CIP'] = '1'
        env['MAYA_DISABLE_CLIC_IPM'] = '1'

        try:
            t0 = time.time()
            cmd = [mayapy_exe, script_path, json_path]
            subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore', env=env, timeout=3600)

            with open(json_path, 'r', encoding='utf-8') as rf:
                out = json.load(rf)

            results = []
            for r in out.get('results', []):
                results.append((r.get('input', ''), bool(r.get('ok')), r.get('info', '')))
            self._log(f"[{self._get_time()}] ⏱️ Maya batch {mode} 用时: {self._format_duration(time.time() - t0)}")
            return results
        except Exception as e:
            return [(item['input'], False, f'批处理异常: {e}') for item in payload['items']]
        finally:
            for p in [json_path, script_path]:
                try:
                    os.unlink(p)
                except Exception:
                    pass

    def _convert_fbx_to_mb_batch(self, files):
        payload = {
            'clean_scene': bool(self.clean_scene.get()),
            'items': []
        }
        for fbx in files:
            mb = os.path.splitext(fbx)[0] + '.mb'
            payload['items'].append({'input': fbx, 'output': mb})
        return self._run_mayapy_batch(payload, mode='fbx_to_mb')

    def _convert_maya_to_fbx_batch(self, files):
        payload = {'items': []}
        for mf in files:
            out = os.path.splitext(mf)[0] + '_static.fbx'
            payload['items'].append({'input': mf, 'output': out})
        return self._run_mayapy_batch(payload, mode='maya_to_fbx')

    def _generate_maya_batch_script(self, mode):
        return f'''# -*- coding: utf-8 -*-
import sys, os, json

json_path = sys.argv[1]
with open(json_path, 'r', encoding='utf-8') as f:
    data = json.load(f)

import maya.standalone
maya.standalone.initialize(name='python')
import maya.cmds as cmds
import maya.mel as mel

MODE = "{mode}"


def ensure_fbx_plugin():
    if not cmds.pluginInfo('fbxmaya', query=True, loaded=True):
        cmds.loadPlugin('fbxmaya')


def group_all_models(group_name):
    all_meshes = cmds.ls(type='mesh', long=True) or []
    transforms = []
    for mesh in all_meshes:
        p = cmds.listRelatives(mesh, parent=True, fullPath=True) or []
        transforms.extend(p)
    transforms = list(set(transforms))
    top_level = []
    for obj in transforms:
        parents = cmds.listRelatives(obj, parent=True, fullPath=True)
        if not parents:
            top_level.append(obj)
    if top_level:
        cmds.select(top_level, replace=True)
        cmds.group(name=group_name)


def cleanup_scene(group_name):
    lights = cmds.ls(type=['light', 'ambientLight', 'directionalLight', 'pointLight', 'spotLight'], long=True) or []
    light_tf = []
    for l in lights:
        light_tf.extend(cmds.listRelatives(l, parent=True, fullPath=True) or [])
    if light_tf:
        cmds.delete(list(set(light_tf)))

    cams = cmds.ls(type='camera', long=True) or []
    cam_tf = []
    for cam in cams:
        try:
            if not cmds.camera(cam, query=True, startupCamera=True):
                cam_tf.extend(cmds.listRelatives(cam, parent=True, fullPath=True) or [])
        except Exception:
            pass
    if cam_tf:
        cmds.delete(list(set(cam_tf)))

    for loc in cmds.ls(type='locator', long=True) or []:
        t = cmds.listRelatives(loc, parent=True, fullPath=True) or []
        if t:
            try: cmds.delete(t[0])
            except: pass

    for node in cmds.ls(type='transform', long=True) or []:
        try:
            shapes = cmds.listRelatives(node, shapes=True, fullPath=True, noIntermediate=True) or []
            children = cmds.listRelatives(node, children=True, fullPath=True) or []
            if (not shapes) and (not children):
                cmds.delete(node)
        except Exception:
            pass

    group_all_models(group_name)
    try:
        mel.eval('MLdeleteUnused();')
    except Exception:
        pass


def export_fbx_selected(output_file):
    mel.eval('FBXResetExport')
    mel.eval('FBXExportSmoothingGroups -v true')
    mel.eval('FBXExportHardEdges -v false')
    mel.eval('FBXExportTangents -v true')
    mel.eval('FBXExportSmoothMesh -v true')
    mel.eval('FBXExportInstances -v false')
    mel.eval('FBXExportTriangulate -v false')
    mel.eval('FBXExportShapes -v true')
    mel.eval('FBXExportAnimationOnly -v false')
    mel.eval('FBXExportBakeComplexAnimation -v false')
    mel.eval('FBXExportCameras -v false')
    mel.eval('FBXExportLights -v false')
    mel.eval('FBXExportUpAxis y')
    mel.eval('FBXExportFileVersion -v FBX202000')
    mel.eval('FBXExportScaleFactor 1.0')

    out_dir = os.path.dirname(output_file)
    if out_dir and not os.path.exists(out_dir):
        os.makedirs(out_dir)
    mel.eval('FBXExport -f "%s" -s' % output_file.replace('\\\\', '/'))


results = []
ensure_fbx_plugin()
cmds.undoInfo(state=False)
cmds.autoSave(enable=False)

for item in data.get('items', []):
    src = item.get('input', '')
    dst = item.get('output', '')
    try:
        if MODE == 'fbx_to_mb':
            cmds.file(f=True, new=True)
            cmds.file(src.replace('\\\\','/'), i=True, type='FBX', ignoreVersion=True, f=True)

            group_name = os.path.splitext(os.path.basename(src))[0]
            if data.get('clean_scene', True):
                cleanup_scene(group_name)
            else:
                group_all_models(group_name)

            out_dir = os.path.dirname(dst)
            if out_dir and not os.path.exists(out_dir):
                os.makedirs(out_dir)
            cmds.file(rename=dst.replace('\\\\','/'))
            cmds.file(save=True, type='mayaBinary', force=True)

            ok = os.path.exists(dst)
            info = os.path.basename(dst) if ok else '未生成输出文件'
            results.append({'input': src, 'ok': ok, 'info': info})

        else:
            cmds.file(src.replace('\\\\','/'), open=True, force=True)
            meshes = cmds.ls(type='mesh', long=True) or []
            if not meshes:
                results.append({'input': src, 'ok': False, 'info': 'no meshes found'})
                continue

            transforms = []
            for mesh in meshes:
                transforms.extend(cmds.listRelatives(mesh, parent=True, fullPath=True) or [])
            transforms = list(set(transforms))
            if not transforms:
                results.append({'input': src, 'ok': False, 'info': 'no transforms found'})
                continue

            cmds.select(transforms, replace=True)
            export_fbx_selected(dst)

            ok = os.path.exists(dst)
            info = os.path.basename(dst) if ok else '未生成输出文件'
            results.append({'input': src, 'ok': ok, 'info': info})

    except Exception as e:
        results.append({'input': src, 'ok': False, 'info': str(e)})

out = {'results': results}
with open(json_path, 'w', encoding='utf-8') as f:
    json.dump(out, f, ensure_ascii=False, indent=2)
'''

    # ---------- MAX 原始方式（逐文件） ----------
    def _convert_max_to_fbx_single(self, max_path):
        try:
            t0 = time.time()
            output_path = os.path.splitext(max_path)[0] + '.fbx'
            max_script = self._generate_max_to_fbx_script(output_path)

            with tempfile.NamedTemporaryFile(mode='w', suffix='.ms', delete=False, encoding='utf-8') as f:
                f.write(max_script)
                script_path = f.name

            try:
                cmd = [self.max_exe, '-sceneFile', max_path, script_path, '-v', '5']
                subprocess.run(cmd, capture_output=True, text=True, timeout=1800, encoding='utf-8', errors='ignore')
                if os.path.exists(output_path):
                    mb = os.path.getsize(output_path) / (1024.0 * 1024.0)
                    cost = self._format_duration(time.time() - t0)
                    return True, f"{os.path.basename(output_path)} ({mb:.2f} MB, {cost})"
                return False, '未生成输出文件'
            finally:
                try:
                    os.unlink(script_path)
                except Exception:
                    pass
        except Exception as e:
            return False, str(e)

    def _generate_max_to_fbx_script(self, output_path):
        output_path = output_path.replace('\\', '/')
        return f'''
(
    try
    (
        if maxFileName == "" then
        (
            print "ERROR: No scene file"
            quitMAX #noPrompt
        )

        local matPairs = #()
        local allMaterials = #()

        for obj in geometry do
        (
            if obj != undefined and not isDeleted obj and obj.material != undefined then
            (
                local mat = obj.material
                if classof mat == Multimaterial then
                (
                    for i = 1 to mat.numsubs do
                    (
                        if mat[i] != undefined then appendIfUnique allMaterials mat[i]
                    )
                )
                else appendIfUnique allMaterials mat
            )
        )

        for currentMat in allMaterials do
        (
            local newMat = StandardMaterial()
            newMat.name = currentMat.name + "_MatID"
            newMat.diffuse = random (color 50 50 50) (color 255 255 255)
            newMat.ambient = newMat.diffuse
            newMat.selfIllumAmount = 0
            append matPairs #(currentMat, newMat)
        )

        for obj in geometry do
        (
            if obj != undefined and not isDeleted obj then
            (
                try
                (
                    if superclassof obj == GeometryClass then
                    (
                        convertTo obj Editable_Poly
                        local oldMat = obj.material
                        if oldMat != undefined then
                        (
                            if classof oldMat == Multimaterial then
                            (
                                local newMultiMat = Multimaterial()
                                newMultiMat.name = oldMat.name + "_MatID"
                                newMultiMat.numsubs = oldMat.numsubs
                                for i = 1 to oldMat.numsubs do
                                (
                                    for pair in matPairs do
                                    (
                                        if pair[1] == oldMat[i] then
                                        (
                                            newMultiMat[i] = pair[2]
                                            exit
                                        )
                                    )
                                )
                                obj.material = newMultiMat
                            )
                            else
                            (
                                for pair in matPairs do
                                (
                                    if pair[1] == oldMat then
                                    (
                                        obj.material = pair[2]
                                        exit
                                    )
                                )
                            )
                        )
                    )
                )
                catch()
            )
        )

        try(renderers.current = ART_Renderer())catch()

        FBXExporterSetParam "FileVersion" "FBX202000"
        FBXExporterSetParam "ConvertUnit" "cm"
        FBXExporterSetParam "ScaleFactor" 1.0
        FBXExporterSetParam "UpAxis" "Y"
        FBXExporterSetParam "SmoothingGroups" true
        FBXExporterSetParam "Triangulate" false
        FBXExporterSetParam "PreserveEdgeOrientation" false
        FBXExporterSetParam "EmbedTextures" true
        FBXExporterSetParam "Animation" false
        FBXExporterSetParam "Cameras" false
        FBXExporterSetParam "Lights" false

        outputPath = "{output_path}"
        makeDir (getFilenamePath outputPath) all:true
        exportFile outputPath #noPrompt selectedOnly:false using:FBXEXP

        if (doesFileExist outputPath) then print "SUCCESS"
        else print "ERROR: FBX not created"
    )
    catch ex
    (
        print ("ERROR: " + (getCurrentException()))
    )
)
'''

    def run(self):
        self.root.mainloop()


if __name__ == "__main__":
    try:
        app = FileFormatConverter()
        app.run()
    except Exception as e:
        print(f"错误: {e}")
        messagebox.showerror("错误", f"程序启动失败:\n{str(e)}")
