package ctbrec.ui;

import static java.nio.charset.StandardCharsets.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
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.StreamRedirector;

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

    private Consumer<String> messageListener;
    private InputStream in;
    private OutputStream out;
    private Socket socket; // NOSONAR
    private Thread reader;
    private volatile boolean stopped = true;
    private volatile boolean browserReady = false;
    private Object browserReadyLock = 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"));

            File configDir = new File(Config.getInstance().getConfigDir(), "ctbrec-minimal-browser");
            String[] cmdline = OS.getBrowserCommand(configDir.getCanonicalPath());
            Process p = new ProcessBuilder(cmdline).start();
            if (LOG.isTraceEnabled()) {
                new Thread(new StreamRedirector(p.getInputStream(), System.out)).start(); // NOSONAR
                new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start(); // NOSONAR
            } else {
                new Thread(new StreamRedirector(p.getInputStream(), OutputStream.nullOutputStream())).start();
                new Thread(new StreamRedirector(p.getErrorStream(), OutputStream.nullOutputStream())).start();
            }
            LOG.debug("Browser started: {}", Arrays.toString(cmdline));

            connectToRemoteControlSocket();
            while (!browserReady) {
                synchronized (browserReadyLock) {
                    browserReadyLock.wait(100);
                }
            }
            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();
            reader = null;
            in = null;
            out = null;
            this.messageListener = 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();
                LOG.debug("Connected to control socket");
                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) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public void executeJavaScript(String javaScript) throws IOException {
        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");
    }

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

    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;
        }
    }
}
