package com.reason.lang.core;

import java.util.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.reason.ide.search.PsiFinder;
import com.reason.lang.QNameFinder;
import com.reason.lang.core.psi.ExpressionScope;
import com.reason.lang.core.psi.PsiClass;
import com.reason.lang.core.psi.PsiDirective;
import com.reason.lang.core.psi.PsiExternal;
import com.reason.lang.core.psi.PsiFunctor;
import com.reason.lang.core.psi.PsiInclude;
import com.reason.lang.core.psi.PsiInnerModule;
import com.reason.lang.core.psi.PsiLet;
import com.reason.lang.core.psi.PsiModule;
import com.reason.lang.core.psi.PsiOpen;
import com.reason.lang.core.psi.PsiType;
import com.reason.lang.core.psi.PsiVal;
import com.reason.lang.ocaml.OclQNameFinder;
import com.reason.lang.reason.RmlLanguage;
import com.reason.lang.reason.RmlQNameFinder;

import static com.reason.lang.core.ORFileType.interfaceOrImplementation;

public class PsiFileHelper {

    private PsiFileHelper() {
    }

    @NotNull
    public static Collection<PsiNameIdentifierOwner> getExpressions(@Nullable PsiFile file, @NotNull ExpressionScope eScope,
                                                                    @Nullable ExpressionFilter filter) {
        ArrayList<PsiNameIdentifierOwner> result = new ArrayList<>();

        if (file != null) {
            PsiFinder psiFinder = PsiFinder.getInstance(file.getProject());
            QNameFinder qnameFinder = file.getLanguage() == RmlLanguage.INSTANCE ? RmlQNameFinder.INSTANCE : OclQNameFinder.INSTANCE;
            processSiblingExpressions(psiFinder, qnameFinder, file.getFirstChild(), eScope, result, filter);
        }

        return result;
    }

    private static void processSiblingExpressions(@Nullable PsiFinder psiFinder, @NotNull QNameFinder qnameFinder, @Nullable PsiElement element,
                                                  @NotNull ExpressionScope eScope, @NotNull List<PsiNameIdentifierOwner> result,
                                                  @Nullable ExpressionFilter filter) {
        while (element != null) {
            if (element instanceof PsiInclude && psiFinder != null) {
                // Recursively include everything from referenced module
                PsiInclude include = (PsiInclude) element;
                GlobalSearchScope scope = GlobalSearchScope.allScope(element.getProject());

                PsiModule includedModule = null;

                String includeName = include.getQualifiedName();
                for (String path : qnameFinder.extractPotentialPaths(include)) {
                    Set<PsiModule> modulesFromQn = psiFinder.findModulesFromQn(path + "." + includeName, true, interfaceOrImplementation, scope);
                    if (!modulesFromQn.isEmpty()) {
                        includedModule = modulesFromQn.iterator().next();
                        break;
                    }
                }
                if (includedModule == null) {
                    Set<PsiModule> modulesFromQn = psiFinder.findModulesFromQn(includeName, true, interfaceOrImplementation, scope);
                    if (!modulesFromQn.isEmpty()) {
                        includedModule = modulesFromQn.iterator().next();
                    }
                }

                if (includedModule != null) {
                    Collection<PsiNameIdentifierOwner> expressions = includedModule.getExpressions(eScope, filter);
                    result.addAll(expressions);
                }
            }

            if (element instanceof PsiDirective) {
                // add all elements found in a directive, can't be resolved
                processSiblingExpressions(psiFinder, qnameFinder, element.getFirstChild(), eScope, result, filter);
            } else if (element instanceof PsiNameIdentifierOwner) {
                boolean include = eScope == ExpressionScope.all || !(element instanceof PsiLet && ((PsiLet) element).isPrivate());
                if (include && (filter == null || filter.accept((PsiNameIdentifierOwner) element))) {
                    result.add((PsiNameIdentifierOwner) element);
                }
            }

            element = element.getNextSibling();
        }
    }

    @NotNull
    public static Collection<PsiNameIdentifierOwner> getExpressions(@NotNull PsiFile file, @Nullable String name) {
        Collection<PsiNameIdentifierOwner> result = new ArrayList<>();

        if (name != null) {
            PsiElement element = file.getFirstChild();
            while (element != null) {
                if (element instanceof PsiNameIdentifierOwner && name.equals(((PsiNameIdentifierOwner) element).getName())) {
                    result.add((PsiNameIdentifierOwner) element);
                }
                element = element.getNextSibling();
            }
        }

        return result;
    }

    @NotNull
    public static List<PsiType> getTypeExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiType.class);
    }

    @NotNull
    public static List<PsiModule> getModuleExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiInnerModule.class);
    }

    @NotNull
    public static List<PsiFunctor> getFunctorExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiFunctor.class);
    }

    @NotNull
    public static List<PsiClass> getClassExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiClass.class);
    }

    @NotNull
    public static List<PsiVal> getValExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiVal.class);
    }

    @NotNull
    public static List<PsiExternal> getExternalExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiExternal.class);
    }

    @NotNull
    public static Collection<PsiOpen> getOpenExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiOpen.class);
    }

    @NotNull
    public static List<PsiInclude> getIncludeExpressions(@Nullable PsiFile file) {
        return PsiTreeUtil.getStubChildrenOfTypeAsList(file, PsiInclude.class);
    }
}