package de.dhbwstuttgart.bytecode;

import de.dhbwstuttgart.syntaxtree.statement.Break;
import de.dhbwstuttgart.target.tree.TargetGeneric;
import de.dhbwstuttgart.target.tree.type.*;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import java.sql.Array;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.objectweb.asm.Opcodes.*;

/**
 * //ToDo beschreiben
 *
 * @since Studienarbeit Type Erasure
 * @author etiennezink
 */
public class FunNGenerator {

    private static final String argumentGenericBase = "T";
    private static final String returnGeneric = "R";
    private static final String methodName = "apply";
    private static final int bytecodeVersion = V1_8;

    private static final String objectSuperType = Type.getInternalName(Object.class).replace('.','/');
    private static final String objectSignature = applySignature(TargetType.Object);

    private static final String VOID = "Ljava/lang/Void;";

    public static class GenericParameters {
        int start;
        public List<TargetType> parameters = new ArrayList<>();
        final String descriptor;
        public final List<TargetType> inParams;
        public final List<TargetType> realParams;

        public GenericParameters(TargetFunNType funNType) {
            this(funNType.funNParams(), funNType.returnArguments());
        }

        public GenericParameters(List<TargetType> params, int numReturns) {
            this.realParams = params;
            this.inParams = flattenTypeParams(params);
            var type = new TargetRefType(FunNGenerator.getSuperClassName(params.size() - 1, numReturns), params);
            descriptor = applyDescriptor(type, this);
        }

        private static List<TargetType> flattenTypeParams(List<TargetType> params) {
            var res = new ArrayList<TargetType>();
            for (var param : params) {
                if (param instanceof TargetSpecializedType tspec) {
                    res.addAll(flattenTypeParams(tspec.params()));
                } else {
                    res.add(param);
                }
            }
            return res;
        }

        public TargetType getReturnType() {
            return FunNGenerator.getReturnType(realParams);
        }

        public List<TargetType> getArguments() {
            return FunNGenerator.getArguments(realParams);
        }
    }

    private static String applyDescriptor(TargetType type, GenericParameters gep) {
        if (type == null) return VOID;
        var res = "L" + type.getInternalName();
        if (type instanceof TargetSpecializedType a) {
            if (!a.params().isEmpty() && !(a.params().size() == 1 && a.params().getFirst() == null)) {
                res += "<";
                for (var param : a.params()) {
                    if (param instanceof TargetGenericType gp) {
                        gep.parameters.add(param);
                        res += "TT" + gep.start + ";";
                        gep.start += 1;
                    } else {
                        res += applyDescriptor(param, gep);
                    }
                }
                res += ">";
            }
        } else {
            gep.parameters.add(null);
            return type.toDescriptor();
        }
        res += ";";

        return res;
    }

    private static String applySignature(TargetType a) { return a.toSignature(); }
    private static String applyNameDescriptor(TargetType a){ return a instanceof TargetGenericType ? "LTPH;" : String.format("%s", a.toDescriptor()); }

    public static String encodeType(TargetType type) {
        if (type == null) return VOID;
        return applyNameDescriptor(type).replace("/", "$").replace(";", "$_$");
    }

    public static byte[] generateSuperBytecode(int numberArguments, int numReturnTypes) {
        StringBuilder superFunNClassSignature = new StringBuilder("<");
        StringBuilder superFunNMethodSignature = new StringBuilder("(");
        StringBuilder superFunNMethodDescriptor = new StringBuilder("(");

        for (int currentParameter = 1; currentParameter <= numberArguments; currentParameter++){
            superFunNClassSignature.append(String.format("%s%d:%s", argumentGenericBase, currentParameter, objectSignature));
            superFunNMethodSignature.append(String.format("T%s;", argumentGenericBase + currentParameter));
            superFunNMethodDescriptor.append(objectSignature);
        }
        if (numReturnTypes > 0) {
            superFunNClassSignature.append(String.format("%s:%s>%s", returnGeneric, objectSignature, objectSignature));
            superFunNMethodSignature.append(String.format(")T%s;", returnGeneric));
            superFunNMethodDescriptor.append(String.format(")%s", objectSignature));
        } else {
            superFunNClassSignature = new StringBuilder(objectSignature);
            superFunNMethodSignature.append(")V");
            superFunNMethodDescriptor.append(")V");
        }

        Codegen.logger.info(superFunNMethodSignature);

        ClassWriter classWriter = new ClassWriter(0);
        MethodVisitor methodVisitor;
        classWriter.visit(bytecodeVersion, ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE, getSuperClassName(numberArguments, numReturnTypes), superFunNClassSignature.toString(), objectSuperType, null);
        methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, methodName, superFunNMethodDescriptor.toString(), superFunNMethodSignature.toString(), null);
        methodVisitor.visitEnd();
        classWriter.visitEnd();
        return classWriter.toByteArray();
    }

    public static String getSuperClassName(int numberArguments, int returnArguments) {
        return returnArguments > 0 ? String.format("Fun%d$$", numberArguments) : String.format("FunVoid%d$$", numberArguments);
    }

    public static byte[] generateSpecializedBytecode(GenericParameters gep, List<String> superInterfaces) {
        var argumentTypes = gep.getArguments();
        var returnType = gep.getReturnType();

        StringBuilder funNClassSignature = new StringBuilder(objectSignature + gep.descriptor);
        boolean containsGeneric = false;

        String genericSignature = "<";
        for (var i = 0; i < gep.start; i++) {
            genericSignature += String.format("T%d:%s", i, objectSignature);
            containsGeneric = true;
        }

        genericSignature += ">";
        if (containsGeneric) funNClassSignature.insert(0, genericSignature);

        for (var superInterface : superInterfaces) {
            funNClassSignature.append('L');
            funNClassSignature.append(superInterface);
            funNClassSignature.append(';');
        }

        var interfaces = new ArrayList<>(superInterfaces);
        interfaces.add(getSuperClassName(argumentTypes.size(), returnType != null ? 1 : 0));

        ClassWriter classWriter = new ClassWriter(0);
        classWriter.visit(bytecodeVersion, ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE, getSpecializedClassName(argumentTypes, returnType), funNClassSignature.toString(), objectSuperType, interfaces.toArray(String[]::new));
        classWriter.visitEnd();
        return classWriter.toByteArray();
    }

    private static void collectGenericTypeArguments(Map<TargetGenericType, Integer> generics, TargetType typeArgument) {
        if (typeArgument instanceof TargetSpecializedType specializedType) {
            for (var arg : specializedType.params())
                collectGenericTypeArguments(generics, arg);
        } else if (typeArgument instanceof TargetGenericType genericType) {
            if (generics.containsKey(genericType))
                generics.put(genericType, generics.get(genericType) + 1);
            else generics.put(genericType, 0);
        }
    }

    public static String getSpecializedClassName(GenericParameters gep) {
        return getSpecializedClassName(gep.getArguments(), gep.getReturnType());
    }

    public static String getSpecializedClassName(List<TargetType> argumentTypes, TargetType returnType) {
        var arguments = argumentTypes
            .stream()
            .map(FunNGenerator::encodeType)
            .collect(Collectors.joining());

        if (returnType != null)
            return String.format("Fun%d$$%s%s",
                argumentTypes.size(),
                arguments,
                encodeType(returnType));
        else return String.format("FunVoidImpl%d$$%s",
            argumentTypes.size(),
            arguments);
    }

    public static List<TargetType> getArguments(List<TargetType> list) {
        return list
                .stream()
                .limit(Math.max(0, list.size() - 1))
                .collect(Collectors.toList());
    }

    public static TargetType getReturnType(List<TargetType> list) {
        if(list.isEmpty())
            throw new IndexOutOfBoundsException();
        return list.getLast();
    }
}
