import sys
from dataclasses import dataclass
from typing import Iterable, NewType, Optional, List, cast



ProcessId = NewType("ProcessId", int)


@dataclass
class ProcessInfo:
    pid: ProcessId
    binary_path: Optional[str]


if sys.platform == "win32":
    from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
    from ctypes.wintypes import DWORD


    def pids() -> Iterable[ProcessId]:
        _PROC_ID_T = DWORD
        list_size = 4096

        def try_get_pids(list_size: int) -> List[ProcessId]:
            result_size = DWORD()
            proc_id_list = (_PROC_ID_T * list_size)()

            if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
                raise WinError(descr="Failed to get process ID list: %s" % FormatError())  # type: ignore

            return cast(List[ProcessId], proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))])

        while True:
            proc_ids = try_get_pids(list_size)
            if len(proc_ids) < list_size:
                return proc_ids

            list_size *= 2


    def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
        _PROC_QUERY_LIMITED_INFORMATION = 0x1000

        process_info = ProcessInfo(pid=pid, binary_path=None)

        h_process = windll.kernel32.OpenProcess(_PROC_QUERY_LIMITED_INFORMATION, False, pid)
        if not h_process:
            return process_info

        try:
            def get_exe_path() -> Optional[str]:
                _MAX_PATH = 260
                _WIN32_PATH_FORMAT = 0x0000

                exe_path_buffer = create_unicode_buffer(_MAX_PATH)
                exe_path_len = DWORD(len(exe_path_buffer))

                return cast(str, exe_path_buffer[:exe_path_len.value]) if windll.kernel32.QueryFullProcessImageNameW(
                    h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
                ) else None

            process_info.binary_path = get_exe_path()
        finally:
            windll.kernel32.CloseHandle(h_process)
            return process_info
else:
    import psutil


    def pids() -> Iterable[ProcessId]:
        for pid in psutil.pids():
            yield pid


    def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
        process_info = ProcessInfo(pid=pid, binary_path=None)
        try:
            process_info.binary_path = psutil.Process(pid=pid).as_dict(attrs=["exe"])["exe"]
        except psutil.NoSuchProcess:
            pass
        finally:
            return process_info


def process_iter() -> Iterable[Optional[ProcessInfo]]:
    for pid in pids():
        yield get_process_info(pid)