import mmap
import os
import sys
from typing import Any, NewType, Optional, TextIO

if sys.platform != "win32":
    from scalene import get_line_atomic  # type: ignore

Filename = NewType("Filename", str)


class ScaleneMapFile:

    # Things that need to be in sync with the C++ side
    # (see include/sampleheap.hpp, include/samplefile.hpp)

    MAX_BUFSIZE = 4096  # Must match SampleFile::MAX_BUFSIZE

    def __init__(self, name: str) -> None:
        self._name = name
        self._buf = bytearray(ScaleneMapFile.MAX_BUFSIZE)
        self._signal_position = 0
        self._lastpos = bytearray(8)
        self._signal_mmap: Optional[mmap.mmap] = None
        self._lock_mmap: Optional[mmap.mmap] = None
        self._signal_fd: Optional[TextIO] = None
        self._lock_fd: Optional[TextIO] = None

        if sys.platform == "win32":
            self._init_windows(name)
        else:
            self._init_unix(name)

    def _init_unix(self, name: str) -> None:
        """Initialize using Unix-style memory-mapped files."""
        #   file to communicate samples (+ PID)
        self._signal_filename = Filename(f"/tmp/scalene-{name}-signal{os.getpid()}")
        self._lock_filename = Filename(f"/tmp/scalene-{name}-lock{os.getpid()}")
        self._init_filename = Filename(f"/tmp/scalene-{name}-init{os.getpid()}")
        self._signal_fd = open(self._signal_filename)  # noqa: SIM115
        os.unlink(self._signal_fd.name)
        self._lock_fd = open(self._lock_filename, "r+")  # noqa: SIM115
        os.unlink(self._lock_fd.name)
        self._signal_mmap = mmap.mmap(
            self._signal_fd.fileno(),
            0,
            mmap.MAP_SHARED,
            mmap.PROT_READ,
        )
        self._lock_mmap = mmap.mmap(
            self._lock_fd.fileno(),
            0,
            mmap.MAP_SHARED,
            mmap.PROT_READ | mmap.PROT_WRITE,
        )

    def _init_windows(self, name: str) -> None:
        """Initialize using Windows named shared memory objects."""
        import ctypes
        from ctypes import wintypes

        # Windows constants
        FILE_MAP_READ = 0x0004
        # FILE_MAP_WRITE = 0x0002 # unused
        FILE_MAP_ALL_ACCESS = 0x001F

        # File sizes must match C++ side (samplefile_win.hpp)
        MAX_FILE_SIZE = 4096 * 65536
        LOCK_SIZE = 4096

        pid = os.getpid()

        # Names must match those generated by samplefile_win.hpp
        # The C++ code converts "/tmp/scalene-malloc-signal%d" to "Local\\scalene-malloc-signal<pid>"
        self._signal_name = f"Local\\scalene-{name}-signal{pid}"
        self._lock_name = f"Local\\scalene-{name}-lock{pid}"
        self._init_filename = Filename(f"Local\\scalene-{name}-init{pid}")

        kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]

        # IMPORTANT: Set proper return types for Windows API functions
        # Default ctypes return type is c_int (32-bit) which truncates 64-bit pointers
        kernel32.OpenFileMappingW.restype = wintypes.HANDLE
        kernel32.MapViewOfFile.restype = ctypes.c_void_p
        kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
        kernel32.CloseHandle.argtypes = [wintypes.HANDLE]

        # Open the signal shared memory (created by the DLL)
        self._signal_handle = kernel32.OpenFileMappingW(
            FILE_MAP_READ,
            False,
            self._signal_name,
        )
        if not self._signal_handle:
            # DLL hasn't created the shared memory yet, or isn't loaded
            raise FileNotFoundError(
                f"Could not open shared memory: {self._signal_name}"
            )

        # Open the lock shared memory
        self._lock_handle = kernel32.OpenFileMappingW(
            FILE_MAP_ALL_ACCESS,
            False,
            self._lock_name,
        )
        if not self._lock_handle:
            kernel32.CloseHandle(self._signal_handle)
            raise FileNotFoundError(f"Could not open shared memory: {self._lock_name}")

        # Map views of the shared memory
        # On Windows, mmap.mmap can work with file mapping handles
        # But we need to use MapViewOfFile for named shared memory
        self._signal_view = kernel32.MapViewOfFile(
            self._signal_handle,
            FILE_MAP_READ,
            0,
            0,
            MAX_FILE_SIZE,
        )
        if not self._signal_view:
            error_code = kernel32.GetLastError()
            kernel32.CloseHandle(self._signal_handle)
            kernel32.CloseHandle(self._lock_handle)
            raise OSError(f"Could not map signal view (error {error_code})")

        self._lock_view = kernel32.MapViewOfFile(
            self._lock_handle,
            FILE_MAP_ALL_ACCESS,
            0,
            0,
            LOCK_SIZE,
        )
        if not self._lock_view:
            kernel32.UnmapViewOfFile(self._signal_view)
            kernel32.CloseHandle(self._signal_handle)
            kernel32.CloseHandle(self._lock_handle)
            raise OSError("Could not map lock view")

        # Create ctypes buffers pointing to the mapped memory
        self._signal_buffer = (ctypes.c_char * MAX_FILE_SIZE).from_address(
            self._signal_view
        )
        self._lock_buffer = (ctypes.c_uint64 * (LOCK_SIZE // 8)).from_address(
            self._lock_view
        )

    def close(self) -> None:
        """Close the map file."""
        if sys.platform == "win32":
            import ctypes

            kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]
            if hasattr(self, "_signal_view") and self._signal_view:
                kernel32.UnmapViewOfFile(self._signal_view)
            if hasattr(self, "_lock_view") and self._lock_view:
                kernel32.UnmapViewOfFile(self._lock_view)
            if hasattr(self, "_signal_handle") and self._signal_handle:
                kernel32.CloseHandle(self._signal_handle)
            if hasattr(self, "_lock_handle") and self._lock_handle:
                kernel32.CloseHandle(self._lock_handle)
        else:
            if self._signal_fd:
                self._signal_fd.close()
            if self._lock_fd:
                self._lock_fd.close()

    def cleanup(self) -> None:
        """Remove all map files."""
        if sys.platform == "win32":
            # Windows named shared memory is automatically cleaned up
            # when all handles are closed
            pass
        else:
            try:
                os.remove(self._init_filename)
                os.remove(self._signal_filename)
            except FileNotFoundError:
                pass

    def read(self) -> Any:
        """Read a line from the map file."""
        if sys.platform == "win32":
            return self._read_windows()
        if not self._signal_mmap:
            return False
        return get_line_atomic.get_line_atomic(
            self._lock_mmap, self._signal_mmap, self._buf, self._lastpos
        )

    def _read_windows(self) -> bool:
        """Read a line from Windows shared memory."""
        import ctypes
        import struct

        if not hasattr(self, "_signal_buffer") or not hasattr(self, "_lock_buffer"):
            return False

        # Get current write position from lock buffer
        current_pos = self._lock_buffer[0]

        # Get our last read position
        last_read = struct.unpack("<Q", self._lastpos)[0]

        if current_pos <= last_read:
            return False

        # Read from last_read to find the next newline
        try:
            # Find the end of the line
            end_pos = last_read
            max_read = min(current_pos, last_read + self.MAX_BUFSIZE - 1)

            # First, check if the data at last_read looks valid (starts with M, F, or f)
            # If not, it might be stale data due to memory ordering - skip this read
            first_byte = self._signal_buffer[int(last_read)]
            # ctypes c_char buffer returns bytes, convert to int for comparison
            if isinstance(first_byte, bytes):
                first_byte = first_byte[0] if first_byte else 0  # type: ignore[assignment]
            valid_actions = [ord("M"), ord("F"), ord("f")]
            if first_byte not in valid_actions:
                # Data not yet visible or corrupted - don't advance position
                return False

            for i in range(int(last_read), int(max_read)):
                byte_val = self._signal_buffer[i]
                # ctypes c_char buffer returns bytes, convert to int for comparison
                if isinstance(byte_val, bytes):
                    byte_val = byte_val[0] if byte_val else 0  # type: ignore[assignment]
                if byte_val == ord("\n"):
                    end_pos = i + 1
                    break
                # Don't treat null byte as terminator - it means data isn't visible yet
                if byte_val == 0:
                    return False
            else:
                # No newline found within buffer - line might be incomplete
                # Only accept if we've reached current_pos (all available data)
                if max_read >= current_pos:
                    end_pos = int(max_read)
                else:
                    return False

            if end_pos <= last_read:
                return False

            # Copy data to buffer
            length = int(end_pos - last_read)
            self._buf[:length] = bytes(self._signal_buffer[int(last_read) : int(end_pos)])  # type: ignore[arg-type]
            self._buf[length:] = b"\x00" * (self.MAX_BUFSIZE - length)

            # Validate the sample has expected format before accepting
            sample_preview = self._buf[:100].decode("ascii", errors="replace")
            if "," not in sample_preview:
                # Malformed sample - skip it but advance position
                self._lastpos = struct.pack("<Q", end_pos)  # type: ignore[assignment]
                return False

            # Update last read position
            self._lastpos = struct.pack("<Q", end_pos)  # type: ignore[assignment]

            return True
        except Exception:
            return False

    def get_str(self) -> str:
        """Get the string from the buffer."""
        return self._buf.rstrip(b"\x00").split(b"\n")[0].decode("ascii")
