package tigerworkshop.webapphardwarebridge;

import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.java_websocket.WebSocket;
import org.java_websocket.exceptions.WebsocketNotConnectedException;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServerInterface;
import tigerworkshop.webapphardwarebridge.interfaces.WebSocketServiceInterface;
import tigerworkshop.webapphardwarebridge.services.SettingService;
import tigerworkshop.webapphardwarebridge.utils.ConnectionAttachment;

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

public class BridgeWebSocketServer extends WebSocketServer implements WebSocketServerInterface {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final HashMap<String, ArrayList<WebSocket>> socketChannelSubscriptions = new HashMap<>();
    private final HashMap<String, ArrayList<WebSocketServiceInterface>> serviceChannelSubscriptions = new HashMap<>();
    private final ArrayList<WebSocketServiceInterface> services = new ArrayList<>();

    private final SettingService settingService = SettingService.getInstance();

    public BridgeWebSocketServer(String address, int port) {
        super(new InetSocketAddress(address, port));
    }

    @Override
    public void onOpen(WebSocket connection, ClientHandshake handshake) {
        try {
            String descriptor = handshake.getResourceDescriptor();

            URI uri = new URI(descriptor);
            String channel = uri.getPath();
            List<NameValuePair> params = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8);
            String token = getToken(params);

            if (settingService.getSetting().getAuthenticationEnabled() && (token == null || !token.equals(settingService.getSetting().getAuthenticationToken()))) {
                connection.close(CloseFrame.REFUSE, "Token Mismatch");
                return;
            }

            connection.setAttachment(new ConnectionAttachment(channel, params, token));
            addSocketToChannel(channel, connection);

            logger.info(connection.getRemoteSocketAddress().toString() + " connected to " + channel);
        } catch (URISyntaxException e) {
            logger.error(connection.getRemoteSocketAddress().toString() + " error", e);
            connection.close();
        }
    }

    @Override
    public void onClose(WebSocket connection, int code, String reason, boolean remote) {
        if (connection.getAttachment() != null) {
            removeSocketFromChannel(((ConnectionAttachment) connection.getAttachment()).getChannel(), connection);
        }
    }

    /*
     * Server to Service communication
     */

    @Override
    public void onMessage(WebSocket connection, String message) {
        logger.trace("onMessage@String: " + connection.getRemoteSocketAddress() + ": " + message);

        String channel = ((ConnectionAttachment) connection.getAttachment()).getChannel();

        processMessage(channel, message);
    }

    @Override
    public void onMessage(WebSocket connection, ByteBuffer blob) {
        logger.trace("onMessage@ByteBuffer: " + connection.getRemoteSocketAddress() + ": " + blob);

        String channel = ((ConnectionAttachment) connection.getAttachment()).getChannel();

        processMessage(channel, blob);
    }

    @Override
    public void onError(WebSocket conn, Exception ex) {
        logger.error(ex.getMessage(), ex);
    }

    @Override
    public void onStart() {
        logger.info("BridgeWebSocketServer started");
        setConnectionLostTimeout(1);
    }

    public void close() {
        for (WebSocket socket : getConnections()) {
            socket.close();
        }

        synchronized (this) {
            for(int i=0; i< services.size(); i++) {
                services.get(i).stop();
            }
        }
    }

    /*
     * Service to Server listener
     */
    @Override
    public void onDataReceived(String channel, String message) {
        logger.trace("Received data from channel: " + channel + ", Data: " + message);

        if (channel.equals("proxy")) {
            processMessage("/printer", message);
        }

        ArrayList<WebSocket> connectionList = socketChannelSubscriptions.get(channel);

        if (connectionList == null) {
            logger.trace("connectionList is null, ignoring the message");
            return;
        }

        for (Iterator<WebSocket> it = connectionList.iterator(); it.hasNext(); ) {
            WebSocket conn = it.next();
            try {
                conn.send(message);
            } catch (WebsocketNotConnectedException e) {
                logger.warn("WebsocketNotConnectedException: Removing connection from list");
                it.remove();
            }
        }
    }

    @Override
    public void subscribe(WebSocketServiceInterface service, String channel) {
        addServiceToChannel(channel, service);
    }

    @Override
    public void unsubscribe(WebSocketServiceInterface service, String channel) {
        removeServiceFromChannel(channel, service);
    }

    private String getToken(List<NameValuePair> params) {
        for (NameValuePair pair : params) {
            if (pair.getName().equals("access_token")) {
                return pair.getValue();
            }
        }
        return null;
    }

    private void processMessage(String channel, String message) {
        ArrayList<WebSocketServiceInterface> services = getServiceListForChannel(channel);
        for (WebSocketServiceInterface service : services) {
            logger.trace("Attempt to send: " + message + " to channel: " + channel);
            service.onDataReceived(message);
        }
    }

    private void processMessage(String channel, ByteBuffer blob) {
        ArrayList<WebSocketServiceInterface> services = getServiceListForChannel(channel);
        for (WebSocketServiceInterface service : services) {
            logger.trace("Attempt to send: " + blob + " to channel: " + channel);
            service.onDataReceived(blob.array());
        }
    }

    private ArrayList<WebSocket> getSocketListForChannel(String channel) {
        ArrayList<WebSocket> socketList = socketChannelSubscriptions.get(channel);
        if (socketList == null) {
            return new ArrayList<>();
        }
        return socketList;
    }

    private void addSocketToChannel(String channel, WebSocket socket) {
        ArrayList<WebSocket> connectionList = getSocketListForChannel(channel);
        connectionList.add(socket);
        socketChannelSubscriptions.put(channel, connectionList);
    }

    private void removeSocketFromChannel(String channel, WebSocket socket) {
        ArrayList<WebSocket> connectionList = getSocketListForChannel(channel);
        connectionList.remove(socket);
        socketChannelSubscriptions.put(channel, connectionList);
    }

    private ArrayList<WebSocketServiceInterface> getServiceListForChannel(String channel) {
        ArrayList<WebSocketServiceInterface> services = new ArrayList<>();

        ArrayList<WebSocketServiceInterface> serviceList = serviceChannelSubscriptions.get(channel);
        if (serviceList != null) {
            services.addAll(serviceList);
        }

        ArrayList<WebSocketServiceInterface> serviceListWildcard = serviceChannelSubscriptions.get("*");
        if (serviceListWildcard != null) {
            services.addAll(serviceListWildcard);
        }

        return services;
    }

    private void addServiceToChannel(String channel, WebSocketServiceInterface service) {
        ArrayList<WebSocketServiceInterface> serviceList = serviceChannelSubscriptions.get(channel);
        if (serviceList == null) {
            serviceList = new ArrayList<>();
        }
        serviceList.add(service);
        serviceChannelSubscriptions.put(channel, serviceList);

        if (!services.contains(service)) {
            services.add(service);
        }
    }

    private void removeServiceFromChannel(String channel, WebSocketServiceInterface service) {
        ArrayList<WebSocketServiceInterface> serviceList = getServiceListForChannel(channel);
        serviceList.remove(service);
        serviceChannelSubscriptions.put(channel, serviceList);

        services.remove(service);
    }

}
