"""
Files
-----

A module containing common Windows file functions for working with files.
"""

from six import integer_types, text_type, binary_type

from pywincffi.core import dist
from pywincffi.core.checks import NON_ZERO, input_check, error_check, NoneType
from pywincffi.exceptions import WindowsAPIError
from pywincffi.wintypes import (
    SECURITY_ATTRIBUTES, OVERLAPPED, HANDLE, wintype_to_cdata
)


def CreateFile(  # pylint: disable=too-many-arguments
        lpFileName, dwDesiredAccess, dwShareMode=None,
        lpSecurityAttributes=None, dwCreationDisposition=None,
        dwFlagsAndAttributes=None, hTemplateFile=None):
    """
    Creates or opens a file or other I/O device.  Default values are
    provided for some of the default arguments for CreateFile() so
    its behavior is close to Pythons :func:`open` function.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa363858
        https://msdn.microsoft.com/en-us/library/gg258116

    :param str lpFileName:
        Type is ``unicode`` on Python 2, ``str`` on Python 3.
        The path to the file or device being created or opened.

    :param int dwDesiredAccess:
        The requested access to the file or device.  Microsoft's documentation
        has extensive notes on this parameter in the seealso links above.

    :keyword int dwShareMode:
        Access and sharing rights to the handle being created.  If not provided
        with an explicit value, ``FILE_SHARE_READ`` will be used which will
        other open operations or process to continue to read from the file.

    :keyword pywincffi.wintypes.SECURITY_ATTRIBUTES lpSecurityAttributes:
        See Microsoft's documentation for more detailed information.

    :keyword int dwCreationDisposition:
        Action to take when the file or device does not exist.  If not
        provided with an explicit value, ``CREATE_ALWAYS`` will be used
        which means existing files will be overwritten.

    :keyword int dwFlagsAndAttributes:
        The file or device attributes and flags.  If not provided an explict
        value, ``FILE_ATTRIBUTE_NORMAL`` will be used giving the handle
        essentially no special attributes.

    :keyword pywincffi.wintypes.HANDLE hTemplateFile:
        A handle to a template file with the ``GENERIC_READ`` access right.
        See Microsoft's documentation for more information.  If not
        provided an explicit value, ``NULL`` will be used instead.

    :return:
        The file :class:`pywincffi.wintypes.HANDLE` created by ``CreateFile``.
    """
    _, library = dist.load()

    if dwShareMode is None:
        dwShareMode = library.FILE_SHARE_READ

    if dwCreationDisposition is None:
        dwCreationDisposition = library.CREATE_ALWAYS

    if dwFlagsAndAttributes is None:
        dwFlagsAndAttributes = library.FILE_ATTRIBUTE_NORMAL

    input_check("lpFileName", lpFileName, text_type)
    input_check("dwDesiredAccess", dwDesiredAccess, integer_types)
    input_check("dwShareMode", dwShareMode, integer_types)
    input_check(
        "lpSecurityAttributes", lpSecurityAttributes,
        allowed_types=(NoneType, SECURITY_ATTRIBUTES)
    )
    input_check(
        "dwCreationDisposition", dwCreationDisposition,
        allowed_values=(
            library.CREATE_ALWAYS,
            library.CREATE_NEW,
            library.OPEN_ALWAYS,
            library.OPEN_EXISTING,
            library.TRUNCATE_EXISTING
        )
    )
    input_check("dwFlagsAndAttributes", dwFlagsAndAttributes, integer_types)
    input_check("hTemplateFile", hTemplateFile, (NoneType, HANDLE))

    handle = library.CreateFile(
        lpFileName, dwDesiredAccess, dwShareMode,
        wintype_to_cdata(lpSecurityAttributes), dwCreationDisposition,
        dwFlagsAndAttributes, wintype_to_cdata(hTemplateFile)
    )

    try:
        error_check("CreateFile")
    except WindowsAPIError as error:
        # ERROR_ALREADY_EXISTS may be a normal condition depending
        # on the creation disposition.
        if (dwCreationDisposition == library.CREATE_ALWAYS and
                error.errno == library.ERROR_ALREADY_EXISTS):
            return HANDLE(handle)
        raise

    return HANDLE(handle)


def WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite=None, lpOverlapped=None):
    """
    Writes data to ``hFile`` which may be an I/O device for file.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa365747

    :param pywincffi.wintypes.HANDLE hFile:
        The handle to write to.

    :type lpBuffer: str/bytes
    :param lpBuffer:
        Type is ``str`` on Python 2, ``bytes`` on Python 3.
        The data to be written to the file or device.

    :keyword int nNumberOfBytesToWrite:
        The number of bytes to be written.  Defaults to len(lpBuffer).

    :keyword pywincffi.wintypes.OVERLAPPED lpOverlapped:
        See Microsoft's documentation for intended usage and below for
        an example.

        >>> from pywincffi.core import dist
        >>> from pywincffi.kernel32 import WriteFile, CreateEvent
        >>> from pywincffi.wintypes import OVERLAPPED
        >>> hEvent = CreateEvent(...)
        >>> lpOverlapped = OVERLAPPED()
        >>> lpOverlapped.hEvent = hEvent
        >>> bytes_written = WriteFile(
        ...     hFile, "Hello world", lpOverlapped=lpOverlapped)

    :returns:
        Returns the number of bytes written.
    """
    ffi, library = dist.load()

    input_check("hFile", hFile, HANDLE)
    input_check("lpBuffer", lpBuffer, binary_type)
    input_check(
        "lpOverlapped", lpOverlapped,
        allowed_types=(NoneType, OVERLAPPED)
    )

    if nNumberOfBytesToWrite is None:
        nNumberOfBytesToWrite = len(lpBuffer)
    else:
        input_check(
            "nNumberOfBytesToWrite", nNumberOfBytesToWrite,
            integer_types
        )

    bytes_written = ffi.new("LPDWORD")
    code = library.WriteFile(
        wintype_to_cdata(hFile), lpBuffer, nNumberOfBytesToWrite,
        bytes_written, wintype_to_cdata(lpOverlapped)
    )
    expected = NON_ZERO if lpOverlapped is None else 0
    error_check("WriteFile", code=code, expected=expected)

    return bytes_written[0]


def FlushFileBuffers(hFile):
    """
    Flushes the buffer of the specified file to disk.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa364439

    :param pywincffi.wintypes.HANDLE hFile:
        The handle to flush to disk.
    """
    input_check("hFile", hFile, HANDLE)
    _, library = dist.load()
    code = library.FlushFileBuffers(wintype_to_cdata(hFile))
    error_check("FlushFileBuffers", code=code, expected=NON_ZERO)


def ReadFile(hFile, nNumberOfBytesToRead, lpOverlapped=None):
    """
    Read the specified number of bytes from ``hFile``.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa365467

    :param pywincffi.wintypes.HANDLE hFile:
        The handle to read from.

    :param int nNumberOfBytesToRead:
        The number of bytes to read from ``hFile``

    :keyword pywincffi.wintypes.OVERLAPPED lpOverlapped:
        See Microsoft's documentation for intended usage and below for
        an example.

        >>> from pywincffi.core import dist
        >>> from pywincffi.kernel32 import ReadFile, CreateEvent
        >>> from pywincffi.wintypes import OVERLAPPED
        >>> hEvent = CreateEvent(...)
        >>> lpOverlapped = OVERLAPPED()
        >>> lpOverlapped.hEvent = hEvent
        >>> read_data = ReadFile(  # read 12 bytes from hFile
        ...     hFile, 12, lpOverlapped=lpOverlapped)

    :returns:
        Returns the binary data read from ``hFile``
        Type is ``str`` on Python 2, ``bytes`` on Python 3.
    """
    ffi, library = dist.load()

    input_check("hFile", hFile, HANDLE)
    input_check("nNumberOfBytesToRead", nNumberOfBytesToRead, integer_types)
    input_check(
        "lpOverlapped", lpOverlapped,
        allowed_types=(NoneType, OVERLAPPED)
    )

    lpBuffer = ffi.new("char []", nNumberOfBytesToRead)
    bytes_read = ffi.new("LPDWORD")
    code = library.ReadFile(
        wintype_to_cdata(hFile), lpBuffer, nNumberOfBytesToRead, bytes_read,
        wintype_to_cdata(lpOverlapped)
    )
    error_check("ReadFile", code=code, expected=NON_ZERO)
    return ffi.unpack(lpBuffer, bytes_read[0])


def MoveFileEx(lpExistingFileName, lpNewFileName, dwFlags=None):
    """
    Moves an existing file or directory, including its children,
    see the MSDN documentation for full options.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa365240

    :param str lpExistingFileName:
        Type is ``unicode`` on Python 2, ``str`` on Python 3.
        Name of the file or directory to perform the operation on.

    :param str lpNewFileName:
        Type is ``unicode`` on Python 2, ``str`` on Python 3.
        Optional new name of the path or directory.  This value may be
        ``None``.

    :keyword int dwFlags:
        Parameters which control the operation of :func:`MoveFileEx`.  See
        the MSDN documentation for full details.  By default
        ``MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH`` is used.
    """
    ffi, library = dist.load()

    if dwFlags is None:
        dwFlags = \
            library.MOVEFILE_REPLACE_EXISTING | library.MOVEFILE_WRITE_THROUGH

    input_check("lpExistingFileName", lpExistingFileName, text_type)
    input_check("dwFlags", dwFlags, integer_types)

    if lpNewFileName is not None:
        input_check("lpNewFileName", lpNewFileName, text_type)
    else:
        lpNewFileName = ffi.NULL

    code = library.MoveFileEx(
        lpExistingFileName,
        lpNewFileName,
        ffi.cast("DWORD", dwFlags)
    )
    error_check("MoveFileEx", code=code, expected=NON_ZERO)


def LockFileEx(
        hFile, dwFlags, nNumberOfBytesToLockLow, nNumberOfBytesToLockHigh,
        lpOverlapped=None):
    """
    Locks ``hFile`` for exclusive access by the calling process.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa365203

    :param pywincffi.wintypes.HANDLE hFile:
        The handle to the file to lock.  This handle must have been
        created with either the ``GENERIC_READ`` or ``GENERIC_WRITE``
        right.

    :param int dwFlags:
        One or more of the following flags:

            * ``LOCKFILE_EXCLUSIVE_LOCK`` - Request an exclusive lock.
            * ``LOCKFILE_FAIL_IMMEDIATELY`` - Return immediately if the lock
              could not be acquired.  Otherwise :func:`LockFileEx` will wait.

    :param int nNumberOfBytesToLockLow:
        The start of the byte range to lock.

    :param int nNumberOfBytesToLockHigh:
        The end of the byte range to lock.

    :keyword pywincffi.wintypes.OVERLAPPED lpOverlapped:
        The underlying Windows API requires lpOverlapped, which acts both
        an input argument and may contain results after calling. If None is
        provided, a throw-away zero-filled instance will be created to
        support such call. See Microsoft's documentation for intended usage.
    """
    input_check("hFile", hFile, HANDLE)
    input_check("dwFlags", dwFlags, integer_types)
    input_check(
        "nNumberOfBytesToLockLow", nNumberOfBytesToLockLow, integer_types)
    input_check(
        "nNumberOfBytesToLockHigh", nNumberOfBytesToLockHigh, integer_types)

    ffi, library = dist.load()

    if lpOverlapped is None:
        # Required by Windows API, create a throw-away zero-filled instance.
        lpOverlapped = OVERLAPPED()
    else:
        input_check("lpOverlapped", lpOverlapped, allowed_types=OVERLAPPED)

    code = library.LockFileEx(
        wintype_to_cdata(hFile),
        ffi.cast("DWORD", dwFlags),
        ffi.cast("DWORD", 0),  # "_Reserveved_"
        ffi.cast("DWORD", nNumberOfBytesToLockLow),
        ffi.cast("DWORD", nNumberOfBytesToLockHigh),
        wintype_to_cdata(lpOverlapped)
    )
    error_check("LockFileEx", code=code, expected=NON_ZERO)


def UnlockFileEx(
        hFile, nNumberOfBytesToUnlockLow, nNumberOfBytesToUnlockHigh,
        lpOverlapped=None):
    """
    Unlocks a region in the specified file.

    .. seealso::

        https://msdn.microsoft.com/en-us/library/aa365716

    :param pywincffi.wintypes.HANDLE hFile:
        The handle to the file to unlock.  This handle must have been
        created with either the ``GENERIC_READ`` or ``GENERIC_WRITE``
        right.

    :param int nNumberOfBytesToUnlockLow:
        The start of the byte range to unlock.

    :param int nNumberOfBytesToUnlockHigh:
        The end of the byte range to unlock.

    :keyword pywincffi.wintypes.OVERLAPPED lpOverlapped:
        The underlying Windows API requires lpOverlapped, which acts both
        an input argument and may contain results after calling. If None is
        provided, a throw-away zero-filled instance will be created to
        support such call. See Microsoft's documentation for intended usage.
    """
    input_check("hFile", hFile, HANDLE)
    input_check(
        "nNumberOfBytesToUnlockLow",
        nNumberOfBytesToUnlockLow, integer_types)
    input_check(
        "nNumberOfBytesToUnlockHigh",
        nNumberOfBytesToUnlockHigh, integer_types)

    ffi, library = dist.load()

    if lpOverlapped is None:
        # Required by Windows API, create a throw-away zero-filled instance.
        lpOverlapped = OVERLAPPED()
    else:
        input_check("lpOverlapped", lpOverlapped, allowed_types=OVERLAPPED)

    code = library.UnlockFileEx(
        wintype_to_cdata(hFile),
        ffi.cast("DWORD", 0),  # "_Reserveved_"
        ffi.cast("DWORD", nNumberOfBytesToUnlockLow),
        ffi.cast("DWORD", nNumberOfBytesToUnlockHigh),
        wintype_to_cdata(lpOverlapped)
    )
    error_check("UnlockFileEx", code=code, expected=NON_ZERO)


def GetTempPath():
    """
    Retrieves the path of the directory designated for temporary files.

    .. seealso::

        https://msdn.microsoft.com/en-us/aa364992

    :returns:
        Returns a string containing the value produced by the underlying
        C function.
    """
    ffi, library = dist.load()
    lpBuffer = ffi.new("TCHAR[{}]".format(library.MAX_PATH + 1))
    code = library.GetTempPath(library.MAX_PATH + 1, lpBuffer)
    error_check("GetTempPath", code=code, expected=NON_ZERO)
    return ffi.string(lpBuffer)
