package chatapp.Controller;

import chatapp.Model.DataBase;
import chatapp.Model.ListUtilisateurs;
import chatapp.Model.Utilisateur;
import chatapp.Protocol.*;
import javafx.application.Application;
import javafx.application.Platform;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.InetAddress;

/**
 * <p> Controller de l'application. C'est par cette méthode que chaque connexion et chaque session sont entamées.
 * <br>Les différents attributs sont:
 * <br> - actifsUsers: qui est la liste des utilisateurs actifs actuellement et connus par l'application
 * <br> - me: qui est l'instance de la classe Utilisateur représentant l'utilisateur se connectant via cette instance de chatapp
 * <br> - main: l'application de l'interface graphique qui s'occupe d'instancier chatapp
 * <br> - runnerEcouteTCP: instance de runnerEcouteTCP, Thread permettant de recevoir des demandes de clavardage
 * <br> - runnerEcouteUDP: instance de runnerEcouteUDP; Thread permettant de communiquer en UDP pour recevoir les demandes de connexion, deconnexion, changements de pseudo et leurs réponses associées
 * <br> - connecte: booleen indiquant si l'on est connecté sur le reseau ou non
 * <br> - db: instance permettant de se connecter à la base de donnees centralisée
 * </p>
 */
public class ChatApp implements PropertyChangeListener {

    /* Liste des utilisateurs actifs */
    private ListUtilisateurs actifUsers ;

    private boolean isExterne; // TRUE si l'utilisateur est externe au reseau FALSE s'il est interne
    private HttpEchange externeEchange;
    /* ChatApp est associe a un utilisateur */
    private Utilisateur me;

    /* ChatApp est associe a une application qui le lance et que chatapp doit fermer a la fin */
    private Application main;

    private static ChatApp chatapp = null;
    private RunnerEcouteTCP runnerEcouteTCP;
    private RunnerEcouteUDP runnerEcouteUDP;
    private boolean connecte;
    //private DataBase db;

    /**
     * <p> Getter : Connecte
     * <br> Permet de savoir si l'on est connecté</p>
     * @return TRUE si connecté FALSE sinon
     */
    public boolean isConnecte() {
        return connecte;
    }

    /**
     * <p>Constructeur de l'application de chat</p>
     * @param pseudo Pseudo de l'utilisateur
     * @param port Port de communication
     * @param main L'Application main
     */
    public ChatApp(String pseudo, Integer port, Application main){
        this.actifUsers = new ListUtilisateurs() ;
        // Recuperer adresse IP de l'utilisateur
        InetAddress ip = UDPEchange.getCurrentIp();
        this.me = new Utilisateur(pseudo,port,ip);
        this.actifUsers.addList(getMe());
        this.main = main;
        //this.db= DataBase.getInstance();
        this.connecte=false;
        this.externeEchange = new HttpEchange(this);
    }

    /**
     * <p>Methode permettant de créer qu'une seule instance de chatapp</p>
     * @param pseudo Le pseudonyme que l'on souhaite donner à l'utilisateur
     * @param port   Le port auquel on souhaite réceptionner les requêtees UDP
     * @param main   Le stage de l'interface graphique qui crée l'instance
     * @return Une instance de chatapp
     */
    public static synchronized ChatApp getInstance(String pseudo, Integer port, Application main){
            if (chatapp == null) {
                chatapp = new ChatApp(pseudo, port, main);
            }
        return chatapp;
    }

    /**
     * <p> Methode permettant de récupérer l'instance de chatapp qui a ete créée au préalable</p>
     * @return L'instance de chatapp
     */
    public static ChatApp getInstance(){
        return chatapp;
    }


    /**
     * <p>Methode appelée pour modifier le pseudo et fait appel a la fonction adequate selon que l'on est en interne ou en externe</p>
     * @param nouveau Nouveau pseudo avec lequel on souhaite se renommer
     * @return TRUE si la modification du pseudo a réussi, FALSE sinon
     * @throws IOException
     */
    public boolean modifierPseudo(String nouveau) throws IOException{
        if(this.isExterne()){
            return this.modifierPseudoExterne(nouveau);
        }
        else{
            return modifierPseudoInterne(nouveau);
        }
    }

    /**
     * <p>Modification du pseudo de l'utilisateur en EXTERNE.
     * <br>Envoie ses informations utilisateurs et son nouveau pseudo au serveur de présence.</p>
     * @param nouveau Le nouveau pseudo
     * @return False si modiferPseudo a échoué, True sinon
     */
    private boolean modifierPseudoExterne(String nouveau) {
        boolean reussi = false;
        try {
            if(externeEchange.sendPost("Pseudo " + nouveau)){
                reussi = true ;
                this.getMe().setPseudo(nouveau);
                //db.majPseudo(this.me.getId(),nouveau);
                System.out.println("Changement pseudo accepte, nouvelle liste des utilisateurs actifs:");
                this.getActifUsers().afficherListeUtilisateurs();
            }
            else{
                System.out.println("Echec changement de pseudo");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return reussi;
    }

    /**
     * <p>Modification du pseudo de l'utilisateur en INTERNE
     * <br> Envoie en broadcast ses informations utilisateurs et son nouveau pseudo</p>
     * @param nouveau Le Nouveau pseudo
     * @return False si modiferPseudo a échoué, True sinon
     */
    public Boolean modifierPseudoInterne(String nouveau) throws IOException {
        // Message que l'on envoie Ã  tous les utilisateurs actifs
        String broadcastMessage = "Demande Modification Pseudo\n" + this.getMe().toString() + "\n" + nouveau + "\n";
        UDPEchange.EnvoiBroadcast(broadcastMessage);
        try {
            Thread.sleep(2000);
            /* L'utilisateur doit attendre la reponse de tous les utilisateurs connectes
             * pour savoir si son pseudo est accepte
             */
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (UDPEchange.getPseudoValide()) {
            System.out.println("Modification pseudo reussi");
            //Envoi un msg en broadcast a tout les utilisateurs pour les prevenir de son nouveau pseudo//
            broadcastMessage = "Modification pseudo reussi\n" + this.getMe().toString() + "\n" + nouveau + "\n";
            UDPEchange.EnvoiBroadcast(broadcastMessage);
            //-------Change son propre nom d'utilisateur-------//
            this.getActifUsers().modifierList(this.getMe().getPseudo(), nouveau);
            this.getMe().setPseudo(nouveau);
            //db.majPseudo(this.me.getId(),nouveau);
            System.out.println("Changement pseudo accepte, nouvelle liste des utilisateurs actifs:");
            this.getActifUsers().afficherListeUtilisateurs();
            return true;
        }
        else
        {
            System.out.println("Echec Modification pseudo");
            return false;
        }
    }

    /**
     * <p>Méthode permettant d'appeler la fonction de connexion adequate selon que l'on est en externe ou en interne</p>
     * @param pseudo Le pseudo avec lequel l'on souhaite se connecter
     * @return True si la connexion a ete autorisée, false sinon
     * @throws IOException
     */
    public boolean connexion(String pseudo) throws IOException{
        if(this.isExterne()){
            try {
                System.out.println("Demande de connexion distante");
                return this.connexionExterne(pseudo);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return false;
            }

        }
        else{
            return connexionInterne(pseudo);
        }
    }

    /**
     * <p>Méthode appelée lors de la connexion de l'utilisateur en EXTERNE.
     * <br> Va prévenir le serveur de présence de l'arrivée de l'utilisateur </p>
     * @param pseudo Le pseudonyme avec lequel l'utilisateur souhaite se connecter
     * @return  False si Connexion a échoué, True sinon
     * @throws IOException
     */
    private boolean connexionExterne(String pseudo) throws IOException, InterruptedException{
        this.me.setPseudo(pseudo);
        if(externeEchange.sendPost("Connexion")){
            this.connecte=true;
            //this.db.ajoutUtilisateurs(this.me.getId(),pseudo);
            //this.db.majUtilisateursActifs(true,this.me.getId());
            return true;
        }
        else{
            return false;
        }
    }


    /**
     * <p>Méthode appelée lors de la connexion de l'utilisateur en INTERNE,
     * <br> Va prevenir les autres utlisateurs du réseau de l'arrivée de l'utilisateur sur celui-ci</p>
     * @param pseudo Le pseudonyme avec lequel l'utilisateur souhaite se connecter
     * @return  False si Connexion a échoué, True sinon
     * @throws IOException
     */
    public Boolean connexionInterne(String pseudo) throws IOException {
        // Message que l'on envoie Ã  tous les utilisateurs actifs
        this.me.setPseudo(pseudo);
        String broadcastMessage = "Connexion\n" + this.getMe().toString() ;
        UDPEchange.EnvoiBroadcast(broadcastMessage);
        try {
            Thread.sleep(2000); // L'utilisateur doit attendre la reponse de tous les utilisateurs connectes
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (UDPEchange.getConnecte()) {
            System.out.println("Connexion reussie");
            this.connecte=true;
            //this.db.ajoutUtilisateurs(this.me.getId(),pseudo);
            //this.db.majUtilisateursActifs(true,this.me.getId());
            return true;
        }
        else
        {
            System.out.println("Connexion echoue");
            UDPEchange.setConnecte(true);
            return false ;
        }
    }

    /**
     * <p>Lance un thread pour écouter les requêtes TCP
     * <br> Informe le pcs que chatapp écoute les notifications qu'il envoie</p>
     */
    public void activerEcouteTCP(){
        this.runnerEcouteTCP = new RunnerEcouteTCP(this);
        this.runnerEcouteTCP.addPropertyChangeListener(this);
    }

    /**
     * <p>Démarre une session de clavardage avec l'utilisateur dont le pseudo est entré en parametre</p>
     * @param pseudo Le pseudo de l'utilisateur avec lequel on souhaite communiquer
     */
    public void demarrerSession(String pseudo){
        Utilisateur u2 = this.actifUsers.getPseudoList(pseudo);
        SessionClavardage session = new SessionClavardage(u2,this);
        runnerEcouteTCP.addSession(session);
    }

    /**
     * <p>Lance le thread pour réceptionner les requetes UDP</p>
     */
    public void activerEcouteUDP(){
        this.runnerEcouteUDP = new RunnerEcouteUDP(this);
        this.runnerEcouteUDP.start();
    }

    /**
     * <p>Méthode appelée lors de la déconnexion de l'utilisateur.
     * <br>La methode va appeler la fonction adequate selon que l'on est en interne ou en externe</p>
     * @throws IOException
     */
    public void deconnexion() throws IOException{
        if(this.isExterne()){
            this.deconnexionExterne();
        }
        else{
            deconnexionInterne();
        }
    }

    /**
     * <p>Methode appelee lors de la deconnexion en EXTERNE de l'utilisateur.
     * <br>Il va prévenir le serveur de présence de son depart.</p>
     *
     */
    private void deconnexionExterne() throws IOException{
        // A IMPLEMENTER
        System.out.println("Deco lancee");
        boolean deco = false;
        try {
            deco = externeEchange.sendPost("Deconnexion");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(deco){
            System.out.print("Deconnexion reussie");
        }
        else{
            System.out.println("Deconnexion echouee");
        }
        //db.majUtilisateursActifs(false,this.me.getId());
        this.connecte=false;
        try {
            main.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Platform.exit();
        System.exit(0);

    }


    /**
     * <p>Methode appelee lors de la deconnexion en INTERNE de l'utilisateur.
     * <br>Il va prevenir les utilisateurs du reseau de son depart.</p>
     * @throws IOException
     */
    public void deconnexionInterne() throws IOException {
        // Message que l'on envoie Ã  tous les utilisateurs actifs
        String broadcastMessage = "Deconnexion\n" + this.getMe().toString() ;
        UDPEchange.EnvoiBroadcast(broadcastMessage);
        //db.majUtilisateursActifs(false,this.me.getId());
        this.connecte=false;
        try {
            main.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Platform.exit();
        System.exit(0);
    }

    /**
     * Getter : Chatapp
     * @return Utilisateur associée a ChatApp
     */
    public Utilisateur getMe() {
        return me;
    }

    /**
     * Getter : actifUsers (Liste des utilisateurs actifs)
     * @return Liste des utilisateurs actifs associée a ChatApp
     */
    public ListUtilisateurs getActifUsers() {
        return actifUsers;
    }


    /**
     * <p>Handler des différents évenements que chatapp écoute</p>
     * @param evt L'évenement qui a ete indiqué par le pcs
     */
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        switch (evt.getPropertyName()){
            case "chatCreated" :
                SessionClavardage session = this.runnerEcouteTCP.getSessionClavardage();
                session.addPropertyChangeListener(this);
                break;
        }
    }

    /**
     * <p> Getter : Main
     *     Permet de récuperer stage de l'interface graphique qui a crée l'instance
     * </p>
     * @return Le stage de l'interface graphique qui a crée l'instance
     */
    public Application getMain() {
        return main;
    }

    /**
     * <p> Getter : BDD
     *     <br> Permet de récuperer la base de donnee associée
     * </p>
     * @return La base de donnee associée
     */
   // public DataBase getDb(){return this.db;}

    /**
     * <p> Getter : Externe
     * <br> Permet de savoir si on est en externe ou en interne</p>
     * @return TRUE si Externe FALSE sinon
     */
    public boolean isExterne() {
        return isExterne;
    }

    /**
     * <p> Setter : Externe
     * <br> Permet de définir si on est un utilisateur externe ou non</p>
     * @param externe TRUE si Externe FALSE sinon
     */
    public void setExterne(boolean externe) {
        isExterne = externe;
    }
}
