/*
 *   The MIT License (MIT)
 *   <p>
 *   Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch)
 *   <p>
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *   <p>
 *   The above copyright notice and this permission notice shall be included in all
 *   copies or substantial portions of the Software.
 *   <p>
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *   SOFTWARE
 *
 */

package com.vladsch.javafx.webview.debugger;

import com.sun.javafx.scene.web.Debugger;
import javafx.application.Platform;
import netscape.javascript.JSObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.InetSocketAddress;
import java.nio.channels.NotYetConnectedException;
import java.util.HashMap;
import java.util.function.Consumer;

public class DevToolsDebuggerServer implements JfxDebuggerConnector {
    final static HashMap<Integer, JfxWebSocketServer> ourServerMap = new HashMap<>();

    final Debugger myDebugger;
    JfxWebSocketServer myServer;
    final LogHandler LOG = LogHandler.getInstance();

    public DevToolsDebuggerServer(@NotNull Debugger debugger, int debuggerPort, final int instanceId, @Nullable Consumer<Throwable> onFailure, @Nullable Runnable onStart) {
        myDebugger = debugger;
        boolean freshStart = false;

        JfxWebSocketServer jfxWebSocketServer;

        synchronized (ourServerMap) {
            jfxWebSocketServer = ourServerMap.get(debuggerPort);

            if (jfxWebSocketServer == null) {
                jfxWebSocketServer = new JfxWebSocketServer(new InetSocketAddress("localhost", debuggerPort), (ex) -> {
                    synchronized (ourServerMap) {
                        if (ourServerMap.get(debuggerPort).removeServer(this)) {
                            // unused
                            ourServerMap.remove(debuggerPort);
                        }
                    }

                    if (onFailure != null) {
                        onFailure.accept(ex);
                    }
                }, (server) -> {
                    myServer = server;
                    initDebugger(instanceId, onStart);
                });

                ourServerMap.put(debuggerPort, jfxWebSocketServer);
                freshStart = true;
            }
        }

        jfxWebSocketServer.addServer(this, instanceId);

        if (freshStart) {
            jfxWebSocketServer.start();
        } else {
            myServer = jfxWebSocketServer;
            initDebugger(instanceId, onStart);
        }
    }

    private void initDebugger(int instanceId, @Nullable Runnable onStart) {
        Platform.runLater(() -> {
            myDebugger.setEnabled(true);
            myDebugger.sendMessage("{\"id\" : -1, \"method\" : \"Network.enable\"}");

            this.myDebugger.setMessageCallback(data -> {
                try {
                    myServer.send(this, data);
                } catch (NotYetConnectedException e) {
                    e.printStackTrace();
                }
                return null;
            });

            if (LOG.isDebugEnabled()) {
                String remoteUrl = getDebugUrl();
                System.out.println("Debug session created. Debug URL: " + remoteUrl);
                LOG.debug("Debug session created. Debug URL: " + remoteUrl);
            }

            if (onStart != null) {
                onStart.run();
            }
        });
    }

    public boolean isDebuggerConnected() {
        return myServer.isDebuggerConnected(this);
    }

    @NotNull
    public String getDebugUrl() {
        // Chrome won't launch first session if we have a query string
        return myServer != null ? myServer.getDebugUrl(this) : "";
    }

    public void stopDebugServer(@NotNull Consumer<Boolean> onStopped) {
        if (myServer != null) {
            Platform.runLater(() -> {
                Runnable action = () -> {
                    if (LOG.isDebugEnabled()) {
                        String remoteUrl = getDebugUrl();
                        System.out.println("Debug session stopped for URL: " + remoteUrl);
                        LOG.debug("Debug session stopped for URL: " + remoteUrl);
                    }

                    boolean handled = false;
                    boolean unusedServer;

                    synchronized (ourServerMap) {
                        unusedServer = myServer.removeServer(this);
                        if (unusedServer) {
                            ourServerMap.remove(myServer.getAddress().getPort());
                        }
                    }

                    if (unusedServer) {
                        // shutdown
                        try {
                            myServer.stop(1000);
                            myServer = null;
                            if (LOG.isDebugEnabled()) {
                                System.out.println("WebView debug server shutdown.");
                                LOG.debug("WebView debug server shutdown.");
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            handled = true;
                            // instance removed and server stopped
                            onStopped.accept(true);
                        }
                    }

                    if (!handled) {
                        // instance removed, server running
                        onStopped.accept(false);
                    }
                };

                if (myDebugger instanceof JfxDebuggerProxy) {
                    // release from break point or it has a tendency to core dump
                    ((JfxDebuggerProxy) myDebugger).removeAllBreakpoints(() -> {
                        ((JfxDebuggerProxy) myDebugger).releaseDebugger(true, () -> {
                            // doing this if was stopped at a break point will core dump the whole app
                            myDebugger.setEnabled(false);
                            myDebugger.setMessageCallback(null);
                            action.run();
                        });
                    });
                } else {
                    myDebugger.setEnabled(false);
                    myDebugger.setMessageCallback(null);
                    action.run();
                }
            });
        } else {
            // have not idea since this instance was already disconnected
            onStopped.accept(null);
        }
    }

    @Override
    public void onOpen() {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).onOpen();
        }
    }

    @Override
    public void pageReloading() {
        if (myDebugger instanceof JfxDebuggerProxy && isDebuggerConnected()) {
            ((JfxDebuggerProxy) myDebugger).pageReloading();
        }
    }

    @Override
    public void reloadPage() {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).reloadPage();
        }
    }

    @Override
    public void onClosed(final int code, final String reason, final boolean remote) {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).onClosed(code, reason, remote);
        }
    }

    @Override
    public void log(final String type, final long timestamp, final JSObject args) {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).log(type, timestamp, args);
        }
    }

    @Override
    public void setDebugOnLoad(final DebugOnLoad debugOnLoad) {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).setDebugOnLoad(debugOnLoad);
        }
    }

    @Override
    public DebugOnLoad getDebugOnLoad() {
        if (myDebugger instanceof JfxDebuggerProxy) {
            return ((JfxDebuggerProxy) myDebugger).getDebugOnLoad();
        }
        return DebugOnLoad.NONE;
    }

    @Override
    public void debugBreak() {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).debugBreak();
        }
    }

    @Override
    public boolean isDebuggerPaused() {
        if (myDebugger instanceof JfxDebuggerProxy) {
            return ((JfxDebuggerProxy) myDebugger).isDebuggerPaused();
        }
        return false;
    }

    @Override
    public void releaseDebugger(final boolean shuttingDown, @Nullable Runnable runnable) {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).releaseDebugger(shuttingDown, runnable);
        }
    }

    @Override
    public void removeAllBreakpoints(@Nullable Runnable runnable) {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).removeAllBreakpoints(runnable);
        }
    }

    @Override
    public void pageLoadComplete() {
        if (myDebugger instanceof JfxDebuggerProxy) {
            ((JfxDebuggerProxy) myDebugger).pageLoadComplete();
        }
    }

    public void sendMessageToBrowser(final String data) {
        Platform.runLater(() -> myDebugger.sendMessage(data));
    }
}