/* 
 *  JavaFXRefClient.java
 * 
 *  Copyright 2016 Avaya Inc. All Rights Reserved.
 * 
 *  Usage of this source is bound to the terms described
 *  in AvayaLicenseSDK.rtf.
 * 
 *  Avaya - Confidential & Proprietary. Use pursuant to your signed agreement
 *  or Avaya Policy
 * 
 */
package com.avaya.ccs.javafxrefclient;

import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

import com.avaya.ccs.api.enums.InteractionType;
import com.avaya.ccs.javafxrefclient.ConnectionDetails;

public class JavaFXRefClient extends Application {

    private static final Logger LOG = Logger.getLogger(JavaFXRefClient.class);
    private String css = "";

    private ConnectionDetails connectionDetails;
    private final int mainSceneWidth = 1600;
    private final int mainSceneHeight = 700;

    // State objects
    private SessionData sessionData = new SessionData(null);
    private UserDataGUI userGUI;
    private ClientData clientData = new ClientData(null);
    private SessionDetailsForm sessionDetailsForm;
    private ClientDetailsForm clientDetailsForm;
    private ResourceDataGUI resourceDataGUI;
    private InteractionDataGUI interactionDataGUI;        
    private CodeSelectionForm codeSelection;
    private PreviewDialForm previewDial;
    private ConsultForm consultForm;
    private JoinForm joinForm;
    private SupervisorDataGUI supervisorDataGUI;
    private CallbackForm callbackForm;

    private MenuItem disconnect;
    private MenuItem signIn;
    private static Text connectionStatus;
    private static Text operationStatus;

    private final String styleSheetName = "javaClientStylesheet.css";
    private final String styleSheetDirectory = "css";

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        // Override the default thread name of: JavaFX Application Thread
        Thread.currentThread().setName("JavaFX");
        String methodName = "start() ";

        LOG.info(methodName + "Reading properties file");
        RefClientProperties props = new RefClientProperties();
        connectionDetails = props.getConnectionDetails();

        // attempt to retrieve the style sheet from the ccs directory (included
        // in the jar)
        URL url = this.getClass().getResource("/" + styleSheetDirectory + "/" + styleSheetName);

        if (url == null) {
            // for this resource to be found the css file must be in the same
            // directory as the compiled main class
            url = this.getClass().getResource(styleSheetName);
            if (url == null) {
                LOG.error(methodName + "Style sheet " + styleSheetName + " not found");
            }
        }

        css = url.toExternalForm();

        Executor.createExecutors(this);

        codeSelection = new CodeSelectionForm(Executor.getUser(), Executor.getInteractionExe(), css);
        previewDial = new PreviewDialForm(Executor.getInteractionExe(), css);
        consultForm = new ConsultForm(Executor.getInteractionExe(), css, InteractionType.VoiceInteraction);
        
        joinForm = new JoinForm(Executor.getInteractionExe(), css);
        userGUI = new UserDataGUI(Executor.getClientSession(), Executor.getUser(), Executor.getInteractionExe(),css, codeSelection);
        sessionDetailsForm = new SessionDetailsForm(Executor.getClientSession(), css);
        clientDetailsForm = new ClientDetailsForm(Executor.getClientSession(), css);
        resourceDataGUI = new ResourceDataGUI(Executor.getResource(), css);
        callbackForm = new CallbackForm(Executor.getInteractionExe(), css);
        interactionDataGUI = new InteractionDataGUI(userGUI, Executor.getInteractionExe(), css, codeSelection, previewDial,
                consultForm, joinForm, callbackForm);
        supervisorDataGUI = new SupervisorDataGUI(Executor.getMonitored(), Executor.getInteractionExe(), css);

        
        // Build main form
        BorderPane border = new BorderPane();
        border.setTop(createMenuBar());
        border.setLeft(interactionDataGUI.getSideBarVbox());
        border.setCenter(createCenter());
        border.setBottom(createStatusBar());

        // Handler for when the supervisor window is closed
        primaryStage.setOnCloseRequest(event -> {
            LOG.trace("primaryStage.setOnCloseRequest(): Client window closed");
            supervisorDataGUI.clearData();
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                // Don't care
            }
            Executor.close();
            System.exit(0);
        });

        Scene primaryScene = new Scene(border, mainSceneWidth, mainSceneHeight);
        primaryScene.getStylesheets().add(css);
        primaryStage.setTitle("Java ReferenceClient");
        primaryStage.setScene(primaryScene);
        primaryStage.show();

        this.setConnectionState();
        
        //disable SSL verification. required for downloading attachments
        disableSslVerification();
        
    }

    private static void disableSslVerification() {
        try
        {
            // Create a trust manager that does not validate certificate chains
            TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }
            };

            // Install the all-trusting trust manager
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

            // Create all-trusting host name verifier
            HostnameVerifier allHostsValid = new HostnameVerifier() {
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };

            // Install the all-trusting host verifier
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
    }

	public MenuBar createMenuBar() {
        MenuBar menuBar = new MenuBar();
        menuBar.getStyleClass().add("mainMenuBar");
        Menu menuSession = new Menu("Session");

        signIn = new MenuItem("SignIn");
        signIn.setOnAction((ActionEvent t) -> {
            LOG.info("SignInMenuItem()");
            createUserLoginDialog();
        });

        disconnect = new MenuItem("Disconnect");
        disconnect.setOnAction((ActionEvent t) -> {
            LOG.info("DisconnectMenuItem()");
            supervisorDataGUI.clearData();
            Executor.getClientSession().disconnect();
        });
        MenuItem exit = new MenuItem("Exit");
        exit.setOnAction((ActionEvent t) -> {
            LOG.info("ExitMenuItem()");
            supervisorDataGUI.clearData();
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                // Don't care
            }
            Executor.close();
            System.exit(0);
        });
        menuSession.getItems().addAll(signIn, disconnect, exit);

        Menu menuView = new Menu("View");
        MenuItem addSupervisor = new MenuItem("Supervisor Window");
        addSupervisor.setOnAction((ActionEvent t) -> {
            LOG.info("SupervisorMenuItem()");
            supervisorDataGUI.show();
        });

        menuView.getItems().addAll(addSupervisor, clientDetailsForm.getMenuItem(), sessionDetailsForm.getMenuItem(),
                getUserGUI().getMenuItem(), getResourceDataGUI().getMenuitem(), interactionDataGUI.getMenuItem());

        menuBar.getMenus().addAll(menuSession, menuView);

        return menuBar;
    }

    private void createUserLoginDialog() {
        // Create the custom dialog.
        Dialog<ConnectionDetails> dialog = new Dialog<>();
        dialog.setTitle("SignIn Dialog");

        // Set the button types.
        ButtonType signinButtonType = new ButtonType("SignIn", ButtonData.OK_DONE);
        dialog.getDialogPane().getButtonTypes().addAll(signinButtonType, ButtonType.CANCEL);

        GridPane grid = new GridPane();
        grid.getStyleClass().add("dialog");

        TextField dialogServer = new TextField();
        dialogServer.setPromptText("Server");
        dialogServer.setText(this.connectionDetails.getServer());
        TextField dialogUsername = new TextField();
        dialogUsername.setPromptText("Username");
        dialogUsername.setText(this.connectionDetails.getUsername());
        PasswordField dialogPassword = new PasswordField();
        dialogPassword.setPromptText("Password");
        dialogPassword.setText(this.connectionDetails.getPassword());
        CheckBox autoReconnect = new CheckBox();
        autoReconnect.setSelected(this.connectionDetails.isAutoReconnect());

        grid.add(new Label("Server:"), 0, 0);
        grid.add(dialogServer, 1, 0);
        grid.add(new Label("Username:"), 0, 1);
        grid.add(dialogUsername, 1, 1);
        grid.add(new Label("Password:"), 0, 2);
        grid.add(dialogPassword, 1, 2);
        grid.add(new Label("AutoReconnect:"), 0, 3);
        grid.add(autoReconnect, 1, 3);
        dialog.getDialogPane().setContent(grid);

        // Request focus on the username field by default.
        Platform.runLater(() -> dialogUsername.requestFocus());

        // Convert the result to a ConnectionDetails object when the login
        // button is clicked
        dialog.setResultConverter(dialogButton -> {
            if (dialogButton == signinButtonType) {
                return new ConnectionDetails(dialogUsername.getText(), dialogPassword.getText(), dialogServer.getText(),
                        autoReconnect.selectedProperty().getValue());
            }
            return null;
        });

        Optional<ConnectionDetails> result = dialog.showAndWait();

        result.ifPresent(connectionDetails -> {
            LOG.trace("SignIn Selected - Username = " + connectionDetails.getUsername() + ", Password num of chars = "
                    + connectionDetails.getPassword().length() + " server = " + connectionDetails.getServer()
                    + " auto reconnect = " + connectionDetails.isAutoReconnect());
            this.connectionDetails = connectionDetails;
            Executor.getClientSession().signinClient(connectionDetails);
        });
    }

    public VBox createCenter() {
        VBox vbox = new VBox();
        vbox.getStyleClass().add("vbox");

        vbox.getChildren().add(getUserGUI().getUserControls());
        vbox.getChildren().add(this.resourceDataGUI.getAddressButtons());
        vbox.getChildren().add(this.interactionDataGUI.getInteractionTable());
        vbox.getChildren().add(interactionDataGUI.getInteractionButtons());
        vbox.getChildren().add(interactionDataGUI.getPOMButtons());
        return vbox;
    }

    public GridPane createStatusBar() {
        GridPane gridPane = new GridPane();
        gridPane.getStyleClass().add("statusBar");

        connectionStatus = new Text("");
        operationStatus = new Text("");
        Label clock = createClock();
        GridPane.setHalignment(clock, HPos.RIGHT);
        gridPane.add(connectionStatus, 0, 0);
        gridPane.add(operationStatus, 1, 0);
        gridPane.add(clock, 2, 0);

        ColumnConstraints col1 = new ColumnConstraints();
        col1.setPercentWidth(20);
        ColumnConstraints col2 = new ColumnConstraints();
        col2.setPercentWidth(55);
        ColumnConstraints col3 = new ColumnConstraints();
        col3.setPercentWidth(25);
        gridPane.getColumnConstraints().addAll(col1, col2, col3);
        return gridPane;
    }

    private Label createClock() {
        final Label clock = new Label();
        final Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), (ActionEvent event) -> {
            LocalDateTime timePoint = LocalDateTime.now();
            clock.setText(timePoint.format(DateTimeFormatter.ISO_DATE) + " "
                    + timePoint.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        }));
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();
        return clock;
    }

    public static void UpdateStatus(String newStatus) {
        operationStatus.setText(newStatus);
    }

    public static String getStatus() {
        return operationStatus.getText();
    }

    /**
     * Update the connection status and client details form
     * 
     * @param data
     */
    public void updateClientData(ClientData data) {
        String methodName = "updateClientData() ";
        this.clientData = data;

        if (this.clientData == null) {
            LOG.trace(methodName + "data == null, creating blank object");
            this.clientData = new ClientData(null);
        }

        LOG.trace(methodName);
        this.setConnectionState();
        clientDetailsForm.update(this.clientData.getClientProperties());
    }

    /**
     * Update the session details form
     * 
     * @param data
     */
    public void updateSessionData(SessionData data) {
        String methodName = "updateSessionData() ";
        this.sessionData = data;

        if (this.sessionData == null) {
            LOG.info(methodName + "data == null, session disconnected");
            this.sessionData = new SessionData(null);
        }
        LOG.info(methodName + " SessionData:" + this.sessionData);

        sessionDetailsForm.update(this.sessionData.getSessionProperties());

        if (!this.sessionData.isConnected()) {
            clearSessionData();
        }
    }

    /**
     * Set the connection status based on the current client data
     */
    public void setConnectionState() {
        String methodName = "setConnectionState() ";
        boolean canDisconnect = false;
        boolean canSignIn = false;
        Status status = Status.Red;
        switch (this.clientData.getState()) {

        case DISCONNECTED:
        default:
            canDisconnect = false;
            canSignIn = true;
            // Need to null out user details and any other objects for which we
            // do not get an explicit event
            supervisorDataGUI.removeData();
            break;
        case CONNECTING:
        case CONNECTED:
            canDisconnect = true;
            canSignIn = false;
            break;
        case AUTHENTICATED:
            canDisconnect = true;
            canSignIn = false;
            LOG.clearStatus();
            if (this.clientData.isHa()) {
                if (this.clientData.isHaOperational()) {
                    status = Status.Green;
                } else {
                    status = Status.Orange;
                }
            } else {
                status = Status.Green;
            }
            break;
        }
        disconnect.setDisable(!canDisconnect);
        signIn.setDisable(!canSignIn);
        LOG.info(methodName + "new state = " + this.clientData.getState().name() + " status = " + status.name());
        switch (status) {
        case Red:
            connectionStatus.fillProperty().set(Color.RED);
            break;
        case Orange:
            connectionStatus.fillProperty().set(Color.ORANGE);
            break;
        case Green:
            connectionStatus.fillProperty().set(Color.GREEN);
            break;
        }

        connectionStatus.setText(this.clientData.getState().name());
    }

    private enum Status {
        Red, Green, Orange,
    }

    public void clearSessionData() {
        String methodName = "clearSessionData() ";
        LOG.info(methodName
                + "session disconnected, invalidate all session related state as no delete events will be received");
        getUserGUI().clearData();
        getResourceDataGUI().clearData();
        getSupervisorGUI().clearData();
        getInteractionDataGUI().clearData();
    }

    public UserDataGUI getUserGUI() {
        return userGUI;
    }

    public SupervisorDataGUI getSupervisorGUI() {
        return supervisorDataGUI;
    }

    public ResourceDataGUI getResourceDataGUI() {
        return resourceDataGUI;
    }

    public InteractionDataGUI getInteractionDataGUI() {
        return interactionDataGUI;
    }
}
