/*
 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.nashorn.internal.runtime.linker.test;

import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import jdk.nashorn.api.scripting.JSObject;
import jdk.nashorn.api.scripting.NashornScriptEngine;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import jdk.nashorn.internal.runtime.Context;
import org.testng.Assert;
import org.testng.annotations.Test;

public class JavaAdapterTest {
    public interface TestConversions {
        public byte getByte(byte b);
        public short getShort(short b);
        public char getChar(char c);
        public int getInt(int i);
        public float getFloat(float f);
        public long getLong(long l);
        public double getDouble(double d);
    }

    @Test
    public static void testBlah() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.eval("new java.util.Comparator({})");
    }

    @Test
    public static void testConversions() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.put("TestConversionsClass", TestConversions.class);
        final TestConversions tc = (TestConversions)e.eval(
                "function id(x) { return x };" +
                "new TestConversionsClass.static({" +
                "  getByte: id, getShort: id, getChar: id, getInt: id," +
                "  getFloat: id, getLong: id, getDouble: id });");

        Assert.assertEquals(Byte.MIN_VALUE, tc.getByte(Byte.MIN_VALUE));
        Assert.assertEquals(Byte.MAX_VALUE, tc.getByte(Byte.MAX_VALUE));

        Assert.assertEquals(Short.MIN_VALUE, tc.getShort(Short.MIN_VALUE));
        Assert.assertEquals(Short.MAX_VALUE, tc.getShort(Short.MAX_VALUE));

        Assert.assertEquals(Character.MIN_VALUE, tc.getChar(Character.MIN_VALUE));
        Assert.assertEquals(Character.MAX_VALUE, tc.getChar(Character.MAX_VALUE));

        Assert.assertEquals(Integer.MIN_VALUE, tc.getInt(Integer.MIN_VALUE));
        Assert.assertEquals(Integer.MAX_VALUE, tc.getInt(Integer.MAX_VALUE));

        Assert.assertEquals(Long.MIN_VALUE, tc.getLong(Long.MIN_VALUE));
        Assert.assertEquals(Long.MAX_VALUE, tc.getLong(Long.MAX_VALUE));

        Assert.assertEquals(Float.MIN_VALUE, tc.getFloat(Float.MIN_VALUE));
        Assert.assertEquals(Float.MAX_VALUE, tc.getFloat(Float.MAX_VALUE));
        Assert.assertEquals(Float.MIN_NORMAL, tc.getFloat(Float.MIN_NORMAL));
        Assert.assertEquals(Float.POSITIVE_INFINITY, tc.getFloat(Float.POSITIVE_INFINITY));
        Assert.assertEquals(Float.NEGATIVE_INFINITY, tc.getFloat(Float.NEGATIVE_INFINITY));
        Assert.assertTrue(Float.isNaN(tc.getFloat(Float.NaN)));

        Assert.assertEquals(Double.MIN_VALUE, tc.getDouble(Double.MIN_VALUE));
        Assert.assertEquals(Double.MAX_VALUE, tc.getDouble(Double.MAX_VALUE));
        Assert.assertEquals(Double.MIN_NORMAL, tc.getDouble(Double.MIN_NORMAL));
        Assert.assertEquals(Double.POSITIVE_INFINITY, tc.getDouble(Double.POSITIVE_INFINITY));
        Assert.assertEquals(Double.NEGATIVE_INFINITY, tc.getDouble(Double.NEGATIVE_INFINITY));
        Assert.assertTrue(Double.isNaN(tc.getDouble(Double.NaN)));
    }

    private static ScriptEngine createEngine() {
        // Use no optimistic typing so we run faster; short-running tests.
        return new NashornScriptEngineFactory().getScriptEngine("-ot=false");
    }

    @Test
    public static void testUnimplemented() throws ScriptException {
        final ScriptEngine e = createEngine();
        final Runnable r = (Runnable) e.eval("new java.lang.Runnable({})");
        Assert.assertNull(Context.getGlobal());
        try {
            r.run();
            Assert.fail();
        } catch(final UnsupportedOperationException x) {
            // This is expected
        }
        // Check global has been restored
        Assert.assertNull(Context.getGlobal());
    }

    public interface ThrowingRunnable {
        public void run() throws Throwable;
    }

    @Test
    public static void testUnimplementedWithThrowable() throws Throwable {
        final ScriptEngine e = createEngine();
        e.put("ThrowingRunnableClass", ThrowingRunnable.class);
        final ThrowingRunnable r = (ThrowingRunnable) e.eval("new ThrowingRunnableClass.static({})");
        Assert.assertNull(Context.getGlobal());
        try {
            r.run();
            Assert.fail();
        } catch(final UnsupportedOperationException x) {
            // This is expected
        }
        // Check global has been restored
        Assert.assertNull(Context.getGlobal());
    }

    public interface IntSupplierWithDefault {
        public default int get() { return 42; }
    }

    @Test
    public static void testUnimplementedWithDefault() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.put("IntSupplierWithDefault", IntSupplierWithDefault.class);
        final IntSupplierWithDefault s1 = (IntSupplierWithDefault) e.eval("new IntSupplierWithDefault.static({})");
        Assert.assertEquals(42, s1.get());
        final IntSupplierWithDefault s2 = (IntSupplierWithDefault) e.eval("new IntSupplierWithDefault.static({ get: function() { return 43 }})");
        Assert.assertEquals(43, s2.get());
    }

    public interface SupplierSupplier {
        public Supplier<Object> getSupplier();
    }

    @Test
    public static void testReturnAdapter() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.put("SupplierSupplier", SupplierSupplier.class);
        final SupplierSupplier s = (SupplierSupplier) e.eval("new SupplierSupplier.static(function(){ return function() { return 'foo' } })");
        Assert.assertEquals("foo", s.getSupplier().get());
    }

    public interface MaxParams {
        public Object method(boolean p1, byte p2, short p3, char p4, int p5, float p6, long p7, double p8,
                Object p9, Object p10, Object p11, Object p12, Object p13, Object p14, Object p15, Object p16,
                Object p17, Object p18, Object p19, Object p20, Object p21, Object p22, Object p23, Object p24,
                Object p25, Object p26, Object p27, Object p28, Object p29, Object p30, Object p31, Object p32,
                Object p33, Object p34, Object p35, Object p36, Object p37, Object p38, Object p39, Object p40,
                Object p41, Object p42, Object p43, Object p44, Object p45, Object p46, Object p47, Object p48,
                Object p49, Object p50, Object p51, Object p52, Object p53, Object p54, Object p55, Object p56,
                Object p57, Object p58, Object p59, Object p60, Object p61, Object p62, Object p63, Object p64,
                Object p65, Object p66, Object p67, Object p68, Object p69, Object p70, Object p71, Object p72,
                Object p73, Object p74, Object p75, Object p76, Object p77, Object p78, Object p79, Object p80,
                Object p81, Object p82, Object p83, Object p84, Object p85, Object p86, Object p87, Object p88,
                Object p89, Object p90, Object p91, Object p92, Object p93, Object p94, Object p95, Object p96,
                Object p97, Object p98, Object p99, Object p100, Object p101, Object p102, Object p103, Object p104,
                Object p105, Object p106, Object p107, Object p108, Object p109, Object p110, Object p111, Object p112,
                Object p113, Object p114, Object p115, Object p116, Object p117, Object p118, Object p119, Object p120,
                Object p121, Object p122, Object p123, Object p124, Object p125, Object p126, Object p127, Object p128,
                Object p129, Object p130, Object p131, Object p132, Object p133, Object p134, Object p135, Object p136,
                Object p137, Object p138, Object p139, Object p140, Object p141, Object p142, Object p143, Object p144,
                Object p145, Object p146, Object p147, Object p148, Object p149, Object p150, Object p151, Object p152,
                Object p153, Object p154, Object p155, Object p156, Object p157, Object p158, Object p159, Object p160,
                Object p161, Object p162, Object p163, Object p164, Object p165, Object p166, Object p167, Object p168,
                Object p169, Object p170, Object p171, Object p172, Object p173, Object p174, Object p175, Object p176,
                Object p177, Object p178, Object p179, Object p180, Object p181, Object p182, Object p183, Object p184,
                Object p185, Object p186, Object p187, Object p188, Object p189, Object p190, Object p191, Object p192,
                Object p193, Object p194, Object p195, Object p196, Object p197, Object p198, Object p199, Object p200,
                Object p201, Object p202, Object p203, Object p204, Object p205, Object p206, Object p207, Object p208,
                Object p209, Object p210, Object p211, Object p212, Object p213, Object p214, Object p215, Object p216,
                Object p217, Object p218, Object p219, Object p220, Object p221, Object p222, Object p223, Object p224,
                Object p225, Object p226, Object p227, Object p228, Object p229, Object p230, Object p231, Object p232,
                Object p233, Object p234, Object p235, Object p236, Object p237, Object p238, Object p239, Object p240,
                Object p241, Object p242, Object p243, Object p244, Object p245, Object p246, Object p247, Object p248,
                Object p249, Object p250, Object p251, Object p252);
    }

    @Test
    public static void testMaxLengthAdapter() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.put("MaxParams", MaxParams.class);
        final MaxParams s = (MaxParams) e.eval("new MaxParams.static(function(){ return arguments })");
        final ScriptObjectMirror m = (ScriptObjectMirror)s.method(true, Byte.MIN_VALUE, Short.MIN_VALUE, 'a', Integer.MAX_VALUE, Float.MAX_VALUE, Long.MAX_VALUE, Double.MAX_VALUE,
                "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26",
                "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44",
                "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "60", "61", "62",
                "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "80",
                "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98",
                "99", "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114",
                "115", "116", "117", "118", "119", "120", "121", "122", "123", "124", "125", "126", "127", "128", "129", "130",
                "131", "132", "133", "134", "135", "136", "137", "138", "139", "140", "141", "142", "143", "144", "145", "146",
                "147", "148", "149", "150", "151", "152", "153", "154", "155", "156", "157", "158", "159", "160", "161", "162",
                "163", "164", "165", "166", "167", "168", "169", "170", "171", "172", "173", "174", "175", "176", "177", "178",
                "179", "180", "181", "182", "183", "184", "185", "186", "187", "188", "189", "190", "191", "192", "193", "194",
                "195", "196", "197", "198", "199", "200", "201", "202", "203", "204", "205", "206", "207", "208", "209", "210",
                "211", "212", "213", "214", "215", "216", "217", "218", "219", "220", "221", "222", "223", "224", "225", "226",
                "227", "228", "229", "230", "231", "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242",
                "243", "244", "245", "246", "247", "248", "249", "250", "251");
        Assert.assertEquals(true, m.getSlot(0));
        Assert.assertEquals(Integer.valueOf(Byte.MIN_VALUE), m.getSlot(1)); // Byte becomes Integer
        Assert.assertEquals(Integer.valueOf(Short.MIN_VALUE), m.getSlot(2)); // Short becomes Integer
        Assert.assertEquals(Character.valueOf('a'), m.getSlot(3));
        Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), m.getSlot(4));
        Assert.assertEquals(Double.valueOf(Float.MAX_VALUE), m.getSlot(5)); // Float becomes Double
        Assert.assertEquals(Long.valueOf(Long.MAX_VALUE), m.getSlot(6)); // Long was untouched
        Assert.assertEquals(Double.valueOf(Double.MAX_VALUE), m.getSlot(7));
        for (int i = 8; i < 252; ++i) {
            Assert.assertEquals(String.valueOf(i), m.getSlot(i));
        }
    }

    public interface TestScriptObjectMirror {
        public JSObject getJSObject();
        public ScriptObjectMirror getScriptObjectMirror();
        public Map<Object, Object> getMap();
        public Bindings getBindings();
    }

    @Test
    public static void testReturnsScriptObjectMirror() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.put("TestScriptObjectMirrorClass", TestScriptObjectMirror.class);
        final TestScriptObjectMirror tsom = (TestScriptObjectMirror)e.eval(
                "new TestScriptObjectMirrorClass.static({\n" +
                "  getJSObject: function() { return { 'kind': 'JSObject' } },\n" +
                "  getScriptObjectMirror: function() { return { 'kind': 'ScriptObjectMirror' } },\n" +
                "  getMap: function() { return { 'kind': 'Map' } },\n" +
                "  getBindings: function() { return { 'kind': 'Bindings' } } })\n");
        Assert.assertEquals(tsom.getJSObject().getMember("kind"), "JSObject");
        Assert.assertEquals(tsom.getScriptObjectMirror().getMember("kind"), "ScriptObjectMirror");
        Assert.assertEquals(tsom.getMap().get("kind"), "Map");
        Assert.assertEquals(tsom.getBindings().get("kind"), "Bindings");
    }

    public interface TestListAdapter {
        public List<Object> getList();
        public Collection<Object> getCollection();
        public Queue<Object> getQueue();
        public Deque<Object> getDequeue();
    }

    @Test
    public static void testReturnsListAdapter() throws ScriptException {
        final ScriptEngine e = createEngine();
        e.put("TestListAdapterClass", TestListAdapter.class);
        final TestListAdapter tla = (TestListAdapter)e.eval(
                "new TestListAdapterClass.static({\n" +
                "  getList: function() { return [ 'List' ] },\n" +
                "  getCollection: function() { return [ 'Collection' ] },\n" +
                "  getQueue: function() { return [ 'Queue' ] },\n" +
                "  getDequeue: function() { return [ 'Dequeue' ] } })\n");
        Assert.assertEquals(tla.getList().get(0), "List");
        Assert.assertEquals(tla.getCollection().iterator().next(), "Collection");
        Assert.assertEquals(tla.getQueue().peek(), "Queue");
        Assert.assertEquals(tla.getDequeue().peek(), "Dequeue");
    }

    @Test
    public static void testMirrorAdapter() throws ScriptException {
        final NashornScriptEngine e = (NashornScriptEngine) createEngine();
        e.setBindings(e.createBindings(), ScriptContext.GLOBAL_SCOPE); // Null by default

        // Referencing functions from across scopes causes them to be wrapped in ScriptObjectMirrors
        e.eval("function convertObjectFromEngineScope(){ return new java.util.concurrent.Callable(o).call(); }", e.getBindings(ScriptContext.ENGINE_SCOPE));
        e.eval("function convertObjectFromGlobalScope(){ return new java.util.concurrent.Callable(o).call(); }", e.getBindings(ScriptContext.GLOBAL_SCOPE));
        e.eval("function convertFuncFromEngineScope(){ return new java.util.concurrent.Callable(g).call(); }", e.getBindings(ScriptContext.ENGINE_SCOPE));
        e.eval("function convertFuncFromGlobalScope(){ return new java.util.concurrent.Callable(g).call(); }", e.getBindings(ScriptContext.GLOBAL_SCOPE));
        e.eval("function convertParamFromEngineScope(){ return Java.type('jdk.nashorn.internal.runtime.linker.test.JavaAdapterTest').m(f);}", e.getBindings(ScriptContext.ENGINE_SCOPE));
        e.eval("function convertParamFromGlobalScope(){ return Java.type('jdk.nashorn.internal.runtime.linker.test.JavaAdapterTest').m(f);}", e.getBindings(ScriptContext.GLOBAL_SCOPE));

        e.eval("var o = { call: function () { return 'ok from o'; } }", e.getBindings(ScriptContext.ENGINE_SCOPE));
        e.eval("function g() { return 'ok from g'; }", e.getBindings(ScriptContext.ENGINE_SCOPE));
        e.eval("function f(a) { return a.toUpperCase(); }", e.getBindings(ScriptContext.ENGINE_SCOPE));

        try {
            Assert.assertEquals(e.invokeFunction("convertObjectFromEngineScope"), "ok from o");
            Assert.assertEquals(e.invokeFunction("convertObjectFromGlobalScope"), "ok from o");
            Assert.assertEquals(e.invokeFunction("convertFuncFromEngineScope"), "ok from g");
            Assert.assertEquals(e.invokeFunction("convertFuncFromGlobalScope"), "ok from g");
            Assert.assertEquals(e.invokeFunction("convertParamFromEngineScope"), "OK");
            Assert.assertEquals(e.invokeFunction("convertParamFromGlobalScope"), "OK");
        } catch (final NoSuchMethodException x) {
            throw new RuntimeException(x);
        }
    }

    public static String m(final Function<String, String> f){
        return f.apply("ok");
    }
}
