"""
    temporary investigation to a suspected bug of multiprocessing.shared_memory
"""

"""Provides shared memory for direct access across processes.

The API of this package is currently provisional. Refer to the
documentation for details.
"""

__all__ = [ 'SharedMemory' ]

from functools import partial
import mmap
import os
import errno
import struct
import secrets

import _winapi
_USE_POSIX = False
_O_CREX = os.O_CREAT | os.O_EXCL

# FreeBSD (and perhaps other BSDs) limit names to 14 characters.
_SHM_SAFE_NAME_LENGTH = 14

# Shared memory block name prefix
_SHM_NAME_PREFIX = 'wnsm_'

def _make_filename():
    "Create a random filename for the shared memory object."
    # number of random bytes to use for name
    nbytes = (_SHM_SAFE_NAME_LENGTH - len(_SHM_NAME_PREFIX)) // 2
    assert nbytes >= 2, '_SHM_NAME_PREFIX too long'
    name = _SHM_NAME_PREFIX + secrets.token_hex(nbytes)
    assert len(name) <= _SHM_SAFE_NAME_LENGTH
    return name


class SharedMemory:
    """Creates a new shared memory block or attaches to an existing
    shared memory block.

    Every shared memory block is assigned a unique name.  This enables
    one process to create a shared memory block with a particular name
    so that a different process can attach to that same shared memory
    block using that same name.

    As a resource for sharing data across processes, shared memory blocks
    may outlive the original process that created them.  When one process
    no longer needs access to a shared memory block that might still be
    needed by other processes, the close() method should be called.
    When a shared memory block is no longer needed by any process, the
    unlink() method should be called to ensure proper cleanup."""

    # Defaults; enables close() and unlink() to run without errors.
    _name = None
    _fd = -1
    _mmap = None
    _buf = None
    _flags = os.O_RDWR
    _mode = 0o600
    _prepend_leading_slash = True if _USE_POSIX else False

    def __init__(self, name=None, create=False, size=0):
        if not size >= 0:
            raise ValueError("'size' must be a positive integer")
        if create:
            self._flags = _O_CREX | os.O_RDWR
        if name is None and not self._flags & os.O_EXCL:
            raise ValueError("'name' can only be None if create=True")

        # Windows Named Shared Memory

        if create:
            while True:
                temp_name = _make_filename() if name is None else name
                # Create and reserve shared memory block with this name
                # until it can be attached to by mmap.
                h_map = _winapi.CreateFileMapping(
                    _winapi.INVALID_HANDLE_VALUE,
                    _winapi.NULL,
                    _winapi.PAGE_READWRITE,
                    (size >> 32) & 0xFFFFFFFF,
                    size & 0xFFFFFFFF,
                    temp_name
                )
                try:
                    last_error_code = _winapi.GetLastError()
                    if last_error_code == _winapi.ERROR_ALREADY_EXISTS:
                        if name is not None:
                            raise FileExistsError(
                                errno.EEXIST,
                                os.strerror(errno.EEXIST),
                                name,
                                _winapi.ERROR_ALREADY_EXISTS
                            )
                        else:
                            continue
                    self._mmap = mmap.mmap(-1, size, tagname=temp_name)
                finally:
                    _winapi.CloseHandle(h_map)
                self._name = temp_name
                break

        else:
            self._name = name
            # Dynamically determine the existing named shared memory
            # block's size which is likely a multiple of mmap.PAGESIZE.
            h_map = _winapi.OpenFileMapping(
                _winapi.FILE_MAP_READ,
                False,
                name
            )
            try:
                p_buf = _winapi.MapViewOfFile(
                    h_map,
                    _winapi.FILE_MAP_READ,
                    0,
                    0,
                    0
                )
            except:
                print('MapViewOfFile Error!')
            finally:
                print('h_map=', h_map)
                _winapi.CloseHandle(h_map)
            size = _winapi.VirtualQuerySize(p_buf)
            self._mmap = mmap.mmap(-1, size, tagname=name)

        self._size = size
        self._buf = memoryview(self._mmap)
        print(__name__, '__init__ ok')

    def __del__(self):
        try:
            self.close()
        except OSError:
            pass

    def __reduce__(self):
        return (
            self.__class__,
            (
                self.name,
                False,
                self.size,
            ),
        )

    def __repr__(self):
        return f'{self.__class__.__name__}({self.name!r}, size={self.size})'

    @property
    def buf(self):
        "A memoryview of contents of the shared memory block."
        return self._buf

    @property
    def name(self):
        "Unique name that identifies the shared memory block."
        reported_name = self._name
        if _USE_POSIX and self._prepend_leading_slash:
            if self._name.startswith("/"):
                reported_name = self._name[1:]
        return reported_name

    @property
    def size(self):
        "Size in bytes."
        return self._size

    def close(self):
        print(__name__, 'close')
        """Closes access to the shared memory from this instance but does
        not destroy the shared memory block."""
        if self._buf is not None:
            self._buf.release()
            self._buf = None
        if self._mmap is not None:
            self._mmap.close()
            self._mmap = None
        if _USE_POSIX and self._fd >= 0:
            os.close(self._fd)
            self._fd = -1
        print(__name__, 'close ok')

    def unlink(self):
        """Requests that the underlying shared memory block be destroyed.

        In order to ensure proper cleanup of resources, unlink should be
        called once (and only once) across all processes which have access
        to the shared memory block."""
        if _USE_POSIX and self._name:
            from .resource_tracker import unregister
            _posixshmem.shm_unlink(self._name)
            unregister(self._name, "shared_memory")
