package de.peeeq.wurstscript.attributes;

import de.peeeq.wurstscript.ast.*;
import de.peeeq.wurstscript.attributes.names.FuncLink;
import de.peeeq.wurstscript.attributes.names.NameLink;
import de.peeeq.wurstscript.attributes.names.OtherLink;
import de.peeeq.wurstscript.attributes.names.Visibility;
import de.peeeq.wurstscript.types.WurstType;
import de.peeeq.wurstscript.types.WurstTypeClassOrInterface;
import de.peeeq.wurstscript.types.WurstTypeUnknown;
import de.peeeq.wurstscript.types.WurstTypeEnum;
import de.peeeq.wurstscript.types.WurstTypeModule;
import org.eclipse.jdt.annotation.Nullable;

/**
 * this attribute find the variable definition for every variable reference
 */
public class AttrNameDef {


    public static NameLink calculate(ExprVarArrayAccess term) {
        return searchNameInScope(term.getVarName(), term);
    }

    public static NameLink calculate(ExprVarAccess term) {
        NameLink result = specialEnumLookupRules(term);
        if (result != null) {
            return result;
        }
        return searchNameInScope(term.getVarName(), term);
    }

    public static @Nullable NameLink specialEnumLookupRules(ExprVarAccess term) {
        NameLink result = null;
        Element parent = term.getParent();
        String varName = term.getVarName();
        if (parent instanceof ExprList) {
            parent = parent.getParent();
            if (parent instanceof SwitchCase) {
                SwitchStmt s = (SwitchStmt) parent.getParent().getParent();
                result = lookupEnumConst(varName, s.getExpr().attrTyp());
            }
        } else if (parent instanceof StmtSet) {
            StmtSet s = (StmtSet) parent;
            if (s.getRight() == term) {
                result = lookupEnumConst(varName, s.getUpdatedExpr().attrTyp());
            }
        } else if (parent instanceof GlobalOrLocalVarDef) {
            GlobalOrLocalVarDef v = (GlobalOrLocalVarDef) parent;
            result = lookupEnumConst(varName, v.getOptTyp().attrTyp());
        } else if (parent instanceof ExprBinary) {
            ExprBinary binary = (ExprBinary) parent;
            if (binary.getRight() == term) {
                result = lookupEnumConst(varName, binary.getLeft().attrTyp());
            }
        }
        return result;
    }

    public static @Nullable NameLink lookupEnumConst(String varName, WurstType t) {
        if (t instanceof WurstTypeEnum) {
            WurstTypeEnum e = (WurstTypeEnum) t;
            // if we expect an enum type we can as well directly look into the enum
            EnumDef eDef = e.getDef();
            return eDef.lookupMemberVar(e, varName, false);
        }
        return null;
    }

    public static NameLink calculate(ExprMemberVar term) {
        return memberVarCase(term.getLeft(), term.getVarName(), isWriteAccess(term), term);
    }

    public static @Nullable NameLink calculate(ExprMemberArrayVar term) {
        return memberVarCase(term.getLeft(), term.getVarName(), isWriteAccess(term), term);
    }


    protected static NameLink searchNameInScope(String varName, NameRef node) {
        boolean showErrors = !varName.startsWith("gg_");
        if (!"it".equals(varName)) {
            return node.lookupVar(varName, showErrors);
        }

        // Normal lexical lookup wins, so user-defined names can shadow implicit closure-self.
        NameLink result = node.lookupVar(varName, false);
        if (result != null) {
            return result;
        }

        NameLink implicitClosureSelf = lookupImplicitClosureSelf(node, showErrors);
        if (implicitClosureSelf != null) {
            return implicitClosureSelf;
        }
        if (node.attrNearestExprClosure() != null) {
            // keep diagnostics focused on closure typing instead of "unknown variable it"
            return null;
        }

        if (!showErrors) {
            return null;
        }

        // Fallback to default diagnostics when no implicit closure-self is available.
        return node.lookupVar(varName, true);
    }

    private static @Nullable NameLink lookupImplicitClosureSelf(NameRef node, boolean showErrors) {
        ExprClosure closure = node.attrNearestExprClosure();
        if (closure == null) {
            return null;
        }
        WurstType expectedTyp = closure.attrExpectedTypAfterOverloading();
        if (!(expectedTyp instanceof WurstTypeClassOrInterface)) {
            if (showErrors && (expectedTyp instanceof WurstTypeUnknown)) {
                node.addError("Cannot use implicit closure self 'it' because the closure target type is unknown.");
            }
            return null;
        }

        return new OtherLink(Visibility.LOCAL, "it", expectedTyp) {
            @Override
            public de.peeeq.wurstscript.jassIm.ImExpr translate(NameRef e, de.peeeq.wurstscript.translation.imtranslation.ImTranslator t, de.peeeq.wurstscript.jassIm.ImFunction f) {
                return de.peeeq.wurstscript.jassIm.JassIm.ImVarAccess(t.getThisVar(closure));
            }
        };
    }

    private static boolean isWriteAccess(final NameRef node) {
        if (node.getParent() instanceof StmtSet) {
            StmtSet stmtSet = (StmtSet) node.getParent();
            return stmtSet.getUpdatedExpr() == node;
        }
        return false;
    }

    private static @Nullable NameLink memberVarCase(Expr left, String varName, boolean writeAccess, Expr node) {
        WurstType receiverType = left.attrTyp();
        NameLink result = node.lookupMemberVar(receiverType, varName);
        if (result == null) {


            node.addError("Could not resolve reference to variable " + varName + " for receiver of type " +
                    receiverType + ".");

        }
        if (receiverType instanceof WurstTypeModule) {
            WurstTypeModule wurstTypeModule = (WurstTypeModule) receiverType;
            ModuleDef module = wurstTypeModule.getDef();
            if (!left.isSubtreeOf(module)) {
                node.addError("Can only reference module variables from within the module.");
            }
        }


        return result;
    }

    public static @Nullable NameDef tryGetNameDef(NameRef e) {
        NameLink link = e.attrNameLink();
        if (link == null || link instanceof OtherLink) {
            return null;
        }
        return link.getDef();
    }

    public static @Nullable NameDef tryGetNameDef(NameDef e) {
        return e;
    }

    public static @Nullable NameDef tryGetNameDef(FuncRef e) {
        FuncLink link = e.attrFuncLink();
        if (link == null) {
            return null;
        }
        return link.getDef();
    }

    public static @Nullable NameDef tryGetNameDef(TypeRef e) {
        return e.attrTypeDef();
    }

    public static @Nullable NameDef tryGetNameDef(Element elem) {
        return null;
    }


    public static NameDef calculateDef(NameRef nameRef) {
        NameLink l = nameRef.attrNameLink();
        if (l instanceof OtherLink) {
            return null;
        }
        return l == null ? null : l.getDef();
    }
}
