from __future__ import annotations

import json
import queue
import shlex
import threading
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple

import frida
from prompt_toolkit.application import Application
from prompt_toolkit.application.current import get_app
from prompt_toolkit.data_structures import Point
from prompt_toolkit.filters import Condition, has_focus
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.containers import (
    ConditionalContainer,
    DynamicContainer,
    Float,
    FloatContainer,
    HSplit,
    VSplit,
    Window,
)
from prompt_toolkit.layout.controls import FormattedTextControl, UIContent, UIControl
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.widgets import Frame, TextArea

from frida_tools.application import ConsoleApplication
from frida_tools.reactor import Reactor


def main() -> None:
    app = StraceApplication()
    app.run()


class StraceApplication(ConsoleApplication):
    def __init__(self) -> None:
        self._state = "starting"
        self._ready = threading.Event()

        super().__init__(self._process_input)

        self._tracer: Optional[SyscallTracer] = None

        self._events: List[SyscallEvent] = []
        self._excluded_syscalls: set[tuple[str, int]] = set()
        self._selected = 0
        self._hscroll = 0
        self._tailing = True

        self._paused_events: Optional[List[SyscallEvent]] = None
        self._pending_by_key: Dict[Tuple[int, int, int], int] = {}

        self._show_details = True
        self._details_breakpoint_cols = 120

        self._lock = threading.RLock()

        self._ui_app: Optional[Application] = None
        self._list_win: Optional[Window] = None

        self._filter_text = ""

        self._search_text = ""
        self._search_editing = False
        self._search_prev_text = ""
        self._search_matches: List[int] = []
        self._search_index = 0

        self._ignore_search_text_change = False
        self._status_message = ""

        self._fd_style_by_pid_value: Dict[Tuple[int, int], str] = {}
        self._fd_color_palette = [
            "ansiblue",
            "ansigreen",
            "ansicyan",
            "ansimagenta",
            "ansiyellow",
            "ansibrightblue",
            "ansibrightgreen",
            "ansibrightcyan",
            "ansibrightmagenta",
            "ansibrightyellow",
        ]
        self._next_fd_color = 0

        self._list_model_version = 0
        self._filter_cache_key: Optional[Tuple[int, str, bool, int]] = None
        self._filter_cache: List[SyscallEvent] = []

        self._list_model = ListModel(
            snapshot=self._get_list_snapshot,
            format_dt=self._format_dt,
            format_line=self._format_event_line,
            highlight_line=self._highlight_line,
        )
        self._list_ctl: UIControl = SyscallListControl(self._list_model)

        self._detail_ctl = FormattedTextControl(text=self._get_detail_text)
        self._status_ctl = FormattedTextControl(text=self._get_status_text)

        self._search_bar = TextArea(
            height=1,
            prompt="/",
            multiline=False,
            wrap_lines=False,
        )
        self._search_bar.buffer.on_text_changed += self._on_search_text_changed

        self._platform: Optional[str] = None
        self._ui_refresh_pending = False

    def _needs_target(self) -> bool:
        return False

    def _add_options(self, parser) -> None:
        parser.add_argument(
            "-f",
            "--file",
            action="append",
            dest="files",
            help="spawn FILE [args...] (repeatable); pass as one string, e.g. -f '/bin/ls -l /'",
        )
        parser.add_argument("-p", "--pid", action="append", type=int, dest="pids", help="Trace PID (repeatable)")
        parser.add_argument(
            "-u", "--user", action="append", dest="users", help="Trace processes owned by USER (repeatable)"
        )
        parser.add_argument("--uid", action="append", type=int, dest="uids", help="Trace UID (repeatable)")
        parser.add_argument("-x", "--exclude", action="append", dest="exclude",
                            help="Exclude syscall by name (repeatable), e.g. --exclude openat")
        parser.add_argument("--limit", type=int, default=5000, help="Max events kept in UI (default: 5000)")

    def _initialize(self, parser, options, args) -> None:
        self._files = options.files
        self._pids = options.pids
        self._users = options.users
        self._uids = options.uids
        self._excluded_names_requested = options.exclude
        self._limit = options.limit

        if self._files is None and self._pids is None and self._users is None and self._uids is None:
            raise ValueError("At least one target must be specified (use --file, --pid, --user, and/or --uid).")

    def _usage(self) -> str:
        return "%(prog)s [options]"

    def _start(self) -> None:
        params = self._device.query_system_parameters()
        self._platform = params["platform"]

        self._tracer = SyscallTracer(self._reactor)
        self._tracer.set_notify(self._notify_ui)
        self._tracer.set_platform(self._platform)

        spawned_pids: List[int] = []
        pids: List[int] = []
        if self._files is not None:
            for spec in self._files:
                argv = shlex.split(spec)
                if not argv:
                    continue
                program = argv[0]
                pid = self._device.spawn(program, argv if len(argv) > 1 else None)
                spawned_pids.append(pid)
                pids.append(pid)
        if self._pids is not None:
            pids.extend(self._pids)

        targets_req: Dict[str, Any] = {}
        if pids:
            targets_req["pids"] = pids
        if self._users is not None:
            targets_req["users"] = self._users
        if self._uids is not None:
            targets_req["uids"] = self._uids

        try:
            self._tracer.start(self._device, targets_req, self._excluded_names_requested)
        except Exception as e:
            self._state = "stopping"
            self._ready.set()
            self._log("error", f"Unable to start: {e}")
            self._exit(1)
            return
        finally:
            for pid in spawned_pids:
                self._device.resume(pid)

        excluded = self._tracer.get_excluded()
        if excluded:
            with self._lock:
                self._excluded_syscalls = set(excluded)

        self._ui_app = self._create_ui()

        self._state = "started"
        self._ready.set()

    def _stop(self) -> None:
        if self._tracer is not None:
            try:
                self._tracer.stop()
            except Exception:
                pass
            self._tracer = None

        if self._ui_app is not None:
            try:
                self._ui_app.exit()
            except Exception:
                pass
            self._ui_app = None

    def _process_input(self, reactor: Reactor) -> None:
        try:
            while not self._ready.wait(0.5):
                if not reactor.is_running():
                    return
        except KeyboardInterrupt:
            reactor.cancel_io()
            return

        if self._state != "started":
            return

        assert self._ui_app is not None
        self._ui_app.run()

    def _create_ui(self) -> Application:
        kb = KeyBindings()

        @kb.add("q")
        @kb.add("c-c")
        def _(event):
            self._reactor.cancel_io()
            self._exit(0)
            event.app.exit()

        @kb.add("/", filter=~has_focus(self._search_bar.control))
        def _(event):
            self._begin_search(event)

        @kb.add("down", filter=~has_focus(self._search_bar.control))
        @kb.add("j", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._move_selected_locked(+1)
            event.app.invalidate()

        @kb.add("up", filter=~has_focus(self._search_bar.control))
        @kb.add("k", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._move_selected_locked(-1)
            event.app.invalidate()

        @kb.add("c-b", filter=~has_focus(self._search_bar.control))
        @kb.add("pageup", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                delta = self._page_delta_locked(-1, fraction=1.0)
                self._move_selected_locked(delta)
            event.app.invalidate()

        @kb.add("c-f", filter=~has_focus(self._search_bar.control))
        @kb.add("pagedown", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                delta = self._page_delta_locked(+1, fraction=1.0)
                self._move_selected_locked(delta)
            event.app.invalidate()

        @kb.add("c-d", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                delta = self._page_delta_locked(+1, fraction=0.5)
                self._move_selected_locked(delta)
            event.app.invalidate()

        @kb.add("c-u", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                delta = self._page_delta_locked(-1, fraction=0.5)
                self._move_selected_locked(delta)
            event.app.invalidate()

        @kb.add("home", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._pause_if_tailing()
                self._selected = 0
                self._clamp_selected_locked()
            event.app.invalidate()

        @kb.add("t", filter=~has_focus(self._search_bar.control))
        @kb.add("end", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._tailing = True
                self._paused_events = None
                self._bump_list_model_version_locked()
                self._invalidate_filter_cache_locked()
                self._recompute_search_matches_locked()
                view = self._get_filtered_view_locked()
                self._selected = max(0, len(view) - 1)
            assert self._list_win is not None
            event.app.layout.focus(self._list_win)
            event.app.invalidate()

        @kb.add("left", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._hscroll = max(0, self._hscroll - 8)
            event.app.invalidate()

        @kb.add("right", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._hscroll += 8
            event.app.invalidate()

        @kb.add("0", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._hscroll = 0
            event.app.invalidate()

        @kb.add("enter", filter=has_focus(self._search_bar.control))
        def _(event):
            self._end_search(keep=True, event=event)

        @kb.add("escape", filter=has_focus(self._search_bar.control))
        def _(event):
            self._end_search(keep=False, event=event)

        @kb.add("c-u", filter=has_focus(self._search_bar.control))
        def _(event):
            self._set_search_bar_text("")
            event.app.invalidate()

        @kb.add("n", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._pause_if_tailing()
                self._recompute_search_matches_locked()
                if self._search_matches:
                    self._status_message = ""
                    self._search_index = (self._search_index + 1) % len(self._search_matches)
                    self._selected = self._search_matches[self._search_index]
            event.app.invalidate()

        @kb.add("N", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._pause_if_tailing()
                self._recompute_search_matches_locked()
                if self._search_matches:
                    self._status_message = ""
                    self._search_index = (self._search_index - 1) % len(self._search_matches)
                    self._selected = self._search_matches[self._search_index]
            event.app.invalidate()

        @kb.add("f", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                if self._search_text != "":
                    self._filter_text = self._search_text
                    self._search_text = ""
                    self._search_prev_text = ""
                    self._search_matches = []
                    self._search_index = 0

                    self._tailing = True
                    self._paused_events = None

                    self._bump_list_model_version_locked()
                    self._invalidate_filter_cache_locked()
                    view = self._get_filtered_view_locked()
                    self._selected = max(0, len(view) - 1)
            event.app.invalidate()

        @kb.add("c-l", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._filter_text = ""
                self._invalidate_filter_cache_locked()
                self._bump_list_model_version_locked()
                self._recompute_search_matches_locked()
                self._clamp_selected_locked()
            event.app.invalidate()

        @kb.add("d", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                self._show_details = not self._show_details
            event.app.layout.focus(self._list_win)
            event.app.invalidate()

        @kb.add("enter", filter=~has_focus(self._search_bar.control))
        def _(event):
            self._on_enter()
            event.app.invalidate()

        @kb.add("x", filter=~has_focus(self._search_bar.control))
        def _(event):
            with self._lock:
                view = self._get_filtered_view_locked()
                if not (0 <= self._selected < len(view)):
                    return
                ev = view[self._selected]

                key = (ev.abi, ev.nr)
                if key in self._excluded_syscalls:
                    self._status_message = f"Already excluded: {ev.name} ({ev.abi} #{ev.nr})"
                    event.app.invalidate()
                    return

                self._excluded_syscalls.add(key)
                removed = self._exclude_events_locked(key)

                self._status_message = f"Excluded: {ev.name} ({ev.abi} #{ev.nr})"
                if removed:
                    self._status_message += f" (purged {removed} events)"

            self._tracer.exclude_syscalls([key])

            event.app.invalidate()

        list_win = Window(
            content=self._list_ctl,
            wrap_lines=False,
            cursorline=True,
            always_hide_cursor=False,
            get_horizontal_scroll=lambda w: self._hscroll,
        )
        self._list_win = list_win

        detail_win = Window(content=self._detail_ctl, wrap_lines=True, always_hide_cursor=True)
        status_win = Window(height=1, content=self._status_ctl, wrap_lines=False)

        list_frame = Frame(list_win, title="Syscalls")
        detail_frame = Frame(detail_win, title="Details")
        status_frame = Frame(status_win, title="Status")

        def panes_container():
            if not self._show_details:
                return list_frame

            cols = get_app().output.get_size().columns
            if cols < self._details_breakpoint_cols:
                return HSplit([list_frame, detail_frame])

            return VSplit(
                [
                    Frame(list_win, title="Syscalls", width=Dimension(weight=3)),
                    Frame(detail_win, title="Details", width=Dimension(weight=2)),
                ]
            )

        body = HSplit(
            [
                DynamicContainer(panes_container),
                status_frame,
            ]
        )

        search_win = Window(height=1, content=self._search_bar.control, wrap_lines=False)
        search_float = ConditionalContainer(
            content=Frame(search_win, title="Search"),
            filter=Condition(lambda: self._search_editing),
        )

        root = FloatContainer(
            content=body,
            floats=[
                Float(
                    content=search_float,
                    bottom=1,
                    left=0,
                    right=0,
                    height=3,
                )
            ],
        )

        layout = Layout(root, focused_element=list_win)
        app = Application(layout=layout, key_bindings=kb, full_screen=True)
        return app

    def _get_list_snapshot(self) -> ListSnapshot:
        with self._lock:
            base = self._get_base_view_locked()
            view = self._get_filtered_view_locked()
            cache_key = (
                self._list_model_version,
                self._filter_text,
                self._tailing,
                id(base),
                self._search_text,
                self._search_editing,
            )
            return ListSnapshot(
                cache_key=cache_key,
                view=view,
                selected=self._selected,
                search_text=self._search_text,
            )

    def _format_event_line(self, ev: SyscallEvent, dt: str) -> FormattedText:
        phase = "→" if ev.phase == "enter" else "←"
        if ev.merged:
            phase = "↔"

        line_style = "fg:ansired" if ev.failed else ""

        line: FormattedText = []
        line.append(("", f"{dt} "))
        line.append((line_style, f"[{ev.pid}:{ev.tid}] {phase} {ev.name}("))

        if ev.enter_args is not None:
            line.extend(self._format_args(ev.pid, ev.enter_args))
        else:
            line.append((line_style, ev.enter_summary or ""))

        line.append((line_style, ")"))

        if ev.exit_retval is not None:
            line.append((line_style, " => "))
            line.extend(self._format_retval(ev, ev.exit_retval, failed=ev.failed))

            if ev.exit_out_args is not None:
                line.append((line_style, ", "))
                line.extend(self._format_args(ev.pid, ev.exit_out_args))

        return line

    def _begin_search(self, event) -> None:
        with self._lock:
            self._pause_if_tailing()
            self._status_message = ""
            self._search_prev_text = self._search_text
            self._search_text = ""
            self._search_editing = True
            self._recompute_search_matches_locked()
            if self._search_matches:
                self._search_index = 0
                self._selected = self._search_matches[0]
            else:
                view = self._get_filtered_view_locked()
                self._selected = 0 if view else 0

        self._set_search_bar_text("")
        event.app.layout.focus(self._search_bar.control)
        event.app.invalidate()

    def _end_search(self, keep: bool, event) -> None:
        with self._lock:
            if not keep:
                self._search_text = self._search_prev_text

            self._search_editing = False
            self._recompute_search_matches_locked()

            view = self._get_filtered_view_locked()

            if keep:
                if self._search_text != "" and not self._search_matches:
                    self._status_message = "Pattern not found"
                    self._selected = 0 if view else 0
                elif self._search_matches:
                    self._status_message = ""
                    self._search_index = 0
                    self._selected = self._search_matches[0]

        if not keep:
            self._set_search_bar_text(self._search_prev_text)

        assert self._list_win is not None
        event.app.layout.focus(self._list_win)
        event.app.invalidate()

    def _set_search_bar_text(self, text: str) -> None:
        self._ignore_search_text_change = True
        try:
            self._search_bar.text = text
        finally:
            self._ignore_search_text_change = False

    def _on_search_text_changed(self, _) -> None:
        if self._ui_app is None or self._ignore_search_text_change:
            return

        with self._lock:
            if not self._search_editing:
                return
            self._search_text = self._search_bar.text
            self._recompute_search_matches_locked()

            if self._search_matches:
                self._status_message = ""
                self._search_index = 0
                self._selected = self._search_matches[0]

        self._ui_app.invalidate()

    def _recompute_search_matches_locked(self) -> None:
        f = self._search_text
        if f == "":
            self._search_matches = []
            self._search_index = 0
            return

        f_lc = f.lower()
        view = self._get_filtered_view_locked()
        matches: List[int] = []
        for i, ev in enumerate(view):
            if self._event_matches_filter(ev, f_lc):
                matches.append(i)
        self._search_matches = matches
        if self._search_index >= len(matches):
            self._search_index = 0

    def _move_selected_locked(self, delta: int) -> None:
        self._pause_if_tailing()
        self._selected += delta
        self._clamp_selected_locked()

    def _clamp_selected_locked(self) -> None:
        view = self._get_filtered_view_locked()
        if not view:
            self._selected = 0
        else:
            self._selected = max(0, min(self._selected, len(view) - 1))

    def _page_delta_locked(self, direction: int, fraction: float = 1.0) -> int:
        self._pause_if_tailing()
        assert self._list_win is not None
        height = self._list_win.render_info.window_height
        page = max(1, height - 1)
        step = max(1, int(page * fraction))
        return direction * step

    def _pause_if_tailing(self) -> None:
        if self._tailing:
            self._tailing = False
            self._paused_events = list(self._events)
            self._selected = min(self._selected, max(0, len(self._paused_events) - 1)) if self._paused_events else 0
            self._bump_list_model_version_locked()
            self._invalidate_filter_cache_locked()
            self._recompute_search_matches_locked()

    def _get_base_view_locked(self) -> List[SyscallEvent]:
        return self._events if self._tailing else self._paused_events

    def _invalidate_filter_cache_locked(self) -> None:
        self._filter_cache_key = None
        self._filter_cache = []

    def _get_filtered_view_locked(self) -> List[SyscallEvent]:
        base = self._get_base_view_locked()
        key = (self._list_model_version, self._filter_text, self._tailing, id(base))
        if self._filter_cache_key == key:
            return self._filter_cache

        f = self._filter_text
        if f == "":
            self._filter_cache = base
            self._filter_cache_key = key
            return self._filter_cache

        f_lc = f.lower()
        out: List[SyscallEvent] = []
        for ev in base:
            if self._event_matches_filter(ev, f_lc):
                out.append(ev)

        self._filter_cache = out
        self._filter_cache_key = key
        return self._filter_cache

    def _event_matches_filter(self, ev: SyscallEvent, f_lc: str) -> bool:
        if f_lc in ev.name.lower():
            return True
        if f_lc in str(ev.pid) or f_lc in str(ev.tid) or f_lc in str(ev.nr):
            return True
        if ev.enter_args is not None:
            for a in ev.enter_args:
                if f_lc in a.name.lower():
                    return True
                if f_lc in a.text.lower():
                    return True
        if ev.exit_retval is not None and f_lc in str(ev.exit_retval).lower():
            return True
        return False

    def _notify_ui(self) -> None:
        if self._ui_app is None:
            return
        with self._lock:
            if self._ui_refresh_pending:
                return
            self._ui_refresh_pending = True
        self._ui_app.loop.call_soon_threadsafe(self._drain_and_refresh_on_ui)

    def _drain_and_refresh_on_ui(self) -> None:
        tracer = self._tracer
        if tracer is None:
            with self._lock:
                self._ui_refresh_pending = False
            return

        changed_list = False

        while True:
            new_events = tracer.drain_events(limit=5000)
            updates = tracer.drain_updates(limit=5000)

            if not new_events and not updates:
                break

            with self._lock:
                if new_events:
                    for ev in new_events:
                        changed_list |= self._append_or_merge_event_locked(ev)

                    if len(self._events) > self._limit:
                        overflow = len(self._events) - self._limit

                        if overflow > 0:
                            new_pending: Dict[Tuple[int, int, int], int] = {}
                            for k, idx in self._pending_by_key.items():
                                if idx >= overflow:
                                    new_pending[k] = idx - overflow
                            self._pending_by_key = new_pending

                        self._events = self._events[overflow:]
                        changed_list = True

                        if self._tailing and not self._search_editing:
                            self._selected = max(0, self._selected - overflow)

                    if self._tailing and not self._search_editing:
                        view = self._get_filtered_view_locked()
                        self._selected = max(0, len(view) - 1)

                if updates:
                    id_to_event = {e.id: e for e in self._events}
                    if self._paused_events is not None:
                        for e in self._paused_events:
                            id_to_event.setdefault(e.id, e)

                    for event_id, stack_or_none, syms_or_exc in updates:
                        ev = id_to_event.get(event_id)
                        if ev is None:
                            continue
                        ev.resolving = False
                        if stack_or_none is None:
                            ev.resolve_error = str(syms_or_exc)
                        else:
                            ev.stack = stack_or_none
                            ev.symbols = syms_or_exc
                            ev.resolve_error = None

                if changed_list:
                    self._bump_list_model_version_locked()
                    self._invalidate_filter_cache_locked()
                    self._recompute_search_matches_locked()

        with self._lock:
            self._ui_refresh_pending = False

        assert self._ui_app is not None
        self._ui_app.invalidate()

    def _bump_list_model_version_locked(self) -> None:
        self._list_model_version += 1

    def _append_or_merge_event_locked(self, ev: SyscallEvent) -> bool:
        if ev.phase == "exit":
            key = (ev.pid, ev.tid, ev.nr)
            idx = self._pending_by_key.get(key)
            if idx is not None and 0 <= idx < len(self._events):
                prev = self._events[idx]
                if prev.phase == "enter" and prev.nr == ev.nr and prev.pid == ev.pid and prev.tid == ev.tid:
                    prev.set_exit(ev.exit_retval, ev.exit_out_args, ev.time_ns, ev.stack_id)
                    return True

        self._events.append(ev)
        if ev.phase == "enter":
            self._pending_by_key[(ev.pid, ev.tid, ev.nr)] = len(self._events) - 1
        return True

    def _on_enter(self) -> None:
        tracer = self._tracer
        if tracer is None:
            return

        with self._lock:
            view = self._get_filtered_view_locked()
            if not (0 <= self._selected < len(view)):
                return
            ev = view[self._selected]
            if ev.stack_id < 0 or ev.resolving or ev.stack is not None:
                return
            ev.resolving = True

        tracer.resolve_backtrace(ev.id, ev.pid, ev.map_gen, ev.stack_id)

    def _exclude_events_locked(self, key: tuple[str, int]) -> None:
        abi, nr = key

        def keep(e: SyscallEvent) -> bool:
            return not (e.abi == abi and e.nr == nr)

        before = len(self._events)
        self._events = [e for e in self._events if keep(e)]
        removed = before - len(self._events)

        if self._paused_events is not None:
            self._paused_events = [e for e in self._paused_events if keep(e)]

        self._pending_by_key.clear()
        for i, e in enumerate(self._events):
            if e.phase == "enter":
                self._pending_by_key[(e.pid, e.tid, e.nr)] = i

        self._bump_list_model_version_locked()
        self._invalidate_filter_cache_locked()
        self._recompute_search_matches_locked()
        self._clamp_selected_locked()

        return removed

    def _highlight_line(self, ft: FormattedText, needle_lc: str) -> FormattedText:
        plain = "".join(text for _, text in ft)
        hay_lc = plain.lower()
        pos = hay_lc.find(needle_lc)
        if pos < 0:
            return ft

        end = pos + len(needle_lc)

        out: FormattedText = []
        i = 0
        for style, text in ft:
            if text == "":
                continue

            chunk_start = i
            chunk_end = i + len(text)

            if chunk_end <= pos or chunk_start >= end:
                out.append((style, text))
            else:
                a = max(0, pos - chunk_start)
                b = min(len(text), end - chunk_start)

                if a > 0:
                    out.append((style, text[:a]))
                if b > a:
                    out.append(("reverse bold", text[a:b]))
                if b < len(text):
                    out.append((style, text[b:]))

            i = chunk_end

        return out

    def _format_args(self, pid: int, args: List[Arg]) -> FormattedText:
        out: FormattedText = []
        for j, a in enumerate(args):
            if j != 0:
                out.append(("", ", "))

            out.append(("", f"{a.name}="))

            if a.kind == "string":
                out.append(("fg:ansigreen", a.text))
            elif a.kind == "bytes":
                out.append(("fg:ansiyellow", a.text))
            elif a.is_fd:
                out.append((self._fd_style(pid, a.value), a.text))
            else:
                out.append(("", a.text))
        return out

    def _format_retval(self, ev: "SyscallEvent", v: Any, failed: bool) -> FormattedText:
        if self._platform == "linux":
            if failed:
                eno = -v
                name = LINUX_ERRNO.get(eno)
                if name is not None:
                    return FormattedText([("fg:ansired bold", f"-{eno} {name}")])
                return FormattedText([("fg:ansired bold", f"-{eno} errno={eno}")])

            if ev.name in ("mmap", "mremap"):
                if v == 0:
                    return FormattedText([("fg:ansigreen", "NULL")])
                return FormattedText([("", f"0x{v:x}")])

        if failed:
            return FormattedText([("fg:ansired bold", str(v))])

        if self._syscall_returns_fd(ev.name):
            fd = to_signed32(v)
            return FormattedText([(self._fd_style(ev.pid, fd), str(fd))])

        return FormattedText([("", str(v))])

    def _syscall_returns_fd(self, name: str) -> bool:
        return self._platform == "linux" and name in LINUX_FD_RETURNING_SYSCALLS

    def _fd_style(self, pid: int, fd: int) -> str:
        key = (pid, fd)
        style = self._fd_style_by_pid_value.get(key)
        if style is None:
            color = self._fd_color_palette[self._next_fd_color % len(self._fd_color_palette)]
            self._next_fd_color += 1
            style = f"fg:{color} bold"
            self._fd_style_by_pid_value[key] = style
        return style

    def _format_dt(self, delta_ns: int) -> str:
        if delta_ns < 0:
            delta_ns = 0

        if delta_ns < 1_000:
            s = f"+{delta_ns}ns"
        elif delta_ns < 1_000_000:
            s = f"+{delta_ns / 1_000:.0f}µs"
        elif delta_ns < 1_000_000_000:
            ms = delta_ns / 1_000_000
            s = f"+{ms:.1f}ms" if ms < 10 else f"+{ms:.0f}ms"
        else:
            sec = delta_ns / 1_000_000_000
            s = f"+{sec:.1f}s" if sec < 10 else f"+{sec:.0f}s"

        if len(s) > 10:
            s = s[:10]
        return s.rjust(10)

    def _get_detail_text(self) -> str:
        with self._lock:
            view = self._get_filtered_view_locked()
            if not view or not (0 <= self._selected < len(view)):
                return ""
            ev = view[self._selected]

            detail = [
                f"id={ev.id}",
                f"pid={ev.pid} tid={ev.tid} abi={ev.abi}",
                f"nr={ev.nr} name={ev.name}",
                f"enter_time_ns={ev.time_ns}",
                f"map_gen={ev.map_gen} stack_id={ev.stack_id}",
                "",
            ]

            if ev.exit_time_ns is not None:
                detail.append(f"exit_time_ns={ev.exit_time_ns}")
                if ev.exit_retval is not None:
                    detail.append(f"retval={ev.exit_retval}")
                detail.append("status=FAILED" if ev.failed else "status=OK")
                detail.append("")

            if ev.stack_id < 0:
                detail.append("(no stack)")
            elif ev.resolving:
                detail.append("Resolving backtrace…")
            elif ev.resolve_error is not None:
                detail.append(f"Resolve failed: {ev.resolve_error}")
            elif ev.stack is None:
                detail.append("Press Enter to resolve backtrace")
            else:
                detail.append("Call stack:")
                detail += self._format_call_stack(ev)

            return "\n".join(detail)

    def _get_status_text(self) -> str:
        with self._lock:
            view = self._get_filtered_view_locked()
            mode = "tail" if self._tailing else "paused (press 't')"
            pieces = [f"events={len(view)}"]
            if view:
                pieces.append(f"selected={self._selected+1}/{len(view)}")
            pieces.append(f"[{mode}]")

            if self._status_message:
                pieces.append(self._status_message)

            if self._search_text != "":
                pieces.append(f"search={json.dumps(self._search_text, ensure_ascii=False)}")
                if self._search_matches:
                    pieces.append(f"match={self._search_index+1}/{len(self._search_matches)}")

            if self._filter_text != "":
                pieces.append(f"filter={json.dumps(self._filter_text, ensure_ascii=False)} (Ctrl-L clears)")

            if self._search_text != "":
                pieces.append("(f = filter search)")

            if self._excluded_syscalls:
                pieces.append(f"excluded={len(self._excluded_syscalls)} (x=exclude)")

            cols = get_app().output.get_size().columns
            if self._show_details:
                placement = "below" if cols < self._details_breakpoint_cols else "right"
                pieces.append(f"details=on ({placement})  d=hide")
            else:
                pieces.append("details=off  d=show")

            return "  ".join(pieces)

    def _format_call_stack(self, ev: SyscallEvent) -> List[str]:
        modules = ev.symbols["modules"]
        entries = ev.symbols["symbols"]
        stack = ev.stack

        out: List[str] = []
        for addr, (mod_index, offset) in zip(stack, entries):
            if mod_index == 0xFFFFFFFF:
                out.append(f"  0x{addr:x}")
            else:
                path, *extra = modules[mod_index]
                base = path.rsplit("/", 1)[-1]
                out.append(f"  {base}+0x{offset:x}")
        for addr in stack[len(entries):]:
            out.append(f"  0x{addr:x}")
        return out


class SyscallListControl(UIControl):
    def __init__(self, model: ListModel) -> None:
        self._model = model

    def is_focusable(self) -> bool:
        return True

    def create_content(self, width: int, height: int) -> UIContent:
        m = self._model
        snap = m.snapshot()
        m.prepare(snap)

        view = snap.view
        if not view:

            def get_line(i: int) -> FormattedText:
                return [("", "(no events)")]

            return UIContent(get_line=get_line, line_count=1, cursor_position=Point(0, 0))

        cursor_y = max(0, min(snap.selected, len(view) - 1))

        def get_line(i: int) -> FormattedText:
            return m.get_line(snap, i)

        return UIContent(get_line=get_line, line_count=len(view), cursor_position=Point(0, cursor_y))


@dataclass(frozen=True)
class ListSnapshot:
    cache_key: tuple
    view: List[SyscallEvent]
    selected: int
    search_text: str


class ListModel:
    def __init__(
        self,
        snapshot: Callable[[], ListSnapshot],
        format_dt: Callable[[int], str],
        format_line: Callable[[SyscallEvent, str], FormattedText],
        highlight_line: Callable[[FormattedText, str], FormattedText],
    ) -> None:
        self._snapshot = snapshot
        self._format_dt = format_dt
        self._format_line = format_line
        self._highlight_line = highlight_line

        self._dt_cache_key: Optional[tuple] = None
        self._dt_cache_len: int = -1
        self._dt_cache: List[str] = []

        self._line_cache_key: Optional[tuple] = None
        self._line_cache: Dict[int, FormattedText] = {}

    def snapshot(self) -> ListSnapshot:
        return self._snapshot()

    def prepare(self, snap: ListSnapshot) -> None:
        if self._line_cache_key != snap.cache_key:
            self._line_cache_key = snap.cache_key
            self._line_cache.clear()

        if self._dt_cache_key == snap.cache_key and self._dt_cache_len == len(snap.view):
            return

        prev_ns_by_thread: Dict[Tuple[int, int], int] = {}
        dt_list: List[str] = []
        for ev in snap.view:
            thread_key = (ev.pid, ev.tid)
            prev_ns = prev_ns_by_thread.get(thread_key)
            if prev_ns is None:
                dt = " " * 10
            else:
                dt = self._format_dt(ev.time_ns - prev_ns)
            prev_ns_by_thread[thread_key] = ev.time_ns
            dt_list.append(dt)

        self._dt_cache_key = snap.cache_key
        self._dt_cache_len = len(snap.view)
        self._dt_cache = dt_list

    def get_line(self, snap: ListSnapshot, i: int) -> FormattedText:
        view = snap.view
        if i < 0 or i >= len(view):
            return [("", "")]

        cached = self._line_cache.get(i)
        if cached is not None:
            return cached

        dt = self._dt_cache[i]
        line = self._format_line(view[i], dt)

        needle_lc = snap.search_text.lower()
        if needle_lc:
            line = self._highlight_line(line, needle_lc)

        if len(self._line_cache) > 2000:
            self._line_cache.clear()
        self._line_cache[i] = line
        return line


class SyscallTracer:
    def __init__(self, reactor: Reactor) -> None:
        self._notify: Optional[callable] = None
        self._platform: Optional[str] = None
        self._reactor = reactor

        self._service: Optional[frida.core.Service] = None

        self._signatures: Dict[str, Dict[int, Dict[str, Any]]] = {}
        self._abi_by_pid: Dict[int, str] = {}

        self._events_out: "queue.Queue[SyscallEvent]" = queue.Queue()
        self._updates_out: "queue.Queue[tuple[int, Optional[list[int]], Any]]" = queue.Queue()

        self._next_id = 1
        self._excluded: set[tuple[str, int]] = set()
        self._stopping = False

        self._schedule_on_message = lambda m: self._reactor.schedule(lambda: self._handle_service_message(m))

    def set_notify(self, notify: callable) -> None:
        self._notify = notify

    def set_platform(self, platform: Optional[str]) -> None:
        self._platform = platform

    def start(
            self,
            device: frida.core.Device,
            targets_req: Dict[str, Any],
            excluded_by_name: list[str]
    ) -> None:
        self._service = device.open_service("syscall-trace")

        raw = self._service.request({"type": "get-signatures"})
        sigs: Dict[str, Dict[int, Dict[str, Any]]] = {}
        for abi, entries in raw.items():
            by_nr: Dict[int, Dict[str, Any]] = {}
            for nr, name, args in entries:
                by_nr[nr] = {"name": name, "args": args}
            sigs[abi] = by_nr
        self._signatures = sigs

        self._service.on("message", self._schedule_on_message)

        if excluded_by_name:
            items = self._resolve_excludes_by_name(excluded_by_name)
            if items:
                by_abi = self._apply_excludes(items)
                self._service.request({"type": "exclude-syscalls", **by_abi})

        add_req = {"type": "add-targets"}
        add_req.update(targets_req)
        self._service.request(add_req)

    def stop(self) -> None:
        self._stopping = True
        if self._service is not None:
            self._service.off("message", self._schedule_on_message)
            self._service.close()
        self._service = None

    def drain_events(self, limit: int = 2000) -> List[SyscallEvent]:
        out: List[SyscallEvent] = []
        for _ in range(limit):
            try:
                out.append(self._events_out.get_nowait())
            except queue.Empty:
                break
        return out

    def drain_updates(self, limit: int = 2000) -> List[tuple[int, Optional[list[int]], Any]]:
        out: List[tuple[int, Optional[list[int]], Any]] = []
        for _ in range(limit):
            try:
                out.append(self._updates_out.get_nowait())
            except queue.Empty:
                break
        return out

    def resolve_backtrace(self, event_id: int, pid: int, map_gen: int, stack_id: int) -> None:
        self._reactor.schedule(lambda: self._resolve_backtrace_on_reactor(event_id, pid, map_gen, stack_id))

    def get_excluded(self) -> set[tuple[str, int]]:
        return self._excluded

    def exclude_syscalls(self, items: list[tuple[str, int]]) -> None:
        by_abi = self._apply_excludes(items)
        self._reactor.schedule(lambda: self._exclude_syscalls_on_reactor(by_abi))

    def _exclude_syscalls_on_reactor(self, by_abi: dict[str, list[int]]) -> None:
        if self._stopping or self._service is None:
            return
        self._service.request({"type": "exclude-syscalls", **by_abi})

    def _resolve_excludes_by_name(self, names: list[str]) -> set[tuple[str, int]]:
        wanted = {n.lower() for n in names if n}
        out: set[tuple[str, int]] = set()
        for abi, by_nr in self._signatures.items():
            for nr, sig in by_nr.items():
                if sig["name"] in wanted:
                    out.add((abi, nr))
        return out

    def _group_by_abi(self, items: set[tuple[str, int]] | list[tuple[str, int]]) -> dict[str, list[int]]:
        by_abi: dict[str, set[int]] = {}
        for abi, nr in items:
            by_abi.setdefault(abi, set()).add(nr)
        return {abi: sorted(nrs) for abi, nrs in by_abi.items()}

    def _apply_excludes(self, items: set[tuple[str, int]] | list[tuple[str, int]]) -> dict[str, list[int]]:
        for k in items:
            self._excluded.add(k)
        return self._group_by_abi(items)

    def _resolve_excludes_by_name(self, names: list[str]) -> list[tuple[str, int]]:
        wanted = [n.lower() for n in names if n]
        out: list[tuple[str, int]] = []
        for abi, by_nr in self._signatures.items():
            for nr, sig in by_nr.items():
                sname = sig["name"].lower()
                if sname in wanted:
                    out.append((abi, nr))
        seen = set()
        uniq = []
        for k in out:
            if k not in seen:
                seen.add(k)
                uniq.append(k)
        return uniq

    def _handle_service_message(self, m) -> None:
        if self._stopping or self._service is None:
            return
        if m["type"] != "events-available":
            return

        self._reactor.schedule(self._read_events_loop)

    def _read_events_loop(self) -> None:
        if self._stopping or self._service is None:
            return

        while True:
            res = self._service.request({"type": "read-events"})
            events = res["events"]
            processes = res["processes"]
            status = res["status"]

            for pid, abi in processes:
                self._abi_by_pid[pid] = abi

            for row in events:
                ev = self._parse_event_row(row)

                if (ev.abi, ev.nr) in self._excluded:
                    continue

                self._events_out.put(ev)

            if events:
                self._notify_ui()

            if status != "more":
                break

        self._notify_ui()

    def _notify_ui(self) -> None:
        if self._notify is not None:
            self._notify()

    def _parse_event_row(self, row) -> SyscallEvent:
        phase, time_ns, tid, pid, nr, stack_id, map_gen, args_or_retval, attachments = row

        abi = self._abi_by_pid[pid]

        name = "some syscall"
        sig_args = None
        if nr != -1:
            sig = self._signatures[abi].get(nr)
            if sig is not None:
                name = sig["name"]
                sig_args = sig["args"]
            else:
                name = f"#{nr}"

        if phase == "enter":
            raw_args = self._apply_attachments(args_or_retval, attachments)

            args: List[Arg] = []
            if sig_args is not None:
                for i, (atype, aname) in enumerate(sig_args):
                    if i >= len(raw_args):
                        break
                    args.append(self._make_arg(name, atype, aname, raw_args[i]))
            else:
                for i, v in enumerate(raw_args):
                    args.append(self._make_arg(name, "", f"arg{i}", v))

            ev = SyscallEvent(
                id=self._next_id,
                phase="enter",
                time_ns=time_ns,
                pid=pid,
                tid=tid,
                nr=nr,
                name=name,
                stack_id=stack_id,
                map_gen=map_gen,
                abi=abi,
                enter_args=args,
                enter_summary=None,
            )
        else:
            retval = args_or_retval
            failed = retval < 0

            out_args: Optional[List[Arg]] = None
            if attachments:
                out: List[Arg] = []
                for ai, av in attachments:
                    if sig_args is not None:
                        atype, aname = sig_args[ai]
                    else:
                        atype, aname = "", f"arg{ai}"
                    out.append(self._make_arg(name, atype, aname, av))
                out_args = out

            ev = SyscallEvent(
                id=self._next_id,
                phase="exit",
                time_ns=time_ns,
                pid=pid,
                tid=tid,
                nr=nr,
                name=name,
                stack_id=stack_id,
                map_gen=map_gen,
                abi=abi,
                enter_args=None,
                enter_summary=None,
                exit_retval=retval,
                exit_out_args=out_args,
                exit_time_ns=time_ns,
                failed=failed,
            )

        self._next_id += 1
        return ev

    def _apply_attachments(self, raw_args: list[Any], attachments: list[tuple[int, Any]]) -> list[Any]:
        if not attachments:
            return raw_args

        out = list(raw_args)
        for ai, av in attachments:
            out[ai] = av
        return out

    def _make_arg(self, syscall_name: str, atype: str, aname: str, value: Any) -> Arg:
        is_linux = self._platform == "linux"

        if is_linux:
            if aname == "sig" and atype == "int":
                txt = linux_signal_text(value)
                return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")
            if syscall_name in ("futex", "futex_time64"):
                if aname == "op":
                    txt = linux_futex_op_text(value)
                    return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")
                if aname == "val":
                    txt = linux_futex_val_text(value)
                    return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")
            if syscall_name in ("mmap", "mprotect") and aname == "prot":
                txt = linux_prot_text(value)
                return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")
            if syscall_name == "mmap" and aname == "flags":
                txt = linux_mmap_flags_text(value)
                return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")
            if syscall_name == "mremap" and aname == "flags":
                txt = linux_mremap_flags_text(value)
                return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")
            if syscall_name == "clone" and aname == "clone_flags":
                txt = linux_clone_flags_text(value)
                return Arg(type=atype, name=aname, value=value, text=txt, is_fd=False, kind="other")

        if (atype.endswith("*") or atype == "unsigned long") and isinstance(value, int):
            if value == 0:
                return Arg(type=atype, name=aname, value=value, text="NULL", is_fd=False, kind="ptr")
            return Arg(type=atype, name=aname, value=value, text=f"0x{value:x}", is_fd=False, kind="ptr")

        is_fd = (atype in ("int", "unsigned int")) and aname.endswith("fd")
        if is_fd:
            fd = to_signed32(value)
            if is_linux and fd == LINUX_AT_FDCWD:
                return Arg(type=atype, name=aname, value=fd, text="AT_FDCWD", is_fd=True, kind="fd")
            return Arg(type=atype, name=aname, value=fd, text=str(fd), is_fd=True, kind="fd")

        if isinstance(value, str):
            s = value if len(value) <= 140 else (value[:137] + "…")
            return Arg(
                type=atype,
                name=aname,
                value=value,
                text=json.dumps(s, ensure_ascii=False),
                is_fd=is_fd,
                kind="string",
            )

        if isinstance(value, (bytes, bytearray)):
            b = bytes(value)
            if len(b) <= 32:
                text = b.hex()
            else:
                text = b[:32].hex() + f"…({len(b)} bytes)"
            return Arg(type=atype, name=aname, value=value, text=text, is_fd=is_fd, kind="bytes")

        return Arg(type=atype, name=aname, value=value, text=repr(value), is_fd=is_fd, kind="other")

    def _resolve_backtrace_on_reactor(self, event_id: int, pid: int, map_gen: int, stack_id: int) -> None:
        if self._stopping or self._service is None:
            return
        try:
            stacks_res = self._service.request({"type": "resolve-stacks", "ids": [stack_id]})
            stack = stacks_res["stacks"][0]

            syms = self._service.request(
                {
                    "type": "resolve-symbols",
                    "pid": pid,
                    "gen": map_gen,
                    "addresses": [("uint64", address) for address in stack],
                }
            )

            self._updates_out.put((event_id, stack, syms))
        except Exception as e:
            self._updates_out.put((event_id, None, e))
        finally:
            self._notify_ui()


@dataclass
class SyscallEvent:
    id: int
    phase: str
    time_ns: int
    pid: int
    tid: int
    nr: int
    name: str

    stack_id: int
    map_gen: int
    abi: str

    enter_args: Optional[List[Arg]] = None
    enter_summary: Optional[str] = None

    exit_retval: Optional[Any] = None
    exit_out_args: Optional[List[Arg]] = None
    exit_time_ns: Optional[int] = None

    merged: bool = False
    failed: bool = False
    resolving: bool = False
    stack: Optional[List[int]] = None
    symbols: Optional[Any] = None
    resolve_error: Optional[str] = None

    def set_exit(self, retval: int, out_args: Optional[List[Arg]], exit_time_ns: int, stack_id: int) -> None:
        self.exit_retval = retval
        self.exit_out_args = out_args
        self.exit_time_ns = exit_time_ns
        if self.stack_id == -1:
            self.stack_id = stack_id
        self.merged = True
        self.failed = retval < 0


@dataclass
class Arg:
    type: str
    name: str
    value: Any
    text: str
    is_fd: bool
    kind: str


LINUX_AT_FDCWD = -100

LINUX_FD_RETURNING_SYSCALLS = {
    "open",
    "openat",
    "openat2",
    "creat",
    "socket",
    "socketpair",
    "accept",
    "accept4",
    "dup",
    "dup2",
    "dup3",
    "pipe",
    "pipe2",
    "eventfd",
    "eventfd2",
    "inotify_init",
    "inotify_init1",
    "memfd_create",
    "signalfd",
    "signalfd4",
    "timerfd_create",
    "pidfd_open",
    "epoll_create",
    "epoll_create1",
}

LINUX_ERRNO: Dict[int, str] = {
    1: "EPERM",
    2: "ENOENT",
    3: "ESRCH",
    4: "EINTR",
    5: "EIO",
    6: "ENXIO",
    7: "E2BIG",
    8: "ENOEXEC",
    9: "EBADF",
    10: "ECHILD",
    11: "EAGAIN",
    12: "ENOMEM",
    13: "EACCES",
    14: "EFAULT",
    15: "ENOTBLK",
    16: "EBUSY",
    17: "EEXIST",
    18: "EXDEV",
    19: "ENODEV",
    20: "ENOTDIR",
    21: "EISDIR",
    22: "EINVAL",
    23: "ENFILE",
    24: "EMFILE",
    25: "ENOTTY",
    26: "ETXTBSY",
    27: "EFBIG",
    28: "ENOSPC",
    29: "ESPIPE",
    30: "EROFS",
    31: "EMLINK",
    32: "EPIPE",
    33: "EDOM",
    34: "ERANGE",
    35: "EDEADLK",
    36: "ENAMETOOLONG",
    37: "ENOLCK",
    38: "ENOSYS",
    39: "ENOTEMPTY",
    40: "ELOOP",
    42: "ENOMSG",
    43: "EIDRM",
    44: "ECHRNG",
    45: "EL2NSYNC",
    46: "EL3HLT",
    47: "EL3RST",
    48: "ELNRNG",
    49: "EUNATCH",
    50: "ENOCSI",
    51: "EL2HLT",
    52: "EBADE",
    53: "EBADR",
    54: "EXFULL",
    55: "ENOANO",
    56: "EBADRQC",
    57: "EBADSLT",
    59: "EBFONT",
    60: "ENOSTR",
    61: "ENODATA",
    62: "ETIME",
    63: "ENOSR",
    64: "ENONET",
    65: "ENOPKG",
    66: "EREMOTE",
    67: "ENOLINK",
    68: "EADV",
    69: "ESRMNT",
    70: "ECOMM",
    71: "EPROTO",
    72: "EMULTIHOP",
    73: "EDOTDOT",
    74: "EBADMSG",
    75: "EOVERFLOW",
    76: "ENOTUNIQ",
    77: "EBADFD",
    78: "EREMCHG",
    79: "ELIBACC",
    80: "ELIBBAD",
    81: "ELIBSCN",
    82: "ELIBMAX",
    83: "ELIBEXEC",
    84: "EILSEQ",
    85: "ERESTART",
    86: "ESTRPIPE",
    87: "EUSERS",
    88: "ENOTSOCK",
    89: "EDESTADDRREQ",
    90: "EMSGSIZE",
    91: "EPROTOTYPE",
    92: "ENOPROTOOPT",
    93: "EPROTONOSUPPORT",
    94: "ESOCKTNOSUPPORT",
    95: "EOPNOTSUPP",
    96: "EPFNOSUPPORT",
    97: "EAFNOSUPPORT",
    98: "EADDRINUSE",
    99: "EADDRNOTAVAIL",
    100: "ENETDOWN",
    101: "ENETUNREACH",
    102: "ENETRESET",
    103: "ECONNABORTED",
    104: "ECONNRESET",
    105: "ENOBUFS",
    106: "EISCONN",
    107: "ENOTCONN",
    108: "ESHUTDOWN",
    109: "ETOOMANYREFS",
    110: "ETIMEDOUT",
    111: "ECONNREFUSED",
    112: "EHOSTDOWN",
    113: "EHOSTUNREACH",
    114: "EALREADY",
    115: "EINPROGRESS",
    116: "ESTALE",
    117: "EUCLEAN",
    118: "ENOTNAM",
    119: "ENAVAIL",
    120: "EISNAM",
    121: "EREMOTEIO",
    122: "EDQUOT",
    123: "ENOMEDIUM",
    124: "EMEDIUMTYPE",
    125: "ECANCELED",
    126: "ENOKEY",
    127: "EKEYEXPIRED",
    128: "EKEYREVOKED",
    129: "EKEYREJECTED",
    130: "EOWNERDEAD",
    131: "ENOTRECOVERABLE",
    132: "ERFKILL",
    133: "EHWPOISON",
    512: "ERESTARTSYS",
    513: "ERESTARTNOINTR",
    514: "ERESTARTNOHAND",
    515: "ENOIOCTLCMD",
    516: "ERESTART_RESTARTBLOCK",
}

LINUX_SIGNAL_NAMES: Dict[int, str] = {
    1: "SIGHUP",
    2: "SIGINT",
    3: "SIGQUIT",
    4: "SIGILL",
    5: "SIGTRAP",
    6: "SIGABRT",
    7: "SIGBUS",
    8: "SIGFPE",
    9: "SIGKILL",
    10: "SIGUSR1",
    11: "SIGSEGV",
    12: "SIGUSR2",
    13: "SIGPIPE",
    14: "SIGALRM",
    15: "SIGTERM",
    16: "SIGSTKFLT",
    17: "SIGCHLD",
    18: "SIGCONT",
    19: "SIGSTOP",
    20: "SIGTSTP",
    21: "SIGTTIN",
    22: "SIGTTOU",
    23: "SIGURG",
    24: "SIGXCPU",
    25: "SIGXFSZ",
    26: "SIGVTALRM",
    27: "SIGPROF",
    28: "SIGWINCH",
    29: "SIGIO",
    30: "SIGPWR",
    31: "SIGSYS",
}

LINUX_FUTEX_CMD_MASK = 0x7F
LINUX_FUTEX_PRIVATE_FLAG = 0x80
LINUX_FUTEX_CLOCK_REALTIME = 0x100

LINUX_FUTEX_OPS: Dict[int, str] = {
    0: "FUTEX_WAIT",
    1: "FUTEX_WAKE",
    2: "FUTEX_FD",
    3: "FUTEX_REQUEUE",
    4: "FUTEX_CMP_REQUEUE",
    5: "FUTEX_WAKE_OP",
    6: "FUTEX_LOCK_PI",
    7: "FUTEX_UNLOCK_PI",
    8: "FUTEX_TRYLOCK_PI",
    9: "FUTEX_WAIT_BITSET",
    10: "FUTEX_WAKE_BITSET",
    11: "FUTEX_WAIT_REQUEUE_PI",
    12: "FUTEX_CMP_REQUEUE_PI",
}

LINUX_PROT_FLAGS: Dict[int, str] = {
    0x1: "PROT_READ",
    0x2: "PROT_WRITE",
    0x4: "PROT_EXEC",
    0x01000000: "PROT_GROWSDOWN",
    0x02000000: "PROT_GROWSUP",
}

LINUX_MMAP_FLAGS: Dict[int, str] = {
    0x01: "MAP_SHARED",
    0x02: "MAP_PRIVATE",
    0x03: "MAP_SHARED_VALIDATE",
    0x0F: "MAP_TYPE",
    0x10: "MAP_FIXED",
    0x20: "MAP_ANONYMOUS",
    0x0100: "MAP_GROWSDOWN",
    0x0800: "MAP_DENYWRITE",
    0x1000: "MAP_EXECUTABLE",
    0x2000: "MAP_LOCKED",
    0x4000: "MAP_NORESERVE",
    0x8000: "MAP_POPULATE",
    0x10000: "MAP_NONBLOCK",
    0x20000: "MAP_STACK",
    0x40000: "MAP_HUGETLB",
    0x80000: "MAP_SYNC",
    0x100000: "MAP_FIXED_NOREPLACE",
}

LINUX_MREMAP_FLAGS: Dict[int, str] = {
    0x1: "MREMAP_MAYMOVE",
    0x2: "MREMAP_FIXED",
    0x4: "MREMAP_DONTUNMAP",
}

LINUX_CLONE_FLAGS: Dict[int, str] = {
    0x00000100: "CLONE_VM",
    0x00000200: "CLONE_FS",
    0x00000400: "CLONE_FILES",
    0x00000800: "CLONE_SIGHAND",
    0x00001000: "CLONE_PIDFD",
    0x00002000: "CLONE_PTRACE",
    0x00004000: "CLONE_VFORK",
    0x00008000: "CLONE_PARENT",
    0x00010000: "CLONE_THREAD",
    0x00020000: "CLONE_NEWNS",
    0x00040000: "CLONE_SYSVSEM",
    0x00080000: "CLONE_SETTLS",
    0x00100000: "CLONE_PARENT_SETTID",
    0x00200000: "CLONE_CHILD_CLEARTID",
    0x00400000: "CLONE_DETACHED",
    0x00800000: "CLONE_UNTRACED",
    0x01000000: "CLONE_CHILD_SETTID",
    0x02000000: "CLONE_NEWCGROUP",
    0x04000000: "CLONE_NEWUTS",
    0x08000000: "CLONE_NEWIPC",
    0x10000000: "CLONE_NEWUSER",
    0x20000000: "CLONE_NEWPID",
    0x40000000: "CLONE_NEWNET",
    0x80000000: "CLONE_IO",
    0x00000080: "CLONE_NEWTIME",
    0x100000000: "CLONE_CLEAR_SIGHAND",
    0x200000000: "CLONE_INTO_CGROUP",
}


def linux_signal_text(n: int) -> str:
    name = LINUX_SIGNAL_NAMES.get(n)
    return name if name is not None else f"SIG{n}"


def linux_futex_op_text(v: int) -> str:
    cmd = v & LINUX_FUTEX_CMD_MASK
    flags = v & ~LINUX_FUTEX_CMD_MASK

    base = LINUX_FUTEX_OPS.get(cmd, f"0x{cmd:x}")
    if flags == 0:
        return base

    parts: List[str] = [base]
    if flags & LINUX_FUTEX_PRIVATE_FLAG:
        parts.append("FUTEX_PRIVATE_FLAG")
        flags &= ~LINUX_FUTEX_PRIVATE_FLAG
    if flags & LINUX_FUTEX_CLOCK_REALTIME:
        parts.append("FUTEX_CLOCK_REALTIME")
        flags &= ~LINUX_FUTEX_CLOCK_REALTIME
    if flags:
        parts.append(f"0x{flags:x}")
    return "|".join(parts)


def linux_futex_val_text(v: int) -> str:
    return str(to_signed32(v))


def linux_prot_text(v: int) -> str:
    if v == 0:
        return "PROT_NONE"
    return format_bitmask(v, {k: n for k, n in LINUX_PROT_FLAGS.items() if k != 0})


def linux_mmap_flags_text(v: int) -> str:
    return format_bitmask(v, LINUX_MMAP_FLAGS)


def linux_mremap_flags_text(v: int) -> str:
    return format_bitmask(v, LINUX_MREMAP_FLAGS)


def linux_clone_flags_text(v: int) -> str:
    sig = v & 0x7F
    flags_part = v & ~0xFF

    if v & 0x80:
        flags_part |= 0x80

    parts: List[str] = []
    if flags_part:
        s = format_bitmask(flags_part, LINUX_CLONE_FLAGS)
        if s != "0":
            parts.append(s)

    if sig:
        parts.append(linux_signal_text(sig))

    return "|".join(parts) if parts else "0"


def format_bitmask(v: int, names: Dict[int, str]) -> str:
    parts: List[str] = []
    remaining = v
    for bit, name in sorted(names.items()):
        if bit != 0 and (v & bit) == bit:
            parts.append(name)
            remaining &= ~bit
    if remaining:
        parts.append(f"0x{remaining:x}")
    return "|".join(parts) if parts else f"0x{v:x}"


def to_signed32(v: int) -> int:
    v &= 0xFFFFFFFF
    if v & 0x80000000:
        return v - 0x100000000
    return v


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass
