/*
 * Copyright 2010-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.resolve.annotation;

import com.intellij.openapi.util.text.StringUtil;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.analyzer.AnalysisResult;
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment;
import org.jetbrains.kotlin.descriptors.*;
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor;
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget;
import org.jetbrains.kotlin.descriptors.annotations.Annotations;
import org.jetbrains.kotlin.descriptors.impl.AnonymousFunctionDescriptor;
import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl;
import org.jetbrains.kotlin.incremental.components.NoLookupLocation;
import org.jetbrains.kotlin.name.FqName;
import org.jetbrains.kotlin.name.Name;
import org.jetbrains.kotlin.psi.KtAnnotationUseSiteTarget;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.renderer.AnnotationArgumentsRenderingPolicy;
import org.jetbrains.kotlin.renderer.ClassifierNamePolicy;
import org.jetbrains.kotlin.renderer.DescriptorRenderer;
import org.jetbrains.kotlin.renderer.DescriptorRendererModifier;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.resolve.lazy.descriptors.PackageDescriptorUtilKt;
import org.jetbrains.kotlin.resolve.scopes.MemberScope;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.KotlinTestWithEnvironment;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static org.jetbrains.kotlin.resolve.DescriptorUtils.isNonCompanionObject;

public abstract class AbstractAnnotationDescriptorResolveTest extends KotlinTestWithEnvironment {
    private static final DescriptorRenderer WITH_ANNOTATION_ARGUMENT_TYPES = DescriptorRenderer.Companion.withOptions(
            options -> {
                options.setVerbose(true);
                options.setAnnotationArgumentsRenderingPolicy(AnnotationArgumentsRenderingPolicy.UNLESS_EMPTY);
                options.setClassifierNamePolicy(ClassifierNamePolicy.SHORT.INSTANCE);
                options.setModifiers(DescriptorRendererModifier.ALL);
                return Unit.INSTANCE;
            }
    );

    private static final String PATH = "compiler/testData/resolveAnnotations/testFile.kt";

    private static final FqName PACKAGE = new FqName("test");

    protected AnalysisResult analysisResult;

    protected BindingContext getContext() {
        return analysisResult.getBindingContext();
    }

    @Override
    protected KotlinCoreEnvironment createEnvironment() {
        return KotlinTestUtils.createEnvironmentWithMockJdkAndIdeaAnnotations(getTestRootDisposable());
    }

    protected void doTest(@NotNull String content, @NotNull String expectedAnnotation) {
        checkAnnotationOnAllExceptLocalDeclarations(content, expectedAnnotation);
        checkAnnotationOnLocalDeclarations(expectedAnnotation);
    }

    protected void checkAnnotationOnAllExceptLocalDeclarations(String content, String expectedAnnotation) {
        KtFile testFile = getFile(content);
        PackageFragmentDescriptor testPackage = getPackage(testFile);

        checkAnnotationsOnFile(expectedAnnotation, testFile);

        ClassDescriptor myClass = getClassDescriptor(testPackage, "MyClass");
        checkDescriptor(expectedAnnotation, myClass);
        ClassDescriptor companionObjectDescriptor = myClass.getCompanionObjectDescriptor();
        assert companionObjectDescriptor != null : "Cannot find companion object for class " + myClass.getName();
        checkDescriptor(expectedAnnotation, companionObjectDescriptor);
        checkDescriptor(expectedAnnotation, getInnerClassDescriptor(myClass, "InnerClass"));

        FunctionDescriptor foo = getFunctionDescriptor(myClass, "foo");
        checkAnnotationsOnFunction(expectedAnnotation, foo);

        SimpleFunctionDescriptor anonymousFun = getAnonymousFunDescriptor();
        if (anonymousFun instanceof AnonymousFunctionDescriptor) {
            for (ValueParameterDescriptor descriptor : anonymousFun.getValueParameters()) {
                List<VariableDescriptor> destructuringVariables = ValueParameterDescriptorImpl.getDestructuringVariablesOrNull(descriptor);
                if (destructuringVariables == null) continue;
                for (VariableDescriptor entry : destructuringVariables) {
                    checkDescriptor(expectedAnnotation, entry);
                }
            }
        }

        PropertyDescriptor prop = getPropertyDescriptor(myClass, "prop");
        checkAnnotationsOnProperty(expectedAnnotation, prop);

        FunctionDescriptor topFoo = getFunctionDescriptor(testPackage, "topFoo");
        checkAnnotationsOnFunction(expectedAnnotation, topFoo);

        PropertyDescriptor topProp = getPropertyDescriptor(testPackage, "topProp", true);
        checkAnnotationsOnProperty(expectedAnnotation, topProp);

        checkDescriptor(expectedAnnotation, getClassDescriptor(testPackage, "MyObject"));

        checkDescriptor(expectedAnnotation, getConstructorParameterDescriptor(myClass, "consProp"));
        checkDescriptor(expectedAnnotation, getConstructorParameterDescriptor(myClass, "param"));
    }

    private void checkAnnotationsOnFile(String expectedAnnotation, KtFile file) {
        String actualAnnotation = StringUtil.join(file.getAnnotationEntries(), annotationEntry -> {
            AnnotationDescriptor annotationDescriptor = getContext().get(BindingContext.ANNOTATION, annotationEntry);
            assertNotNull(annotationDescriptor);

            KtAnnotationUseSiteTarget target = annotationEntry.getUseSiteTarget();

            if (target != null) {
                return WITH_ANNOTATION_ARGUMENT_TYPES.renderAnnotation(
                        annotationDescriptor, target.getAnnotationUseSiteTarget());
            }

            return WITH_ANNOTATION_ARGUMENT_TYPES.renderAnnotation(annotationDescriptor, null);
        }, " ");

        String expectedAnnotationWithTarget = "@" + AnnotationUseSiteTarget.FILE.getRenderName() + ":" + expectedAnnotation.substring(1);

        assertEquals(expectedAnnotationWithTarget, actualAnnotation);
    }

    private void checkAnnotationOnLocalDeclarations(String expectedAnnotation) {
        checkDescriptor(expectedAnnotation, getLocalClassDescriptor("LocalClass"));
        checkDescriptor(expectedAnnotation, getLocalObjectDescriptor("LocalObject"));
        checkDescriptor(expectedAnnotation, getLocalFunDescriptor("localFun"));
        checkDescriptor(expectedAnnotation, getLocalVarDescriptor(getContext(), "localVar"));
    }

    private static void checkAnnotationsOnProperty(String expectedAnnotation, PropertyDescriptor prop) {
        checkDescriptor(expectedAnnotation, prop);
        checkDescriptor(expectedAnnotation, prop.getGetter());
        PropertySetterDescriptor propSetter = prop.getSetter();
        assertNotNull(propSetter);
        checkAnnotationsOnFunction(expectedAnnotation, propSetter);
    }

    private static void checkAnnotationsOnFunction(String expectedAnnotation, FunctionDescriptor foo) {
        checkDescriptor(expectedAnnotation, foo);
        checkDescriptor(expectedAnnotation, getFunctionParameterDescriptor(foo, "param"));
    }

    @NotNull
    protected static FunctionDescriptor getFunctionDescriptor(@NotNull PackageFragmentDescriptor packageView, @NotNull String name) {
        Name functionName = Name.identifier(name);
        MemberScope memberScope = packageView.getMemberScope();
        Collection<? extends SimpleFunctionDescriptor> functions =
                memberScope.getContributedFunctions(functionName, NoLookupLocation.FROM_TEST);
        assert functions.size() == 1 : "Failed to find function " + functionName + " in class" + "." + packageView.getName();
        return functions.iterator().next();
    }

    @NotNull
    private static FunctionDescriptor getFunctionDescriptor(@NotNull ClassDescriptor classDescriptor, @NotNull String name) {
        Name functionName = Name.identifier(name);
        MemberScope memberScope = classDescriptor.getMemberScope(Collections.emptyList());
        Collection<? extends SimpleFunctionDescriptor> functions =
                memberScope.getContributedFunctions(functionName, NoLookupLocation.FROM_TEST);
        assert functions.size() == 1 : "Failed to find function " + functionName + " in class" + "." + classDescriptor.getName();
        return functions.iterator().next();
    }

    @Nullable
    protected static PropertyDescriptor getPropertyDescriptor(@NotNull PackageFragmentDescriptor packageView, @NotNull String name, boolean failOnMissing) {
        Name propertyName = Name.identifier(name);
        MemberScope memberScope = packageView.getMemberScope();
        Collection<? extends PropertyDescriptor> properties = memberScope.getContributedVariables(propertyName, NoLookupLocation.FROM_TEST);
        if (properties.isEmpty()) {
            for (DeclarationDescriptor descriptor : DescriptorUtils.getAllDescriptors(memberScope)) {
                if (descriptor instanceof ClassDescriptor) {
                    Collection<? extends PropertyDescriptor> classProperties =
                            ((ClassDescriptor) descriptor).getMemberScope(Collections.emptyList())
                                    .getContributedVariables(propertyName, NoLookupLocation.FROM_TEST);
                    if (!classProperties.isEmpty()) {
                        properties = classProperties;
                        break;
                    }
                }
            }
        }
        if (failOnMissing) {
            assert properties.size() == 1 : "Failed to find property " + propertyName + " in class " + packageView.getName();
        }
        else if (properties.size() != 1) {
            return null;
        }
        return properties.iterator().next();
    }

    @NotNull
    private static PropertyDescriptor getPropertyDescriptor(@NotNull ClassDescriptor classDescriptor, @NotNull String name) {
        Name propertyName = Name.identifier(name);
        MemberScope memberScope = classDescriptor.getMemberScope(Collections.emptyList());
        Collection<? extends PropertyDescriptor> properties = memberScope.getContributedVariables(propertyName, NoLookupLocation.FROM_TEST);
        assert properties.size() == 1 : "Failed to find property " + propertyName + " in class " + classDescriptor.getName();
        return properties.iterator().next();
    }

    @NotNull
    protected static ClassDescriptor getClassDescriptor(@NotNull PackageFragmentDescriptor packageView, @NotNull String name) {
        Name className = Name.identifier(name);
        ClassifierDescriptor aClass = packageView.getMemberScope().getContributedClassifier(className, NoLookupLocation.FROM_TEST);
        assertNotNull("Failed to find class: " + packageView.getName() + "." + className, aClass);
        assert aClass instanceof ClassDescriptor : "Not a class: " + aClass;
        return (ClassDescriptor) aClass;
    }

    @NotNull
    private static ClassDescriptor getInnerClassDescriptor(@NotNull ClassDescriptor classDescriptor, @NotNull String name) {
        Name propertyName = Name.identifier(name);
        MemberScope memberScope = classDescriptor.getMemberScope(Collections.emptyList());
        ClassifierDescriptor innerClass = memberScope.getContributedClassifier(propertyName, NoLookupLocation.FROM_TEST);
        assert innerClass instanceof ClassDescriptor : "Failed to find inner class " +
                                                       propertyName +
                                                       " in class " +
                                                       classDescriptor.getName();
        return (ClassDescriptor) innerClass;
    }

    @NotNull
    private ClassDescriptor getLocalClassDescriptor(@NotNull String name) {
        for (ClassDescriptor descriptor : getContext().getSliceContents(BindingContext.CLASS).values()) {
            if (descriptor.getName().asString().equals(name)) {
                return descriptor;
            }
        }

        fail("Failed to find local class " + name);
        return null;
    }

    @NotNull
    private ClassDescriptor getLocalObjectDescriptor(@NotNull String name) {
        ClassDescriptor localClassDescriptor = getLocalClassDescriptor(name);
        if (isNonCompanionObject(localClassDescriptor)) {
            return localClassDescriptor;
        }

        fail("Failed to find local object " + name);
        return null;
    }

    @NotNull
    private SimpleFunctionDescriptor getLocalFunDescriptor(@NotNull String name) {
        for (SimpleFunctionDescriptor descriptor : getContext().getSliceContents(BindingContext.FUNCTION).values()) {
            if (descriptor.getName().asString().equals(name)) {
                return descriptor;
            }
        }

        fail("Failed to find local fun " + name);
        return null;
    }

    @NotNull
    protected static VariableDescriptor getLocalVarDescriptor(@NotNull BindingContext context, @NotNull String name) {
        for (VariableDescriptor descriptor : context.getSliceContents(BindingContext.VARIABLE).values()) {
            if (descriptor.getName().asString().equals(name)) {
                return descriptor;
            }
        }

        fail("Failed to find local variable " + name);
        return null;
    }

    @NotNull
    private SimpleFunctionDescriptor getAnonymousFunDescriptor() {
        for (SimpleFunctionDescriptor descriptor : getContext().getSliceContents(BindingContext.FUNCTION).values()) {
            if (descriptor instanceof AnonymousFunctionDescriptor) {
                return descriptor;
            }
        }

        fail("Failed to find anonymous fun");
        return null;
    }

    @NotNull
    private static ValueParameterDescriptor getConstructorParameterDescriptor(
            @NotNull ClassDescriptor classDescriptor,
            @NotNull String name
    ) {
        ConstructorDescriptor constructorDescriptor = getConstructorDescriptor(classDescriptor);
        ValueParameterDescriptor parameter = findValueParameter(constructorDescriptor.getValueParameters(), name);
        assertNotNull("Cannot find constructor parameter with name " + name, parameter);
        return parameter;
    }

    @NotNull
    private static ConstructorDescriptor getConstructorDescriptor(@NotNull ClassDescriptor classDescriptor) {
        Collection<ClassConstructorDescriptor> constructors = classDescriptor.getConstructors();
        assert constructors.size() == 1;
        return constructors.iterator().next();
    }

    private static ValueParameterDescriptor findValueParameter(List<ValueParameterDescriptor> parameters, String name) {
        for (ValueParameterDescriptor parameter : parameters) {
            if (parameter.getName().asString().equals(name)) {
                return parameter;
            }
        }
        return null;
    }

    @NotNull
    private static ValueParameterDescriptor getFunctionParameterDescriptor(
            @NotNull FunctionDescriptor functionDescriptor,
            @NotNull String name
    ) {
        ValueParameterDescriptor parameter = findValueParameter(functionDescriptor.getValueParameters(), name);
        assertNotNull("Cannot find function parameter with name " + name, parameter);
        return parameter;
    }

    @NotNull
    protected KtFile getFile(@NotNull String content) {
        KtFile ktFile = KotlinTestUtils.createFile("dummy.kt", content, getProject());
        analysisResult = KotlinTestUtils.analyzeFile(ktFile, getEnvironment());

        return ktFile;
    }

    @NotNull
    protected PackageFragmentDescriptor getPackage(@NotNull KtFile ktFile) {
        PackageFragmentDescriptor packageFragment =
                PackageDescriptorUtilKt.findPackageFragmentForFile(analysisResult.getModuleDescriptor(), ktFile);
        assertNotNull("Failed to find package: " + PACKAGE, packageFragment);
        return packageFragment;
    }

    @NotNull
    protected PackageFragmentDescriptor getPackage(@NotNull String content) {
        return getPackage(getFile(content));
    }

    protected static String getContent(@NotNull String annotationText) throws IOException {
        File file = new File(PATH);
        return KotlinTestUtils.doLoadFile(file).replaceAll("ANNOTATION", annotationText);
    }

    public static String renderAnnotations(Annotations annotations) {
        return StringUtil.join(annotations, annotation -> WITH_ANNOTATION_ARGUMENT_TYPES.renderAnnotation(annotation, null), " ");
    }

    private static void checkDescriptor(String expectedAnnotation, DeclarationDescriptor member) {
        String actual = renderAnnotations(member.getAnnotations());
        assertEquals("Failed to resolve annotation descriptor for " + member.toString(), expectedAnnotation, actual);
    }

    @Override
    public void tearDown() throws Exception {
        analysisResult = null;
        super.tearDown();
    }
}
