#!/usr/bin/env python3
"""Helpers for scanning direct x86 call instructions in Hwanse2.exe."""
from __future__ import annotations

import re
import struct
import subprocess
from pathlib import Path


CALL_RE = re.compile(
    r"^\s*([0-9a-fA-F]+):\s+(?:[0-9a-fA-F]{2}\s+)+\s*call\s+(0x[0-9a-fA-F]+|[0-9a-fA-F]{5,})"
)


def hex32(value: int) -> str:
    return f"0x{value:08x}"


def section_for_name(sections: list[dict], name: str) -> dict:
    return next(section for section in sections if section["name"] == name)


def section_for_va(sections: list[dict], va: int) -> dict | None:
    for section in sections:
        if section["va"] <= va < section["va"] + section["raw_size"]:
            return section
    return None


def va_in_section(section: dict, va: int) -> bool:
    return section["va"] <= va < section["va"] + section["raw_size"]


def objdump_direct_calls(exe_path: Path, sections: list[dict]) -> list[dict]:
    text = section_for_name(sections, ".text")
    result = subprocess.run(
        ["objdump", "-Mintel", "-D", "-b", "pei-i386", "-m", "i386", str(exe_path)],
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        errors="ignore",
    )
    calls = []
    for line in result.stdout.splitlines():
        match = CALL_RE.match(line)
        if not match:
            continue
        call_va = int(match.group(1), 16)
        if not va_in_section(text, call_va):
            continue
        target_text = match.group(2)
        target_va = int(target_text, 16)
        target_section = section_for_va(sections, target_va)
        if not target_section or target_section["name"] != ".text":
            continue
        calls.append({
            "callVa": call_va,
            "callVaHex": hex32(call_va),
            "targetVa": target_va,
            "targetVaHex": hex32(target_va),
            "scanMethod": "objdump-direct-call",
        })
    return calls


def raw_rel32_calls(exe: bytes, sections: list[dict]) -> list[dict]:
    text = section_for_name(sections, ".text")
    raw_start = text["raw"]
    raw = exe[raw_start: raw_start + text["raw_size"]]
    calls = []
    for index in range(0, max(0, len(raw) - 4)):
        if raw[index] != 0xE8:
            continue
        call_va = text["va"] + index
        rel = struct.unpack_from("<i", raw, index + 1)[0]
        target_va = call_va + 5 + rel
        target_section = section_for_va(sections, target_va)
        if not target_section or target_section["name"] != ".text":
            continue
        calls.append({
            "callVa": call_va,
            "callVaHex": hex32(call_va),
            "targetVa": target_va,
            "targetVaHex": hex32(target_va),
            "scanMethod": "raw-rel32-fallback",
        })
    return calls


def direct_text_calls(exe: bytes, sections: list[dict], exe_path: Path | None = None) -> tuple[list[dict], str]:
    if exe_path is not None:
        try:
            calls = objdump_direct_calls(exe_path, sections)
            return calls, "objdump-direct-call"
        except (OSError, subprocess.SubprocessError, ValueError):
            pass
    return raw_rel32_calls(exe, sections), "raw-rel32-fallback"
