/*
 * 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.idea.debugger.stepping;

import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.engine.SuspendContextImpl;
import com.intellij.debugger.engine.evaluation.EvaluateException;
import com.intellij.debugger.impl.DebuggerUtilsEx;
import com.intellij.debugger.jdi.StackFrameProxyImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.ui.classFilter.DebuggerClassFilterProvider;
import com.sun.jdi.Location;
import com.sun.jdi.ObjectCollectedException;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.StepRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.debugger.NoStrataPositionManagerHelperKt;
import org.jetbrains.kotlin.psi.KtFunctionLiteral;
import org.jetbrains.kotlin.psi.KtNamedFunction;

import java.util.ArrayList;
import java.util.List;


public class DebuggerSteppingHelper {
    private static Logger LOG = Logger.getInstance(DebuggerSteppingHelper.class);

    public static DebugProcessImpl.ResumeCommand createStepOverCommand(
            final SuspendContextImpl suspendContext,
            final boolean ignoreBreakpoints,
            final KotlinSteppingCommandProvider.KotlinSourcePosition kotlinSourcePosition
    ) {
        final DebugProcessImpl debugProcess = suspendContext.getDebugProcess();

        return debugProcess.new ResumeCommand(suspendContext) {
            @Override
            public void contextAction() {
                boolean isDexDebug = NoStrataPositionManagerHelperKt.isDexDebug(suspendContext.getDebugProcess());

                try {
                    StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
                    if (frameProxy != null) {
                        Action action = KotlinSteppingCommandProviderKt.getStepOverAction(
                                frameProxy.location(),
                                kotlinSourcePosition,
                                frameProxy,
                                isDexDebug
                        );

                        createStepRequest(
                                suspendContext, getContextThread(),
                                debugProcess.getVirtualMachineProxy().eventRequestManager(),
                                StepRequest.STEP_LINE, StepRequest.STEP_OUT);

                        action.apply(debugProcess, suspendContext, ignoreBreakpoints);
                        return;
                    }

                    debugProcess.createStepOutCommand(suspendContext).contextAction();
                }
                catch (EvaluateException e) {
                    LOG.error(e);
                }
            }
        };
    }

    public static DebugProcessImpl.ResumeCommand createStepOutCommand(
            final SuspendContextImpl suspendContext,
            final boolean ignoreBreakpoints,
            final List<KtNamedFunction> inlineFunctions,
            final KtFunctionLiteral inlineArgument
    ) {
        final DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
        return debugProcess.new ResumeCommand(suspendContext) {
            @Override
            public void contextAction() {
                try {
                    StackFrameProxyImpl frameProxy = suspendContext.getFrameProxy();
                    if (frameProxy != null) {
                        Action action = KotlinSteppingCommandProviderKt.getStepOutAction(
                                frameProxy.location(),
                                suspendContext,
                                inlineFunctions,
                                inlineArgument
                        );

                        createStepRequest(
                                suspendContext, getContextThread(),
                                debugProcess.getVirtualMachineProxy().eventRequestManager(),
                                StepRequest.STEP_LINE, StepRequest.STEP_OUT);

                        action.apply(debugProcess, suspendContext, ignoreBreakpoints);
                        return;
                    }

                    debugProcess.createStepOverCommand(suspendContext, ignoreBreakpoints).contextAction();
                }
                catch (EvaluateException ignored) {
                }
            }
        };
    }

    // copied from DebugProcessImpl.doStep
    private static void createStepRequest(
            @NotNull SuspendContextImpl suspendContext,
            @Nullable ThreadReferenceProxyImpl stepThread,
            @NotNull EventRequestManager requestManager,
            int size, int depth
    ) {
        if (stepThread == null) {
            return;
        }
        try {
            ThreadReference stepThreadReference = stepThread.getThreadReference();

            requestManager.deleteEventRequests(requestManager.stepRequests());

            StepRequest stepRequest = requestManager.createStepRequest(stepThreadReference, size, depth);

            List<ClassFilter> activeFilters = getActiveFilters();

            if (!activeFilters.isEmpty()) {
                String currentClassName = getCurrentClassName(stepThread);
                if (currentClassName == null || !DebuggerUtilsEx.isFiltered(currentClassName, activeFilters)) {
                    // add class filters
                    for (ClassFilter filter : activeFilters) {
                        stepRequest.addClassExclusionFilter(filter.getPattern());
                    }
                }
            }

            // suspend policy to match the suspend policy of the context:
            // if all threads were suspended, then during stepping all the threads must be suspended
            // if only event thread were suspended, then only this particular thread must be suspended during stepping
            stepRequest.setSuspendPolicy(suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD
                                         ? EventRequest.SUSPEND_EVENT_THREAD
                                         : EventRequest.SUSPEND_ALL);

            stepRequest.enable();
        }
        catch (ObjectCollectedException ignored) {

        }
    }

    // copied from DebugProcessImpl.getActiveFilters
    @NotNull
    private static List<ClassFilter> getActiveFilters() {
        List<ClassFilter> activeFilters = new ArrayList<ClassFilter>();
        DebuggerSettings settings = DebuggerSettings.getInstance();
        if (settings.TRACING_FILTERS_ENABLED) {
            for (ClassFilter filter : settings.getSteppingFilters()) {
                if (filter.isEnabled()) {
                    activeFilters.add(filter);
                }
            }
        }
        for (DebuggerClassFilterProvider provider : Extensions.getExtensions(DebuggerClassFilterProvider.EP_NAME)) {
            for (ClassFilter filter : provider.getFilters()) {
                if (filter.isEnabled()) {
                    activeFilters.add(filter);
                }
            }
        }
        return activeFilters;
    }

    // copied from DebugProcessImpl.getActiveFilters
    @Nullable
    private static String getCurrentClassName(ThreadReferenceProxyImpl thread) {
        try {
            if (thread != null && thread.frameCount() > 0) {
                StackFrameProxyImpl stackFrame = thread.frame(0);
                if (stackFrame != null) {
                    Location location = stackFrame.location();
                    ReferenceType referenceType = location == null ? null : location.declaringType();
                    if (referenceType != null) {
                        return referenceType.name();
                    }
                }
            }
        }
        catch (EvaluateException ignored) {
        }
        return null;
    }
}
