package ctbrec.ui;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.Config;
import ctbrec.OS;
import ctbrec.Settings.ProxyType;
import ctbrec.io.StreamRedirectThread;

public class ExternalBrowser implements AutoCloseable {
    private static final transient Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class);
    private static final ExternalBrowser INSTANCE = new ExternalBrowser();
    private Lock lock = new ReentrantLock();

    private Process p;
    private Consumer<String> messageListener;
    private InputStream in;
    private OutputStream out;
    private Socket socket;
    private Thread reader;
    private volatile boolean stopped = true;
    private Object ready = new Object();

    public static ExternalBrowser getInstance() {
        return INSTANCE;
    }

    public void run(JSONObject jsonConfig, Consumer<String> messageListener) throws InterruptedException, IOException {
        LOG.debug("Running browser with config {}", jsonConfig);
        lock.lock();
        try {
            stopped = false;
            this.messageListener = messageListener;

            addProxyConfig(jsonConfig.getJSONObject("config"));

            p = new ProcessBuilder(OS.getBrowserCommand()).start();
            new StreamRedirectThread(p.getInputStream(), System.err);
            new StreamRedirectThread(p.getErrorStream(), System.err);
            LOG.debug("Browser started");

            connectToRemoteControlSocket();
            synchronized (ready) {
                ready.wait();
            }
            if(LOG.isTraceEnabled()) {
                LOG.debug("Connected to remote control server. Sending config {}", jsonConfig);
            } else {
                LOG.debug("Connected to remote control server. Sending config");
            }
            out.write(jsonConfig.toString().getBytes("utf-8"));
            out.write('\n');
            out.flush();

            LOG.debug("Waiting for browser to terminate");
            p.waitFor();
            int exitValue = p.exitValue();
            p = null;
            LOG.debug("Browser Process terminated with {}", exitValue);
        } finally {
            lock.unlock();
        }
    }

    private void connectToRemoteControlSocket() throws IOException {
        for (int i = 0; i < 20; i++) {
            try {
                socket = new Socket("localhost", 3202);
                in = socket.getInputStream();
                out = socket.getOutputStream();
                reader = new Thread(this::readBrowserOutput);
                reader.start();
                return;
            } catch (IOException e) {
                if(i == 19) {
                    LOG.error("Connection to remote control socket failed", e);
                    throw e;
                }
            }

            // wait a bit, so that the remote server socket can be opened by electron
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
            }
        }
    }

    public void executeJavaScript(String javaScript) throws IOException {
        //LOG.debug("Executing JS {}", javaScript);
        JSONObject script = new JSONObject();
        script.put("execute", javaScript);
        out.write(script.toString().getBytes("utf-8"));
        out.write('\n');
        out.flush();
        if(javaScript.equals("quit")) {
            stopped = true;
        }
    }

    @Override
    public void close() throws IOException {
        if(stopped) {
            return;
        }
        stopped = true;
        executeJavaScript("quit");
        if(socket != null) {
            socket.close();
            socket = null;
        }
        messageListener = null;
        reader = null;
        in = null;
        out = null;
        if(p != null) {
            p.destroy();
        }
    }

    private void readBrowserOutput() {
        LOG.debug("Browser output reader started");
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line;
            synchronized (ready) {
                ready.notify();
            }
            while( !Thread.interrupted() && (line = br.readLine()) != null ) {
                LOG.debug("Browser output: {}", line);
                if(!line.startsWith("{")) {
                } else {
                    if(messageListener != null) {
                        messageListener.accept(line);
                    }
                }
            }
        } catch (IOException e) {
            if(!stopped) {
                LOG.error("Couldn't read browser output", e);
            }
        }
    }

    private void addProxyConfig(JSONObject jsonConfig) {
        ProxyType proxyType = Config.getInstance().getSettings().proxyType;
        switch (proxyType) {
        case HTTP:
            JSONObject proxy = new JSONObject();
            proxy.put("address",
                    "http=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort
                    + ";https=" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
            if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
                String username = Config.getInstance().getSettings().proxyUser;
                String password = Config.getInstance().getSettings().proxyPassword;
                proxy.put("user", username);
                proxy.put("password", password);
            }
            jsonConfig.put("proxy", proxy);
            break;
        case SOCKS4:
            proxy = new JSONObject();
            proxy.put("address", "socks4://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
            jsonConfig.put("proxy", proxy);
            break;
        case SOCKS5:
            proxy = new JSONObject();
            proxy.put("address", "socks5://" + Config.getInstance().getSettings().proxyHost + ':' + Config.getInstance().getSettings().proxyPort);
            if(Config.getInstance().getSettings().proxyUser != null && !Config.getInstance().getSettings().proxyUser.isEmpty()) {
                String username = Config.getInstance().getSettings().proxyUser;
                String password = Config.getInstance().getSettings().proxyPassword;
                proxy.put("user", username);
                proxy.put("password", password);
            }
            jsonConfig.put("proxy", proxy);
            break;
        case DIRECT:
        default:
            // nothing to do here
            break;
        }
    }
}
