#!/usr/bin/env python
#
# dtifit.py - The DTIFitTensor class, and some related utility functions.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
# Author: Michiel Cottaar <michiel.cottaar@ndcn.ox.ac.uk>
#
"""This module provides the :class:`.DTIFitTensor` class, which encapsulates
the diffusion tensor data generated by the FSL ``dtifit`` tool.

There are also conversion tools between the diffusion tensors defined in 3 formats:

* (..., 3, 3) array with the full diffusion tensor
* (..., 6) array with the unique components (Dxx, Dxy, Dxz, Dyy, Dyz, Dzz)
* Tuple with the eigenvectors and eigenvalues (V1, V2, V3, L1, L2, L3)


Finally the following utility functions are also defined:

  .. autosummary::
     :nosignatures:

     getDTIFitDataPrefix
     isDTIFitPath
     looksLikeTensorImage
     decomposeTensorMatrix
"""


import                 logging
import                 re
import                 glob
import os.path      as op

import numpy        as np
import numpy.linalg as npla

from . import image as fslimage


log = logging.getLogger(__name__)


def eigendecompositionToTensor(V1, V2, V3, L1, L2, L3):
    """
    Converts the eigenvalues/eigenvectors into a 3x3 diffusion tensor

    :param V1: (..., 3) shaped array with the first eigenvector
    :param V2: (..., 3) shaped array with the second eigenvector
    :param V3: (..., 3) shaped array with the third eigenvector
    :param L1: (..., ) shaped array with the first eigenvalue
    :param L2: (..., ) shaped array with the second eigenvalue
    :param L3: (..., ) shaped array with the third eigenvalue
    :return: (..., 3, 3) array with the diffusion tensor
    """
    check_shape = L1.shape
    for eigen_value in (L2, L3):
        if eigen_value.shape != check_shape:
            raise ValueError("Not all eigenvalues have the same shape")
    for eigen_vector in (V1, V2, V3):
        if eigen_vector.shape != eigen_value.shape + (3, ):
            raise ValueError("Not all eigenvectors have the same shape as the eigenvalues")
    return (
        L1[..., None, None] * V1[..., None, :] * V1[..., :, None] +
        L2[..., None, None] * V2[..., None, :] * V2[..., :, None] +
        L3[..., None, None] * V3[..., None, :] * V3[..., :, None]
    )


def tensorToEigendecomposition(matrices):
    """
    Decomposes the 3x3 diffusion tensor into eigenvalues and eigenvectors

    :param matrices: (..., 3, 3) array-like with diffusion tensor
    :return: Tuple containing the eigenvectors and eigenvalues (V1, V2, V3, L1, L2, L3)
    """
    matrices = np.asanyarray(matrices)
    if matrices.shape[-2:] != (3, 3):
        raise ValueError("Expected 3x3 diffusion tensors")

    shape = matrices.shape[:-2]
    nvoxels = np.prod(shape)

    # Calculate the eigenvectors and
    # values on all of those matrices
    flat_matrices = matrices.reshape((-1, 3, 3))
    vals, vecs = npla.eigh(flat_matrices)
    vecShape   = shape + (3, )

    l1 = vals[:, 2]   .reshape(shape)
    l2 = vals[:, 1]   .reshape(shape)
    l3 = vals[:, 0]   .reshape(shape)
    v1 = vecs[:, :, 2].reshape(vecShape)
    v2 = vecs[:, :, 1].reshape(vecShape)
    v3 = vecs[:, :, 0].reshape(vecShape)
    return v1, v2, v3, l1, l2, l3


def tensorToComponents(matrices):
    """
    Extracts the 6 unique components from a 3x3 diffusion tensor

    :param matrices: (..., 3, 3) array-like with diffusion tensors
    :return: (..., 6) array with the unique components sorted like Dxx, Dxy, Dxz, Dyy, Dyz, Dzz
    """
    matrices = np.asanyarray(matrices)
    if matrices.shape[-2:] != (3, 3):
        raise ValueError("Expected 3x3 diffusion tensors")
    return np.stack([
        matrices[..., 0, 0],
        matrices[..., 0, 1],
        matrices[..., 0, 2],
        matrices[..., 1, 1],
        matrices[..., 1, 2],
        matrices[..., 2, 2],
    ], -1)


def componentsToTensor(components):
    """
    Creates 3x3 diffusion tensors from the 6 unique components

    :param components: (..., 6) array-like with Dxx, Dxy, Dxz, Dyy, Dyz, Dzz
    :return: (..., 3, 3) array with the diffusion tensors
    """
    components = np.asanyarray(components)
    if components.shape[-1] != 6:
        raise ValueError("Expected 6 unique components of diffusion tensor")
    first = np.stack([components[..., index] for index in (0, 1, 2)], -1)
    second = np.stack([components[..., index] for index in (1, 3, 4)], -1)
    third = np.stack([components[..., index] for index in (2, 4, 5)], -1)
    return np.stack([first, second, third], -1)


def eigendecompositionToComponents(V1, V2, V3, L1, L2, L3):
    """
    Converts the eigenvalues/eigenvectors into the 6 unique components of the diffusion tensor

    :param V1: (..., 3) shaped array with the first eigenvector
    :param V2: (..., 3) shaped array with the second eigenvector
    :param V3: (..., 3) shaped array with the third eigenvector
    :param L1: (..., ) shaped array with the first eigenvalue
    :param L2: (..., ) shaped array with the second eigenvalue
    :param L3: (..., ) shaped array with the third eigenvalue
    :return: (..., 6) array with the unique components sorted like Dxx, Dxy, Dxz, Dyy, Dyz, Dzz
    """
    return tensorToComponents(eigendecompositionToTensor(V1, V2, V3, L1, L2, L3))


def componentsToEigendecomposition(components):
    """
    Decomposes diffusion tensor defined by its 6 unique components

    :param components: (..., 6) array-like with Dxx, Dxy, Dxz, Dyy, Dyz, Dzz
    :return: Tuple containing the eigenvectors and eigenvalues (V1, V2, V3, L1, L2, L3)
    """
    return tensorToEigendecomposition(componentsToTensor(components))



def getDTIFitDataPrefix(path):
    """Returns the prefix (a.k,a, base name) used for the ``dtifit`` file
    names in the given directory, or ``None`` if the ``dtifit`` files could
    not be identified.
    """

    v1s   = glob.glob(op.join(path, '*_V1.*'))
    v2s   = glob.glob(op.join(path, '*_V2.*'))
    v3s   = glob.glob(op.join(path, '*_V3.*'))
    l1s   = glob.glob(op.join(path, '*_L1.*'))
    l2s   = glob.glob(op.join(path, '*_L2.*'))
    l3s   = glob.glob(op.join(path, '*_L3.*'))
    files = [v1s, v2s, v3s, l1s, l2s, l3s]

    # Gather all of the existing file
    # prefixes into a dictionary of
    # prefix : [file list] mappings.
    pattern  = r'^(.*)_(?:V1|V2|V3|L1|L2|L3).*$'
    prefixes = {}

    for f in [f for flist in files for f in flist]:
        prefix = re.findall(pattern, f)[0]

        if prefix not in prefixes: prefixes[prefix] = [f]
        else:                      prefixes[prefix].append(f)

    # Discard any prefixes which are
    # not present for every file type.
    for prefix, files in list(prefixes.items()):
        if len(files) != 6:
            prefixes.pop(prefix)

    # Discard any prefixes which
    # match any files that do
    # not look like image files
    for prefix, files in list(prefixes.items()):
        if not all([fslimage.looksLikeImage(f) for f in files]):
            prefixes.pop(prefix)

    prefixes = list(prefixes.keys())

    # No more prefixes remaining -
    # this is probably not a dtifit
    # directory
    if len(prefixes) == 0:
        return None

    # If there's more than one remaining
    # prefix, I don't know what to do -
    # just return the first one.
    if len(prefixes) > 1:
        log.warning('Multiple dtifit prefixes detected: {}'.format(prefixes))

    return op.basename(sorted(prefixes)[0])


def isDTIFitPath(path):
    """Returns ``True`` if the given directory path looks like it contains
    ``dtifit`` data, ``False`` otherwise.
    """

    return getDTIFitDataPrefix(path) is not None


def looksLikeTensorImage(image):
    """Returns ``True`` if the given :class:`.Image` looks like it could
    contain tensor matrix data, ``False`` otherwise.
    """

    return len(image.shape) == 4 and image.shape[3] == 6


def decomposeTensorMatrix(data):
    """Decomposes the given ``numpy`` array into six separate arrays,
    containing the eigenvectors and eigenvalues of the tensor matrix
    decompositions.

    :arg image: A 4D ``numpy`` array with 6 volumes, which contains
                the unique elements of diffusion tensor matrices at
                every voxel.

    :returns:   A tuple containing the principal eigenvectors and
                eigenvalues of the tensor matrix.
    """
    return componentsToEigendecomposition(data)


class DTIFitTensor(fslimage.Nifti):
    """The ``DTIFitTensor`` class is able to load and encapsulate the diffusion
    tensor data generated by the FSL ``dtifit`` tool.  The ``DtiFitTensor``
    class supports tensor model data generated by ``dtifit``, where the
    eigenvectors and eigenvalues of the tensor matrices have been saved as six
    separate NIFTI images.
    """


    def __init__(self, path):
        """Create a ``DTIFitTensor``.

        :arg path: A path to a ``dtifit`` directory.
        """

        prefix      = getDTIFitDataPrefix(path)
        isDTIfitDir = prefix is not None

        if not isDTIfitDir:
            raise ValueError('{} does not look like a dtifit '
                             'output directory!'.format(path))

        # DTIFit output directory with separate
        # eigenvector/eigenvalue images

        self.__v1 = fslimage.Image(op.join(path, '{}_V1'.format(prefix)))
        self.__v2 = fslimage.Image(op.join(path, '{}_V2'.format(prefix)))
        self.__v3 = fslimage.Image(op.join(path, '{}_V3'.format(prefix)))
        self.__l1 = fslimage.Image(op.join(path, '{}_L1'.format(prefix)))
        self.__l2 = fslimage.Image(op.join(path, '{}_L2'.format(prefix)))
        self.__l3 = fslimage.Image(op.join(path, '{}_L3'.format(prefix)))

        fslimage.Nifti.__init__(self, self.__l1.header)

        self.dataSource = op.abspath(path)
        self.name       = '{}'.format(op.basename(path))

    def V1(self):
        return self.__v1
    def V2(self):
        return self.__v2
    def V3(self):
        return self.__v3
    def L1(self):
        return self.__l1
    def L2(self):
        return self.__l2
    def L3(self):
        return self.__l3
