package com.neueda.jetbrains.plugin.graphdb.language.cypher.util;

import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.FileTypeIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.indexing.FileBasedIndex;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.file.CypherFile;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.file.CypherFileType;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.references.CypherNamedElement;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Utility method for finding PSI elements.
 *
 * @author [email protected]
 */
public class CypherUtil {

    private static final Filter INCLUDE_ALL = (element) -> true;

    public static <T extends CypherNamedElement> List<T> findAll(Project project, IElementType elementType) {
        return findAllAndFilter(project, elementType, INCLUDE_ALL);
    }

    public static <T extends CypherNamedElement> List<T> findAll(PsiFile file, IElementType elementType) {
        return findAllAndFilter(file, elementType, INCLUDE_ALL);
    }

    public static <T extends CypherNamedElement> List<T> findAllByName(Project project, IElementType elementType, String name) {
        return findAllAndFilter(project, elementType, (element) -> name.equals(element.getName()));
    }

    public static <T extends CypherNamedElement> List<T> findAllByName(PsiFile file, IElementType elementType, String name) {
        return findAllAndFilter(file, elementType, (element) -> name.equals(element.getName()));
    }

    public static <T extends CypherNamedElement> List<T> findAllByName(PsiElement psiElement, IElementType elementType, String name) {
        return findAllAndFilter(psiElement, elementType, (element) -> name.equals(element.getName()));
    }

    public static <T extends CypherNamedElement> List<T> findAllAndFilter(Project project, IElementType elementType, Filter filter) {
        Collection<VirtualFile> virtualFiles = FileBasedIndex.getInstance()
                .getContainingFiles(FileTypeIndex.NAME, CypherFileType.INSTANCE, GlobalSearchScope.allScope(project));

        List<T> result = new ArrayList<>();
        for (VirtualFile virtualFile : virtualFiles) {
            CypherFile cypherFile = (CypherFile) PsiManager.getInstance(project).findFile(virtualFile);
            if (cypherFile != null) {
                List<T> found = findAllAndFilter(cypherFile, elementType, filter);
                result.addAll(found);
            }
        }

        return result;
    }

    public static <T extends CypherNamedElement> List<T> findAllAndFilter(PsiFile file, IElementType elementType, Filter filter) {
        List<T> candidates = TraverseUtil.collectPsiElementsByType(file, elementType);
        return filterSameElementsFromCandidates(candidates, filter);
    }

    public static <T extends CypherNamedElement> List<T> findAllAndFilter(PsiElement psiElement, IElementType elementType, Filter filter) {
        List<T> candidates = TraverseUtil.collectPsiElementsByType(psiElement, elementType);
        return filterSameElementsFromCandidates(candidates, filter);
    }

    public interface Filter {
        boolean filter(CypherNamedElement element);
    }

    private static <T extends CypherNamedElement> List<T> filterSameElementsFromCandidates(List<T> candidates, Filter filter) {
        List<T> sameElements = null;
        for (T candidate : candidates) {
            if (filter.filter(candidate)) {
                if (sameElements == null) {
                    sameElements = new ArrayList<>();
                }
                sameElements.add(candidate);
            }
        }

        return sameElements != null ? sameElements : Collections.emptyList();
    }
}