import numpy as np
import scipy.sparse.linalg
import scipy.linalg

from .dense import Dense
from .csr import CSR
from .dia import Dia
from . import dia
from .properties import isdiag_csr, isdiag_dia
from qutip.settings import settings
from .base import idxint_dtype

__all__ = [
    'expm', 'expm_csr', 'expm_csr_dense', 'expm_dense', 'expm_dia',
    'logm', 'logm_dense', 'sqrtm', 'sqrtm_dense'
]


def expm_csr(matrix: CSR) -> CSR:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only exponentiate square matrix")
    if isdiag_csr(matrix):
        matrix_sci = matrix.as_scipy()
        data = np.ones(matrix.shape[0], dtype=np.complex128)
        data[matrix_sci.indices] += np.expm1(matrix_sci.data)
        return CSR(
            (
                data,
                np.arange(matrix.shape[0], dtype=idxint_dtype),
                np.arange(matrix.shape[0] + 1, dtype=idxint_dtype),
            ),
            shape=matrix.shape,
            copy=False,
        )
    # The scipy solvers for the Pade approximant are more efficient with the
    # CSC format than the CSR one.
    csc = matrix.as_scipy().tocsc()
    return CSR(scipy.sparse.linalg.expm(csc).tocsr(),
               tidyup=settings.core['auto_tidyup'])


def expm_dia(matrix: Dia) -> Dia:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only exponentiate square matrix")
    if matrix.num_diag == 0:
        out = dia.identity(matrix.shape[0])
    elif matrix.num_diag > 1:
        csc = matrix.as_scipy().tocsc()
        out = Dia(
            scipy.sparse.linalg.expm(csc).todia(),
            tidyup=settings.core['auto_tidyup'], copy=False
        )
    elif isdiag_dia(matrix):
        matrix_sci = matrix.as_scipy()
        data = np.exp(matrix_sci.data[0, :])
        out = dia.diags(data, shape=matrix.shape)
    else:
        mat = matrix.as_scipy()
        size = matrix.shape[0]
        offset = mat.offsets[0]
        n_offset = offset
        a_offset = abs(offset)
        data = mat.data[0, max(0, offset): min(size, size + offset)]
        data_0 = data
        out_oufsets = np.arange(0, size, a_offset, dtype=idxint_dtype)
        out_oufsets *= np.sign(offset)
        out_data = np.zeros((len(out_oufsets), size), dtype=complex)
        out_data[0, :] += 1.
        for i in range(1, len(out_oufsets)):
            out_data[i, max(0, n_offset): min(size, size + n_offset)] = data
            data = data_0[:-abs(n_offset)] * data[a_offset:] / (i+1)
            n_offset += offset
        out = Dia((out_data, out_oufsets), shape=matrix.shape, copy=False)

    return out


def expm_csr_dense(matrix: CSR) -> Dense:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only exponentiate square matrix")
    return Dense(scipy.linalg.expm(matrix.to_array()))


def expm_dia_dense(matrix: Dia) -> Dense:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only exponentiate square matrix")
    return Dense(scipy.linalg.expm(matrix.to_array()))


def expm_dense(matrix: Dense) -> Dense:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only exponentiate square matrix")
    return Dense(scipy.linalg.expm(matrix.as_ndarray()), copy=False)


from .dispatch import Dispatcher as _Dispatcher
import inspect as _inspect


expm = _Dispatcher(
    _inspect.Signature([
        _inspect.Parameter('matrix', _inspect.Parameter.POSITIONAL_ONLY),
    ]),
    name='expm',
    module=__name__,
    inputs=('matrix',),
    out=True,
)
expm.__doc__ = """Matrix exponential `e**A` for a matrix `A`."""
expm.add_specialisations([
    (CSR, CSR, expm_csr),
    (CSR, Dense, expm_csr_dense),
    (Dia, Dense, expm_dia_dense),
    (Dense, Dense, expm_dense),
    (Dia, Dia, expm_dia),
], _defer=True)


def logm_dense(matrix: Dense) -> Dense:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only compute logarithm square matrix")
    return Dense(scipy.linalg.logm(matrix.as_ndarray()), copy=False)


logm = _Dispatcher(
    _inspect.Signature([
        _inspect.Parameter('matrix', _inspect.Parameter.POSITIONAL_ONLY),
    ]),
    name='logm',
    module=__name__,
    inputs=('matrix',),
    out=True,
)
logm.__doc__ = """Matrix logarithm `ln(A)` for a matrix `A`."""
logm.add_specialisations([
    (Dense, Dense, logm_dense),
], _defer=True)


def sqrtm_dense(matrix) -> Dense:
    if matrix.shape[0] != matrix.shape[1]:
        raise ValueError("can only compute logarithm square matrix")
    # As of scipy 1.16.0, it's bugged and overly eager for warning.
    # Tests filter warnings, and 1.16.1 will fix the bug.
    # See qutip#2711
    return Dense(scipy.linalg.sqrtm(matrix.as_ndarray()))


sqrtm = _Dispatcher(
    _inspect.Signature([
        _inspect.Parameter('matrix', _inspect.Parameter.POSITIONAL_ONLY),
    ]),
    name='sqrtm',
    module=__name__,
    inputs=('matrix',),
    out=True,
)
sqrtm.__doc__ = """Matrix square root `sqrt(A)` for a matrix `A`."""
sqrtm.add_specialisations([
    (Dense, Dense, sqrtm_dense),
], _defer=True)

del _inspect, _Dispatcher
