# -*- coding: utf-8 -*-
"""
LazyIDA (Enhanced)
==================

A single-file enhanced version of LazyIDA that:
- Works with IDA 9.0 and Python 3.13
- Shows a full menu under Edit → Plugins (actions prefixed with "LazyIDA:")
- Keeps right‑click popup integrations (Disasm/Hex views) & Hex‑Rays additions
- Exposes a stable Python console API: import LazyIDA ; LazyIDA.main()/helpers
- Preserves original logic/shortcuts wherever possible

Place this file as:
    /opt/Ida/idapro9.0/plugins/LazyIDA.py

Tested against IDAPython 9.0 ABI. The code guards for PySide6/PyQt5.

Author: enhanced by ChatGPT based on LazyIDA v1.0~v1.1 public interface.
License: Same spirit as original LazyIDA (educational/demo). No warranty.
"""
from __future__ import annotations
from __future__ import print_function, division

import os
from struct import unpack

# ------------------------- IDA/PyQt compatibility -------------------------
try:
    import idaapi
    import idc
    import idautils
    import ida_kernwin
    import ida_bytes
    import ida_funcs
    import ida_name
    import ida_segment
    import ida_ua
    import ida_auto
    import ida_loader
    import ida_typeinf
    import ida_lines
    import ida_hexrays  # optional
except Exception as _e:
    print("[LazyIDA] 导入IDA模块失败:", _e)

# Qt clipboard support (PySide6 on IDA ≥ 9.2; IDA 9.0 ships with PyQt5)
_QAPP = None
try:
    from PySide6.QtWidgets import QApplication  # IDA ≥ 9.2
    _QAPP = QApplication.instance() or QApplication([])
except Exception:
    try:
        from PyQt5.QtWidgets import QApplication  # IDA 9.0 / Qt5.15
        _QAPP = QApplication.instance() or QApplication([])
    except Exception as _qe:
        print("[LazyIDA] Qt无法访问剪贴板:", _qe)
        _QAPP = None

# ------------------------------ Constants ---------------------------------
PLUGIN_NAME = "LazyIDA"
PLUGIN_VERSION = "1.3.0"

# Action ids (names must be globally unique)
A_PREFIX = "lazyida:"
A_CONVERT = [f"{A_PREFIX}convert{i}" for i in range(10)]
A_XORDATA = f"{A_PREFIX}xordata"
A_FILLNOP = f"{A_PREFIX}fillnop"
A_SCANVUL = f"{A_PREFIX}scanvul"
A_COPYEA  = f"{A_PREFIX}copyea"
A_COPYFO  = f"{A_PREFIX}copyfo"
A_GOTOEA  = f"{A_PREFIX}gotoclipea"
A_GOTOFO  = f"{A_PREFIX}gotoclipfo"

# Hex‑Rays actions
A_HX_REMRET = f"{A_PREFIX}hx_removerettype"
A_HX_COPYEA = f"{A_PREFIX}hx_copyea"
A_HX_COPYFO = f"{A_PREFIX}hx_copyfo"
A_HX_COPYNM = f"{A_PREFIX}hx_copyname"
A_HX_GOTOEA = f"{A_PREFIX}hx_gotoclipea"
A_HX_GOTOFO = f"{A_PREFIX}hx_gotoclipfo"

# --------------------------- Helper utilities -----------------------------

def _ensure_tmp_dir()->None:
    try:
        if not os.path.isdir("/tmp/ida"):
            print("Directory /tmp/ida 不存在，正在创建...")
            os.makedirs("/tmp/ida", exist_ok=True)
    except Exception as e:
        print("[LazyIDA] Failed to create /tmp/ida:", e)


def _copy_to_clip(text:str)->None:
    if _QAPP is not None:
        _QAPP.clipboard().setText(text)
    else:
        try:
            ida_kernwin.set_clipboard(text)
        except Exception:
            pass


def _clip_text()->str:
    if _QAPP is not None:
        return _QAPP.clipboard().text() or ""
    try:
        return ida_kernwin.get_clipboard() or ""
    except Exception:
        return ""


u16 = lambda x: unpack("<H", x)[0]
u32 = lambda x: unpack("<I", x)[0]
u64 = lambda x: unpack("<Q", x)[0]


# ---------------------------- Selection helpers ---------------------------

def _read_sel_or_item():
    """Return (start, end_exclusive, data_bytes) from selection or current item.
    Returns None if nothing reasonable is selected."""
    t0 = ida_kernwin.twinpos_t()
    t1 = ida_kernwin.twinpos_t()
    v = ida_kernwin.get_current_viewer()
    if v and ida_kernwin.read_selection(v, t0, t1):
        start = t0.place(v).toea()
        end   = t1.place(v).toea() + 1
    else:
        ea = idc.get_screen_ea()
        if ea == idaapi.BADADDR:
            return None
        size = idc.get_item_size(ea)
        if size <= 0:
            return None
        start, end = ea, ea + size
    data = ida_bytes.get_bytes(start, end - start)
    if data is None:
        return None
    if isinstance(data, str):  # python2 safety; here for completeness
        data = data.encode("latin-1", "ignore")
    return start, end, data


def _name_at(ea:int)->str:
    nm = idc.get_name(ea, idc.GN_VISIBLE)
    return nm if nm else "data"


def _filo(ea:int)->int:
    return idaapi.get_fileregion_offset(ea)


def _fileea(fo:int)->int:
    return idaapi.get_fileregion_ea(fo)


def _parse_loc(txt:str, is_fo:bool=False):
    try:
        val = int(txt.strip(), 16)
        return _fileea(val) if is_fo else val
    except Exception:
        # try name
        ea = idc.get_name_ea_simple(txt.strip())
        return ea


# ------------------------------ Converters --------------------------------

def _dump_escaped(data:bytes)->str:
    return '"' + ''.join("\\x%02X" % b for b in data) + '"'


def _dump_hex(data:bytes)->str:
    return ''.join("%02X" % b for b in data)


def _dump_c_array(data:bytes, width:int, name:str)->str:
    # width in bytes: 1/2/4/8
    pad = (width - (len(data) % width)) % width
    if pad:
        data = data + b"\x00" * pad
    if width == 1:
        hdr = f"无符号字符 {name}[{len(data)}] = {{"
        body = []
        for i,b in enumerate(data):
            if i % 16 == 0:
                body.append("\n    ")
            body.append("0x%02X, " % b)
        return hdr + ''.join(body)[:-2] + "\n};"
    if width == 2:
        hdr = f"无符号短整型 {name}[{len(data)//2}] = {{"
        body = []
        for i in range(0, len(data), 2):
            if i % 16 == 0:
                body.append("\n    ")
            body.append("0x%04X, " % u16(data[i:i+2]))
        return hdr + ''.join(body)[:-2] + "\n};"
    if width == 4:
        hdr = f"无符号整数 {name}[{len(data)//4}] = {{"
        body = []
        for i in range(0, len(data), 4):
            if i % 32 == 0:
                body.append("\n    ")
            body.append("0x%08X, " % u32(data[i:i+4]))
        return hdr + ''.join(body)[:-2] + "\n};"
    if width == 8:
        hdr = f"无符号长整型 {name}[{len(data)//8}] = {{"
        body = []
        for i in range(0, len(data), 8):
            if i % 32 == 0:
                body.append("\n    ")
            body.append("0x%016X, " % u64(data[i:i+8]))
        return hdr + ''.join(body)[:-2] + "\n};"
    return ""  # unreachable


def _dump_py_list(data:bytes, width:int)->str:
    pad = (width - (len(data) % width)) % width
    if pad:
        data = data + b"\x00" * pad
    if width == 1:
        return "[" + ", ".join("0x%02X" % b for b in data) + "]"
    if width == 2:
        return "[" + ", ".join("0x%04X" % u16(data[i:i+2]) for i in range(0, len(data), 2)) + "]"
    if width == 4:
        return "[" + ", ".join("0x%08X" % u32(data[i:i+4]) for i in range(0, len(data), 4)) + "]"
    if width == 8:
        return "[" + ", ".join(("%#018X" % u64(data[i:i+8])).replace("0X","0x") for i in range(0, len(data), 8)) + "]"
    return "[]"


# ------------------------------ Actions -----------------------------------
class _HotkeyHandler(ida_kernwin.action_handler_t):
    def __init__(self, kind:str):
        super().__init__()
        self.kind = kind

    def activate(self, ctx):
        if self.kind == A_COPYEA:
            ea = idc.get_screen_ea()
            if ea != idaapi.BADADDR:
                _copy_to_clip("0x%X" % ea)
                print("[LazyIDA] 复制EA:", "0x%X" % ea)
        elif self.kind == A_COPYFO:
            ea = idc.get_screen_ea()
            if ea != idaapi.BADADDR:
                fo = _filo(ea)
                if fo != idaapi.BADADDR:
                    _copy_to_clip("0x%X" % fo)
                    print("[LazyIDA] 复制FO:", "0x%X" % fo)
        elif self.kind == A_GOTOEA:
            txt = _clip_text()
            ea = _parse_loc(txt, False)
            if ea and ea != idaapi.BADADDR:
                idc.jumpto(ea)
        elif self.kind == A_GOTOFO:
            txt = _clip_text()
            ea = _parse_loc(txt, True)
            if ea and ea != idaapi.BADADDR:
                idc.jumpto(ea)
        return 1

    def update(self, ctx):
        return ida_kernwin.AST_ENABLE_ALWAYS


class _MenuHandler(ida_kernwin.action_handler_t):
    def __init__(self, dispatch:str):
        super().__init__()
        self.dispatch = dispatch

    def activate(self, ctx):
        # Dump/convert/XOR/NOP/Scan entrypoint dispatcher
        if self.dispatch in A_CONVERT:
            si = _read_sel_or_item()
            if not si:
                return 0
            start, end, data = si
            name = _name_at(start)
            if self.dispatch == A_CONVERT[0]:
                out = _dump_escaped(data)
            elif self.dispatch == A_CONVERT[1]:
                out = _dump_hex(data)
            elif self.dispatch == A_CONVERT[2]:
                out = _dump_c_array(data, 1, name)
            elif self.dispatch == A_CONVERT[3]:
                out = _dump_c_array(data, 2, name)
            elif self.dispatch == A_CONVERT[4]:
                out = _dump_c_array(data, 4, name)
            elif self.dispatch == A_CONVERT[5]:
                out = _dump_c_array(data, 8, name)
            elif self.dispatch == A_CONVERT[6]:
                out = _dump_py_list(data, 1)
            elif self.dispatch == A_CONVERT[7]:
                out = _dump_py_list(data, 2)
            elif self.dispatch == A_CONVERT[8]:
                out = _dump_py_list(data, 4)
            elif self.dispatch == A_CONVERT[9]:
                out = _dump_py_list(data, 8)
            else:
                out = ""
            _copy_to_clip(out)
            print("\n[LazyIDA] 转储 0x%X-0x%X (%u bytes):\n%s" % (start, end-1, end-start, out))
            return 1

        if self.dispatch == A_XORDATA:
            si = _read_sel_or_item()
            if not si:
                return 0
            start, end, data = si
            x = ida_kernwin.ask_long(0, "XOR 与 (字节 0-255)")
            if x is None:
                return 0
            x &= 0xFF
            res = ''.join(chr(b ^ x) for b in data)
            print("\n[LazyIDA] XOR 0x%X-0x%X (%u) 与 0x%02X:\n%s" % (start, end-1, end-start, x, repr(res)))
            return 1

        if self.dispatch == A_FILLNOP:
            si = _read_sel_or_item()
            if not si:
                return 0
            start, end, data = si
            ida_bytes.patch_bytes(start, b"\x90"*(end-start))
            print("[LazyIDA] 填充NOP: 0x%X-0x%X" % (start, end-1))
            return 1

        if self.dispatch == A_SCANVUL:
            _scan_format_vulns()
            return 1

        return 0

    def update(self, ctx):
        # enable in disasm/hex
        wt = ida_kernwin.get_widget_type(ctx.widget)
        try:
            hv = ida_kernwin.BWN_HEXVIEW
        except Exception:
            hv = ida_kernwin.BWN_DUMP
        return (ida_kernwin.AST_ENABLE_FOR_WIDGET
                if wt in (ida_kernwin.BWN_DISASM, hv) else ida_kernwin.AST_DISABLE_FOR_WIDGET)


# --------------------------- Vulnerability scan ---------------------------

def _scan_format_vulns():
    print("\n[LazyIDA] 查找格式字符串漏洞 ...")
    found = []
    for f_ea in idautils.Functions():
        fname = idc.get_func_name(f_ea) or ""
        # quick filter for printf-like calls present in the function name (plt stubs, imports)
        if "printf" not in fname:
            # Still need to scan xrefs to imported printf; iterate code xrefs to external symbol
            pass
        # Walk code xrefs to this function if it's a *printf
        if fname.endswith(("printf", "sprintf", "snprintf", "dprintf", "fprintf", "printf_chk", "sprintf_chk", "snprintf_chk")):
            for xr in idautils.CodeRefsTo(f_ea, False):
                rec = _analyze_possible_format_arg(fname, xr)
                if rec:
                    found.append(rec)

    if found:
        for addr, name, opnd in found:
            print("0x%X: %s 格式=%s" % (addr, name, opnd))
        try:
            _VulnChoose("LazyIDA: 格式字符串漏洞", found).Show()
        except Exception:
            pass
        print("[LazyIDA] 完成！%d 个潜在位置." % len(found))
    else:
        print("[LazyIDA] 未找到。")


def _analyze_possible_format_arg(callee_name:str, call_ea:int):
    fn = ida_funcs.get_func(call_ea)
    if not fn:
        return None
    ea = call_ea
    # Walk backward in the function looking for the instruction that sets the
    # format argument register/stack location. This is heuristic and arch‑dependent.
    # We implement the same simplified heuristics as classic LazyIDA.
    while True:
        ea = idc.prev_head(ea)
        if ea == idaapi.BADADDR or ea < fn.start_ea:
            return None
        mnem = idc.print_insn_mnem(ea).lower()
        dst = idc.print_operand(ea, 0)
        if mnem in ("ret", "retn", "jmp", "b"):
            return None
        cmt = idc.get_cmt(ea, 0)
        if cmt and cmt.lower() == "format":
            break
        # micro rules adapted from original LazyIDA
        if callee_name.endswith(("snprintf_chk",)):
            if mnem in ("mov", "lea") and dst.endswith(("r8", "r8d", "[esp+10h]")):
                break
        elif callee_name.endswith(("sprintf_chk",)):
            if mnem in ("mov", "lea") and (dst.endswith(("rcx", "[esp+0Ch]", "R3")) or dst.endswith("ecx")):
                break
        elif callee_name.endswith(("snprintf", "fnprintf")):
            if mnem in ("mov", "lea") and (dst.endswith(("rdx", "[esp+8]", "R2")) or dst.endswith("edx")):
                break
        elif callee_name.endswith(("sprintf", "fprintf", "dprintf", "printf_chk")):
            if mnem in ("mov", "lea") and (dst.endswith(("rsi", "[esp+4]", "R1")) or dst.endswith("esi")):
                break
        elif callee_name.endswith("printf"):
            if mnem in ("mov", "lea") and (dst.endswith(("rdi", "[esp]", "R0")) or dst.endswith("edi")):
                break

    # Resolve operand for that instruction (register or memory)
    op_index = idc.generate_disasm_line(ea, 0).count(",")
    op_type = idc.get_operand_type(ea, op_index)
    opnd = idc.print_operand(ea, op_index)

    if op_type == idc.o_reg:
        # try to track data‑flow one step back (simple heuristic)
        bea = ea
        while True:
            bea = idc.prev_head(bea)
            if bea == idaapi.BADADDR or bea < fn.start_ea:
                break
            bm = idc.print_insn_mnem(bea).lower()
            if bm in ("ret", "retn", "jmp", "b"):
                break
            if bm in ("mov", "lea", "ldr") and idc.print_operand(bea, 0) == opnd:
                op_type = idc.get_operand_type(bea, 1)
                opnd = idc.print_operand(bea, 1)
                ea = bea
                break

    if op_type in (idc.o_imm, idc.o_mem):
        op_addr = idc.get_operand_value(ea, op_index)
        seg = ida_segment.getseg(op_addr)
        if seg and (seg.perm & idaapi.SEGPERM_WRITE) == 0:
            # read‑only constant format string -> likely safe
            return None

    return (ea, callee_name, opnd)


class _VulnChoose(ida_kernwin.Choose):
    def __init__(self, title, items):
        super().__init__(title, [["Address", 18], ["Function", 26], ["Format", 32]])
        # normalize to strings for the chooser
        self.items = [["0x%X" % a, n, f] for a,n,f in items]
        self.icon = 45
    def OnGetLine(self, n):
        return self.items[n]
    def OnGetSize(self):
        return len(self.items)
    def OnSelectLine(self, n):
        try:
            idc.jumpto(int(self.items[n][0], 16))
        except Exception:
            pass


# ------------------------------ Hex‑Rays ----------------------------------
class _HexRaysHandler(ida_kernwin.action_handler_t):
    def __init__(self, which:str):
        super().__init__()
        self.which = which
        self._saved_rettypes = {}

    def activate(self, ctx):
        if self.which == A_HX_REMRET:
            vdui = ida_hexrays.get_widget_vdui(ctx.widget)
            if vdui:
                self._toggle_remove_ret(vdui)
                vdui.refresh_ctext()
                return 1
            return 0
        elif self.which == A_HX_COPYEA:
            ea = idc.get_screen_ea()
            if ea != idaapi.BADADDR:
                _copy_to_clip("0x%X" % ea)
                print("[LazyIDA] HexRays: 复制 EA")
            return 1
        elif self.which == A_HX_COPYFO:
            ea = idc.get_screen_ea()
            if ea != idaapi.BADADDR:
                fo = _filo(ea)
                if fo != idaapi.BADADDR:
                    _copy_to_clip("0x%X" % fo)
                    print("[LazyIDA] HexRays: 复制 FO")
            return 1
        elif self.which == A_HX_COPYNM:
            hv = ida_kernwin.get_current_viewer()
            hl = ida_kernwin.get_highlight(hv)
            name = hl[0] if hl and hl[1] else None
            if name:
                _copy_to_clip(name)
                print("[LazyIDA] HexRays: 复制名称 '", name, "'", sep="")
            return 1
        elif self.which == A_HX_GOTOEA:
            ea = _parse_loc(_clip_text(), False)
            if ea and ea != idaapi.BADADDR:
                idc.jumpto(ea)
            return 1
        elif self.which == A_HX_GOTOFO:
            ea = _parse_loc(_clip_text(), True)
            if ea and ea != idaapi.BADADDR:
                idc.jumpto(ea)
            return 1
        return 0

    def update(self, ctx):
        return ida_kernwin.AST_ENABLE_FOR_WIDGET if ida_hexrays.get_widget_vdui(ctx.widget) else ida_kernwin.AST_DISABLE_FOR_WIDGET

    # Toggle between void/non‑void return type for the current function/call
    def _toggle_remove_ret(self, vdui):
        item = vdui.item
        if item.citype == ida_hexrays.VDI_FUNC:
            ea = vdui.cfunc.entry_ea
            tif = ida_typeinf.tinfo_t()
            if not vdui.cfunc.get_func_type(tif):
                return False
        elif item.citype == ida_hexrays.VDI_EXPR and item.e.is_expr() and item.e.type.is_funcptr():
            ea = item.get_ea()
            f = ida_funcs.get_func(ea)
            if not f:
                return False
            try:
                cfunc = ida_hexrays.decompile(f)
            except Exception:
                return False
            tif = ida_typeinf.tinfo_t()
            if not cfunc.get_func_type(tif):
                return False
        else:
            return False

        ftd = ida_typeinf.func_type_data_t()
        if not tif.get_func_details(ftd):
            return False

        # already void? restore if saved
        key = int(ea)
        if ftd.rettype.is_decl_void():
            if key not in self._saved_rettypes:
                return True
            new_ret = self._saved_rettypes[key]
        else:
            # save & set to void
            self._saved_rettypes[key] = ftd.rettype
            new_ret = ida_typeinf.tinfo_t(ida_typeinf.BT_VOID)

        ftd.rettype = new_ret
        new_tif = ida_typeinf.tinfo_t()
        new_tif.create_func(ftd)
        if ida_typeinf.apply_tinfo(ea, new_tif, ida_typeinf.TINFO_DEFINITE):
            vdui.refresh_view(True)
            return True
        return False


class _UIHooks(ida_kernwin.UI_Hooks):
    def finish_populating_widget_popup(self, widget, popup):
        wt = ida_kernwin.get_widget_type(widget)
        try:
            hv = ida_kernwin.BWN_HEXVIEW
        except Exception:
            hv = ida_kernwin.BWN_DUMP
        if wt in (ida_kernwin.BWN_DISASM, hv):
            si = _read_sel_or_item()
            if si:
                ida_kernwin.attach_action_to_popup(widget, popup, A_XORDATA, None)
                ida_kernwin.attach_action_to_popup(widget, popup, A_FILLNOP, None)
                for a in A_CONVERT:
                    ida_kernwin.attach_action_to_popup(widget, popup, a, "转储/")
        if wt == ida_kernwin.BWN_DISASM:
            ida_kernwin.attach_action_to_popup(widget, popup, A_SCANVUL, None)


# ----------------------------- Registration -------------------------------
_registered = []
_hx_registered = []
_hooks = None


def _register_actions_and_menus():
    global _registered, _hooks

    labels = [
        (A_CONVERT[0], "LazyIDA: 转储为转义字符串"),
        (A_CONVERT[1], "LazyIDA: 以十六进制字符串形式转储"),
        (A_CONVERT[2], "LazyIDA: 转储为C数组(BYTE)"),
        (A_CONVERT[3], "LazyIDA: 转储为C数组(WORD)"),
        (A_CONVERT[4], "LazyIDA: 转储为C数组(DWORD)"),
        (A_CONVERT[5], "LazyIDA: 转储为C数组(QWORD)"),
        (A_CONVERT[6], "LazyIDA: 作为 Python 列表转储(BYTE)"),
        (A_CONVERT[7], "LazyIDA: 作为 Python 列表转储(WORD)"),
        (A_CONVERT[8], "LazyIDA: 作为 Python 列表转储(DWORD)"),
        (A_CONVERT[9], "LazyIDA: 作为 Python 列表转储(QWORD)"),
        (A_XORDATA,    "LazyIDA: XOR 数据"),
        (A_FILLNOP,    "LazyIDA: 填充NOP指令"),
        (A_SCANVUL,    "LazyIDA: 扫描格式字符串漏洞"),
        (A_COPYEA,     "LazyIDA: 复制EA"),
        (A_COPYFO,     "LazyIDA: 复制FO"),
        (A_GOTOEA,     "LazyIDA: 转到剪贴板 EA"),
        (A_GOTOFO,     "LazyIDA: 前往剪贴板 FO"),
    ]

    # Register menu/action handlers
    for aid, lbl in labels:
        handler = _MenuHandler(aid) if aid in (A_CONVERT + [A_XORDATA, A_FILLNOP, A_SCANVUL]) else _HotkeyHandler(aid)
        desc = ida_kernwin.action_desc_t(aid, lbl, handler, None, lbl, -1)
        if ida_kernwin.register_action(desc):
            _registered.append(aid)
            # Attach under existing menu node. IDA doesn't allow creating our own submenu easily here,
            # so we prefix labels with "LazyIDA:" for grouping.
            ida_kernwin.attach_action_to_menu("Edit/Plugins/", aid, ida_kernwin.SETMENU_APP)

    # Install UI hooks for popup menus
    _hooks = _UIHooks()
    _hooks.hook()


def _register_hexrays():
    global _hx_registered
    if not ida_hexrays.init_hexrays_plugin():
        return False
    try:
        addon = idaapi.addon_info_t()
        addon.id = "tw.l4ys.lazyida.enhanced"
        addon.name = PLUGIN_NAME
        addon.producer = "Lays + Enhanced"
        addon.version = PLUGIN_VERSION
        addon.url = "https://github.com/L4ys/LazyIDA"
        idaapi.register_addon(addon)
    except Exception:
        pass

    hx_labels = [
        (A_HX_REMRET, "LazyIDA: HexRays 移除返回类型"),
        (A_HX_COPYEA, "LazyIDA: HexRays 复制 EA"),
        (A_HX_COPYFO, "LazyIDA: HexRays 复制 FO"),
        (A_HX_GOTOEA, "LazyIDA: HexRays 转到剪贴板 EA"),
        (A_HX_GOTOFO, "LazyIDA: HexRays 转到剪贴板 FO"),
        (A_HX_COPYNM, "LazyIDA: HexRays 复制名称"),
    ]

    for aid, lbl in hx_labels:
        desc = ida_kernwin.action_desc_t(aid, lbl, _HexRaysHandler(aid), None, lbl, -1)
        if ida_kernwin.register_action(desc):
            _hx_registered.append(aid)
            ida_kernwin.attach_action_to_menu("Edit/Plugins/", aid, ida_kernwin.SETMENU_APP)
    return True


def _unregister_all():
    global _registered, _hx_registered, _hooks
    for a in _registered:
        try:
            ida_kernwin.unregister_action(a)
        except Exception:
            pass
    _registered = []
    for a in _hx_registered:
        try:
            ida_kernwin.unregister_action(a)
        except Exception:
            pass
    _hx_registered = []
    if _hooks:
        try:
            _hooks.unhook()
        except Exception:
            pass
        _hooks = None


# --------------------------- Console API layer ----------------------------
class LazyIDA:
    """Console‑friendly API (keeps original spirit & names where possible).

    Example usage in IDA Python console:
        import LazyIDA
        # dump current item as hex string
        LazyIDA.dump_current("hex")
        # XOR selection with 0xAA (prints result string)
        LazyIDA.xor_current(0xAA)
        # Convert 16 bytes from EA to python list of dwords
        LazyIDA.dump_bytes(0x401000, 16, fmt="py_dword")
        # Scan printf‑like format string issues
        LazyIDA.scan_fmt_vuln()
    """

    @staticmethod
    def main():
        print(f"{PLUGIN_NAME} v{PLUGIN_VERSION} main() — actions are under Edit → Plugins, prefixed with 'LazyIDA:'")

    @staticmethod
    def dump_current(fmt:str="hex")->str:
        si = _read_sel_or_item()
        if not si:
            print("[LazyIDA] No selection/item.")
            return ""
        _, _, data = si
        out = LazyIDA._format_data(data, fmt, name="data")
        _copy_to_clip(out)
        print(out)
        return out

    @staticmethod
    def dump_bytes(ea:int, size:int, fmt:str="hex")->str:
        data = ida_bytes.get_bytes(ea, size) or b""
        out = LazyIDA._format_data(data, fmt, name=_name_at(ea))
        _copy_to_clip(out)
        print(out)
        return out

    @staticmethod
    def xor_current(key:int)->str:
        si = _read_sel_or_item()
        if not si:
            print("[LazyIDA] No selection/item.")
            return ""
        _, _, data = si
        key &= 0xFF
        res = ''.join(chr(b ^ key) for b in data)
        print(res)
        return res

    @staticmethod
    def fill_nop_current()->None:
        si = _read_sel_or_item()
        if not si:
            print("[LazyIDA] No selection/item.")
            return None
        s,e,_ = si
        ida_bytes.patch_bytes(s, b"\x90"*(e-s))
        print("[LazyIDA] 填充NOP: 0x%X-0x%X" % (s, e-1))

    @staticmethod
    def scan_fmt_vuln()->None:
        _scan_format_vulns()

    @staticmethod
    def _format_data(data:bytes, fmt:str, name:str)->str:
        fmt = (fmt or "").lower()
        if fmt in ("str", "escaped", "string"):
            return _dump_escaped(data)
        if fmt in ("hex", "hexstr"):
            return _dump_hex(data)
        if fmt in ("c", "c_byte"):
            return _dump_c_array(data, 1, name)
        if fmt in ("c_word",):
            return _dump_c_array(data, 2, name)
        if fmt in ("c_dword",):
            return _dump_c_array(data, 4, name)
        if fmt in ("c_qword",):
            return _dump_c_array(data, 8, name)
        if fmt in ("py", "py_byte", "list", "python"):
            return _dump_py_list(data, 1)
        if fmt in ("py_word",):
            return _dump_py_list(data, 2)
        if fmt in ("py_dword",):
            return _dump_py_list(data, 4)
        if fmt in ("py_qword",):
            return _dump_py_list(data, 8)
        # default
        return _dump_hex(data)


# ------------------------------- Plugin -----------------------------------
class _Plugin(idaapi.plugin_t):
    flags = idaapi.PLUGIN_FIX  # load on startup & keep resident
    comment = "LazyIDA Enhanced"
    help = "Data dumper, helpers, Hex‑Rays tweaks"
    wanted_name = PLUGIN_NAME
    wanted_hotkey = ""

    def init(self):
        _ensure_tmp_dir()
        if not ida_auto.auto_is_ok():
            # still allow registration; actions will work post‑analysis
            pass
        _register_actions_and_menus()
        if 'ida_hexrays' in globals():
            try:
                _register_hexrays()
            except Exception as e:
                print("[LazyIDA] HexRays registration failed:", e)
        print(f"{PLUGIN_NAME} (v{PLUGIN_VERSION}) 永乐汉化版插件已加载。")
        return idaapi.PLUGIN_KEEP

    def run(self, arg):
        LazyIDA.main()

    def term(self):
        _unregister_all()
        print(f"[LazyIDA] terminated.")


def PLUGIN_ENTRY():
    return _Plugin()