from numpy import *
import os

try:
    import ctypes
except:
    print '''
ERROR: ctypes not installed properly. (ctypes needed for opt1d)
        '''
    import sys
    sys.exit()

def platform_shared_library_suffix():
    import sys
    result = 'so'
    if 'win' in sys.platform.lower(): result = 'dll'
    ## No else if, because we want darwin to override win (which is a substring of darwin)
    if 'darwin' in sys.platform.lower(): result = 'dylib'
    return result

def to_ctypes_array( sequence, ctypes_type ):
    '''
    Returns a contiguous ctypes array suitable for extracting
    a pointer (with .ctypes.data_as() or ctypes_array_ref()) to pass to a C function.
    '''
    return ascontiguousarray( asarray( sequence, dtype = ctypes_type ) )
def ctypes_array_ref( ctypes_array, ctypes_type ):
    '''
    Returns a ctypes.POINTER to the internal data of 'ctypes_array' of type 'ctypes_type'.
    
    WARNING: Do not chain the calls to to_ctypes_array() and ctypes_array_ref() as in
               ctypes_array_ref( to_ctypes_array( sequence, ctypes_type ), ctypes_type ).
             ctypes_array_ref returns a pointer to the internal numpy storage of the array and
             cannot keep a reference to the array, so it will be garbage collected if these
             functions are chained together.
             Instead, keep a temporary reference to the result of to_ctypes_array():
               contig = to_ctypes_array( sequence, ctypes_type )
               ctypes_array_ref( contig, ctypes_type )
    '''
    return ctypes_array.ctypes.data_as( ctypes.POINTER( ctypes_type ) )

real_t = ctypes.c_double
size_t = ctypes.c_long
lib = ctypes.cdll.LoadLibrary( os.path.join( '.', 'libtoposmooth2.' + platform_shared_library_suffix() ) )

__smooth1d_monotonically_decreasing = lib.smooth1d_monotonically_decreasing_d
__smooth1d_monotonically_decreasing.argtypes = \
    [
        size_t,
        ctypes.POINTER( real_t ),
        ctypes.POINTER( real_t ),
        ctypes.POINTER( real_t ),
        real_t,
        real_t,
        real_t,
        ctypes.c_int
    ]
__smooth1d_monotonically_decreasing.restype = None
def smooth1d_monotonically_decreasing( original_values, values_spacing, gradient0, gradientN, stay_close_to_original_values_weight = 0., smooth_deformation_not_smooth_function = False ):
    '''
    Given a 1d sequence of values 'original_values' with 'num_values' elements,
    returns a modified sequence guaranteed to be monotonically decreasing,
    and yet smooth and close to 'original_values'.
    The array 'values_spacing' is an array of length 'num_values' - 1,
    where each element i is the finite element spacing between the
    i and i+1 elements in 'original_values'.
    The gradient at the start and end of the sequence are given by 'gradient0' and 'gradientN'.
    NOTE: The gradient at the end should point *away* from the sequence, in the same direction
          as it would point if there were an additional value.
    '''
    
    original_values = to_ctypes_array( original_values, real_t )
    values_spacing = to_ctypes_array( values_spacing, real_t )
    
    assert len( original_values ) == len( values_spacing ) + 1
    
    new_values_out = to_ctypes_array( zeros( original_values.shape ), real_t )
    
    __smooth1d_monotonically_decreasing(
        len( original_values ),
        ctypes_array_ref( original_values, real_t ),
        ctypes_array_ref( new_values_out, real_t ),
        ctypes_array_ref( values_spacing, real_t ),
        gradient0,
        gradientN,
        stay_close_to_original_values_weight,
        smooth_deformation_not_smooth_function
        )
    
    return new_values_out


def test_minimal():
    result = smooth1d_monotonically_decreasing( zeros(10), ones(9), 0, 0 )
    print result

def run_test( original_values, distances, gradient0, gradientN, test_name = '_untitled' ):
    print "================ test" + test_name + " ================"
    
    assert len( original_values ) == len( distances ) + 1
    
    print 'original values (%d):' % (len( original_values ),)
    print original_values
    print 'distances:'
    print distances
    if min( distances ) <= 0:
        print 'WARNING: some distances are 0 or negative'
        raise RuntimeError
    
    print 'gradient0:', gradient0
    print 'gradientN:', gradientN
    
    new_values = smooth1d_monotonically_decreasing( original_values, distances, gradient0, gradientN )
    
    print 'new values:'
    print new_values

def test1():
    run_test( zeros(10), ones(9), 0, 0, '1' )

#def test2( N = 1000 ):
#def test2( N = 10 ):
def test2( N = 200 ):
    run_test( linspace( 1, 0, N), ones(N-1), 0, 0, '2' )

def test_tino0():
    ## This is the same as:
    # test_tino_i( parse_tino_test_data( 'data/Values.txt' ), 0 )
    
    original_values = [ float(val) for val in '1.01117 0.97666 0.944567 1.00215 1.00255 0.999369 0.846576 0.798515 0.709767 0.620929 0.533211 0.484028 0.346073 0.371246 0.111244 -0.0145089 -0.0867039 -0.326172 -0.408045 -0.519565 -0.732758 -0.863446 -0.956953 -1.05907 -1.07205 -1.24082 -1.15965 -1.21432 -1.29279'.strip().split() ]
    distances = [ float(val) for val in '0.047619 0.0952381 0.0952381 0.0952381 0.0952381 0.0952381 0.0952381 0.0952381 0.114185 0.114185 0.114185 0.114185 0.0952381 0.114185 0.0952381 0.0952381 0.114185 0.114185 0.114185 0.0952381 0.0952381 0.0952381 0.0952381 0.114185 0.114185 0.0629921 0.0629923 0.114185'.strip().split() ]
    
    print 'original values (%d):' % (len( original_values ),)
    print original_values
    print 'distances:'
    print distances
    
    new_values = smooth1d_monotonically_decreasing( original_values, distances, 0, 0 )
    
    print 'new values:'
    print new_values

def parse_tino_test_data( fname ):
    ovs = []
    ds = []
    for line in file( fname ):
        sline = line.strip().split()
        if sline[0] == 'OrigValues':
            ovs.append( [ float(val) for val in sline[2:] ] )
        elif sline[0] == 'Distances':
            ds.append( [ float(val) for val in sline[2:] ] )
    
    assert len( ovs ) == len( ds )
    return ovs, ds

def test_tino_i( data, i ):
    run_test( data[0][i], data[1][i], 0., 0. )

def main():
    import sys
    if len( sys.argv ) == 3:
        test_tino_i( parse_tino_test_data( sys.argv[1] ), int( sys.argv[2] ) )
        sys.exit(0)
    
    #test_minimal()
    #test1()
    test2()
    
    #test_tino0()

if __name__ == "__main__": main()
