
import copy
import numpy as np

AGENTS = [
    'gadoxetate',
    'gadobutrol',
    'gadopentetate',
    'gadobenate',
    'gadodiamide',
    'gadoterate',
    'gadoteridol',
    'gadopiclenol',
]


def ca_injection(t: np.ndarray, weight: float, conc: float,
                dose: float, rate: float, t0: float) -> np.ndarray:
    """Contrast agent flux (mmol/sec) generated by step injection.

    Args:
        t (numpy.ndarray): time points in sec where the flux is to be calculated.
        weight (float): weight of the subject in kg.
        conc (float): concentration of the contrast agent in mmol/mL.
        dose (float): injected dose in mL/kg body weight.
        rate (float): rate of contrast agent injection in mL/sec.
        t0 (float): start of the injection in sec.

    Raises:
        ValueError: if the injection duration is zero, or smaller than the time step of the time array.

    Returns:
        numpy.ndarray: contrast agent flux for each time point in units of mmol/sec.

    Example:

        >>> import numpy as np
        >>> import dcmri as dc

        Create an array of time points covering 20sec in steps of 1.5sec.

        >>> t = np.arange(0, 20, 1.5)

        Inject a dose of 0.2 mL/kg bodyweight at a rate of 3mL/sec starting at t=5sec.
        For a subject weighing 70 kg and a contrast agent with concentration 0.5M, this produces the flux:

        >>> dc.ca_injection(t, 70, 0.5, 5, 0.2, 3)
        array([0. , 0. , 0. , 0. , 1.5, 1.5, 1.5, 0. , 0. , 0. , 0. , 0. , 0. ,0. ])
    """

    # Get timings
    duration = weight * dose / rate     # sec = kg * (mL/kg) / (mL/sec)
    dt = np.amin(t[1:] - t[:-1])

    # Check consistency of timings
    if dose > 0:
        if duration == 0:
            msg = 'Invalid input variables. '
            msg += 'The injection duration is zero.'
            raise ValueError(msg)
        if dt >= duration:
            msg = 'Invalid input variables. \n'
            msg += 'The smallest time step dt (' + str(
                dt) + ' sec) is larger than the injection duration 1 (' + str(duration) + 'sec). \n'
            msg += 'We would recommend dt to be at least 5 times smaller.'
            raise ValueError(msg)

    # Build flux
    Jmax = conc * rate                # mmol/sec = (mmol/ml) * (ml/sec)
    J = np.zeros(t.size)
    J[(0 < t) & (t < duration)] = Jmax
    return np.interp(t - t0, t, J, left=0)


def ca_conc(agent: str) -> float:
    """Contrast agent concentration

    Args:
        agent (str): Generic contrast agent name, all lower case. Examples are 'gadobutrol', 'gadobenate', etc.

    Raises:
        ValueError: If no data are available for the agent.

    Returns:
        float: concentration in mmol/mL

    Sources:
        - `mri questions <https://mriquestions.com/so-many-gd-agents.html>`_
        - `gadoxetate <https://www.bayer.com/sites/default/files/2020-11/primovist-pm-en.pdf>`_
        - `gadobutrol <https://www.medicines.org.uk/emc/product/2876/smpc#gref>`_

    Example:

        Print the concentration of the agents gadobutrol and gadoterate:

    .. exec_code::

        import dcmri as dc

        print('gadobutrol is available in a solution of', dc.ca_conc('gadobutrol'), 'M')
        print('gadoterate is available in a solution of', dc.ca_conc('gadoterate'), 'M')
    """
    if agent == 'gadoxetate':
        return 0.25     # mmol/mL
    if agent == 'gadobutrol':
        return 1.0      # mmol/mL
    if agent in [
        'gadopentetate',
        'gadobenate',
        'gadodiamide',
        'gadoterate',
        'gadoteridol',
        'gadopiclenol',
    ]:
        return 0.5  # mmol/mL
    raise ValueError(
        f"No concentration data for contrast agent {agent}."
        f"Possible values are {AGENTS}."
    )


def ca_std_dose(agent: str) -> float:
    """Standard injection volume (dose) in mL per kg body weight.

    Args:
        agent (str): Generic contrast agent name, all lower case. Examples are 'gadobutrol', 'gadobenate', etc.

    Raises:
        ValueError: If no data are available for the agent.

    Returns:
        float: Standard injection volume in mL/kg.

    Note:

        Available agents:
            - gadoxetate
            - gadobutrol
            - gadopiclenol
            - gadopentetate
            - gadobenate
            - gadodiamide
            - gadoterate
            - gadoteridol

        Sources:
            - `mri questions <https://mriquestions.com/so-many-gd-agents.html>`_
            - `gadoxetate <https://www.bayer.com/sites/default/files/2020-11/primovist-pm-en.pdf>`_
            - `gadobutrol <https://www.medicines.org.uk/emc/product/2876/smpc#gref>`_

    Example:

        >>> import dcmri as dc
        >>> print('The standard clinical dose of gadobutrol is', dc.ca_std_dose('gadobutrol'), 'mL/kg')
        The standard clinical dose of gadobutrol is 0.1 mL/kg
    """
    # """Standard dose in mL/kg""" # better in mmol/kg, or offer it as an option
    if agent == 'gadoxetate':
        # https://www.bayer.com/sites/default/files/2020-11/primovist-pm-en.pdf
        return 0.1  # mL/kg
    if agent == 'gadobutrol':
        return 0.1      # mL/kg
    if agent == 'gadopiclenol':
        return 0.1      # mL/kg
    if agent in [
            'gadopentetate',
            'gadobenate',
            'gadodiamide',
            'gadoterate',
            'gadoteridol',
    ]:
        return 0.2      # mL/kg  # 0.5 mmol/mL = 0.1 mmol/kg
    raise ValueError(
        f"No data available for contrast agent {agent}."
        f"Possible values are {AGENTS}."
    )


def relaxivity(field_strength=3.0, tissue='plasma',
               agent='gadoxetate', type='T1') -> float:
    """Contrast agent relaxivity values in units of Hz/M

    Args:
        field_strength (float, optional): Field strength in Tesla. Defaults to 3.0.
        tissue (str, optional): Tissue type - options are 'plasma', 'hepatocytes'. Defaults to 'plasma'.
        agent (str, optional): Generic contrast agent name, all lower case. Examples are 'gadobutrol', 'gadobenate', etc.. Defaults to 'gadoxetate'.
        type (str, optional): transverse (T2 or T2*) or longitudinal (T1) relaxivity. Defaults to 'T1'.

    Returns:
        float: relaxivity in Hz/M or 1/(sec*M)

    Note:

        This library is in construction and not all known values are currently available through this function.

        Available contrasts:
            - T1

        Available tissues:
            - blood
            - plasma
            - hepatocytes

        Available agents:
            - gadopentetate
            - gadobutrol
            - gadoteridol
            - gadobenade
            - gadoterate
            - gadodiamide
            - mangafodipir
            - gadoversetamide
            - ferucarbotran
            - ferumoxide
            - gadoxetate

        Available field strengths:
            - 0.47
            - 1.5
            - 3
            - 4.7
            - 7.0 (gadoxetate)
            - 9.0 (gadoxetate)

        Sources:
            - Rohrer M, et al. Comparison of Magnetic Properties of MRI Contrast Media Solutions at Different Magnetic Field Strengths. Investigative Radiology 40(11):p 715-724, November 2005. DOI: `10.1097/01.rli.0000184756.66360.d3 <https://journals.lww.com/investigativeradiology/FullText/2005/11000/Comparison_of_Magnetic_Properties_of_MRI_Contrast.5.aspx>`_
            - Szomolanyi P, et al. Comparison of the Relaxivities of Macrocyclic Gadolinium-Based Contrast Agents in Human Plasma at 1.5, 3, and 7 T, and Blood at 3 T. Invest Radiol. 2019 Sep;54(9):559-564. doi: `10.1097/RLI.0000000000000577 <https://pubmed.ncbi.nlm.nih.gov/31124800/>`_

    Example:

        >>> import dcmri as dc
        >>> print('The plasma relaxivity of gadobutrol at 3T is', 1e-3*dc.relaxivity(3.0, 'plasma', 'gadobutrol'), 'Hz/mM')
        The plasma relaxivity of gadobutrol at 3T is 5.0 Hz/mM
    """
    # Blood and plasma have (theoretically) the same relaxivity
    if tissue == 'blood':
        tissue = 'plasma'
    rel = {}
    rel['T1'] = {
        'plasma': {
            'gadopentetate': {  # Magnevist
                0.47: 3.8,
                1.5: 4.1,
                3.0: 3.7,
                4.7: 3.8,
            },
            'gadobutrol': {  # Gadovist
                0.47: 6.1,
                1.5: 5.2,
                3.0: 5.0,
                4.7: 4.7,
            },
            'gadoteridol': {  # Prohance
                0.47: 4.8,
                1.5: 4.1,
                3.0: 3.7,
                4.7: 3.7,
            },
            'gadobenade': {  # Multihance
                0.47: 9.2,
                1.5: 6.3,
                3.0: 5.5,
                4.7: 5.2,
            },
            'gadoterate': {  # Dotarem
                0.47: 4.3,
                1.5: 3.6,
                3.0: 3.5,
                4.7: 3.3,
            },
            'gadodiamide': {  # Omniscan
                0.47: 4.4,
                1.0: 4.35,  # Interpolated
                1.5: 4.3,
                3.0: 4.0,
                4.7: 3.9,
            },
            'mangafodipir': {  # Teslascan
                0.47: 3.6,
                1.5: 3.6,
                3.0: 2.7,
                4.7: 2.2,
            },
            'gadoversetamide': {  # Optimark
                0.47: 5.7,
                1.5: 4.7,
                3.0: 4.5,
                4.7: 4.4,
            },
            'ferucarbotran': {  # Resovist
                0.47: 15,
                1.5: 7.4,
                3.0: 3.3,
                4.7: 1.7,
            },
            'ferumoxide': {  # Feridex
                1.5: 4.5,
                3.0: 2.7,
                4.7: 1.2,
            },
            'gadoxetate': {  # Primovist
                0.47: 8.7,
                1.5: 8.1,
                3.0: 6.4,
                4.7: 6.4,
                7.0: 6.2,
                9.0: 6.1
            },
        },
    }
    rel['T1']['hepatocytes'] = copy.deepcopy(rel['T1']['plasma'])
    rel['T1']['hepatocytes']['gadoxetate'] = {
        1.5: 14.6,
        3.0: 9.8,
        4.7: 7.6,
        7.0: 6.0,
        9.0: 6.1,
    }
    try:
        return 1000 * rel[type][tissue][agent][field_strength]
    except KeyError:
        msg = 'No relaxivity data for ' + agent + \
            ' at ' + str(field_strength) + ' T.'
        raise ValueError(msg)


def T1(field_strength=3.0, tissue='blood', Hct=0.45) -> float:
    """T1 value of selected tissue types.

    Values are taken from literature, mostly from `Stanisz et al 2005 <https://doi.org/10.1002/mrm.20605>`_

    Args:
        field_strength (float, optional): Field strength in Tesla (see below for options). Defaults to 3.0.
        tissue (str, optional): Tissue type (see below for options). Defaults to 'blood'.
        Hct (float, optional): Hematocrit value - ignored when tissue is not blood. Defaults to 0.45.

    Raises:
        ValueError: If the requested T1 values are not available.

    Returns:
        float: T1 values in sec

    Note:

        This library is in construction and not all known values are currently available through this function.

        Available tissue types:
            - skin
            - bone marrow
            - csf
            - muscle
            - heart
            - cartilage
            - white matter
            - gray matter
            - optic nerve
            - spinal cord
            - blood
            - spleen
            - liver
            - kidney

        Available field strengths:
            - 1.5
            - 3
            - 4.7 (spleen, liver)
            - 7.0 (spleen, liver)
            - 9.0 (spleen, liver)

    Example:

        >>> import dcmri as dc
        >>> print('The T1 of liver at 1.5T is', 1e3*dc.T1(1.5, 'liver'), 'msec')
        The T1 of liver at 1.5T is 602.0 msec
    """
    # H. M. Gach, C. Tanase and F. Boada, "2D & 3D Shepp-Logan Phantom
    # Standards for MRI," 2008 19th International Conference on Systems
    # Engineering, Las Vegas, NV, USA, 2008, pp. 521-526, doi:
    # 10.1109/ICSEng.2008.15.

    T1val = {
        'skin': {  # Gach 2008 (scalp)
            1.5: 0.324 * (1.5**0.137),
            3.0: 0.324 * (3.0**0.137),
        },
        'bone marrow': {  # Gach 2008
            1.5: 0.533 * (1.5**0.088),
            3.0: 0.533 * (3.0**0.088),
        },
        'csf': {  # Gach 2008
            1.5: 4.20,
            3.0: 4.20,
        },
        'muscle': {
            1.5: 1.008,
            3.0: 1.412,
        },
        'heart': {
            1.5: 1.030,
            3.0: 1.471,
        },
        'cartilage': {
            1.5: 1.024,
            3.0: 1.168,
        },
        'white matter': {
            1.5: 0.884,
            3.0: 1.084,
        },
        'gray matter': {
            1.5: 1.124,
            3.0: 1.820,
        },
        'optic nerve': {
            1.5: 0.815,
            3.0: 1.083,
        },
        'spinal cord': {
            1.5: 0.745,
            3.0: 0.993,
        },
        'blood': {
            1.0: 1.378,  # Extrapolated
            1.5: 1.441,
            3.0: 1 / (0.52 * Hct + 0.38),  # Lu MRM 2004
            4.7: 1 / 1.70,  # https://cds.ismrm.org/ismrm-2002/PDF4/1048.PDF
            7.0: 1 / 2.29,  # 10.1016/j.mri.2012.08.008
        },
        'spleen': {
            4.7: 1 / 0.631,
            7.0: 1 / 0.611,
            9.0: 1 / 0.600,
        },
        'liver': {
            1.5: 0.602,  # liver R1 in 1/sec (Waterton 2021)
            3.0: 0.752,  # liver R1 in 1/sec (Waterton 2021)
            # liver R1 in 1/sec (Changed from 1.285 on 06/08/2020)
            4.7: 1 / 1.281,
            # liver R1 in 1/sec (Changed from 0.8350 on 06/08/2020)
            7.0: 1 / 1.109,
            # per sec - liver R1 (https://doi.org/10.1007/s10334-021-00928-x)
            9.0: 1 / 0.920,
        },
        'kidney': {
            # Reference values average over cortex and medulla from Cox et al
            # https://academic.oup.com/ndt/article/33/suppl_2/ii41/5078406
            1.0: 1.017,  # Extrapolated
            1.5: (1.024 + 1.272) / 2,
            3.0: (1.399 + 1.685) / 2,
        },
    }
    try:
        return T1val[tissue][field_strength]
    except BaseException:
        msg = 'No T1 values for ' + tissue + \
            ' at ' + str(field_strength) + ' T.'
        raise ValueError(msg)


def T2(field_strength=3.0, tissue='gray matter') -> float:
    """T2 value of selected tissue types.

    Values are taken from `Gach et al 2008 <https://ieeexplore.ieee.org/document/4616690>`_

    Args:
        field_strength (float, optional): Field strength in Tesla. Defaults to 3.0.
        tissue (str, optional): Tissue type. Defaults to 'gray matter'.

    Raises:
        ValueError: If the requested T2 values are not available.

    Returns:
        float: T2 values in sec

    Note:

        This library is in construction and not all known values are currently available through this function.

        Available tissue types:
            - skin
            - bone marrow
            - csf
            - white matter
            - gray matter

        Available field strengths:
            - 1.5
            - 3

    Example:

        >>> import dcmri as dc
        >>> print('The T2 of skin at 1.5T is', 1e3*dc.T2(1.5, 'skin'), 'msec')
        The T2 of skin at 1.5T is 70.0 msec
    """
    # H. M. Gach, C. Tanase and F. Boada, "2D & 3D Shepp-Logan Phantom
    # Standards for MRI," 2008 19th International Conference on Systems
    # Engineering, Las Vegas, NV, USA, 2008, pp. 521-526, doi:
    # 10.1109/ICSEng.2008.15.

    T2val = {
        'skin': {  # Gach 2008 (scalp)
            1.5: 0.07,
            3.0: 0.07,
        },
        'bone marrow': {  # Gach 2008
            1.5: 0.05,
            3.0: 0.05,
        },
        'csf': {  # Gach 2008
            1.5: 1.99,
            3.0: 1.99,
        },
        'white matter': {
            1.5: 0.08,
            3.0: 0.08,
        },
        'gray matter': {
            1.5: 0.1,
            3.0: 0.1,
        },
    }
    try:
        return T2val[tissue][field_strength]
    except BaseException:
        msg = 'No T2 values for ' + tissue + \
            ' at ' + str(field_strength) + ' T.'
        raise ValueError(msg)


def PD(tissue='gray matter') -> float:
    """Relative proton density (PD) value of selected tissue types.

    Values are taken from `Gach et al 2008 <https://ieeexplore.ieee.org/document/4616690>`_

    Args:
        tissue (str, optional): Tissue type. Defaults to 'gray matter'.

    Raises:
        ValueError: If the requested PD values are not available.

    Returns:
        float: PD values (dimensionless, range 0-1)

    Note:

        This library is in construction and not all known values are currently available through this function.

        Available tissue types:
            - skin
            - bone marrow
            - csf
            - white matter
            - gray matter

    Example:

        >>> import dcmri as dc
        >>> print('The PD of skin is', dc.PD('skin'))
        The PD of skin is 0.8
    """
    # H. M. Gach, C. Tanase and F. Boada, "2D & 3D Shepp-Logan Phantom
    # Standards for MRI," 2008 19th International Conference on Systems
    # Engineering, Las Vegas, NV, USA, 2008, pp. 521-526, doi:
    # 10.1109/ICSEng.2008.15.

    PDval = {
        'skin': 0.8,
        'bone marrow': 0.12,
        'csf': 0.98,
        'white matter': 0.617,
        'gray matter': 0.745,
    }
    try:
        return PDval[tissue]
    except BaseException:
        msg = 'No PD values for ' + tissue
        raise ValueError(msg)


def perfusion(parameter='Fb', tissue='gray matter') -> float:
    """perfusion parameters of selected tissue types.

    Args:
        parameter (str, optional): perfusion parameter. Options are 'Fb' (blood flow), 'vb' (Blood volume), 'PS' (permeability-surface area product) and 'vi' (interstitial volume). Defaults to 'Fb'.
        tissue (str, optional): Tissue type. Defaults to 'gray matter'.

    Raises:
        ValueError: If the requested parameter values are not available.

    Returns:
        float: parameter value in standard units

    Note:

        This library is in construction and not all known values are currently available through this function.

        Available tissue types:
            - skin
            - bone marrow
            - csf
            - white matter
            - gray matter

    Example:

        >>> import dcmri as dc
        >>> print('The BF of gray matter is', dc.perfusion('Fb', 'gray matter'), 'mL/sec/mL')
        The BF of gray matter is 0.01 mL/sec/mL
    """
    if parameter == 'Fb':
        val = {
            'skin': 0.005,  # 5 kg/s/m**3 = 5000mL/sec/100*100*100 mL = 0.005mL/sec/mL
            'bone marrow': 0.0013,  # 0.08 ml/ml/min
            'csf': 0.0,
            'white matter': 0.0033,
            'gray matter': 0.01,
        }
    elif parameter == 'vb':
        val = {
            'skin': 0.03,
            'bone marrow': 0.25,
            'csf': 0.0,
            'white matter': 0.02,
            'gray matter': 0.05,
        }
    elif parameter == 'PS':  # Needs verification
        val = {
            'skin': 0.001,
            'bone marrow': 0.0002,
            'csf': 0.0,
            'white matter': 0.0,
            'gray matter': 0.0,
        }
    elif parameter == 'vi':  # Needs verification
        val = {
            'skin': 0.03,
            'bone marrow': 0.2,
            'csf': 0.0,
            'white matter': 0.3,
            'gray matter': 0.35,
        }
    try:
        return val[tissue]
    except BaseException:
        msg = 'No ' + parameter + ' values for ' + tissue
        raise ValueError(msg)


def _ellipse(array, center, axes, angle, value):
    n = array.shape[0]
    major = np.amax(axes)
    minor = np.amin(axes)
    c = np.sqrt(major**2 - minor**2)
    dx = np.cos(angle * np.pi / 180)
    dy = np.sin(angle * np.pi / 180)
    f1 = [center[0] + dx * c, center[1] + dy * c]
    f2 = [center[0] - dx * c, center[1] - dy * c]
    d = 2.0 / n
    for i in range(n):
        for j in range(n):
            x = d * (i - (n - 1) / 2)
            y = d * (j - (n - 1) / 2)
            d1 = np.sqrt((x - f1[0])**2 + (y - f1[1])**2)
            d2 = np.sqrt((x - f2[0])**2 + (y - f2[1])**2)
            if d1 + d2 <= 2 * major:
                array[i, j] = value
    return array


def _shepp_logan_mask(n=256) -> dict:

    mask = {}

    # Background
    array = np.ones((n, n)).astype(bool)
    array = _ellipse(array, [0, 0], [0.72, 0.95], 0, 0)
    mask['background'] = np.flip(array, axis=0)

    # 1 Scalp
    array = _ellipse(np.zeros((n, n)).astype(
        bool), [0, 0], [0.72, 0.95], 0, 1)
    array = _ellipse(array, [0, 0], [0.69, 0.92], 0, 0)
    mask['scalp'] = np.flip(array, axis=0)

    # 2 Bone and marrow
    array = _ellipse(np.zeros((n, n)).astype(
        bool), [0, 0], [0.69, 0.92], 0, 1)
    array = _ellipse(array, [-0.0184, 0], [0.6624, 0.874], 0, 0)
    mask['bone'] = np.flip(array, axis=0)

    # 3 CSF
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.0184, 0], [0.6624, 0.874], 0, 1)
    array = _ellipse(array, [-0.0184, 0], [0.6524, 0.864], 0, 0)
    mask['CSF skull'] = np.flip(array, axis=0)

    # 4 Gray matter
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.0184, 0], [0.6524, 0.864], 0, 1)
    array = _ellipse(array, [0, -0.22], [0.41, 0.16], 180 - 18, 0)
    array = _ellipse(array, [0, 0.22], [0.31, 0.11], 180 + 18, 0)
    array = _ellipse(array, [0.35, 0], [0.21, 0.25], 0, 0)
    array = _ellipse(array, [0.1, 0], [0.046, 0.046], 0, 0)
    array = _ellipse(array, [-0.605, -0.08], [0.046, 0.023], 90, 0)
    array = _ellipse(array, [-0.605, 0.06], [0.023, 0.046], 0, 0)
    array = _ellipse(array, [-0.1, 0.0], [0.046, 0.046], 0, 0)
    array = _ellipse(array, [-0.605, 0.0], [0.023, 0.023], 0, 0)
    array = _ellipse(array, [-0.83, 0], [0.05, 0.05], 0, 0)
    array = _ellipse(array, [0.66, 0], [0.02, 0.02], 0, 0)
    mask['gray matter'] = np.flip(array, axis=0)

    # 5 CSF
    array = _ellipse(np.zeros((n, n)).astype(bool), [
                     0, -0.22], [0.41, 0.16], 180 - 18, 1)
    array = _ellipse(array, [0.35, 0], [0.21, 0.25], 0, 0)
    array = _ellipse(array, [-0.1, 0.0], [0.046, 0.046], 0, 0)
    mask['CSF right'] = np.flip(array, axis=0)

    # 6 CSF
    array = _ellipse(np.zeros((n, n)).astype(bool), [
                     0, 0.22], [0.31, 0.11], 180 + 18, 1)
    mask['CSF left'] = np.flip(array, axis=0)

    # 7 Tumor
    array = _ellipse(np.zeros((n, n)).astype(
        bool), [0.35, 0], [0.21, 0.25], 0, 1)
    array = _ellipse(array, [0.1, 0], [0.046, 0.046], 0, 0)
    mask['tumor 1'] = np.flip(array, axis=0)

    # 8 Tumor
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [0.1, 0], [0.046, 0.046], 0, 1)
    mask['tumor 2'] = np.flip(array, axis=0)

    # 9 Tumor
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.605, -0.08], [0.046, 0.023], 90, 1)
    mask['tumor 4'] = np.flip(array, axis=0)

    # 10 Tumor
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.605, 0.06], [0.023, 0.046], 0, 1)
    mask['tumor 6'] = np.flip(array, axis=0)

    # 11 Tumor
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.1, 0.0], [0.046, 0.046], 0, 1)
    mask['tumor 3'] = np.flip(array, axis=0)

    # 12 Tumor
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.605, 0.0], [0.023, 0.023], 0, 1)
    mask['tumor 5'] = np.flip(array, axis=0)

    # 13 Sagittal sinus
    array = _ellipse(np.zeros((n, n)).astype(bool),
                     [-0.83, 0], [0.05, 0.05], 0, 1)
    mask['sagittal sinus'] = np.flip(array, axis=0)

    # 14 Anterior cerebral artery
    array = _ellipse(np.zeros((n, n)).astype(
        bool), [0.66, 0], [0.02, 0.02], 0, 1)
    mask['anterior artery'] = np.flip(array, axis=0)

    return mask


def _shepp_logan(param, n=256, B0=3) -> np.ndarray:
    if param == 'PD':
        scalp = PD('skin')
        bone = PD('bone marrow')
        csf = PD('csf')
        gm = PD('gray matter')
        tumor = 0.95
        blood = 0.9
    elif param == 'T2':
        scalp = T2(B0, 'skin')
        bone = T2(B0, 'bone marrow')
        csf = T2(B0, 'csf')
        gm = T2(B0, 'gray matter')
        tumor = 0.25
        blood = 0.1
    elif param == 'T1':
        scalp = T1(B0, 'skin')
        bone = T1(B0, 'bone marrow')
        csf = T1(B0, 'csf')
        gm = T1(B0, 'gray matter')
        tumor = 0.926 * (B0**0.217)
        blood = T1(B0, 'blood')
    elif param == 'Fb':
        scalp = perfusion('Fb', 'skin')
        bone = perfusion('Fb', 'bone marrow')
        csf = perfusion('Fb', 'csf')
        gm = perfusion('Fb', 'gray matter')
        tumor = 0.02
        blood = 0
    elif param == 'vb':
        scalp = perfusion('vb', 'skin')
        bone = perfusion('vb', 'bone marrow')
        csf = perfusion('vb', 'csf')
        gm = perfusion('vb', 'gray matter')
        tumor = 0.1
        blood = 1
    elif param == 'PS':
        scalp = perfusion('PS', 'skin')
        bone = perfusion('PS', 'bone marrow')
        csf = perfusion('PS', 'csf')
        gm = perfusion('PS', 'gray matter')
        tumor = 0.001
        blood = 0
    elif param == 'vi':
        scalp = perfusion('vi', 'skin')
        bone = perfusion('vi', 'bone marrow')
        csf = perfusion('vi', 'csf')
        gm = perfusion('vi', 'gray matter')
        tumor = 0.3
        blood = 0

    array = np.zeros((n, n)).astype(np.float64)
    # 1 Scalp
    array = _ellipse(array, [0, 0], [0.72, 0.95], 0, scalp)
    # 2 Bone and marrow
    array = _ellipse(array, [0, 0], [0.69, 0.92], 0, bone)
    # 3 CSF
    array = _ellipse(array, [-0.0184, 0], [0.6624, 0.874], 0, csf)
    # 4 Gray matter
    array = _ellipse(array, [-0.0184, 0], [0.6524, 0.864], 0, gm)
    # 5 CSF
    array = _ellipse(array, [0, -0.22], [0.41, 0.16], 180 - 18, csf)
    # 6 CSF
    array = _ellipse(array, [0, 0.22], [0.31, 0.11], 180 + 18, csf)
    # 7 Tumor
    array = _ellipse(array, [0.35, 0], [0.21, 0.25], 0, tumor)
    # 8 Tumor
    array = _ellipse(array, [0.1, 0], [0.046, 0.046], 0, tumor)
    # 9 Tumor
    array = _ellipse(array, [-0.605, -0.08], [0.046, 0.023], 90, tumor)
    # 10 Tumor
    array = _ellipse(array, [-0.605, 0.06], [0.023, 0.046], 0, tumor)
    # 11 Tumor
    array = _ellipse(array, [-0.1, 0.0], [0.046, 0.046], 0, tumor)
    # 12 Tumor
    array = _ellipse(array, [-0.605, 0.0], [0.023, 0.023], 0, tumor)
    # 13 Sagittal sinus
    array = _ellipse(array, [-0.83, 0], [0.05, 0.05], 0, blood)
    # 14 Anterior cerebral artery
    array = _ellipse(array, [0.66, 0], [0.02, 0.02], 0, blood)
    # Flip
    array = np.flip(array, axis=0)
    return array


def shepp_logan(*params, n=256, B0=3):
    """Modified Shepp-Logan phantom mimicking an axial slice through the brain.

    The phantom is based on an MRI adaptation of the Shepp-Logan phantom 
    (Gach et al 2008), but with added features for use in a DC-MRI setting: 
    (1) additional regions for anterior cerebral artery and sinus sagittalis; 
    (2) additional optional contrasts blood flow (BF), blood volume (BV), 
    permeability-surface area product (PS) and interstitial volume (IV).

    Args:
        params (str or tuple): parameter or parameters shown in the image. 
          The options are 'PD' (proton density), 'T1', 'T2', 'Fb' (blood flow), 
          'vb' (Blood volume), 'PS' (permeability-surface area product) and 
          'vi' (interstitial volume). If no parameters are provided, the 
          function returns a dictionary with 14 masks, one for each region.
        n (int, optional): matrix size. Defaults to 256.
        B0 (int, optional): field strength in T. Defaults to 3.

    Reference:
        H. M. Gach, C. Tanase and F. Boada, "2D & 3D Shepp-Logan Phantom 
        Standards for MRI," 2008 19th International Conference on Systems 
        Engineering, Las Vegas, NV, USA, 2008, pp. 521-526, 
        `doi 10.1109/ICSEng.2008.15 <https://ieeexplore.ieee.org/document/4616690>`_.

    Returns:
        numpy.array or dict: if only one parameter is provided, this returns 
        an array. In all other conditions this returns a dictionary where keys 
        are the parameter- or region names, and values are square arrays with 
        image values.

    Note:
        Mask names:
            - background
            - scalp
            - bone
            - CSF skull
            - CSF left
            - CSF right
            - gray matter
            - tumor 1 to tumor 6
            - sagittal sinus
            - anterior artery

    Example:

    Generate a single contrast:

    .. plot::
        :include-source:

        >>> import matplotlib.pyplot as plt
        >>> import dcmri as dc

        Simulate a synthetic blood flow image:

        >>> im = dc.shepp_logan('Fb')

        Plot the result in units of mL/min/100mL:

        >>> fig, ax = plt.subplots(figsize=(5, 5), ncols=1)
        >>> pos = ax.imshow(6000*im, cmap='gray', vmin=0.0, vmax=80)
        >>> fig.colorbar(pos, ax=ax, label='blood flow (mL/min/100mL)')
        >>> plt.show()

    Generate multiple contrasts in one function call:

    .. plot::
        :include-source:

        >>> import matplotlib.pyplot as plt
        >>> import dcmri as dc

        Generate the MR Shepp-Logan phantom in low resolution:

        >>> im = dc.shepp_logan('PD', 'T1', 'T2', n=64)

        Plot the result:

        >>> fig, (ax1, ax2, ax3) = plt.subplots(figsize=(12, 5), ncols=3)
        >>> ax1.imshow(im['PD'], cmap='gray')
        >>> ax2.imshow(im['T1'], cmap='gray')
        >>> ax3.imshow(im['T2'], cmap='gray')
        >>> plt.show()

    Generate masks for the different regions-of-interest:

    .. plot::
        :include-source:

        >>> import matplotlib.pyplot as plt
        >>> import dcmri as dc

        Generate the MR Shepp-Logan phantom masks:

        >>> im = dc.shepp_logan(n=128)

        Plot all masks:

        >>> fig, ax = plt.subplots(figsize=(8, 8), ncols=4, nrows=4)
        >>> ax[0,0].imshow(im['background'], cmap='gray')
        >>> ax[0,1].imshow(im['scalp'], cmap='gray')
        >>> ax[0,2].imshow(im['bone'], cmap='gray')
        >>> ax[0,3].imshow(im['CSF skull'], cmap='gray')
        >>> ax[1,0].imshow(im['CSF left'], cmap='gray')
        >>> ax[1,1].imshow(im['CSF right'], cmap='gray')
        >>> ax[1,2].imshow(im['gray matter'], cmap='gray')
        >>> ax[1,3].imshow(im['tumor 1'], cmap='gray')
        >>> ax[2,0].imshow(im['tumor 2'], cmap='gray')
        >>> ax[2,1].imshow(im['tumor 3'], cmap='gray')
        >>> ax[2,2].imshow(im['tumor 4'], cmap='gray')
        >>> ax[2,3].imshow(im['tumor 5'], cmap='gray')
        >>> ax[3,0].imshow(im['tumor 6'], cmap='gray')
        >>> ax[3,1].imshow(im['sagittal sinus'], cmap='gray')
        >>> ax[3,2].imshow(im['anterior artery'], cmap='gray')
        >>> for i in range(4):
        >>>     for j in range(4):
        >>>         ax[i,j].set_yticklabels([])
        >>>         ax[i,j].set_xticklabels([])
        >>> plt.show()
    """
    if len(params) == 0:
        return _shepp_logan_mask(n=n)
    elif len(params) == 1:
        return _shepp_logan(params[0], n=n, B0=B0)
    else:
        phantom = {}
        for p in params:
            phantom[p] = _shepp_logan(p, n=n, B0=B0)
        return phantom
