import numpy as np
from emva1288.camera import routines


class Camera(object):
    """Camera simulator. It creates images according to the given parameters.
    """
    def __init__(self,
                 f_number=8,  # F-number of the light source/camera setup
                 pixel_area=25,  # um^2
                 bit_depth=8,  # Bit depth of the image [8, 10, 12, 14]
                 width=640,
                 height=480,

                 temperature=22,  # Sensor temperature in ^oC
                 temperature_ref=30,  # Reference temperature
                 temperature_doubling=8,  # Doubling temperature

                 qe=None,  # Quantum efficiency for the given wavelength
                 exposure=1000000,  # Exposure time in ns
                 exposure_min=500000,  # Minimum exposure time in ns
                 exposure_max=500000000,  # Maximum exposure time in ns

                 K=0.1,  # Overall system gain
                 K_min=0.1,
                 K_max=17.,
                 K_steps=255,

                 blackoffset=0,
                 blackoffset_min=0,
                 blackoffset_max=None,
                 blackoffset_steps=255,

                 dark_current_ref=30,
                 dark_signal_0=10.,
                 sigma2_dark_0=10.,

                 u_esat=15000.,

                 dsnu=None,
                 prnu=None,
                 seed=None
                 ):
        """Camera simulator init method.

        Parameters
        ----------
        f_number : float, optional
                   The emva1288 f_number for the camera.
        pixel_area : float, optional
                     The area of one pixel (in um ^ 2)
        bit_depth : int, optional
                    The number of bits allowed for one pixel value.
        width : int, optional
                The number of columns in the the image.
        height : int, optional
                The number of rows in the image.
        temperature : float, optional
                      The camera's sensor temperature in degrees Celsius.
        temperature_ref : float, optional
                          The reference temperature (at which the dark current
                          is equal to the reference dark current).
        temperature_doubling: float, optional
                              The doubling temperature (at which the dark
                              current is two times the reference dark
                              current).
        qe : float, optional
             Quantum efficiency (between 0 and 1). If None, a simulated
             quantum efficiency is choosen with the
             :func:`~emva1288.camera.routines.qe` function.
        exposure : float, optional
                   The camera's exposure time in ns.
        exposure_min : float, optional
                       The camera's minimal exposure time in ns.
        exposure_max : float, optional
                       The camera's maximal exposure time in ns.
        K : float, optional
            The overall system gain (in DN/e^-).
        K_min : float, optional
                The overall minimal system gain (in DN/e^-).
        K_max : float, optional
                The overall maximal system gain (in DN/e^-).
        K_steps : int, optional
                  The number of available intermediate overall system gains
                  between K_min and K_max.
        blackoffset : float, optional
                      The dark signal offset for each pixel (in DN).
        blackoffset_min: float, optional
                         The minimal dark signal offset for each pixel (in DN).
        blackoffset_max : float, optional
                          The maximal dark signal offset for each pixel
                          (in DN).
        blackoffset_steps : int, optional
                            The number of available blackoffsets between the
                            mimimal and maximal blackoffsets.
        dark_current_ref : float, optional
                           The reference dark current used for computing the
                           total dark current.
        dark_signal_0 : float, optional
            Mean number of electrons generated by the electronics (offset)
        sigma2_dark_0 : float, optional
            Variance of electrons generated by the electronics
        u_esat : float, optional
            Full well capacity
        dsnu : np.array, optional
               DSNU image in e^-, array with the same shape of the image
               that is added to every image.
        prnu : np.array, optional
               PRNU image in percentages (1 = 100%), array with the same shape
               of the image. Every image is multiplied by it.
        seed : int, optional
               A seed to initialize the random number generator.
        """
        self._pixel_area = pixel_area
        self._bit_depth = bit_depth
        self._img_max = 2 ** int(bit_depth) - 1

        if bit_depth <= 8:
            self._img_type = np.uint8
        elif bit_depth <= 16:
            self._img_type = np.uint16
        else:
            self._img_type = np.uint64

        self._width = width
        self._height = height
        self._shape = (self.height, self.width)

        self._temperature_ref = temperature_ref
        self._temperature_doubling = temperature_doubling
        self._qe = qe
        # When no specific qe is provided we simulate one
        if qe is None:
            self._qe = routines.Qe(height=self._height, width=self._width)

        self._dark_current_ref = dark_current_ref
        self._dark_signal_0 = dark_signal_0
        self._sigma2_dark_0 = sigma2_dark_0

        self._exposure = exposure
        self._exposure_min = exposure_min
        self._exposure_max = exposure_max

        self._u_esat = u_esat

        self.__Ks = np.linspace(K_min, K_max, num=K_steps)
        self._K = None
        self.K = K

        # A good gestimate for maximum blackoffset is 1/16th of the full range
        if not blackoffset_max:
            blackoffset_max = self.img_max // 16
        self.__blackoffsets = np.linspace(blackoffset_min, blackoffset_max,
                                          num=blackoffset_steps)
        self._blackoffset = None
        self.blackoffset = blackoffset

        self._dsnu = dsnu
        self._prnu = prnu

        if dsnu is None:
            self._dsnu = np.zeros(self._shape)
        if prnu is None:
            self._prnu = np.ones(self._shape)
        self.environment = {'temperature': temperature,
                            'f_number': f_number}

        self._rng = np.random.default_rng(seed)

    @property
    def bit_depth(self):
        """The number of bits allowed for a gray value for one pixel."""
        return self._bit_depth

    @property
    def pixel_area(self):
        """The area of one pixel (in um ^ 2)."""
        return self._pixel_area

    @property
    def img_max(self):
        """The maximal value for one pixel (in DN)."""
        return self._img_max

    @property
    def width(self):
        """The number of columns"""
        return self._width

    @property
    def height(self):
        """The number of rows"""
        return self._height

    @property
    def exposure(self):
        """The camera's exposure time (in ns)."""
        return self._exposure

    @exposure.setter
    def exposure(self, value):
        self._exposure = value

    @property
    def exposure_min(self):
        """The camera's minimal exposure time (in ns)."""
        return self._exposure_min

    @property
    def exposure_max(self):
        """The camera's maximal exposure time (in ns)."""
        return self._exposure_max

    @property
    def K(self):
        """The overall system gain (in DN/e^-).

        :Setter: The setter uses the
                 :func:`~emva1288.camera.routines.nearest_value` function
                 to set the system gain to the nearest value given to the
                 setter.
                 This is because not all system gains are possible but rather a
                 linear sample between the minimal and maximal value.
        """
        return self._K

    @K.setter
    def K(self, value):
        self._K = routines.nearest_value(value, self.__Ks)

    @property
    def Ks(self):
        """The array of all the available system gains (in DN/e^-)."""
        return self.__Ks

    @property
    def blackoffset(self):
        """The system dark signal offset (in DN).

        :Setter: The setter uses the
                 :func:`~emva1288.camera.routines.nearest_value` function
                 to set the black signal offset to the nearest value given
                 to the setter. This is because not all black offsets are
                 possible but rather a linear sample between the minimal
                 and maximal value.
        """
        return self._blackoffset

    @blackoffset.setter
    def blackoffset(self, value):
        self._blackoffset = routines.nearest_value(value,
                                                   self.__blackoffsets)

    @property
    def blackoffsets(self):
        """The array of all blackoffsets (in DN)."""
        return self.__blackoffsets

    @property
    def DSNU(self):
        """The Dark Signal non uniformity, DSNU (in e^-).

        :Setter: The setter allow the option to set a new dsnu who have the
                 same shape than the image. See how it's used
                 :func:`~emva1288.camera.bad_images.bad_pixel`.
        """
        return self._dsnu

    @DSNU.setter
    def DSNU(self, value):
        # TODO: Conditions to be sure than the dsnu gived has the good shape.
        self._dsnu = value

    @property
    def qe(self):
        return self._qe

    def grab(self, radiance, temperature=None, f_number=None):
        """
        Create an image based on the mean and standard deviation from the
        EMVA1288 parameters.

        The image is generated using a normal distribution for each pixel.

        Parameters
        ----------
        radiance : ndarray
                   The sensor's illumination in W/cm^2/sr. This is the only
                   mandatory argument because it is frequently changed during
                   an test.
        temperature : float, optional
                      The camera's temperature in degrees Celsius.
                      If None, the environment's
                      temperature will be taken.
        f_number : float, optional
                   The optical setup f_number.
                   If None, the environment's f_number will be taken.
        """
        clipping_point = int(self.img_max)

        ###################################
        # Thermally induced electrons image
        u_d = self._u_therm(temperature=temperature)
        # Noise centred on the number of electrons thermally generated
        img_e = self._rng.poisson(u_d, size=self._shape)

        ###############################
        # Light induced electrons image
        u_e = self._u_e(radiance, f_number=f_number)
        # Noise centred on the number of electrons from the Light
        img_e += self._rng.poisson(u_e, size=self._shape)
        ####################################################################
        # Electronics induced electrons image and Dark Signal non uniformity
        variance = np.sqrt(self._sigma2_dark_0)
        dark_signal = self._dsnu + self._rng.normal(loc=self._dark_signal_0,
                                                    scale=variance,
                                                    size=self._shape)
        img_e = img_e + dark_signal

        ###########################################
        # Clip of the Full well electrons capacity
        np.clip(img_e, 0, self._u_esat, img_e)

        # Analog to Digital gain
        img = self.K * img_e

        # Quantization noise image
        img_q = self._rng.uniform(-0.5, 0.5, self._shape)
        img += img_q

        # Offset on the dark_signal
        img += self.blackoffset

        np.rint(img, img)
        np.clip(img, 0, clipping_point, img)
        return img.astype(self._img_type)

    def _u_e(self, radiance, f_number=None):
        """
        Light induced electrons image for the exposure time.
        """
        # number of photons for each pixel during exposure time.
        photons = self.get_photons(radiance, f_number=f_number)
        # copy the filter so we don't alter the camera's filter
        u_e = np.sum(np.multiply(photons, self._qe.qe), axis=2)
        u_e *= self._prnu
        return u_e

    def _u_therm(self, temperature=None):
        """
        Mean number of electrons due to temperature.
        """
        u_d = self._u_i(temperature=temperature) * self.exposure / (10 ** 9)
        return u_d

    def _u_i(self, temperature=None):
        """
        Dark current (in DN/s).
        """
        if temperature is None:
            temperature = self.environment['temperature']
        u_i = 1. * self._dark_current_ref * 2 ** (
            (temperature - self._temperature_ref) / self._temperature_doubling)
        return u_i

    def get_radiance_for(self, mean=None, exposure=None):
        """Radiance to achieve saturation.

        Calls the :func:`~emva1288.camera.routines.get_radiance` function
        to get the radiance for saturation.

        Parameters
        ----------
        mean : float, optional
               The saturation value of the camera. If None, this value
               is set to the :attr:`img_max` attribute.
        exposure : float, optional
                   The camera's exposure time at which the radiance
                   for saturation value will be computed. If None, the
                   exposure time taken will be the camera's actual exposure
                   time.

        Returns
        -------
        float :
            The radiance at which, for the given saturation value and
            the given exposure time, the camera saturates.
        """
        if not mean:
            mean = self.img_max
        if not exposure:
            exposure = self.exposure
        ud = self._u_therm()
        ue = ((mean / self.K) - ud)/len(self._qe.w)
        ue *= np.ones(len(self._qe.w))
        up = np.zeros((self.height, self.width, len(self._qe.w)))
        up[:, :] = ue / self._qe.qe.mean()
        radiance = routines.get_radiance(exposure,
                                         up,
                                         self.pixel_area,
                                         self.environment['f_number'],
                                         self._qe.w)
        return radiance

    def get_photons(self, radiance, exposure=None, f_number=None):
        """Computes the number of photons received by one pixel.

        Uses the :func:`~emva1288.camera.routines.get_photons` function
        to compute this number.

        Parameters
        ----------
        radiance : float
                   The radiance exposed to the camera (in Wsr^-1cm^-2).
        exposure : float, optional
                   The pixel's exposure time in ns.

        Returns
        -------
        float :
            The number of photons received by one pixel.
        """
        if exposure is None:
            exposure = self.exposure
        if f_number is None:
            f_number = self.environment['f_number']
        return routines.get_photons(exposure,
                                    radiance,
                                    self.pixel_area,
                                    self._qe.w,
                                    f_number)
