/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.reflection;

import java.util.Arrays;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import net.ranides.assira.generic.TypeToken;
import static net.ranides.assira.reflection.IAttribute.*;
import static net.ranides.assira.reflection.mockup.ForIClass.*;
import org.junit.Test;
import static net.ranides.assira.junit.NewAssert.*;
import net.ranides.assira.reflection.mockup.MTFunction;
import net.ranides.assira.reflection.mockup.MTFunction.MTRecord;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class IMethodTest {
    
    Set<IAttribute> ATTR_LAMBDA = IAttribute.collect(PUBLIC, STATIC, DECLARED, SYNTHETIC, LAMBDA);
    
    Set<IAttribute> ATTR_FUNCTOR = IAttribute.collect(PUBLIC, DECLARED);
    
    int _value1 = 1;
    int[] _array2 = new int[]{1,2,3,4,5};
    Function<String, Character> _function1 = s -> s.charAt(1);
    Function<String, Character> _function2 = s -> s.charAt(_array2[_value1]);
    BiFunction<String, Float, Character> _function3 = (a,b) -> a.charAt(b.intValue());
    Supplier<Character> _function4 = () -> 'z';
    Runnable _function5 = () -> { };
    
    @Test
    public void testInvalidLambda() {
        assertThrows(IllegalArgumentException.class, ()->{
            IMethod.function("Hello world");
        });
    }
    
    @Test
    public void testLambda() {
        int value1 = 1;
		int[] array2 = new int[]{1,2,3,4,5};
		Function<String, Character> function1 = s -> s.charAt(1);
		Function<String, Character> function2 = s -> s.charAt(array2[value1]);
		BiFunction<String, Float, Character> function3 = (a,b) -> a.charAt(b.intValue());
        Supplier<Character> function4 = () -> 'z';
        Runnable function5 = () -> { };
        
        // signature
        assertFunction0(function1, CHARACTER, STRING);
        assertFunction0(function2, CHARACTER, STRING);
        
        assertFunction1(function1, CHARACTER, STRING);
        assertFunction1(function2, CHARACTER, STRING);
        
        assertFunction2(function1, CHARACTER, STRING);
        assertFunction2(function2, CHARACTER, STRING);
        assertFunction2(function3, CHARACTER, STRING, FLOAT);
        assertFunction2(function4, CHARACTER);
        assertFunction2(function5, P_VOID);
        
        testAPI(ATTR_LAMBDA, function1, function2, function3, function4, function5);
        testLambdaCall(function1, function2, function3, function4, function5);
    }
    
    @Test
    public void testMember() {
        
        // signature
        assertFunction0(_function1, CHARACTER, STRING);
        assertFunction0(_function2, CHARACTER, STRING);
        
        assertFunction1(_function1, CHARACTER, STRING);
        assertFunction1(_function2, CHARACTER, STRING);
        
        assertFunction2(_function1, CHARACTER, STRING);
        assertFunction2(_function2, CHARACTER, STRING);
        assertFunction2(_function3, CHARACTER, STRING, FLOAT);
        assertFunction2(_function4, CHARACTER);
        assertFunction2(_function5, P_VOID);
        
        testAPI(ATTR_LAMBDA, _function1, _function2, _function3, _function4, _function5);
        testLambdaCall(_function1, _function2, _function3, _function4, _function5);
    }
    
    @Test
    public void testFromFunctor() {
        int value1 = 1;
		int[] array2 = new int[]{1,2,3,4,5};
        @SuppressWarnings("Convert2Lambda")
		Function<String, Character> function1 = new Function<String, Character>(){
            @Override
            public Character apply(String s) {
                return s.charAt(1);
            }
        };
        @SuppressWarnings("Convert2Lambda")
		Function<String, Character> function2 = new Function<String, Character>() {
            @Override
            public Character apply(String s) {
                return s.charAt(array2[value1]);
            }
        };
        @SuppressWarnings("Convert2Lambda")
		BiFunction<String, Float, Character> function3 = new BiFunction<String, Float, Character>() {
            @Override
            public Character apply(String a, Float b) {
                return a.charAt(b.intValue());
            }
        };
        @SuppressWarnings("Convert2Lambda")
        Supplier<Character> function4 = new Supplier<Character>() {
            @Override
            public Character get() {
                return 'z';
            }
        };
        @SuppressWarnings("Convert2Lambda")
        Runnable function5 = new Runnable() {
            @Override
            public void run() {
                // do nothing
            }
        };
        
        // signature
        assertFunction0(function1, CHARACTER, STRING);
        assertFunction0(function2, CHARACTER, STRING);
        
        assertFunction1(function1, CHARACTER, STRING);
        assertFunction1(function2, CHARACTER, STRING);
        
        assertFunction2(function1, CHARACTER, STRING);
        assertFunction2(function2, CHARACTER, STRING);
        assertFunction2(function3, CHARACTER, STRING, FLOAT);
        assertFunction2(function4, CHARACTER);
        assertFunction2(function5, P_VOID);
        
        testAPI(ATTR_FUNCTOR, function1, function2, function3, function4, function5);
        testFunctorCall(function1, function2, function3, function4, function5);
    }
    
    @Test
    public void testInline() {
		assertFunction0((int[] array) -> 5.7f, FLOAT, INT_ARRAY);
		assertFunction0((int[] array) -> new float[2], IFLOAT_ARRAY, INT_ARRAY);
		assertFunction0((int[] array) -> new Float[2], FLOAT_ARRAY, INT_ARRAY);
        
        assertFunction1((int[] array) -> 5.7f, OBJECT, INT_ARRAY);
		assertFunction1((int[] array) -> null, OBJECT, INT_ARRAY);
		assertFunction1((String array) -> new Float[2], OBJECT, STRING);
    }
    
    @Test
    public void testInlineCall() {
        IMethod m21 = IMethod.function(pass2((Integer a) -> 3*a));
        IMethod m11 = IMethod.function(pass1((Integer a) -> 3*a));
        
        assertEquals(24, m21.call(8));
        assertEquals(24, m21.apply(null, 8));
        assertThrows(IllegalArgumentException.class, ()->{
            m21.apply(m21, 8);
        });
        
        assertEquals(24, m11.call(8));
        assertEquals(24, m11.apply(null, 8));
        assertThrows(IllegalArgumentException.class, ()->{
            m21.apply(m11, 8);
        });
        
    }
    

    @Test
    public void testToFunctor() throws Throwable {
        
        Object that = new MTRecord(300);
        IMethod m1 = IClass.typefor(that).method("sum").get();
        IMethod m2 = IClass.typefor(that).method("mul").get();
        
        MTFunction<MTRecord> f11 = m1.handle(MTFunction.GENERIC);
        MTFunction<MTRecord> f12 = m1.handle(MTFunction.RAW);
        
        assertEquals(309, f11.calculate(new MTRecord(300), 3, 6));
        assertEquals(410, f12.calculate(new MTRecord(400), 3, 7));
        
        MTFunction<MTRecord> f21 = m2.handle(MTFunction.GENERIC);
        MTFunction<MTRecord> f22 = m2.handle(MTFunction.RAW);
        
        assertEquals(318, f21.calculate(new MTRecord(300), 3, 6));
        assertEquals(421, f22.calculate(new MTRecord(400), 3, 7));
    }
    
    @Test
    public void testToFunctorConstruct() {
        IMethod c1 = IClass.typeinfo(MTRecord.class)
            .constructors()
            .first()
            .get();
        
        MTFunction.Generator g1 = c1.handle(new TypeToken<MTFunction.Generator>(){});
        MTFunction.Generator g2 = c1.handle(MTFunction.Generator.class);

        assertEquals(503, g1.build(500).sum(1, 2));
        assertEquals(603, g2.build(600).sum(1, 2));
    }
    
    @Test
    public void testToFunctorLambda() {
        MTFunction.Lame f1 = (a,b,c) -> a.intValue() + b + c;
        MTFunction.LameLong f2 = (a,b,c) -> (int)a + b + c;

        IMethod m1 = IMethod.function(f1);
        IMethod m2 = IMethod.function(f2);
        
        MTFunction<Long> g1 = m1.handle(new TypeToken<MTFunction<Long>>(){});
        MTFunction<Long> g2 = m2.handle(new TypeToken<MTFunction<Long>>(){});

        assertEquals(503, g1.calculate(500L, 1, 2));
        assertEquals(607, g2.calculate(600L, 3, 4));
    }
    
    private void testAPI(Set<IAttribute> exp, Object function1, Object function2, Object function3, Object function4, Object function5) {
        IMethod m1 = IMethod.function(function1);
		IMethod m2 = IMethod.function(function2);
		IMethod m3 = IMethod.function(function3);
		IMethod m4 = IMethod.function(function4);
		IMethod m5 = IMethod.function(function5);
        
        // name
        assertEquals("apply", m1.name());
        assertEquals("apply", m2.name());
        assertEquals("apply", m3.name());
        assertEquals("get", m4.name());
        assertEquals("run", m5.name());
        
        // attributes
        assertEquals(exp, m1.attributes());
        assertEquals(exp, m2.attributes());
        assertEquals(exp, m3.attributes());
        assertEquals(exp, m4.attributes());
        assertEquals(exp, m5.attributes());

        // parent
        assertEquals(IClass.typefor(function1), m1.parent());
        assertEquals(IClass.typefor(function2), m2.parent());
        assertEquals(IClass.typefor(function3), m3.parent());
        assertEquals(IClass.typefor(function4), m4.parent());
        assertEquals(IClass.typefor(function5), m5.parent());

        // reflective
        assertEquals(m1.name(), m1.reflective().getName());
        assertEquals(m2.name(), m2.reflective().getName());
        assertEquals(m3.name(), m3.reflective().getName());
        assertEquals(m4.name(), m4.reflective().getName());
        assertEquals(m5.name(), m5.reflective().getName());
        
        // params
        assertEquals(0, m1.params().size());
        assertEquals(0, m2.params().size());
        assertEquals(0, m3.params().size());
        assertEquals(0, m4.params().size());
        assertEquals(0, m5.params().size());
        
        // annotations
        assertEquals(0, count(m1.annotations()));
        assertEquals(0, count(m2.annotations()));
        assertEquals(0, count(m3.annotations()));
        assertEquals(0, count(m4.annotations()));
        assertEquals(0, count(m5.annotations()));
    }
    
    private static int count(IAnnotations list) {
        return list.discard(a -> a.annotationType().getName().equals("java.lang.invoke.LambdaForm$Hidden")).count();
    }
    
    private void testLambdaCall(Object function1, Object function2, Object function3, Object function4, Object function5) {
        IMethod m1 = IMethod.function(function1);
		IMethod m2 = IMethod.function(function2);
		IMethod m3 = IMethod.function(function3);
		IMethod m4 = IMethod.function(function4);
		IMethod m5 = IMethod.function(function5);
        
        // call
        assertEquals('e', m1.call("hello!"));
        assertEquals('e', m1.apply(null, "hello!"));
        assertThrows(IllegalArgumentException.class, ()->{
            m1.apply(new Object(), "hello!");
        });
        assertThrows(IllegalArgumentException.class, ()->{
            m1.apply(new Object());
        });
        assertThrows(ClassCastException.class, ()->{
            m1.call(888);
        });
        
        assertEquals('r', m2.call("world!"));
        assertEquals('r', m2.apply(null, "world!"));
        
        assertEquals('o', m3.call("hello!", 4.1f));
        assertEquals('o', m3.apply(null, "hello!", 4.1f));
        
        assertEquals('z', m4.call());
        assertEquals('z', m4.apply());
        assertEquals('z', m4.apply(null));
        
        assertEquals(null, m5.call());
        assertEquals(null, m5.apply());
        assertEquals(null, m5.apply(null));
    }
    
    private void testFunctorCall(Object function1, Object function2, Object function3, Object function4, Object function5) {
        IMethod m1 = IMethod.function(function1);
		IMethod m2 = IMethod.function(function2);
		IMethod m3 = IMethod.function(function3);
		IMethod m4 = IMethod.function(function4);
		IMethod m5 = IMethod.function(function5);
        
        // call
        assertEquals('e', m1.call("hello!"));
        assertEquals('e', m1.apply(function1, "hello!"));
        assertThrows(IllegalArgumentException.class, ()->{
            m1.apply(new Object(), "hello!");
        });
        assertThrows(IllegalArgumentException.class, ()->{
            m1.apply(new Object());
        });
        assertThrows(ClassCastException.class, ()->{
            m1.call(888);
        });
        
        assertEquals('r', m2.call("world!"));
        assertEquals('r', m2.apply(function2, "world!"));
        
        assertEquals('o', m3.call("hello!", 4.1f));
        assertEquals('o', m3.apply(function3, "hello!", 4.1f));
        
        assertEquals('z', m4.call());
        assertThrows(IllegalArgumentException.class, ()->{
            m4.apply();
        });
        
        assertEquals('z', m4.apply(function4));
        
        assertEquals(null, m5.call());
        assertThrows(IllegalArgumentException.class, ()->{
            m5.apply();
        });
        assertEquals(null, m5.apply(function5));
    }
    
    private <T,R> void assertFunction0(Function<T,R> function, IClass<?> ret, IClass<?>... args) {
        assertFunction2(function, ret, args);
    }
    
    private <T,R> Function<?,?> pass2(Function<?,?> function) {
        return function;
    }
    
    private Function<?,?> pass1(Function<?,?> function) {
        return function;
    }
    
    private void assertFunction1(Function<?,?> function, IClass<?> ret, IClass<?>... args) {
        assertFunction2(function, ret, args);
    }
    
    private void assertFunction2(Object function, IClass<?> ret, IClass<?>... args) {
        IMethod m = IMethod.function(function);
        
//        Class<?> type = function.getClass();
//        System.out.printf(">>>> %s -> %s%n", m.arguments().types(), m.returns());
//        CPInspector.getConstructors(type).forEach(v -> {
//            System.out.printf(" - c: %s%n", v);
//        });
//        CPInspector.getMethods(type).forEach(v -> {
//            System.out.printf(" - m: %s%n", v);
//        });
//        IClass.typefor(function).methods().require(IAttribute.DECLARED).each(v -> {
//            System.out.printf(" - s: %s%n", v);
//        });
//        System.out.printf("%n");
        
        assertEquals(ret, m.returns());
        assertEquals(Arrays.asList(args), m.arguments().types().list());
        
    }

    
    
}
