package net.ranides.demo.debugger.util;

import com.sun.jdi.*;
import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.LocatableEvent;
import com.sun.jdi.request.BreakpointRequest;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import net.ranides.demo.debugger.DiClass;
import net.ranides.demo.debugger.DiConnection;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SimpleDebugger implements AutoCloseable {

    private DiConnection di;

    public SimpleDebugger connect(String port) {
        di = DiConnection.connect(port);
        return this;
    }

    public SimpleDebugger connect(String host, String port) {
        di = DiConnection.connect(host, port);
        return this;
    }

    public RemoteLocation locate(String classWithLine) {
        String[] parts = classWithLine.split("@");
        return new RemoteLocation(parts[0], Integer.parseInt(parts[1]));
    }

    public RemoteLocation locate(String className, int line) {
        return new RemoteLocation(className, line);
    }

    public BreakpointRequest breakpoint(RemoteLocation remote) {
        DiClass type = di.types(remote.className).first()
            .orElseThrow(() -> new IllegalArgumentException("Unknown class: " + remote.className));

        BreakpointRequest breakpoint = type.locate(remote.line).breakpoint();
        breakpoint.enable();
        return breakpoint;
    }

    public <T> T loop(LoopHandler<T> handler) throws Exception {
        EventSet eventSet;
        while ((eventSet = di.vm().eventQueue().remove()) != null) {
            for (Event event : eventSet) {
                try {
                    LoopContext context = new LoopContext(event);
                    T result = handler.handle(context);
                    if(context.stop) {
                        return result;
                    }
                } catch (RuntimeException error) {
                    throw error;
                } catch (Exception error) {
                    throw new RuntimeException(error);
                } finally {
                    di.vm().resume();
                }
            }
        }
        throw new IllegalStateException("Abnormal termination");
    }

    private Object variable0(LocatableEvent event, String name) throws Exception {
        StackFrame frame = event.thread().frame(0);
        LocalVariable variable = frame.visibleVariableByName(name);
        if(variable == null) {
            throw new IllegalArgumentException("Unknown variable: " + name);
        }
        Value mirror = frame.getValue(variable);
        return di.mirror().asPrimitive(mirror);
    }

    private Object call0(LocatableEvent event, String methodName, Object[] arguments) throws Exception {
        List<Value> input = Arrays.stream(arguments).map(di.mirror()::asMirror).collect(Collectors.toList());
        Value out = call0(event, methodName, input);
        return di.mirror().asPrimitive(out);
    }

    private Value call0(LocatableEvent event, String methodName, List<? extends Value> arguments) throws Exception {
        ThreadReference thread = event.thread();
        ObjectReference that = thread.frame(0).thisObject();
        List<Method> methods = that.referenceType().methodsByName(methodName);
        if(methods.isEmpty()) {
            throw new IllegalArgumentException("Unknown method: " + methodName);
        }
        return that.invokeMethod(thread, methods.get(0), arguments, 0);
    }

    @Override
    public void close() {
        di.vm().dispose();
    }

    public class RemoteLocation {
        private final String className;
        private final int line;

        public RemoteLocation(String className, int line) {
            this.className = className;
            this.line = line;
        }

        public boolean matches(LocatableEvent event) {
            var location = event.location();
            return location.declaringType().name().equals(className) && location.lineNumber() == line;
        }

        public Object call(String name, Object[] arguments) throws Exception {
            return eval(context -> context.call(name, arguments));
        }

        public Object variable(String name) throws Exception {
            return eval(context -> context.variable(name));
        }

        public <T> T eval(EvalHandler<T> handler) throws Exception {
            T none = null;
            BreakpointRequest breakpoint = SimpleDebugger.this.breakpoint(this);

            return loop(context -> {
                if(context.event instanceof BreakpointEvent) {
                    BreakpointEvent bp = (BreakpointEvent) context.event;
                    if(bp.request() == breakpoint) {
                        breakpoint.setEnabled(false);
                        context.stop();
                        return handler.handle(new EvalContext(bp));
                    }
                }
                // that was not "our" breakpoint, return anything to the loop & continue
                return none;
            });
        }
    }

    public class LoopContext {
        private final Event event;
        private boolean stop = false;

        public LoopContext(Event event) {
            this.event = event;
        }

        public Event event() {
            return event;
        }

        public void stop() {
            this.stop = true;
        }

        public boolean isLocatable() {
            return event instanceof LocatableEvent;
        }

        public boolean matches(RemoteLocation location) {
            return isLocatable() && location.matches((LocatableEvent)event);
        }

        public Object variable(String name) throws Exception {
            if(!isLocatable()) {
                throw new IllegalStateException("Can't access variable from unlocatable event: " + event);
            }
            return variable0((LocatableEvent)event, name);
        }

        public Object call(String methodName, Object[] arguments) throws Exception {
            if(!isLocatable()) {
                throw new IllegalStateException("Can't invoke method from unlocatable event: " + event);
            }
            return call0((LocatableEvent)event, methodName, arguments);
        }
    }

    @RequiredArgsConstructor(access = AccessLevel.PACKAGE)
    public class EvalContext {

        private final LocatableEvent event;

        public LocatableEvent event() {
            return event;
        }

        public boolean matches(RemoteLocation location) {
            return location.matches(event);
        }

        public Object variable(String name) throws Exception {
            return variable0(event, name);
        }

        public Object call(String methodName, Object[] arguments) throws Exception {
            return call0(event, methodName, arguments);
        }
    }

    public interface LoopHandler<T> {
        T handle(LoopContext context) throws Exception;
    }

    public interface EvalHandler<T> {
        T handle(EvalContext context) throws Exception;
    }

}
