package org.elmlang.intellijplugin.psi.impl;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.psi.PsiElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.elmlang.intellijplugin.psi.*;
import org.elmlang.intellijplugin.psi.references.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;

public class ElmPsiImplUtil {
    public static String getName(ElmUpperCaseId element) {
        return element.getText();
    }

    public static PsiElement setName(ElmUpperCaseId element, String newName) {
        Optional.ofNullable(element.getParent())
                .filter(e -> e instanceof ElmUpperCasePath)
                .flatMap(e -> Optional.ofNullable(e.getParent()))
                .filter(e -> e instanceof ElmModuleDeclaration)
                .ifPresent(e -> Messages.showWarningDialog(element.getProject(), "Unfortunately, functionality of renaming module names has not been implemented yet.", "It's not implemented yet"));
        return setName(element, ElmTypes.UPPER_CASE_IDENTIFIER, ElmElementFactory::createUpperCaseId, newName);
    }

    public static String getName(ElmLowerCaseId element) {
        return element.getText();
    }

    public static PsiElement setName(ElmLowerCaseId element, String newName) {
        return setName(element, ElmTypes.LOWER_CASE_IDENTIFIER, ElmElementFactory::createLowerCaseId, newName);
    }

    private static <T extends PsiElement> PsiElement setName(T element, IElementType elementType, BiFunction<Project, String, T> elementFactory, String newName) {
        ASTNode node = element.getNode().findChildByType(elementType);
        if (node != null) {
            Optional.ofNullable(elementFactory.apply(element.getProject(), newName))
                    .ifPresent(id -> element.getNode().replaceChild(node, id.getFirstChild().getNode()));
        }
        return element;
    }

    public static PsiElement getNameIdentifier(ElmLowerCaseId element) {
        ASTNode node = element.getNode();
        if (node != null) {
            return node.getPsi();
        } else {
            return null;
        }
    }

    public static PsiElement getNameIdentifier(ElmUpperCaseId element) {
        ASTNode node = element.getNode();
        if (node != null) {
            return node.getPsi();
        } else {
            return null;
        }
    }

    public static Stream<ElmReference> getReferencesStream(ElmExpression element) {
        return getReferencesInAncestor(
                element,
                element.getListOfOperandsList().stream().flatMap(ElmPsiImplUtil::getReferencesStream)
        );
    }

    private static Stream<ElmReference> getReferencesInAncestor(PsiElement ancestor, Stream<ElmReference> references) {
        return references
                .map(r -> r.referenceInAncestor(ancestor));
    }

    public static Stream<ElmReference> getReferencesStream(ElmListOfOperands element) {
        Stream<ElmReference> references = Arrays.stream(element.getChildren())
                .flatMap(child -> {
                    if (child instanceof ElmWithExpression) {
                        return ElmPsiImplUtil.getReferencesStream(((ElmWithExpression) child));
                    } else if (child instanceof ElmWithExpressionList) {
                        return ElmPsiImplUtil.getReferencesStream(((ElmWithExpressionList) child));
                    } else if (child instanceof ElmLowerCasePathImpl) {
                        return ((ElmLowerCasePathImpl) child).getReferencesStream();
                    } else {
                        return Stream.empty();
                    }
                });
        return getReferencesInAncestor(element, references);
    }

    public static Stream<ElmReference> getReferencesStream(ElmWithExpressionList element) {
        return getReferencesInAncestor(
                element,
                element.getExpressionList().stream()
                        .flatMap(ElmPsiImplUtil::getReferencesStream)
        );
    }

    public static Stream<ElmReference> getReferencesStream(ElmWithExpression element) {
        return getReferencesStream(element.getExpression())
                .map(r -> r.referenceInAncestor(element));
    }

    public static ValueDeclarationRole getRole(ElmValueDeclarationBase element) {
        return ElmValueDeclarationMixin.getRole(element);
    }

    public static String getDisplayName(ElmValueDeclarationBase element) {
        return ElmValueDeclarationMixin.getDisplayName(element);
    }

    public static String getDisplayName(ElmTypeAliasDeclaration element) {
        return element.getUpperCaseId().getName();
    }

    public static String getDisplayName(ElmTypeDeclaration element) {
        return element.getUpperCaseId().getName();
    }

    public static Stream<ElmReference> getReferencesStream(ElmRecord record) {

        Stream<ElmReference> recordBase = Optional.ofNullable(record.getLowerCaseId())
                .map(id -> new ElmValueReference(id).referenceInAncestor(record))
                .map(Stream::of)
                .orElse(Stream.empty());

        Stream<ElmReference> fields = record.getFieldList().stream()
                .map(f ->
                        ElmPsiImplUtil.getReferencesStream(f)
                                .map(r -> r.referenceInAncestor(record))
                )
                .reduce(Stream.empty(), Stream::concat);

        return Stream.concat(recordBase, fields);
    }

    public static ElmUpperCasePath getModuleName(ElmModuleDeclaration module) {
        return PsiTreeUtil.findChildOfType(module, ElmUpperCasePath.class);
    }

    public static ElmUpperCasePath getModuleName(ElmImportClause module) {
        return PsiTreeUtil.findChildOfType(module, ElmUpperCasePath.class);
    }

    public static boolean isExposingAll(ElmModuleDeclaration element) {
        return isAnyChildDoubleDot(element) || !ElmTreeUtil.isAnyChildOfType(element, ElmTypes.LEFT_PARENTHESIS);
    }

    public static boolean isExposingAll(ElmExposingClause element) {
        return isAnyChildDoubleDot(element);
    }

    public static boolean isExposingAll(ElmExposedUnionConstructors element) {
        return isAnyChildDoubleDot(element);
    }

    private static boolean isAnyChildDoubleDot(PsiElement element) {
        return ElmTreeUtil.isAnyChildOfType(element, ElmTypes.DOUBLE_DOT);
    }

    @NotNull
    public static Stream<ElmValueDeclarationBase> getValueDeclarations(ElmWithValueDeclarations element) {
        return Arrays.stream(element.getChildren())
                .filter(e -> e instanceof ElmValueDeclarationBase)
                .map(e -> (ElmValueDeclarationBase) e);
    }

    @NotNull
    public static Stream<ElmLowerCaseId> getDeclarationsFromPattern(@Nullable ElmPattern pattern) {
        if (pattern == null) {
            return Stream.empty();
        }

        return Stream.concat(
                Stream.concat(
                        pattern.getLowerCaseIdList().stream(),
                        pattern.getPatternList().stream()
                                .flatMap(ElmPsiImplUtil::getDeclarationsFromPattern)
                ),
                pattern.getUnionPatternList().stream().flatMap(ElmPsiImplUtil::getDeclarationsFromParentPattern)
        );

    }

    private static Stream<ElmLowerCaseId> getDeclarationsFromParentPattern(ElmWithPatternList parentPattern) {
        return parentPattern.getPatternList().stream()
                .flatMap(ElmPsiImplUtil::getDeclarationsFromPattern);
    }

    public static Stream<ElmLowerCaseId> getDefinedValues(ElmValueDeclarationBase element) {
        return Arrays.stream(element.getChildren())
                .map(child -> {
                    if (child instanceof ElmPattern) {
                        return getDeclarationsFromPattern((ElmPattern) child);
                    } else if (child instanceof ElmWithSingleId) {
                        return Stream.of(((ElmWithSingleId) child).getLowerCaseId());
                    } else {
                        return Stream.<ElmLowerCaseId>empty();
                    }
                })
                .reduce(Stream.empty(), Stream::concat);
    }

    public static Stream<ElmReference> getReferencesStream(ElmTypeAnnotationBase typeAnnotation) {
        return Optional.ofNullable(typeAnnotation.getLowerCaseId())
                .map(e -> Stream.of((ElmReference) new ElmTypeAnnotationReference(e)))
                .orElse(Stream.empty());
    }

    public static Stream<ElmReference> getReferencesStream(ElmExposingClause element) {
        return getReferencesStream(element, ElmImportedValueReference::new, ElmImportedTypeReference::new);
    }

    public static Stream<ElmReference> getReferencesStream(ElmModuleDeclaration element) {
        return getReferencesStream(element, ElmExposedValueReference::new, ElmExposedTypeReference::new);
    }

    private static Stream<ElmReference> getReferencesStream(ElmExposingBase element,
                                                            Function<ElmLowerCaseId, ElmReference> valueReferenceConstructor,
                                                            Function<ElmUpperCaseId, ElmReference> typeReferenceConstructor) {
        return Stream.concat(
                getTypeReferences(element, typeReferenceConstructor),
                element.getLowerCaseIdList().stream()
                        .map(id -> valueReferenceConstructor.apply(id).referenceInAncestor(element))
        );
    }

    private static Stream<ElmReference> getTypeReferences(ElmExposingBase element,
                                                          Function<ElmUpperCaseId, ElmReference> referenceConstructor) {
        return element.getExposedUnionList().stream()
                .flatMap(e -> Stream.concat(
                        Stream.of(
                                referenceConstructor.apply(e.getUpperCaseId())
                                        .referenceInAncestor(element)
                        ),
                        getExposedUnionMembersReferences(e.getExposedUnionConstructors(), referenceConstructor)
                                .map(r -> r.referenceInAncestor(element))
                ));
    }

    private static Stream<ElmReference> getExposedUnionMembersReferences(@Nullable ElmExposedUnionConstructors element,
                                                                         Function<ElmUpperCaseId, ElmReference> referenceConstructor) {
        return Optional.ofNullable(element)
                .map(e -> e.getUpperCaseIdList().stream().map(referenceConstructor))
                .orElse(Stream.empty());
    }
}