/*
 * Copyright 2010-2015 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.cfg.pseudocode;

import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.cfg.ControlFlowProcessor;
import org.jetbrains.kotlin.cfg.pseudocode.instructions.Instruction;
import org.jetbrains.kotlin.cfg.pseudocode.instructions.eval.AccessTarget;
import org.jetbrains.kotlin.cfg.pseudocode.instructions.eval.AccessValueInstruction;
import org.jetbrains.kotlin.cfg.pseudocode.instructions.special.VariableDeclarationInstruction;
import org.jetbrains.kotlin.config.LanguageVersionSettings;
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl;
import org.jetbrains.kotlin.descriptors.VariableDescriptor;
import org.jetbrains.kotlin.diagnostics.Diagnostic;
import org.jetbrains.kotlin.psi.KtDeclaration;
import org.jetbrains.kotlin.psi.KtElement;
import org.jetbrains.kotlin.psi.KtExpression;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.resolve.BindingTrace;
import org.jetbrains.kotlin.resolve.PropertyImportedFromObject;
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
import org.jetbrains.kotlin.resolve.calls.util.ResolvedCallUtilKt;
import org.jetbrains.kotlin.types.KotlinType;
import org.jetbrains.kotlin.util.slicedMap.ReadOnlySlice;
import org.jetbrains.kotlin.util.slicedMap.WritableSlice;

import java.util.Collection;

import static org.jetbrains.kotlin.resolve.BindingContextUtils.variableDescriptorForDeclaration;

public class PseudocodeUtil {
    @NotNull
    public static Pseudocode generatePseudocode(@NotNull KtDeclaration declaration, @NotNull BindingContext bindingContext) {
        return generatePseudocode(declaration, bindingContext, LanguageVersionSettingsImpl.DEFAULT);
    }

    @NotNull
    public static Pseudocode generatePseudocode(@NotNull KtDeclaration declaration, @NotNull BindingContext bindingContext, @NotNull LanguageVersionSettings languageVersionSettings) {
        BindingTrace mockTrace = new BindingTrace() {
            @NotNull
            @Override
            public BindingContext getBindingContext() {
                return bindingContext;
            }

            @Override
            public <K, V> void record(WritableSlice<K, V> slice, K key, V value) {
            }

            @Override
            public <K> void record(WritableSlice<K, Boolean> slice, K key) {
            }

            @Override
            public <K, V> V get(ReadOnlySlice<K, V> slice, K key) {
                return bindingContext.get(slice, key);
            }

            @NotNull
            @Override
            public <K, V> Collection<K> getKeys(WritableSlice<K, V> slice) {
                return bindingContext.getKeys(slice);
            }

            @Nullable
            @Override
            public KotlinType getType(@NotNull KtExpression expression) {
                return bindingContext.getType(expression);
            }

            @Override
            public void recordType(@NotNull KtExpression expression, @Nullable KotlinType type) {
            }

            @Override
            public void report(@NotNull Diagnostic diagnostic) {
            }

            @Override
            public boolean wantsDiagnostics() {
                return false;
            }

            @Nullable
            @Override
            public Project getProject() {
                return bindingContext.getProject();
            }
        };
        return new ControlFlowProcessor(mockTrace, languageVersionSettings).generatePseudocode(declaration);
    }

    @Nullable
    public static VariableDescriptor extractVariableDescriptorFromReference(
            @NotNull Instruction instruction,
            @NotNull BindingContext bindingContext
    ) {
        if (instruction instanceof AccessValueInstruction) {
            KtElement element = ((AccessValueInstruction) instruction).getElement();
            if (element instanceof KtDeclaration) return null;
            VariableDescriptor descriptor = extractVariableDescriptorIfAny(instruction, bindingContext);
            if (descriptor instanceof PropertyImportedFromObject) {
                return ((PropertyImportedFromObject) descriptor).getCallableFromObject();
            }
            return descriptor;
        }
        return null;
    }


    @Nullable
    public static VariableDescriptor extractVariableDescriptorIfAny(
            @NotNull Instruction instruction,
            @NotNull BindingContext bindingContext
    ) {
        if (instruction instanceof VariableDeclarationInstruction) {
            KtDeclaration declaration = ((VariableDeclarationInstruction) instruction).getVariableDeclarationElement();
            return variableDescriptorForDeclaration(bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, declaration));
        }
        else if (instruction instanceof AccessValueInstruction) {
            AccessTarget target = ((AccessValueInstruction) instruction).getTarget();
            if (target instanceof AccessTarget.Declaration) {
                return ((AccessTarget.Declaration) target).getDescriptor();
            }
            else if (target instanceof AccessTarget.Call) {
                return variableDescriptorForDeclaration(((AccessTarget.Call) target).getResolvedCall().getResultingDescriptor());
            }
        }
        return null;
    }

    // When deal with constructed object (not this) treat it like it's fully initialized
    // Otherwise (this or access with empty receiver) access instruction should be handled as usual
    public static boolean isThisOrNoDispatchReceiver(
            @NotNull AccessValueInstruction instruction,
            @NotNull BindingContext bindingContext
    ) {
        if (instruction.getReceiverValues().isEmpty()) {
            return true;
        }
        AccessTarget accessTarget = instruction.getTarget();
        if (accessTarget instanceof AccessTarget.BlackBox) return false;
        assert accessTarget instanceof AccessTarget.Call :
                "AccessTarget.Declaration has no receivers and it's not BlackBox, so it should be Call";

        ResolvedCall<?> accessResolvedCall = ((AccessTarget.Call) accessTarget).getResolvedCall();
        return ResolvedCallUtilKt.hasThisOrNoDispatchReceiver(accessResolvedCall, bindingContext);
    }
}
