""" **Description**

        A z transform converts a continuous s-domain differential equation to a
        discrete z-domain difference equation as a function of a recursive
        coefficient array and a forward coefficient array of a specified order.
        A normalized frequency and bilinear condition are specified.

        Singleton.

        .. math::

            y_{n} = \sum_{i = 1}^{N} a_{i} y_{n-i} + \sum_{i = 0}^{N} b_{i} x_{n-i} = \sum_{i = 1}^{N} (\ a_{i} b_{0} + b_{i}\ ) s_{i,n} + b_{0} x_{n}\qquad a_{0} = 0

        A frequency response is expressed as a function of a recursive coefficient
        array and a forward coefficient array, in s-domain and z-domain.

        .. math::

            H_{s,n} = \\frac{\sum_{i = 0}^{N} v_{i} s^{N-i}}{{\sum_{i = 0}^{N} u_{i} s^{N-i}}}

        .. math::

            H_{z,n} = \\frac{\sum_{i = 0}^{N} b_{i} z^{-i}}{{1 - \sum_{i = 1}^{N} a_{i} z^{-i}}}

    **Example**

        ::

            from diamondback import ZTransform
            import math
            import numpy

            frequency, order, ripple = 0.1, 2, 0.125

            u = numpy.array( [ numpy.exp( 1j * math.pi * x / ( 2.0 * order ) ) for x in range( 1, 2 * order, 2 ) ] )

            v = math.asinh( 1.0 / ( ( 10.0 ** ( 0.1 * ripple ) - 1.0 ) ** 0.5 ) ) / order

            a = ( numpy.poly( ( -math.sinh( v ) * u.imag + 1j * math.cosh( v ) * u.real ) * 2.0 * math.pi ) ).real

            a /= a[ -1 ]

            # Transform z-domain coefficients with s-domain coefficients, frequency, and bilinear.

            a, b = ZTransform.transform( a = a, b = [ 1.0 ], frequency = frequency, bilinear = True )

            # Define zeros and normalize gain.

            b = numpy.poly( -numpy.ones( order ) )

            b *= ( 1.0 - sum( a ) ) / sum( b )

    **License**

        `BSD-3C.  <https://github.com/larryturner/diamondback/blob/master/license>`_

        © 2018 - 2021 Larry Turner, Schneider Electric Industries SAS. All rights reserved.

    **Author**

        Larry Turner, Schneider Electric, Analytics & AI, 2018-01-26.

    **Definition**

"""

from typing import List, Tuple, Union
import math
import numpy
import scipy.signal

class ZTransform( object ) :

    """ Z transform realizes continuous s-domain to discrete z-domain transformation, through application of bilinear
        or impulse invariant methods.
    """

    @staticmethod
    def transform( u : Union[ List, numpy.ndarray ], v : Union[ List, numpy.ndarray ], frequency : float, bilinear : bool = True ) -> Tuple[ numpy.ndarray, numpy.ndarray ] :

        """ Transforms continuous s-domain coefficient arrays and produces
            discrete z-domain coefficient arrays.

            Arguments :

                u : Union[ List, numpy.ndarray ] - recursive coefficient, s-domain.

                v : Union[ List, numpy.ndarray ] - forward coefficient, s-domain.

                frequency : float - relative to Nyquist in ( 0.0, 1.0 ).

                bilinear : bool.

            Returns :

                a : numpy.ndarray - recursive coefficient, z-domain.

                b : numpy.ndarray - forward coefficient, z-domain.
        """

        if ( ( not numpy.isscalar( u ) ) and ( not isinstance( u, numpy.ndarray ) ) ) :

            u = numpy.array( list( u ) )

        if ( ( len( u.shape ) != 1 ) or ( len( u ) == 0 ) or ( not u.any( ) ) ) :

            raise ValueError( f'U = {u}' )

        if ( ( not numpy.isscalar( v ) ) and ( not isinstance( v, numpy.ndarray ) ) ) :

            v = numpy.array( list( v ) )

        if ( ( len( v.shape ) != 1 ) or ( len( v ) == 0 ) or ( not v.any( ) ) ) :

            raise ValueError( f'V = {v}' )

        if ( frequency <= 0.0 ) :

            raise ValueError( f'Frequency = {frequency}' )

        while ( not u[ 0 ] ) :

            u = numpy.delete( u, 0 )

        while ( not v[ 0 ] ) :

            v = numpy.delete( v, 0 )

        v /= u[ -1 ]

        u /= u[ -1 ]

        t = 1.0 / ( math.pi * frequency )

        if ( bilinear ) :

            p = numpy.roots( u )

            p = ( 1.0 + p / ( 2.0 * t ) ) / ( 1.0 - p / ( 2.0 * t ) )

            z = numpy.roots( v )

            z = ( 1.0 + z / ( 2.0 * t ) ) / ( 1.0 - z / ( 2.0 * t ) )

            if ( len( z ) < len( p ) ) :

                z = numpy.concatenate( ( z, numpy.zeros( len( p ) - len( z ) ) ) )

            a, b = numpy.poly( p ).real, numpy.poly( z ).real

        else :

            r, p, k = scipy.signal.residue( v, u )

            a, b = numpy.ones( 1 ) + 0j, 0j

            for ii in range( 0, len( r ) ) :

                a = numpy.convolve( a, numpy.array( [ 1.0, -numpy.exp( p[ ii ] / t ) ] ) )

                q = numpy.ones( 1 )

                for jj in range( 0, len( r ) ) :

                    if ( jj != ii ) :

                        q = numpy.convolve( q, numpy.array( [ 1.0, -numpy.exp( p[ jj ] / t ) ] ) )

                b += r[ ii ] * q

            if ( len( b ) < len( a ) ) :

                b = numpy.concatenate( ( b, numpy.zeros( len( a ) - len( b ) ) ) )

            a, b = a.real, b.real

        a /= -a[ 0 ]

        a[ 0 ] = 0.0

        b *= ( ( 1.0 - sum( a ) ) / sum( b ) )

        return a, b
