/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.common.bytecode;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import javassist.ClassPool;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

interface Builder<T> {
    T getName(Bean bean);

    void setName(Bean bean, T name);
}

class BaseClass {

    public BaseClass() {}

    public BaseClass(StringBuilder sb) {
        sb.append("constructor comes from BaseClass");
    }

    public String baseClassMethod() {
        return "method comes from BaseClass";
    }
}

interface BaseInterface {}

class ClassGeneratorTest {

    @Test
    void test() throws Exception {
        ClassGenerator cg = ClassGenerator.newInstance();

        // add className, interface, superClass
        String className = BaseClass.class.getPackage().getName() + ".TestClass"
                + UUID.randomUUID().toString().replace("-", "");
        cg.setClassName(className);
        cg.addInterface(BaseInterface.class);
        cg.setSuperClass(BaseClass.class);

        // add constructor
        cg.addDefaultConstructor();
        cg.addConstructor(
                Modifier.PUBLIC,
                new Class[] {String.class, int.class},
                new Class[] {Throwable.class, RuntimeException.class},
                "this.strAttr = arg0;this.intAttr = arg1;");
        cg.addConstructor(BaseClass.class.getConstructor(StringBuilder.class));

        // add field
        cg.addField(
                "staticAttr", Modifier.PUBLIC | Modifier.STATIC | Modifier.VOLATILE, String.class, "\"defaultVal\"");
        cg.addField("strAttr", Modifier.PROTECTED | Modifier.VOLATILE, String.class);
        cg.addField("intAttr", Modifier.PRIVATE | Modifier.VOLATILE, int.class);

        // add method
        cg.addMethod(
                "setStrAttr",
                Modifier.PUBLIC,
                void.class,
                new Class[] {String.class, int.class},
                new Class[] {Throwable.class, RuntimeException.class},
                "this.strAttr = arg0;");
        cg.addMethod(
                "setIntAttr",
                Modifier.PUBLIC,
                void.class,
                new Class[] {String.class, int.class},
                new Class[] {Throwable.class, RuntimeException.class},
                "this.intAttr = arg1;");
        cg.addMethod(
                "getStrAttr",
                Modifier.PUBLIC,
                String.class,
                new Class[] {},
                new Class[] {Throwable.class, RuntimeException.class},
                "return this.strAttr;");
        cg.addMethod(
                "getIntAttr",
                Modifier.PUBLIC,
                int.class,
                new Class[] {},
                new Class[] {Throwable.class, RuntimeException.class},
                "return this.intAttr;");
        cg.addMethod(BaseClass.class.getMethod("baseClassMethod"));

        // cg.toClass
        Class<?> clz = cg.toClass(Bean.class);
        // after cg.toClass, the TestClass.class generated by javassist is like the following file content
        getClass().getResource("/org/apache/dubbo/common/bytecode/TestClass");

        // verify class, superClass, interfaces
        Assertions.assertTrue(ClassGenerator.isDynamicClass(clz));
        Assertions.assertEquals(clz.getName(), className);
        Assertions.assertEquals(clz.getSuperclass(), BaseClass.class);
        Assertions.assertArrayEquals(clz.getInterfaces(), new Class[] {ClassGenerator.DC.class, BaseInterface.class});

        // get constructors
        Constructor<?>[] constructors = clz.getConstructors();
        Assertions.assertEquals(constructors.length, 3);
        Constructor<?> constructor0 = clz.getConstructor();
        Constructor<?> constructor1 = clz.getConstructor(new Class[] {String.class, int.class});
        Constructor<?> constructor2 = clz.getConstructor(new Class[] {StringBuilder.class});
        Assertions.assertEquals(constructor1.getModifiers(), Modifier.PUBLIC);
        Assertions.assertArrayEquals(
                constructor1.getExceptionTypes(), new Class[] {Throwable.class, RuntimeException.class});

        // get fields
        Field staticAttrField = clz.getDeclaredField("staticAttr");
        Field strAttrField = clz.getDeclaredField("strAttr");
        Field intAttrField = clz.getDeclaredField("intAttr");
        Assertions.assertNotNull(staticAttrField);
        Assertions.assertNotNull(strAttrField);
        Assertions.assertNotNull(intAttrField);
        Assertions.assertEquals(staticAttrField.get(null), "defaultVal");

        // get methods
        Method setStrAttrMethod = clz.getMethod("setStrAttr", new Class[] {String.class, int.class});
        Method setIntAttrMethod = clz.getMethod("setIntAttr", new Class[] {String.class, int.class});
        Method getStrAttrMethod = clz.getMethod("getStrAttr");
        Method getIntAttrMethod = clz.getMethod("getIntAttr");
        Method baseClassMethod = clz.getMethod("baseClassMethod");
        Assertions.assertNotNull(setStrAttrMethod);
        Assertions.assertNotNull(setIntAttrMethod);
        Assertions.assertNotNull(getStrAttrMethod);
        Assertions.assertNotNull(getIntAttrMethod);
        Assertions.assertNotNull(baseClassMethod);

        // verify constructor0
        Object objByConstructor0 = constructor0.newInstance();
        Assertions.assertEquals(getStrAttrMethod.invoke(objByConstructor0), null);
        Assertions.assertEquals(getIntAttrMethod.invoke(objByConstructor0), 0);

        // verify constructor1
        Object objByConstructor1 = constructor1.newInstance("v1", 1);
        Assertions.assertEquals(getStrAttrMethod.invoke(objByConstructor1), "v1");
        Assertions.assertEquals(getIntAttrMethod.invoke(objByConstructor1), 1);

        // verify getter setter method
        setStrAttrMethod.invoke(objByConstructor0, "v2", 2);
        setIntAttrMethod.invoke(objByConstructor0, "v3", 3);
        Assertions.assertEquals(getStrAttrMethod.invoke(objByConstructor0), "v2");
        Assertions.assertEquals(getIntAttrMethod.invoke(objByConstructor0), 3);

        // verify constructor and method witch comes from baseClass
        StringBuilder sb = new StringBuilder();
        Object objByConstructor2 = constructor2.newInstance(sb);
        Assertions.assertEquals(sb.toString(), "constructor comes from BaseClass");

        Object res = baseClassMethod.invoke(objByConstructor2);
        Assertions.assertEquals(res, "method comes from BaseClass");

        cg.release();
    }

    @SuppressWarnings("unchecked")
    @Test
    void testMain() throws Exception {
        Bean b = new Bean();
        Field fname = Bean.class.getDeclaredField("name");
        fname.setAccessible(true);

        ClassGenerator cg = ClassGenerator.newInstance();
        cg.setClassName(Bean.class.getName() + "$Builder" + UUID.randomUUID().toString());
        cg.addInterface(Builder.class);

        cg.addField("public static java.lang.reflect.Field FNAME;");

        cg.addMethod("public Object getName(" + Bean.class.getName()
                + " o){ boolean[][][] bs = new boolean[0][][]; return (String)FNAME.get($1); }");
        cg.addMethod("public void setName(" + Bean.class.getName() + " o, Object name){ FNAME.set($1, $2); }");

        cg.addDefaultConstructor();
        Class<?> cl = cg.toClass(Bean.class);
        cl.getField("FNAME").set(null, fname);

        System.out.println(cl.getName());
        Builder<String> builder = (Builder<String>) cl.getDeclaredConstructor().newInstance();
        System.out.println(b.getName());
        builder.setName(b, "ok");
        System.out.println(b.getName());
    }

    @Test
    void testMain0() throws Exception {
        Bean b = new Bean();
        Field fname = Bean.class.getDeclaredField("name");
        fname.setAccessible(true);

        ClassGenerator cg = ClassGenerator.newInstance();
        cg.setClassName(Bean.class.getName() + "$Builder2" + UUID.randomUUID().toString());
        cg.addInterface(Builder.class);

        cg.addField("FNAME", Modifier.PUBLIC | Modifier.STATIC, java.lang.reflect.Field.class);

        cg.addMethod("public Object getName(" + Bean.class.getName()
                + " o){ boolean[][][] bs = new boolean[0][][]; return (String)FNAME.get($1); }");
        cg.addMethod("public void setName(" + Bean.class.getName() + " o, Object name){ FNAME.set($1, $2); }");

        cg.addDefaultConstructor();
        Class<?> cl = cg.toClass(Bean.class);
        cl.getField("FNAME").set(null, fname);

        System.out.println(cl.getName());
        Builder<String> builder = (Builder<String>) cl.getDeclaredConstructor().newInstance();
        System.out.println(b.getName());
        builder.setName(b, "ok");
        System.out.println(b.getName());
    }

    @Test
    public void test_getClassPool() throws InterruptedException {
        int threadCount = 5;
        CountDownLatch LATCH = new CountDownLatch(threadCount);
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        List<Integer> hashCodeList = new ArrayList<>();
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Runnable() {
                        @Override
                        public void run() {
                            ClassPool classPool = ClassGenerator.getClassPool(loader);
                            int currentHashCode = classPool.hashCode();
                            hashCodeList.add(currentHashCode);
                            System.out.println(currentHashCode);
                            LATCH.countDown();
                        }
                    })
                    .start();
        }
        LATCH.await();
        Integer firstHashCode = null;
        for (Integer currentHashCode : hashCodeList) {
            if (firstHashCode == null) {
                firstHashCode = currentHashCode;
                continue;
            }
            Assertions.assertTrue(firstHashCode.intValue() == currentHashCode.intValue());
        }
    }
}

class Bean {
    int age = 30;

    private String name = "qianlei";

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public static volatile String abc = "df";
}
