#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Computation of apparent Young's modulus for RT-DC measurements"""
from __future__ import division, print_function, unicode_literals

import pathlib
from pkg_resources import resource_filename

import numpy as np
import scipy.interpolate as spint

from .emodulus_viscosity import get_viscosity


def convert(area_um, deform, channel_width_in, channel_width_out,
            emodulus=None, flow_rate_in=None, flow_rate_out=None,
            viscosity_in=None, viscosity_out=None, inplace=False):
    """convert area-deformation-emodulus triplet

    The conversion formula is described in :cite:`Mietke2015`.

    Parameters
    ----------
    area_um: ndarray
        Convex cell area [µm²]
    deform: ndarray
        Deformation
    channel_width_in: float
        Original channel width [µm]
    channel_width_out: float
        Target channel width [µm]
    emodulus: ndarray
        Young's Modulus [kPa]
    flow_rate_in: float
        Original flow rate [µl/s]
    flow_rate_in: float
        Target flow rate [µl/s]
    viscosity_in: float
        Original viscosity [mPa*s]
    viscosity_out: float or ndarray
        Target viscosity [mPa*s]; This can be an array
    inplace: bool
        If True, override input arrays with corrected data

    Returns
    -------
    area_um_corr: ndarray
        Corrected cell area [µm²]
    deform_corr: ndarray
        Deformation (a copy if `inplace` is False)
    emodulus_corr: ndarray
        Corrected emodulus [kPa]; only returned if `emodulus` is given.

    Notes
    -----
    If only `area_um`, `deform`, `channel_width_in` and
    `channel_width_out` are given, then only the area is
    corrected and returned together with the original deform.
    If all other arguments are not set to None, the emodulus
    is corrected and returned as well.
    """
    copy = not inplace
    # make sure area_um_corr is not an integer array
    area_um_corr = np.array(area_um, dtype=float, copy=copy)
    deform_corr = np.array(deform, copy=copy)

    if channel_width_in != channel_width_out:
        area_um_corr *= (channel_width_out / channel_width_in)**2

    if emodulus is not None:
        emodulus_corr = np.array(emodulus, copy=copy)

        if viscosity_in is not None:
            if isinstance(viscosity_in, np.ndarray):
                raise ValueError("`viscosity_in` must not be an array!")

        if (flow_rate_in is not None
            and flow_rate_out is not None
            and viscosity_in is not None
            and viscosity_out is not None
            and (flow_rate_in != flow_rate_out
                 or (isinstance(viscosity_out, np.ndarray)  # check b4 compare
                     or viscosity_in != viscosity_out)
                 or channel_width_in != channel_width_out)):
            emodulus_corr *= (flow_rate_out / flow_rate_in) \
                * (viscosity_out / viscosity_in) \
                * (channel_width_in / channel_width_out)**3

    if emodulus is None:
        return area_um_corr, deform_corr
    else:
        return area_um_corr, deform_corr, emodulus_corr


def corrpix_deform_delta(area_um, px_um=0.34):
    """Deformation correction term for pixelation effects

    The contour in RT-DC measurements is computed on a
    pixelated grid. Due to sampling problems, the measured
    deformation is overestimated and must be corrected.

    The correction formula is described in :cite:`Herold2017`.

    Parameters
    ----------
    area_um: float or ndarray
        Apparent (2D image) area in µm² of the event(s)
    px_um: float
        The detector pixel size in µm.
    inplace: bool
        Change the deformation values in-place

    Returns
    -------
    deform_delta: float or ndarray
        Error of the deformation of the event(s) that must be
        subtracted from `deform`.
        deform_corr = deform -  deform_delta
    """
    # A triple-exponential decay can be used to correct for pixelation
    # for apparent cell areas between 10 and 1250µm².
    # For 99 different radii between 0.4 μm and 20 μm circular objects were
    # simulated on a pixel grid with the pixel resolution of 340 nm/pix. At
    # each radius 1000 random starting points were created and the
    # obtained contours were analyzed in the same fashion as RT-DC data.
    # A convex hull on the contour was used to calculate the size (as area)
    # and the deformation.
    # The pixel size correction `pxcorr` takes into account the pixel size
    # in the pixelation correction formula.
    pxcorr = (.34 / px_um)**2
    offs = 0.0012
    exp1 = 0.020 * np.exp(-area_um * pxcorr / 7.1)
    exp2 = 0.010 * np.exp(-area_um * pxcorr / 38.6)
    exp3 = 0.005 * np.exp(-area_um * pxcorr / 296)
    delta = offs + exp1 + exp2 + exp3

    return delta


def get_emodulus(area_um, deform, medium="CellCarrier",
                 channel_width=20.0, flow_rate=0.16, px_um=0.34,
                 temperature=23.0, copy=True):
    """Compute apparent Young's modulus using a look-up table

    Parameters
    ----------
    area_um: float or ndarray
        Apparent (2D image) area [µm²] of the event(s)
    deform: float or ndarray
        The deformation (1-circularity) of the event(s)
    medium: str or float
        The medium to compute the viscosity for. If a string
        is given, the viscosity is computed. If a float is given,
        this value is used as the viscosity in mPa*s (Note that
        `temperature` must be set to None in this case).
    channel_width: float
        The channel width [µm]
    flow_rate: float
        Flow rate [µl/s]
    px_um: float
        The detector pixel size [µm] used for pixelation correction.
        Set to zero to disable.
    temperature: float, ndarray, or None
        Temperature [°C] of the event(s)
    copy: bool
        Copy input arrays. If set to false, input arrays are
        overridden.

    Returns
    -------
    elasticity: float or ndarray
        Apparent Young's modulus in kPa

    Notes
    -----
    - The look-up table used was computed with finite elements methods
      according to :cite:`Mokbel2017`.
    - The computation of the Young's modulus takes into account
      corrections for the viscosity (medium, channel width, flow rate,
      and temperature) :cite:`Mietke2015` and corrections for
      pixelation of the area and the deformation which are computed
      from a (pixelated) image :cite:`Herold2017`.

    See Also
    --------
    dclab.features.emodulus_viscosity.get_viscosity: compute viscosity
        for known media
    """
    # copy input arrays so we can use in-place calculations
    deform = np.array(deform, copy=copy, dtype=float)
    area_um = np.array(area_um, copy=copy, dtype=float)
    # Get lut data
    lut_path = resource_filename("dclab.features", "emodulus_lut.txt")
    with pathlib.Path(lut_path).open("rb") as lufd:
        lut = np.loadtxt(lufd)
    # These meta data are the simulation parameters of the lut
    lut_channel_width = 20.0
    lut_flow_rate = 0.04
    lut_visco = 15.0
    # Compute viscosity
    if isinstance(medium, (float, int)):
        visco = medium
        if temperature is not None:
            raise ValueError("It `medium` is given in Pa*s, then "
                             + "`temperature` must be set to None!")
    else:
        visco = get_viscosity(medium=medium, channel_width=channel_width,
                              flow_rate=flow_rate, temperature=temperature)

    if px_um:
        # Correct deformation for pixelation effect (subtract ddelt).
        ddelt = corrpix_deform_delta(area_um=area_um, px_um=px_um)
        deform -= ddelt

    if isinstance(visco, np.ndarray):
        # New in dclab 0.20.0
        # Convert the input area_um to that of the LUT (deform does not change)
        area_um_4lut, deform_4lut = convert(
            area_um=area_um,
            deform=deform,
            channel_width_in=channel_width,
            channel_width_out=lut_channel_width,
            inplace=False)

        # Normalize interpolation data such that the spacing for
        # area and deformation is about the same during interpolation.
        area_norm = lut[:, 0].max()
        normalize(lut[:, 0], area_norm)
        normalize(area_um_4lut, area_norm)

        defo_norm = lut[:, 1].max()
        normalize(lut[:, 1], defo_norm)
        normalize(deform_4lut, defo_norm)

        # Perform interpolation
        emod = spint.griddata((lut[:, 0], lut[:, 1]), lut[:, 2],
                              (area_um_4lut, deform_4lut),
                              method='linear')

        # Convert the LUT-interpolated emodulus back
        convert(area_um=area_um_4lut,
                deform=deform_4lut,
                emodulus=emod,
                channel_width_in=lut_channel_width,
                channel_width_out=channel_width,
                flow_rate_in=lut_flow_rate,
                flow_rate_out=flow_rate,
                viscosity_in=lut_visco,
                viscosity_out=visco,
                inplace=True)
    else:
        # Corrections
        # We correct the lut, because it contains less points than
        # the event data. Furthermore, the lut could be cached
        # in the future, if this takes up a lot of time.
        convert(area_um=lut[:, 0],
                deform=lut[:, 1],
                emodulus=lut[:, 2],
                channel_width_in=lut_channel_width,
                channel_width_out=channel_width,
                flow_rate_in=lut_flow_rate,
                flow_rate_out=flow_rate,
                viscosity_in=lut_visco,
                viscosity_out=visco,
                inplace=True)

        # Normalize interpolation data such that the spacing for
        # area and deformation is about the same during interpolation.
        area_norm = lut[:, 0].max()
        normalize(lut[:, 0], area_norm)
        normalize(area_um, area_norm)

        defo_norm = lut[:, 1].max()
        normalize(lut[:, 1], defo_norm)
        normalize(deform, defo_norm)

        # Perform interpolation
        emod = spint.griddata((lut[:, 0], lut[:, 1]), lut[:, 2],
                              (area_um, deform),
                              method='linear')
    return emod


def normalize(data, dmax):
    """Perform normalization inplace"""
    data /= dmax
    return data
