import numpy as np
# from scipy.fftpack import _fftpack
from scipy.fftpack import dct as _dct
from scipy.fftpack import idct as _idct
import os
path = os.path.dirname(os.path.realpath(__file__))

__all__ = ['dct', 'idct', 'dctn', 'idctn']


def _truncate_or_zero_pad(x, n, axis):
    nt = n - np.shape(x)[axis]
    if nt < 0:
        x = np.take(x, np.r_[0:n], axis=axis)
    elif 0 < nt:
        pads = [(0, 0)] * np.ndim(x)
        pads[axis] = 0, nt
        x = np.pad(x, pads, 'constant')
    return x


def dct(x, type=2, n=None, axis=-1, norm='ortho'):  # @ReservedAssignment
    '''
    Return the Discrete Cosine Transform of arbitrary type sequence x.

    Parameters
    ----------
    x : array_like
        The input array.
    type : {1, 2, 3}, optional
        Type of the DCT (see Notes). Default type is 2.
    n : int, optional
        Length of the transform.
    axis : int, optional
        Axis over which to compute the transform.
    norm : {None, 'ortho'}, optional
        Normalization mode (see Notes). Default is 'ortho'.

    Returns
    -------
    y : ndarray of real
        The transformed input array.

    See Also
    --------
    idct

    Notes
    -----
    For a single dimension array ``x``, ``dct(x, norm='ortho')`` is equal to
    MATLAB ``dct(x)``.

    There are theoretically 8 types of the DCT, only the first 3 types are
    implemented in scipy. 'The' DCT generally refers to DCT type 2, and 'the'
    Inverse DCT generally refers to DCT type 3.

    type I
    ~~~~~~
    There are several definitions of the DCT-I; we use the following
    (for ``norm=None``)::

                                         N-2
      y[k] = x[0] + (-1)**k x[N-1] + 2 * sum x[n]*cos(pi*k*n/(N-1))
                                         n=1

    Only None is supported as normalization mode for DCT-I. Note also that the
    DCT-I is only supported for input size > 1

    type II
    ~~~~~~~
    There are several definitions of the DCT-II; we use the following
    (for ``norm=None``)::


                N-1
      y[k] = 2* sum x[n]*cos(pi*k*(2n+1)/(2*N)), 0 <= k < N.
                n=0

    If ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor `f`::

      f = sqrt(1/(4*N)) if k = 0,
      f = sqrt(1/(2*N)) otherwise.

    Which makes the corresponding matrix of coefficients orthonormal
    (``OO' = Id``).

    type III
    ~~~~~~~~

    There are several definitions, we use the following
    (for ``norm=None``)::

                        N-1
      y[k] = x[0] + 2 * sum x[n]*cos(pi*(k+0.5)*n/N), 0 <= k < N.
                        n=1

    or, for ``norm='ortho'`` and 0 <= k < N::

                                          N-1
      y[k] = x[0] / sqrt(N) + sqrt(1/N) * sum x[n]*cos(pi*(k+0.5)*n/N)
                                          n=1

    The (unnormalized) DCT-III is the inverse of the (unnormalized) DCT-II, up
    to a factor `2N`. The orthonormalized DCT-III is exactly the inverse of
    the orthonormalized DCT-II.

    Examples
    --------
    >>> import numpy as np
    >>> x = np.arange(5)
    >>> np.abs(x-idct(dct(x)))<1e-14
    array([ True,  True,  True,  True,  True], dtype=bool)
    >>> np.abs(x-dct(idct(x)))<1e-14
    array([ True,  True,  True,  True,  True], dtype=bool)

    References
    ----------

    http://en.wikipedia.org/wiki/Discrete_cosine_transform
    http://users.ece.utexas.edu/~bevans/courses/ee381k/lectures/

    'A Fast Cosine Transform in One and Two Dimensions', by J. Makhoul, `IEEE
    Transactions on acoustics, speech and signal processing` vol. 28(1),
    pp. 27-34, http://dx.doi.org/10.1109/TASSP.1980.1163351 (1980).
    '''
    if n is not None:
        # Hack until scipy.fftpack.dct implement this functionality
        x = _truncate_or_zero_pad(x, n, axis)
        n = None
    # return _dct(x, type, n, axis, norm)
    dtype = np.result_type(np.float32, np.asarray(x))
    x0 = np.asarray(x, dtype=dtype)
    if dtype == complex:
        return (_dct(x0.real, type, n, axis, norm) + 1j *
                _dct(x0.imag, type, n, axis, norm))
    else:
        return _dct(x0, type, n, axis, norm)


# def test(type=2, dtype=None, normalize='ortho'):  # @ReservedAssignment
#     dtype = np.result_type(np.float64)
#     try:
#         nm = {None: 0, 'ortho': 1}[normalize]
#     except KeyError:
#         raise ValueError("Unknown normalize mode %s" % normalize)
#     try:
#         name = {'float64': 'ddct%d', 'float32': 'dct%d'}[dtype.name]
#     except KeyError:
#         raise ValueError("dtype %s not supported" % dtype)
#     try:
#         f = getattr(_fftpack, name % type)
#     except AttributeError as e:
#         raise ValueError(str(e) + ". Type %d not understood" % type)
#     return f, nm


def idct(x, type=2, n=None, axis=-1, norm='ortho'):  # @ReservedAssignment
    '''
    Return the Inverse Discrete Cosine Transform of an arbitrary type sequence.

    Parameters
    ----------
    x : array_like
        The input array.
    type : {1, 2, 3}, optional
        Type of the DCT (see Notes). Default type is 2.
    n : int, optional
        Length of the transform.
    axis : int, optional
        Axis over which to compute the transform.
    norm : {None, 'ortho'}, optional
        Normalization mode (see Notes). Default is 'ortho'.

    Returns
    -------
    y : ndarray of real
        The transformed input array.

    See Also
    --------
    dct

    Notes
    -----
    For a single dimension array `x`, ``idct(x, norm='ortho')`` is equal to
    matlab ``idct(x)``.

    'The' IDCT is the IDCT of type 2, which is the same as DCT of type 3.

    IDCT of type 1 is the DCT of type 1, IDCT of type 2 is the DCT of type 3,
    and IDCT of type 3 is the DCT of type 2. For the definition of these types,
    see `dct`.
    '''
    farr = np.asarray
    if n is not None:
        # Hack until scipy.fftpack.idct implement this functionality
        x = _truncate_or_zero_pad(x, n, axis)
        n = None
    if np.iscomplex(x).any():
        x0 = farr(x, dtype=complex)
        return (_idct(x0.real, type, n, axis, norm) + 1j *
                _idct(x0.imag, type, n, axis, norm))
    else:
        return _idct(farr(x, dtype=float), type, n, axis, norm)


def _get_shape(y, shape, axes):
    if shape is None:
        if axes is None:
            shape = y.shape
        else:
            shape = np.take(y.shape, axes)
    shape = tuple(shape)
    return shape


def _get_axes(y, shape, axes):
    if axes is None:
        axes = range(y.ndim)
    if len(axes) != len(shape):
        raise ValueError("when given, axes and shape arguments "
                         "have to be of the same length")
    return list(axes)


def _raw_dctn(y, type, shape, axes, norm, fun):  # @ReservedAssignment
    shape = _get_shape(y, shape, axes)
    axes = _get_axes(y, shape, axes)
    shape0 = list(y.shape)
    ndim = y.ndim
    isvector = max(shape0) == y.size
    if isvector and ndim == 1:
        y = np.atleast_2d(y)
        y = y.T
    for dim in range(ndim):
        y = shiftdim(y, 1)
        if dim not in axes:
            continue
        n = shape[axes.index(dim)]
        shape0[dim] = n
        y = fun(y, type, n, norm=norm)

    return y.reshape(shape0)


def dctn(x, type=2, shape=None, axes=None,  # @ReservedAssignment
         norm='ortho'):
    '''
    Return the N-D Discrete Cosine Transform of array x.

    Parameters
    ----------
    x : array_like
        The input array.
    type : {1, 2, 3}, optional
        Type of the DCT (see Notes). Default type is 2.
    shape : tuple of ints, optional
        The shape of the result.  If both `shape` and `axes` (see below) are
        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
        not None, then `shape` is ``scipy.take(x.shape, axes, axis=0)``.
        If ``shape[i] > x.shape[i]``, the i-th dimension is padded with zeros.
        If ``shape[i] < x.shape[i]``, the i-th dimension is truncated to
        length ``shape[i]``.
    axes : array_like of ints, optional
        The axes of `x` (`y` if `shape` is not None) along which the
        transform is applied.
    norm : {None, 'ortho'}, optional
        Normalization mode (see Notes in dct). Default is 'ortho'.

    Returns
    -------
    y : ndarray of real
        The transformed input array.


    Notes
    -----
    Y = DCTN(X) returns the discrete cosine transform of X. The array Y is
    the same size as X and contains the discrete cosine transform
    coefficients. This transform can be inverted using IDCTN.

    Input array can be numeric or logical. The returned array is of class
    double.

    Reference
    ---------
    Narasimha M. et al, On the computation of the discrete cosine
    transform, IEEE Trans Comm, 26, 6, 1978, pp 934-936.

    Example
    -------
    >>> import os
    >>> import numpy as np
    >>> import scipy.ndimage as sn
    >>> import matplotlib.pyplot as plt
    >>> name = os.path.join(path, 'autumn.gif')
    >>> rgb = sn.imread(name)
    >>> J = dctn(rgb)
    >>> (np.abs(rgb-idctn(J))<1e-7).all()
    True

    plt.imshow(np.log(np.abs(J)))
    plt.colorbar() #colormap(jet), colorbar

    The commands below set values less than magnitude 10 in the DCT matrix
    to zero, then reconstruct the image using the inverse DCT.

    J[np.abs(J)<10] = 0
    K = idctn(J)
    plt.figure(0)
    plt.imshow(rgb)
    plt.figure(1)
    plt.imshow(K,[0 255])

    See also
    --------
    idctn, dct, idct
    '''
    y = np.atleast_1d(x)
    return _raw_dctn(y, type, shape, axes, norm, dct)


def idctn(x, type=2, shape=None, axes=None,  # @ReservedAssignment
          norm='ortho'):
    '''Return inverse N-D Discrete Cosine Transform of array x.

    For description of parameters see `dctn`.

    See Also
    --------
    dctn : for detailed information.
    '''
    y = np.atleast_1d(x)
    return _raw_dctn(y, type, shape, axes, norm, idct)


def num_leading_ones(x):
    first = 0
    for i, xi in enumerate(x):
        if xi != 1:
            first = i
            break
    return first


def no_leading_ones(x):
    first = num_leading_ones(x)
    return x[first:]


def shiftdim(x, n=None):
    '''
    Shift dimensions

    Parameters
    ----------
    x : array
    n : int

    Notes
    -----
    Shiftdim is handy for functions that intend to work along the first
    non-singleton dimension of the array.

    If n is None returns the array with the same number of elements as X but
    with any leading singleton dimensions removed.

    When n is positive, shiftdim shifts the dimensions to the left and wraps
    the n leading dimensions to the end.

    When n is negative, shiftdim shifts the dimensions to the right and pads
    with singletons.

    See also
    --------
    reshape, squeeze
    '''
    if n is None:
        return x.reshape(no_leading_ones(x.shape))
    elif n >= 0:
        return x.transpose(np.roll(range(x.ndim), -n))
    else:
        return x.reshape((1,) * -n + x.shape)


def test_shiftdim():
    a = np.arange(6).reshape((1, 1, 3, 1, 2))

    print a.shape
    print a.ndim

    print range(a.ndim)
    # move items 2 places to the left so that x0 <- x2, x1 <- x3, etc
    print np.roll(range(a.ndim), -2)
    print a.transpose(np.roll(range(a.ndim), -2))  # transposition of the axes
    # with a matrix 2x2, A.transpose((1,0)) would be the transpose of A
    b = shiftdim(a)
    print b.shape

    c = shiftdim(b, -2)
    print c.shape

    print(c == a)


def test_dct3():
    a = np.array([[[0.51699637,  0.42946223,  0.89843545],
                   [0.27853391,  0.8931508,  0.34319118],
                   [0.51984431,  0.09217771,  0.78764716]],
                  [[0.25019845,  0.92622331,  0.06111409],
                   [0.81363641,  0.06093368,  0.13123373],
                   [0.47268657,  0.39635091,  0.77978269]],
                  [[0.86098829,  0.07901332,  0.82169182],
                   [0.12560088,  0.78210188,  0.69805434],
                   [0.33544628,  0.81540172,  0.9393219]]])

    d = dct(dct(dct(a).transpose(0, 2, 1)).transpose(2, 1, 0)
            ).transpose(2, 1, 0).transpose(0, 2, 1)
    d0 = dctn(a)
    e = idct(idct(idct(d).transpose(0, 2, 1)).transpose(2, 1, 0)
             ).transpose(2, 1, 0).transpose(0, 2, 1)
    print(d)
    print(d0)
    print(np.allclose(d, d0))
    print(np.allclose(a, e))


def test_dctn():
    a = np.arange(12).reshape((3, -1))
    print('a = ', a)
    print(' ')
    y = dct(a, n=10)
    x = idct(y)
    print('y = dct(a)')
    print(y)
    print('x = idct(y)')
    print(x)
    print(' ')

    yn = dctn(a)  # , shape=(10,), axes=(1,))
    xn = idctn(yn)  # , axes=(1,))
    print('yn = dctn(a)')
    print(yn)
    print('xn = idctn(yn)')
    print(xn)
    print(' ')
    print xn-a


def test_dct2():
    import scipy.ndimage as sn
    import matplotlib.pyplot as plt
    name = os.path.join(path, 'autumn.gif')
    rgb = np.asarray(sn.imread(name), dtype=np.float16)
    # np.fft.fft(rgb)
    print(np.max(rgb), np.min(rgb))
    plt.set_cmap('jet')
    J = dctn(rgb)
    irgb = idctn(J)
    print(np.abs(rgb-irgb).max())
    plt.imshow(np.log(np.abs(J)))
    # plt.colorbar() #colormap(jet), colorbar
    plt.show('hold')
    v = np.percentile(np.abs(J.ravel()), 10.0)
    print(len(np.flatnonzero(J)), v)
    J[np.abs(J) < v] = 0
    print(len(np.flatnonzero(J)))
    plt.imshow(np.log(np.abs(J)))
    plt.show('hold')
    K = idctn(J)
    print(np.abs(rgb-K).max())
    plt.figure(1)
    plt.imshow(rgb)
    plt.figure(2)
    plt.imshow(K, vmin=0, vmax=255)
    plt.show('hold')


def test_docstrings():
    import doctest
    print('Testing docstrings in %s' % __file__)
    doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)


def test_commands():
    import commands
    commands.getstatusoutput('preprocess -DFORMAT=html -DDEVICE=screen tutorial.do.txt > tmp_preprocess__tutorial.do.txt')


if __name__ == '__main__':
    print(test_commands())
    # test_dct2()
    # test_docstrings()
    # test_dctn()
    # test_shiftdim()
    # test_dct3()
    # test()
