/*
 * Copyright 2021 Alibaba Group Holding Limited.
 *
 * Licensed 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 com.alibaba.graphscope.utils;

import com.alibaba.graphscope.app.DefaultAppBase;
import com.alibaba.graphscope.app.DefaultPropertyAppBase;
import com.alibaba.graphscope.app.ParallelAppBase;
import com.alibaba.graphscope.app.ParallelPropertyAppBase;
import com.alibaba.graphscope.context.LabeledVertexDataContext;
import com.alibaba.graphscope.context.LabeledVertexPropertyContext;
import com.alibaba.graphscope.context.VertexDataContext;
import com.alibaba.graphscope.context.VertexPropertyContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Objects;

public class AppBaseParser {
    private static Logger logger = LoggerFactory.getLogger(AppBaseParser.class.getName());
    private static String GIRAPH_APP_ABSTRACT_NAME = "org.apache.giraph.graph.AbstractComputation";
    private static String GIRAPH_APP_BASIC_NAME = "org.apache.giraph.graph.BasicComputation";

    public static void main(String[] args) {
        if (args.length != 1) {
            logger.error("Error: Expected only one class, fully named.");
            return;
        }
        loadClassAndParse(args[0]);
    }

    private static void loadClassAndParse(String className) {
        try {
            // we don't giraphAppBaseClass is defined in giraph-sdk, and we don't want to introduce
            // circular dependency.
            Class<?> giraphDefaultAppBase = Class.forName(GIRAPH_APP_BASIC_NAME);
            logger.info("loaded giraph default app base: " + giraphDefaultAppBase.getName());
            Class<?> clz = Class.forName(className);
            Type[] typeParams;
            // Input class name can be a giraph app, But we can use isAssignableFrom, since it
            // will introduce circular dependency. We judge by using get super class.
            if (tryGiraphClass(clz)) {
                return;
            }
            if (DefaultAppBase.class.isAssignableFrom(clz)) {
                logger.info("DefaultAppBase");
                typeParams = getTypeParams(clz, 5);
            } else if (ParallelAppBase.class.isAssignableFrom(clz)) {
                logger.info("ParallelAppBase");
                typeParams = getTypeParams(clz, 5);
            } else if (DefaultPropertyAppBase.class.isAssignableFrom(clz)) {
                logger.info("DefaultPropertyAppBase");
                typeParams = getTypeParams(clz, 2);
            } else if (ParallelPropertyAppBase.class.isAssignableFrom(clz)) {
                logger.info("ParallelPropertyAppBase");
                typeParams = getTypeParams(clz, 2);
            } else {
                logger.error("No matching app bases");
                return;
            }
            if (typeParams.length == 2) {
                logger.info("TypeParams: " + typeParams[0].getTypeName());
                Class<?> ctxType = (Class<?>) typeParams[1];
                logger.info("ContextType:" + javaContextToCppContextName(ctxType));
                logger.info("VertexData: " + getVertexDataType(ctxType));
            } else {
                String typeParamNames[] = new String[4];
                for (int i = 0; i < 4; ++i) {
                    typeParamNames[i] = typeParams[i].getTypeName();
                }
                logger.info("TypeParams: " + String.join(",", typeParamNames));
                Class<?> ctxType = (Class<?>) typeParams[4];
                logger.info("ContextType:" + javaContextToCppContextName(ctxType));
                logger.info("VertexData: " + getVertexDataType(ctxType));
            }
        } catch (Exception e) {
            logger.info("Exception occurred");
            e.printStackTrace();
        }
    }

    private static String getVertexDataType(Class<?> ctxClass) {
        Type[] types = getExtendTypeParams(ctxClass, 2);
        return types[1].getTypeName();
    }

    private static boolean tryGiraphClass(Class<?> claz) {
        Class<?> father = claz.getSuperclass();
        if (Objects.isNull(father)) {
            logger.info("Received an interface");
            return false;
        } else if (father.equals(Object.class)) {
            logger.info("super class is object");
            return false;
        }
        Type[] types;
        if (father.getName().equals(GIRAPH_APP_ABSTRACT_NAME)) {
            logger.info("Giraph Extend abstract computation");
            types = getExtendTypeParams(claz, 5);
        } else if (father.getName().equals(GIRAPH_APP_BASIC_NAME)) {
            logger.info("Giraph Extend basic computation");
            types = getExtendTypeParams(claz, 4);
        } else {
            return false;
        }
        String typeParamNames[] = new String[types.length];
        for (int i = 0; i < types.length; ++i) {
            typeParamNames[i] = writableToJava(types[i].getTypeName());
        }
        logger.info("TypeParams: " + String.join(",", typeParamNames));
        logger.info("ContextType:vertex_data");
        logger.info("VertexData: " + typeParamNames[1]);
        return true;
    }

    private static String javaContextToCppContextName(Class<?> ctxClass) {
        if (LabeledVertexDataContext.class.isAssignableFrom(ctxClass)) {
            return "labeled_vertex_data";
        } else if (VertexDataContext.class.isAssignableFrom(ctxClass)) {
            return "vertex_data";
        } else if (LabeledVertexPropertyContext.class.isAssignableFrom(ctxClass)) {
            return "labeled_vertex_property";
        } else if (VertexPropertyContext.class.isAssignableFrom(ctxClass)) {
            return "vertex_property";
        }
        return "null";
    }

    private static Type[] getExtendTypeParams(Class<?> klass, int expectedSize) {
        ParameterizedType type = (ParameterizedType) (klass.getGenericSuperclass());
        Type[] classes = type.getActualTypeArguments();
        if (classes.length != expectedSize) {
            logger.error(
                    "Error: Number of params error, expected "
                            + expectedSize
                            + ", actual "
                            + classes.length);
            throw new IllegalStateException("Type params size not match");
        }
        return classes;
    }

    private static Type[] getTypeParams(Class<?> clz, int size) {
        Type type = clz.getGenericInterfaces()[0];
        if (!(type instanceof ParameterizedType)) {
            logger.error("Not a parameterized type" + type.getTypeName());
            throw new IllegalStateException("not a parameterized type" + type.getTypeName());
        }
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Type[] typeParams = parameterizedType.getActualTypeArguments();
        if (typeParams.length != size) {
            logger.error(
                    "Error: Number of params error, expected "
                            + size
                            + ", actual "
                            + typeParams.length);
            throw new IllegalStateException("Type params size not match");
        }
        return typeParams;
    }

    private static String writableToJava(String typeName) {
        if (typeName.contains("DoubleWritable")) {
            return "java.lang.Double";
        } else if (typeName.contains("IntWritable")) {
            return "java.lang.Integer";
        } else if (typeName.contains("LongWritable")) {
            return "java.lang.Long";
        } else throw new IllegalStateException("Not recognized writable " + typeName);
    }

    private static Method getMethod(Class<?> clz) {
        Method[] methods = clz.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equals("PEval")) {
                return method;
            }
        }
        return null;
    }

    private static Class<?> getFragmentClassFromMethod(Method method) {
        Class<?>[] params = method.getParameterTypes();
        if (params.length != 3) {
            logger.error("Expected 3 parameters for this method: " + method.getName());
            return null;
        }
        return params[0];
    }
}
