package edu.whs.spin2018.baoapp.service;

import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.net.URISyntaxException;

import io.socket.client.IO;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;
import io.socket.emitter.Emitter.Listener;
import io.socket.engineio.client.transports.WebSocket;

/**
 * Ein ClientSocket ist die Client Seite unserer Client-Server-Struktur. Er wird benutzt um die
 * Verbindung zum Server aufzubauen und Informationen als Events auszutauschen.
 */
public class ClientSocket {

    //Events die vom Client Socket gesendet werden
    public static final String EVENT_VERBUNDEN = "verbunden";
    public static final String EVENT_VERBINDEN = "connection";
    public static final String EVENT_RAUM_BETRETEN = "raumBeitreten";
    public static final String EVENT_RAUM_VERLASSEN = "raumVerlassen";
    public static final String EVENT_NACHRICHT = "nachricht";
    public static final String EVENT_ZEIGE_BENUTZER_IM_RAUM = "zeigeBenutzerImRaum";
    public static final String EVENT_LOGIN = "login";
    
    //Events die ausschliesslich vom Server kommen
    public static final String EVENT_BENUTZER_TRITT_RAUM_BEI = "userTrittRaumBei";
    public static final String EVENT_BENUTZER_VERLAESST_RAUM = "userVerlaesstRaum";

    //Keys von Key-Value-Paaren
    public static final String EXTRA_TYPE = "type";
    public static final String EXTRA_INHALT = "inhalt";
    public static final String EXTRA_UUID = "UUID";
    public static final String EXTRA_RAUM = "raum";
    public static final String EXTRA_APP = "app";
    public static final String EXTRA_BENUTZERNAME = "benutzername";
    private static final String TAG = "ClientSocket";
    private Socket socket;


    /**
     * Erzeugt einen ClientSocket, der eine Verbindung zu einem Server unter der angegebenen URL
     * aufbauen kann.
     *
     * @param url URL des Servers
     * @throws URISyntaxException
     */
    public ClientSocket(String url) throws URISyntaxException {
        IO.Options opts = new IO.Options();
        opts.transports = new String[]{WebSocket.NAME};
        socket = IO.socket(url, opts);
    }

    /**
     * Baut eine Socketverbindung zu dem Server auf.
     */
    public void verbinden() {

        // Verbindungsaufbau mit den Verbindungsinformationen.
        socket.connect();
        socket.io().reconnection(false);
        socket.emit(EVENT_VERBINDEN);
    }

    /**
     * Bindet einen Listener, der bei einem bestimmten Event benachrichtigt werden soll, an jenes
     * Event.
     *
     * @param event    Das Event, bei dem der Listener benachrichtigt werden soll.
     * @param listener Der zu benachrichtigende Listener
     */
    private void setzeListener(String event, Listener listener) {
        socket.off(event);
        socket.on(event, listener);
    }

    /**
     * Entfernt einen registrierten Listener, der auf das angegebene Event hoert.
     *
     * @param event    - Event auf das der angegebene Listener hoert.
     * @param listener - Listener, der entfernt werden soll
     */
    public void entferneListener(String event, Emitter.Listener listener) {
        socket.off(event, listener);
    }

    /**
     * Setzt einen Listener, der benachrichtigt wird, wenn eine Verbindung hergestellt wurde.
     *
     * @param listener Der Listener, der benachrichtigt wird
     */
    public void setVerbundenListener(Listener listener) {
        setzeListener(EVENT_VERBUNDEN, listener);
    }
	
    /**
     * Setzt einen Listener, der benachrichtigt wird, wenn eine Benutzerliste empfangen wurde.
     *
     * @param listener Der Listener, der benachrichtigt wird
     */
    public void setZeigeBenutzerImRaumListener(Listener listener) {
        setzeListener(EVENT_ZEIGE_BENUTZER_IM_RAUM, listener);
    }

    /**
     * Setzt einen Listener, der benachrichtigt wird, wenn ein Benutzer einen Raum betreten hat.
     *
     * @param listener Der Listener, der benachrichtigt wird
     */
    public void setBenutzerBetrittRaumListener(Listener listener) {
        setzeListener(EVENT_BENUTZER_TRITT_RAUM_BEI, listener);
    }

    /**
     * Setzt einen Listener, der benachrichtigt wird, wenn ein Benutzer einen Raum verlassen hat.
     *
     * @param listener Der Listener, der benachrichtigt wird
     */
    public void setBenutzerVerlaesstRaumListener(Listener listener) {
        setzeListener(EVENT_BENUTZER_VERLAESST_RAUM, listener);
    }
	
    /**
     * Setzt einen Listener, der benachrichtigt wird, wenn eine Nachricht empfangen wurde.
     *
     * @param listener Der Listener, der benachrichtigt wird
     */
	public void setNachrichtenListener(Listener listener) {
		setzeListener(EVENT_NACHRICHT, listener);
	}
	
    /**
     * Setzt einen Listener, der benachrichtigt wird, ob ein Login erfolgreich oder fehlgeschlagen ist.
     *
     * @param listener Der Listener, der benachrichtigt wird
     */
	public void setLoginListener(Listener listener) {
		setzeListener(EVENT_LOGIN, listener);
	}

    /**
     * Meldet einen Benutzer am Server an appName und appVersion werden serverseitig verwendet um
     * festzustellen ob der Client kompatibel ist.
     *
     * @param benutzername Waehlbarer Name des Benutzers
     * @param uuid         Einzigartige ID des Benutzers (generiert)
     * @param appName      Name der App, die sich anmelden will
     * @param appVersion   Version der App, die sich anmelden will
     */
    public void login(String benutzername, String uuid, String appName, String appVersion) {
        JSONObject loginDaten = new JSONObject();
        try {
            loginDaten.put(EXTRA_UUID, uuid);
            loginDaten.put(EXTRA_BENUTZERNAME, benutzername);
            loginDaten.put(EXTRA_APP, appName + appVersion);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        emit(EVENT_LOGIN, loginDaten);
    }

    /**
     * Loest das angegebene Event beim Server aus und sendet die Daten.
     *
     * @param event      Das auszuloesene Event
     * @param eventDaten Die zu sendenden Daten
     */
    private void emit(String event, JSONObject eventDaten) {
        socket.emit(event, eventDaten);
    }


    /**
     * Loest das angegebene Event beim Server aus.
     *
     * @param event Das auszuloesene Event
     */
    private void emit(String event) {
        socket.emit(event);
    }

    /**
     * Trennt die Verbindung zum Server.
     */
    public void trennen() {
        socket.off();
        socket.disconnect();
    }

    /**
     * Betritt einen Raum auf dem Server Andere Raeume werden nicht automatisch verlassen.
     *
     * @param raum Der Raum, der betreten werden soll.
     */
    public void raumBetreten(String raum) {
        emitRaumEvent(EVENT_RAUM_BETRETEN, raum);
    }

    /**
     * Verlaesst einen Raum auf dem Server Nach dem Verlassen koennen keine Broadcasts, die an den
     * Raum gesendet werden, empfangen werden.
     *
     * @param raum Der Raum, der verlassen wird
     */
    public void raumVerlassen(String raum) {
        emitRaumEvent(EVENT_RAUM_VERLASSEN, raum);
    }

    /**
     * Hilfsmethode um Events auszuloesen, die nur einen Raum als Parameter haben
     *
     * @param event Das Event, das ausgeloest werden soll (z.B. Raum betreten/verlassen)
     * @param raum  Parameter des Events
     */
    private void emitRaumEvent(String event, String raum) {
        JSONObject raumObjekt = new JSONObject();

        try {
            raumObjekt.put(Konstanten.EXTRA_RAUM, raum);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        emit(event, raumObjekt);
    }

    /**
     * Diese Methode sendet ein Event an den Server, das ihn veranlasst eine Liste mit allen
     * Benutzern des angegebenen Raumes zurueckzusenden.
     *
     * @param raum Der Raum, dessen Benutzer gesendet werden sollen
     */
    public void zeigeBenutzerImRaum(String raum) {
        emitRaumEvent(EVENT_ZEIGE_BENUTZER_IM_RAUM, raum);
    }

    /**
     * Ein Broadcast ist eine Nachricht an alle Benutzer eines Raumes ausser den Sender (siehe
     * sendeNachricht)
     *
     * @param inhalt Der Inhalt der Nachricht (z.B. Text, ein Spielzug etc)
     * @param typ    Beschreibt die Art der Nachricht
     * @param raum   Der Raum, dessen Benutzer die Nachricht erhalten sollen
     */
    public void sendeBroadcast(String inhalt, String typ, String raum) {
		sendeNachricht(inhalt, typ, raum, true);
    }

    /**
     * Sendet eine Nachricht an einen Benutzer Eine Nachricht ist ein JSONObject, dass an genau
     * einen Benutzer des Servers geschickt wird um diesem Benutzer Informationen beliebigen Typs
     * zukommen zu lassen.
     *
     * @param inhalt         Der Inhalt der Nachricht (z.B. Text, ein Spielzug etc)
     * @param typ            Beschreibt die Art der Nachricht
     * @param empfaengerUUID Die UUID des Empfaengers der Nachricht
     */
    public void sendeNachricht(String inhalt, String typ, String empfaengerUUID) {
        sendeNachricht(inhalt, typ, empfaengerUUID, false);
    }
	
	/**
	 * Hilfsmethode, die das Senden von Nachrichten und Broadcasts verallgemeinert.
     * @param inhalt         Der Inhalt der Nachricht (z.B. Text, ein Spielzug etc)
     * @param typ            Beschreibt die Art der Nachricht
     * @param empfaenger     Die UUID des Empfaengers oder der Raum, an den die Nachricht gesendet wird
	 * @param istBroadcast   true fuer Broadcasts, false fuer Nachrichten mit einem Empfaenger
	 */
	private void sendeNachricht(String inhalt, String typ, String empfaenger, boolean istBroadcast) {
        JSONObject nachricht = new JSONObject();
        try {
            nachricht.put(EXTRA_TYPE, typ);
            nachricht.put(EXTRA_INHALT, inhalt);
			if(istBroadcast) {
            	nachricht.put(EXTRA_RAUM, empfaenger);
			} else {
				nachricht.put(EXTRA_UUID, empfaenger);
			}
        } catch (JSONException e) {
            e.printStackTrace();
        }
        Log.d(TAG, "sendeNachricht: " + inhalt + " an " + empfaenger);
        emit(EVENT_NACHRICHT, nachricht);
	}


}
