package de.peeeq.wurstscript.attributes.names;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import de.peeeq.wurstscript.ast.*;
import de.peeeq.wurstscript.parser.WPos;
import de.peeeq.wurstscript.types.VariableBinding;
import de.peeeq.wurstscript.types.WurstType;
import de.peeeq.wurstscript.types.WurstTypeVararg;
import de.peeeq.wurstscript.utils.Utils;
import org.eclipse.jdt.annotation.Nullable;

import java.util.*;
import java.util.stream.Collectors;


public class FuncLink extends DefLink {
    private final FunctionDefinition def;
    private final WurstType returnType;
    private final List<String> parameterNames;
    private final List<WurstType> parameterTypes;
    private final VariableBinding mapping;

    public FuncLink(Visibility visibility, WScope definedIn, List<TypeParamDef> typeParams,
                    @Nullable WurstType receiverType, FunctionDefinition def, List<String> parameterNames, List<WurstType> parameterTypes, WurstType returnType, VariableBinding mapping) {
        super(visibility, definedIn, typeParams, receiverType);
        this.def = def;
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.parameterNames = parameterNames;
        this.mapping = mapping;
    }

    public static FuncLink create(FunctionDefinition func, WScope definedIn) {
        Visibility visibility = calcVisibility(definedIn, func);

        // Collect all type params visible at this function:
        // 1) function's own type params
        // 2) enclosing/owning structure type params (class/module/inner class chain)
        java.util.ArrayList<TypeParamDef> typeParams = new java.util.ArrayList<>();
        typeParams.addAll(typeParams(func).collect(Collectors.toList()));

        Element cur = definedIn;
        while (cur != null) {
            if (cur instanceof AstElementWithTypeParameters) {
                typeParams.addAll(((AstElementWithTypeParameters) cur).getTypeParameters());
            }
            if (cur instanceof WPackage || cur instanceof CompilationUnit) {
                break;
            }
            cur = cur.getParent();
        }

        // De-dup by identity (same TypeParamDef object may appear more than once)
        java.util.Set<TypeParamDef> seen = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
        typeParams.removeIf(tp -> !seen.add(tp));

        // Parameter names/types
        java.util.ArrayList<String> paramNames = new java.util.ArrayList<>();
        for (WParameter p : func.getParameters()) {
            paramNames.add(p.getName());
        }

        java.util.ArrayList<WurstType> paramTypes = new java.util.ArrayList<>();
        for (WParameter p : func.getParameters()) {
            paramTypes.add(p.attrTyp());
        }

        WurstType returnType = func.attrReturnTyp();
        WurstType receiverType = calcReceiverType(definedIn, func);

        // Only the function's OWN type params need inference via argument matching.
        // Enclosing structure type params (class/module) are resolved through
        // receiver type matching in matchDefLinkReceiver, not through inference.
        List<TypeParamDef> ownTypeParams = typeParams(func).collect(Collectors.toList());
        VariableBinding mapping = VariableBinding.emptyMapping().withTypeVariables(ownTypeParams);

        return new FuncLink(visibility, definedIn, typeParams, receiverType, func, paramNames, paramTypes, returnType, mapping);
    }



    private static @Nullable WurstType calcReceiverType(WScope definedIn, NameDef nameDef) {
        if (nameDef instanceof ExtensionFuncDef) {
            ExtensionFuncDef exF = (ExtensionFuncDef) nameDef;
            return exF.getExtendedType().attrTyp().dynamic();
        } else if (nameDef instanceof FuncDef) {
            return getReceiverType(definedIn);
        }
        return null;
    }


    public String getName() {
        return def.getName();
    }


    @Override
    public FunctionDefinition getDef() {
        return def;
    }


    public FuncLink withVisibility(Visibility newVis) {
        if (newVis == getVisibility()) {
            return this;
        }
        return new FuncLink(newVis, getDefinedIn(), getTypeParams(), getReceiverType(), def, parameterNames, parameterTypes, returnType, mapping);
    }


    public String getParameterDescription() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < getParameterTypes().size(); i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(getParameterType(i));
            if (i < getParameterNames().size()) {
                sb.append(" ");
                sb.append(getParameterName(i));
            }
        }
        return sb.toString();
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        result.append(getVisibility());
        result.append(" ");
        if (getReceiverType() != null) {
            result.append(getReceiverType()).append(".");
        }
        result.append(getName());
        if (!typeParams.isEmpty()) {
            result.append("<");
            StringJoiner joiner = new StringJoiner(", ");
            for (TypeParamDef typeParam : typeParams) {
                String name = typeParam.getName();
                joiner.add(name);
            }
            for (String s : Arrays.asList(joiner.toString(), ">")) {
                result.append(s);
            }
        }
        result.append("(");
        result.append(getParameterDescription());
        result.append(") returns ");
        result.append(returnType);
        WPos src = def.attrSource();
        result.append(" (")
                .append(src.getFile())
                .append(":")
                .append(src.getLine())
                .append(")");
        return result.toString();
    }


    @Override
    public NameLinkType getType() {
        return NameLinkType.FUNCTION;
    }

    public FuncLink withTypeArgBinding(Element context, VariableBinding binding) {
        if (binding.isEmpty()) {
            return this;
        }
        WurstType newReturnType = adjustType(context, getReturnType(), binding);
        boolean changed = newReturnType != returnType;
        List<WurstType> newParamTypes;
        if (getParameterTypes().isEmpty()) {
            newParamTypes = getParameterTypes();
        } else {
            newParamTypes = Lists.newArrayListWithCapacity(getParameterTypes().size());
            for (WurstType pt : getParameterTypes()) {
                WurstType newPt = adjustType(context, pt, binding);
                if (newPt != pt) {
                    changed = true;
                }
                newParamTypes.add(newPt);
            }
        }
        WurstType newReceiverType = adjustType(context, getReceiverType(), binding);
        changed = changed || newReceiverType != getReceiverType();
        // Also check if any type params are being bound (even if types don't
        // change structurally — e.g., T bound to itself within a generic module)
        if (!changed) {
            for (TypeParamDef tp : getTypeParams()) {
                if (binding.contains(tp)) {
                    changed = true;
                    break;
                }
            }
        }
        if (changed) {
            // remove type parameters that are now bound:
            List<TypeParamDef> newTypeParams = new ArrayList<>();
            for (TypeParamDef tp : getTypeParams()) {
                if (!binding.contains(tp)) {
                    newTypeParams.add(tp);
                }
            }
            return new FuncLink(getVisibility(), getDefinedIn(), newTypeParams, newReceiverType, def, parameterNames, newParamTypes, newReturnType, binding);
        } else {
            return this;
        }
    }

    @Override
    public DefLink withGenericTypeParams(List<TypeParamDef> typeParams) {
        if (typeParams.isEmpty()) {
            return this;
        }
        ImmutableList<TypeParamDef> newTypeParams = Utils.concatLists(getTypeParams(), typeParams);
        return new FuncLink(getVisibility(), getDefinedIn(), newTypeParams, getReceiverType(), def, parameterNames, parameterTypes, returnType, mapping);
    }

    @Override
    public WurstType getTyp() {
        return getReturnType();
    }

    @Override
    public FuncLink withDef(NameDef def) {
        return new FuncLink(getVisibility(), getDefinedIn(), getTypeParams(), getReceiverType(), (FunctionDefinition) def, getParameterNames(), getParameterTypes(), getReturnType(), mapping);
    }

    private WurstType adjustType(Element context, WurstType t, VariableBinding binding) {
        if (t == null) {
            return null;
        }
        return t.setTypeArgs(binding);
    }


    public WurstType getReturnType() {
        return returnType;
    }

    public List<WurstType> getParameterTypes() {
        return parameterTypes;
    }

    public WurstType getParameterType(int i) {
        List<WurstType> parameterTypes = getParameterTypes();
        if (isVarargMethod() && i >= parameterTypes.size() - 1) {
            return ((WurstTypeVararg) parameterTypes.get(parameterTypes.size() - 1)).getBaseType();
        }
        return parameterTypes.get(i);
    }

    public String getParameterName(int i) {
        if (isVarargMethod() && i >= parameterNames.size() - 1) {
            return parameterNames.get(parameterNames.size() - 1);
        }
        return parameterNames.get(i);
    }

    public boolean isVarargMethod() {
        List<WurstType> parameterTypes = getParameterTypes();
        if (parameterTypes.size() > 0) {
            return parameterTypes.get(parameterTypes.size() - 1) instanceof WurstTypeVararg;
        }
        return false;
    }

    public FuncLink withConfigDef() {
        FunctionDefinition def = (FunctionDefinition) this.def.attrConfigActualNameDef();
        return new FuncLink(getVisibility(), getDefinedIn(), getTypeParams(), getReceiverType(), def, parameterNames, parameterTypes, returnType, mapping);
    }

    public FuncLink hidingPrivate() {
        return (FuncLink) super.hidingPrivate();
    }

    public FuncLink hidingPrivateAndProtected() {
        return (FuncLink) super.hidingPrivateAndProtected();
    }


    public List<String> getParameterNames() {
        return parameterNames;
    }

    public FuncLink adaptToReceiverType(WurstType receiverType) {
        return (FuncLink) super.adaptToReceiverType(receiverType);
    }


    public String printFunctionTemplate() {
        StringBuilder res = new StringBuilder("function ");
        res.append(getName());
        res.append("(");
        for (int i = 0; i < parameterNames.size(); i++) {
            if (i > 0) {
                res.append(", ");
            }
            res.append(parameterTypes.get(i));
            res.append(" ");
            res.append(parameterNames.get(i));
        }
        res.append(")");
        if (!getReturnType().isVoid()) {
            res.append(" returns ");
            res.append(getReturnType());
        }
        return res.toString();
    }

    public boolean isStatic() {
        return def.attrIsStatic();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        FuncLink funcLink = (FuncLink) o;
        return Objects.equals(def, funcLink.def);
    }

    @Override
    public int hashCode() {
        return Objects.hash(def);
    }

    public VariableBinding getVariableBinding() {
        return mapping;
    }

    public boolean hasIfNotDefinedAnnotation() {
        return def.attrHasAnnotation("ifNotDefined");
    }
}
