# Copyright (c) 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

'''
Bindings to the `tee` and `splice` system calls
'''

import os
import operator
import six
import ctypes
import ctypes.util

__all__ = ['tee', 'splice']


c_loff_t = ctypes.c_long


class Tee(object):
    '''Binding to `tee`'''

    __slots__ = '_c_tee',

    def __init__(self):
        libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)

        try:
            c_tee = libc.tee
        except AttributeError:
            self._c_tee = None
            return

        c_tee.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_size_t,
            ctypes.c_uint
        ]

        c_tee.restype = ctypes.c_ssize_t

        def errcheck(result, func, arguments):
            if result == -1:
                errno = ctypes.set_errno(0)

                raise IOError(errno, 'tee: %s' % os.strerror(errno))
            else:
                return result

        c_tee.errcheck = errcheck

        self._c_tee = c_tee

    def __call__(self, fd_in, fd_out, len_, flags):
        '''See `man 2 tee`

        File-descriptors can be file-like objects with a `fileno` method, or
        integers.

        Flags can be an integer value, or a list of flags (exposed on
        `splice`).

        This function returns the number of bytes transferred (i.e. the actual
        result of the call to `tee`).

        Upon other errors, an `IOError` is raised with the proper `errno` set.
        '''

        if not self.available:
            raise EnvironmentError('tee not available')

        if not isinstance(flags, six.integer_types):
            c_flags = six.moves.reduce(operator.or_, flags, 0)
        else:
            c_flags = flags

        c_fd_in = getattr(fd_in, 'fileno', lambda: fd_in)()
        c_fd_out = getattr(fd_out, 'fileno', lambda: fd_out)()

        return self._c_tee(c_fd_in, c_fd_out, len_, c_flags)

    @property
    def available(self):
        '''Availability of `tee`'''

        return self._c_tee is not None


tee = Tee()
del Tee


class Splice(object):
    '''Binding to `splice`'''

    # From `bits/fcntl-linux.h`
    SPLICE_F_MOVE = 1
    SPLICE_F_NONBLOCK = 2
    SPLICE_F_MORE = 4
    SPLICE_F_GIFT = 8

    __slots__ = '_c_splice',

    def __init__(self):
        libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)

        try:
            c_splice = libc.splice
        except AttributeError:
            self._c_splice = None
            return

        c_loff_t_p = ctypes.POINTER(c_loff_t)

        c_splice.argtypes = [
            ctypes.c_int, c_loff_t_p,
            ctypes.c_int, c_loff_t_p,
            ctypes.c_size_t,
            ctypes.c_uint
        ]

        c_splice.restype = ctypes.c_ssize_t

        def errcheck(result, func, arguments):
            if result == -1:
                errno = ctypes.set_errno(0)

                raise IOError(errno, 'splice: %s' % os.strerror(errno))
            else:
                off_in = arguments[1]
                off_out = arguments[3]

                return (
                    result,
                    off_in.contents.value if off_in is not None else None,
                    off_out.contents.value if off_out is not None else None)

        c_splice.errcheck = errcheck

        self._c_splice = c_splice

    def __call__(self, fd_in, off_in, fd_out, off_out, len_, flags):
        '''See `man 2 splice`

        File-descriptors can be file-like objects with a `fileno` method, or
        integers.

        Flags can be an integer value, or a list of flags (exposed on this
        object).

        Returns a tuple of the result of the `splice` call, the output value of
        `off_in` and the output value of `off_out` (or `None` for any of these
        output values, if applicable).

        Upon other errors, an `IOError` is raised with the proper `errno` set.

        Note: if you want to pass `NULL` as value for `off_in` or `off_out` to
        the system call, you must pass `None`, *not* 0!
        '''

        if not self.available:
            raise EnvironmentError('splice not available')

        if not isinstance(flags, six.integer_types):
            c_flags = six.moves.reduce(operator.or_, flags, 0)
        else:
            c_flags = flags

        c_fd_in = getattr(fd_in, 'fileno', lambda: fd_in)()
        c_fd_out = getattr(fd_out, 'fileno', lambda: fd_out)()

        c_off_in = \
            ctypes.pointer(c_loff_t(off_in)) if off_in is not None else None
        c_off_out = \
            ctypes.pointer(c_loff_t(off_out)) if off_out is not None else None

        return self._c_splice(
            c_fd_in, c_off_in, c_fd_out, c_off_out, len_, c_flags)

    @property
    def available(self):
        '''Availability of `splice`'''

        return self._c_splice is not None


splice = Splice()
del Splice
