/*
 * Copyright (C) 2018-2020, 2022 Andrew Gegg
 *
 *	This file is part of the Garden Notebook application
 *
 * The Garden Notebook application is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl.html>.
 */

/*
	Change Log
    2.1.0   handle multiple (possible) database servers
            split Preferences into sub-nodes
    2.2.0   Support for in-process DB hsqldb
            Include connection timeout configuration
    2.4.0   Support MS SQLServer
    2.6.1   hsqldb default username changed to 'SA' - first time in the database
                doesn't exist yet so there can be no user already set up
                For other users, use the current username as default, not just on first entry
            on login, if the DB tab selected is NOT for the DB to use, check with the user
            if not used at login, do not allow DB change
    2.8.1   Set 'JSON dump on exit' true by default
            Support multiple dump generations and tidy-up
            Code tidy
	3.0.6	Improve first sign-in, especially if using .msi installer
	
 */

package uk.co.gardennotebook;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Spinner;
import javafx.scene.layout.AnchorPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.EntryMessage;
import static uk.co.gardennotebook.GardenNotebook.NOTEBOOK_VERSION;

import uk.co.gardennotebook.spi.TrugServer;

/**
 * Displays various options to configure the Application.
 * In particular, the log-in options for database managers can be set here.
 * 
 * This class should be used to populate a pop-up or initial window.
 *
 * @author Andrew Gegg
*	@version	3.0.6
*	@since	1.0
 */
final class NotebookConfig extends AnchorPane
{	
	private static final Logger LOGGER = LogManager.getLogger();

	@FXML
	private TabPane tabPane;
	@FXML
	private Tab generalTab;
	@FXML
	private CheckBox chkRequireLogin;
	@FXML
	private Tab licenceTab;
	@FXML
	private TextArea txtLicence;
	@FXML
	private CheckBox chkLicenceAgree;
//	@FXML
//	private Label lblMustAgree;

	@FXML
	private Tab mysqlTab;
	@FXML
	private RadioButton chkMySQLUse;
	@FXML
	private CheckBox chkMySQLCache;
	@FXML
	private TextField txtMySQLHost;
	@FXML
	private TextField txtMySQLPort;
	@FXML
	private TextField txtMySQLDatabase;
	@FXML
	private TextField txtMySQLUser;
	@FXML
	private PasswordField txtMySQLPassword;
	@FXML
	private Spinner<Integer> txtMySQLTimeout;
    
	@FXML
	private Tab mariaDBTab;
	@FXML
	private RadioButton chkMariaDBUse;
	@FXML
	private CheckBox chkMariaDBCache;
	@FXML
	private TextField txtMariaDBHost;
	@FXML
	private TextField txtMariaDBPort;
	@FXML
	private TextField txtMariaDBDatabase;
	@FXML
	private TextField txtMariaDBUser;
	@FXML
	private PasswordField txtMariaDBPassword;
	@FXML
	private Spinner<Integer> txtMariaDBTimeout;

	@FXML
	private Tab hsqldbTab;
	@FXML
	private RadioButton chkhsqldbUse;
	@FXML
	private CheckBox chkhsqldbCache;
//	@FXML
//	TextField txthsqldbHost;
//	@FXML
//	TextField txthsqldbPort;
	@FXML
	private TextField txthsqldbDirectory;
	@FXML
	private TextField txthsqldbDatabase;
    @FXML
    private RadioButton chkhsqlCreateEmpty;
    @FXML
    private RadioButton chkhsqlCreateInit;
	@FXML
	private TextField txthsqldbUser;
	@FXML
	private PasswordField txthsqldbPassword;

	@FXML
	private Tab mssqlserverTab;
	@FXML
	private RadioButton chkMSSQLServerUse;
	@FXML
	private CheckBox chkMSSQLServerCache;
	@FXML
	private TextField txtMSSQLServerHost;
	@FXML
	private TextField txtMSSQLServerPort;
	@FXML
	private TextField txtMSSQLServerDatabase;
	@FXML
	private TextField txtMSSQLServerUser;
	@FXML
	private PasswordField txtMSSQLServerPassword;
	@FXML
	private Spinner<Integer> txtMSSQLServerTimeout;
    
	@FXML
	private CheckBox chkJsonOnExit;
	@FXML
	private TextField txtJSONDumpDir;
	@FXML
	private TextField txtJSONLoadDir;
	@FXML
	private Spinner<Integer> txtJSONRetainDumps;    //  2.8.1
	@FXML
	private TextField txtJSONMostRecentDumpDir;

	@FXML
	private Button btnCancel;
	@FXML
	private Button btnApply;
	@FXML
	private Button btnHelp;
	
	@FXML
	private ResourceBundle resources;

	private final Preferences prefs = Preferences.userNodeForPackage(GardenNotebook.class);
	private boolean firstTime = prefs.getBoolean("firstTime", true);
    
    private String defaultJSONFileLocation;
    private String defaultHsqldbLocation;

	private boolean calledAsLogin = false;
	

	/**
	 * Equivalent to {@code NotebookConfig(true)}.
	 * Use this call as an initial entry point from the main window {@code GardenNotebook}.
	 * If the Cancel button is pressed, the entire application will be closed.
	 */
	NotebookConfig()
	{
		this(true);
	}
	
	/**
	 * Displays the Configuration popup.
	 * 
	 * @param	asLogin	controls the behaviour of the Cancel button.  If true, this
	 *					is assumed to be an initial or log-in window and the whole application will be terminated by Cancel.
	 */
	NotebookConfig(boolean asLogin)
	{

		calledAsLogin = asLogin;
		FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/NotebookConfig.fxml"),
				ResourceBundle.getBundle("notebook") );
		fxmlLoader.setRoot(this);
		fxmlLoader.setController(this);
		try {
			fxmlLoader.load();	// NB initialize is called from in here
		} catch (IOException exception) {
			throw new RuntimeException(exception);
		}
	}

	/*
	 * Initializes the controller class.
	 */
	@FXML
	public void initialize()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("initialize()");
		
		// terms and conditions
		final boolean licenceAgreed = prefs.getBoolean("licenceAgreed", false);
		chkLicenceAgree.setSelected(licenceAgreed);
		chkLicenceAgree.setDisable(licenceAgreed);
        
        chkRequireLogin.setSelected(prefs.getBoolean("requireLogin", true));
		
		StringBuilder licText = new StringBuilder();
		try (	InputStream ips1bis = GardenNotebook.class.getResourceAsStream("/gpl.txt");
				Reader licRead = new InputStreamReader(ips1bis);
				BufferedReader licBuff = new BufferedReader(licRead);
			)
		{	
			while (licBuff.ready())
			{
				licText.append(licBuff.readLine()).append("\n");
			}
		} catch (FileNotFoundException ex) {
            LOGGER.error("No licence file found");
			Platform.runLater(() -> PanicHandler.panic(ex));
		} catch (IOException ex) {
			Platform.runLater(() -> PanicHandler.panic(ex));
		}
		txtLicence.setText(licText.toString());
		if (!licenceAgreed)
		{
			chkLicenceAgree.selectedProperty().addListener((obj, oldVal, newVal) -> {
				if (!licenceAgreed && !oldVal && newVal)
				{// licence agreed
					chkLicenceAgree.setDisable(true);
					tabPane.getSelectionModel().select(hsqldbTab);
				}
			});
		}
        
        //  set up the default location for JSON dump files and hsqldb database
        if (System.getProperty("os.name").startsWith("Window"))
        {
            LOGGER.debug("using Windows");
            defaultJSONFileLocation = System.getenv("APPDATA") + System.getProperty("file.separator") + "GardenNotebook";
            defaultHsqldbLocation = System.getenv("APPDATA") + System.getProperty("file.separator") + "GardenNotebook" + System.getProperty("file.separator") + "hsqldb";
        }
        else
        {
            LOGGER.debug("not Windows");
            defaultJSONFileLocation = System.getProperty("user.dir") + System.getProperty("file.separator") + "GardenNotebook";
            defaultHsqldbLocation = System.getProperty("user.home") + System.getProperty("file.separator") + "GardenNotebook" + System.getProperty("file.separator") + "hsqldb";
        }
        LOGGER.info("defaultJSONFileLocation: {}", defaultJSONFileLocation);
        LOGGER.info("defaultHsqldbLocation: {}", defaultHsqldbLocation);

		// DB handling - Map<service_type, name>, e.g. (SQL, MySQL)
		Map<String, Map<String, Properties>> services = TrugServer.getTrugServer().getServices();
		for (String key : services.keySet())
		{
            LOGGER.debug("Walking services for key:{}", key);
            if ("SQL".equalsIgnoreCase(key))
            {
                setupSQLServicesTabs(services.get(key));
            }
		}
		
		// JSON handling
        {
            Preferences jp = prefs.node("JSON");
            chkJsonOnExit.setSelected(jp.getBoolean("dumpOnExit", true));   //  2.8.1
            txtJSONDumpDir.setText(jp.get("dumpDir", defaultJSONFileLocation + System.getProperty("file.separator") + "JSONDumpDir"));
            txtJSONLoadDir.setText(jp.get("loadDir", defaultJSONFileLocation + System.getProperty("file.separator") + "JSONLoadDir"));
            txtJSONRetainDumps.getValueFactory().setValue(jp.getInt("retainDumps", 0));
			txtJSONMostRecentDumpDir.setText(jp.get("mostrecentdumpdir", ""));
        }

//        btnApply.disableProperty().bind(hsqldbTab.disableProperty().not().and(hsqldbTab.selectedProperty()).
//                                            and(chkhsqlCreateEmpty.disabledProperty().not()).
//                                                and(chkhsqlCreateInit.disabledProperty().not()).
//                                                    and(chkhsqlCreateEmpty.selectedProperty().not()).
//                                                        and(chkhsqlCreateInit.selectedProperty().not()));
		BooleanBinding hsqlUsable = hsqldbTab.disableProperty().not().and(chkhsqldbUse.selectedProperty()).
				and( chkhsqlCreateEmpty.disabledProperty().or(chkhsqlCreateEmpty.selectedProperty()).
						or(chkhsqlCreateInit.disabledProperty().or(chkhsqlCreateInit.selectedProperty())) );

		BooleanBinding goodToGo = chkLicenceAgree.selectedProperty().and(
				hsqlUsable.or(chkMySQLUse.selectedProperty()).or(chkMariaDBUse.selectedProperty()).or(chkMSSQLServerUse.selectedProperty())
		);

		btnApply.disableProperty().bind(chkLicenceAgree.selectedProperty().not().or(goodToGo.not()));
	}	// initialize
    
    private void setupSQLServicesTabs(Map<String, Properties> props)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("setupSQLServicesTabs()");
        
        String selectedServer = prefs.node("SQL").get("selectedServer", "");

		boolean hsqldbFound = false;
		boolean serverSelected = false;

        for (var key : props.keySet())
        {
            if ("MySQL".equalsIgnoreCase(key))
            {
                mysqlTab.setDisable(false);
				chkMySQLUse.setSelected(selectedServer.equalsIgnoreCase("MySQL"));
                //  2.6.1   don't allow DB change other than at login
                chkMySQLUse.setDisable(!calledAsLogin);
                if (selectedServer.equalsIgnoreCase("MySQL"))
                {
                    tabPane.getSelectionModel().select(mysqlTab);
					serverSelected = true;
                }
                
                Preferences lp = prefs.node("SQL/MySQL");
				final boolean cachedVals = lp.getBoolean("cache", true);
				chkMySQLCache.setSelected(cachedVals);
				if (cachedVals)
				{
					txtMySQLHost.setText(lp.get("host", "localhost"));
					txtMySQLPort.setText(lp.get("port", "3306"));
					txtMySQLDatabase.setText(lp.get("database", "gardennotebook"));
					txtMySQLUser.setText(lp.get("user", System.getProperty("user.name")));  //  2.6.1
					txtMySQLPassword.setText(lp.get("password", ""));
                    txtMySQLTimeout.getValueFactory().setValue(lp.getInt("timeout", 1));
				}
            }
            else if ("MariaDB".equalsIgnoreCase(key))
            {
                mariaDBTab.setDisable(false);
				chkMariaDBUse.setSelected(selectedServer.equalsIgnoreCase("MariaDB"));
                //  2.6.1   don't allow DB change other than at login
                chkMariaDBUse.setDisable(!calledAsLogin);
                if (selectedServer.equalsIgnoreCase("MariaDB"))
                {
                    tabPane.getSelectionModel().select(mariaDBTab);
					serverSelected = true;
                }
                
                Preferences lp = prefs.node("SQL/MariaDB");
				final boolean cachedVals = lp.getBoolean("cache", true);
				chkMariaDBCache.setSelected(cachedVals);
				if (cachedVals)
				{
					txtMariaDBHost.setText(lp.get("host", "localhost"));
					txtMariaDBPort.setText(lp.get("port", "3307"));
					txtMariaDBDatabase.setText(lp.get("database", "gardennotebook"));
					txtMariaDBUser.setText(lp.get("user", System.getProperty("user.name")));    //  2.6.1
					txtMariaDBPassword.setText(lp.get("password", ""));
                    txtMariaDBTimeout.getValueFactory().setValue(lp.getInt("timeout", 1));
				}
            }
            else if ("hsqldb".equalsIgnoreCase(key))
            {
				hsqldbFound = true;
                hsqldbTab.setDisable(false);
				chkhsqldbUse.setSelected(selectedServer.equalsIgnoreCase("hsqldb"));
                //  2.6.1   don't allow DB change other than at login
                chkhsqldbUse.setDisable(!calledAsLogin);
                if (selectedServer.equalsIgnoreCase("hsqldb"))
                {
                    tabPane.getSelectionModel().select(hsqldbTab);
					serverSelected = true;
                }
                
                Preferences lp = prefs.node("SQL/hsqldb");
				final boolean cachedVals = lp.getBoolean("cache", true);
				chkhsqldbCache.setSelected(cachedVals);
				if (cachedVals)
				{
					txthsqldbDatabase.setText(lp.get("database", "gardennotebook"));
					txthsqldbDirectory.setText(lp.get("directory", defaultHsqldbLocation));
                    //  2.6.1   there's no way the user can have set up additional users if this is the first time
					txthsqldbUser.setText(lp.get("user", "SA"));
					txthsqldbPassword.setText(lp.get("password", ""));
				}
                txthsqldbDatabase.focusedProperty().addListener( new ChangeListener<>() {
                    @Override
                    public void changed(ObservableValue obs, Boolean oldVal, Boolean newVal)
                    {
                        if (oldVal && !newVal)
                        {
                            LOGGER.debug("hsqldb database name lost focus");
                            enableHsqldbCreation(!checkHSQLDBExists(txthsqldbDirectory.getText(), txthsqldbDatabase.getText())); 
                        }
                    }
                });
        
                enableHsqldbCreation(!checkHSQLDBExists(txthsqldbDirectory.getText(), txthsqldbDatabase.getText()));        
            }
            else if ("MSSQLServer".equalsIgnoreCase(key))
            {
                mssqlserverTab.setDisable(false);
				chkMSSQLServerUse.setSelected(selectedServer.equalsIgnoreCase("MSSQLServer"));
                //  2.6.1   don't allow DB change other than at login
                chkMSSQLServerUse.setDisable(!calledAsLogin);
                if (selectedServer.equalsIgnoreCase("MSSQLServer"))
                {
                    tabPane.getSelectionModel().select(mssqlserverTab);
					serverSelected = true;
                }
                
                Preferences lp = prefs.node("SQL/MSSQLServer");
				final boolean cachedVals = lp.getBoolean("cache", true);
				chkMSSQLServerCache.setSelected(cachedVals);
				if (cachedVals)
				{
					txtMSSQLServerHost.setText(lp.get("host", "localhost"));
					txtMSSQLServerPort.setText(lp.get("port", "1433"));
					txtMSSQLServerDatabase.setText(lp.get("database", "gardennotebook"));
					txtMSSQLServerUser.setText(lp.get("user", System.getProperty("user.name")));    //  2.6.1
					txtMSSQLServerPassword.setText(lp.get("password", ""));
                    txtMSSQLServerTimeout.getValueFactory().setValue(lp.getInt("timeout", 1));
				}
            }
        }

		//	if there's no server and no hsqldb, the installation has failed
		if (!hsqldbFound && !serverSelected)
		{
			LOGGER.error("No database server found, installation not usable");
			Alert errBox = new Alert(Alert.AlertType.ERROR, "No database server found, installation not usable", ButtonType.OK);
			errBox.showAndWait();
			Platform.runLater(() -> PanicHandler.panic(new IllegalStateException("No database server found, installation not usable")));
			return;
		}
		//	if this is the first time in, licence agreement is not required and no RDBMS is selected, use hsqldb
		if (firstTime && !serverSelected && prefs.getBoolean("licenceAgreed", false))
		{
			tabPane.getSelectionModel().select(hsqldbTab);
		}
		else if (firstTime && !serverSelected)
		{
			tabPane.getSelectionModel().select(licenceTab);
		}
    }	//	setupSQLServicesTabs()

	@FXML
	private void btnApplyOnAction(ActionEvent event) throws Exception
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnApplyOnAction(): login: {}", calledAsLogin);
		if (calledAsLogin)
		{
			if (!chkLicenceAgree.isSelected())
			{
				if (!licenceTab.isSelected())
				{
					tabPane.getSelectionModel().select(licenceTab);
//					lblMustAgree.setVisible(true);
					return;
				}
//				if (lblMustAgree.isVisible())
//				{// we've already been through here once, user obviously does not want to play
//					Platform.exit();
//				}
				tabPane.getSelectionModel().select(licenceTab);
//				lblMustAgree.setVisible(true);
				return;
			}
            //  2.6.1
            //  It's really easy to forget to click the 'use this?' button...
            if (!checkDBSwitchOK())
            {
                return;
            }
			handleApplyForLogin();
		}
		else
		{
			handleApplyConfig();
		}
		this.getScene().getWindow().hide();
	}
    
    //  2.6.1
    //  It's really easy to forget to click the 'use this?' button...
    private boolean checkDBSwitchOK()
    {
        if (    (mysqlTab.isSelected() && !chkMySQLUse.isSelected()) ||
                (mariaDBTab.isSelected() && !chkMariaDBUse.isSelected()) ||
                (hsqldbTab.isSelected() && !chkhsqldbUse.isSelected()) ||
                (mssqlserverTab.isSelected() && !chkMSSQLServerUse.isSelected()) )
        {
            LOGGER.debug("RDBMS tab selected but not checked for use");
            Alert checkDelete = new Alert(Alert.AlertType.CONFIRMATION, resources.getString("alert.config.confirmnodbchange"), ButtonType.NO, ButtonType.YES);
            Optional<ButtonType> result = checkDelete.showAndWait();
            LOGGER.debug("after delete dialog: result:{}, result.get:{}",result, result.orElse(null));
            if (result.isPresent() && result.get() == ButtonType.YES)
            {
                return false;
            }
        }
        return true;
    }

	@FXML
	private void btnCancelOnAction(ActionEvent event)
	{
		if (calledAsLogin)
		{
			Platform.exit();
		}
		this.getScene().getWindow().hide();
	}

	@FXML
	private void btnHelpOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnHelpOnAction()");
        
        final String focusedTabId = tabPane.getSelectionModel().getSelectedItem().getId();
        LOGGER.debug("focusedTabId: {}", focusedTabId);
        
		Parent root = new NotebookHelp("Configuration", focusedTabId);
		Scene scene = new Scene(root);
		Stage stage = new Stage();

        stage.setTitle(String.format(resources.getString("app.title"), NOTEBOOK_VERSION));  //  2.4.0
		stage.setScene(scene);
		stage.show();
	}

	@FXML
	private void btnResetOnAction(ActionEvent event)
	{

        clearPrefs(prefs);

		// since the user has decided to change all the DB settings, easiest to close down and restart
		// this can be done at any time so there could be catalogues, diary, etc fully populated but with no DB!
		Platform.exit();
	}
    private void clearPrefs(Preferences prefs)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("NotebookConfig: clearPrefs()");
        try 
        {
            prefs.removeNode();
            prefs.flush();
            prefs = null;
        } catch (BackingStoreException ex) {
            LOGGER.error("Failed to remove Preferences node");
            ex.printStackTrace();
        }
		LOGGER.traceExit(log4jEntryMsg);
    }

	/**
	 * Set the Directory to hold JSON files produced by the JSON dump functionality
	 * 
	 * @param event not used
	 */
	@FXML
	private void btnBrowseJSONDumpOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnBrowseJSONDumpOnAction(): initial dir: {}", txtJSONDumpDir.getText());
		
		DirectoryChooser dumpDirCh = new DirectoryChooser();
		File initialDir = new File(txtJSONDumpDir.getText());
		if (!initialDir.isDirectory())
		{
			try {
				Files.createDirectories(initialDir.toPath());
			} catch (IOException ex) {
				LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
			}
		}
		dumpDirCh.setInitialDirectory(new File(txtJSONDumpDir.getText()));
		File dumpDir = dumpDirCh.showDialog(this.getScene().getWindow());
		LOGGER.debug("got directory: {}", dumpDir);
		// null value can happen if the user just cancels
		if (dumpDir != null)
		{
			txtJSONDumpDir.setText(dumpDir.getPath());
            Preferences jp = prefs.node("JSON");
			if (!jp.get("dumpDir", "JSONDumpDir").equalsIgnoreCase(dumpDir.getPath()))
			{// selected directory has changed
				if (!dumpDir.isDirectory())
				{
					try {
						Files.createDirectories(dumpDir.toPath());
					} catch (IOException ex) {
						LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
					}
				}
			}
		}
		LOGGER.traceExit();
	}

	/*
	 * Set the Directory to hold JSON files used by the JSON load functionality
	 * 
	 * @param event 
	 */
	@FXML
	private void btnBrowseJSONLoadOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnBrowseJSONLoadOnAction(): initial dir: {}", txtJSONLoadDir.getText());
		
		DirectoryChooser dumpDirCh = new DirectoryChooser();
		File initialDir = new File(txtJSONLoadDir.getText());
		if (!initialDir.isDirectory())
		{
			try {
				Files.createDirectories(initialDir.toPath());
			} catch (IOException ex) {
				LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
			}
		}
		dumpDirCh.setInitialDirectory(initialDir);
		File dumpDir = dumpDirCh.showDialog(this.getScene().getWindow());
		LOGGER.debug("got directory: {}", dumpDir);
		// null value can happen if the user just cancels
		if (dumpDir != null)
		{
			txtJSONLoadDir.setText(dumpDir.getPath());
            Preferences jp = prefs.node("JSON");
			if (!jp.get("loadDir", "JSONLoadDir").equalsIgnoreCase(dumpDir.getPath()))
			{// selected directory has changed
				if (!dumpDir.isDirectory())
				{
					try {
						Files.createDirectories(dumpDir.toPath());
					} catch (IOException ex) {
						LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
					}
				}
			}
		}
		LOGGER.traceExit();
	}

	@FXML
	private void txtJSONDumpDirOnAction(ActionEvent event)
	{// user has overtyped the text field
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("txtJSONDumpDirOnAction(): text: {}", txtJSONDumpDir.getText());
		
        Preferences jp = prefs.node("JSON");
		if (!jp.get("dumpDir", "JSONDumpDir").equalsIgnoreCase(txtJSONDumpDir.getText()))
		{// selected directory has changed, possibly by overwriting the value in the text field
			File dumpDir = new File(txtJSONDumpDir.getText());
			if (!dumpDir.isDirectory())
			{
				try {
					Files.createDirectories(dumpDir.toPath());
				} catch (IOException ex) {
					LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
				}
			}
		}
		LOGGER.traceExit();
	}

	@FXML
	private void txtJSONLoadDirOnAction(ActionEvent event)
	{// user has overtyped the text field
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("txtJSONLoadDirOnAction(): text: {}", txtJSONLoadDir.getText());
		
        Preferences jp = prefs.node("JSON");
		if (!jp.get("loadDir", "JSONLoadDir").equalsIgnoreCase(txtJSONDumpDir.getText()))
		{// selected directory has changed, possibly by overwriting the value in the text field
			File dumpDir = new File(txtJSONLoadDir.getText());
			if (!dumpDir.isDirectory())
			{
				try {
					Files.createDirectories(dumpDir.toPath());
				} catch (IOException ex) {
					LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
				}
			}
		}
		LOGGER.traceExit();
	}
    
    @FXML
    private void chkMySQLUseOnAction(ActionEvent event)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("chkMySQLUseOnAction()");
        
        prefs.put("selectedTrug", "SQL");
        Preferences jp = prefs.node("SQL");
        if (chkMySQLUse.isSelected())
        {
            jp.put("selectedServer", "MySQL");
        }
        else if ("MySQL".equalsIgnoreCase(prefs.get("selectedServer", "")))
        {// it was previously selected, so clear it
            jp.put("selectedServer", "");
        }
    }

    @FXML
    private void chkMariaDBUseOnAction(ActionEvent event)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("chkMariaDBUseOnAction()");
        
        prefs.put("selectedTrug", "SQL");
        Preferences jp = prefs.node("SQL");
        if (chkMariaDBUse.isSelected())
        {
            jp.put("selectedServer", "MariaDB");
        }
        else if ("MariaDB".equalsIgnoreCase(prefs.get("selectedServer", "")))
        {// it was previously selected, so clear it
            jp.put("selectedServer", "");
        }
    }

    @FXML
    private void chkhsqldbUseOnAction(ActionEvent event)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("chkhsqldbUseOnAction()");
        
        prefs.put("selectedTrug", "SQL");
        Preferences jp = prefs.node("SQL");
        if (chkhsqldbUse.isSelected())
        {
            jp.put("selectedServer", "hsqldb");
        }
        else if ("hsqldb".equalsIgnoreCase(prefs.get("selectedServer", "")))
        {// it was previously selected, so clear it
            jp.put("selectedServer", "");
        }
    }

    @FXML
    private void chkMSSQLServerUseOnAction(ActionEvent event)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("chkMSSQLServerUseOnAction()");
        
        prefs.put("selectedTrug", "SQL");
        Preferences jp = prefs.node("SQL");
        if (chkMySQLUse.isSelected())
        {
            jp.put("selectedServer", "MSSQLServer");
        }
        else if ("MSSQLServer".equalsIgnoreCase(prefs.get("selectedServer", "")))
        {// it was previously selected, so clear it
            jp.put("selectedServer", "");
        }
    }

	/*
	 * Set the Directory to hold JSON files used by the JSON load functionality
	 * 
	 * @param event 
	 */
	@FXML
	private void btnBrowsehsqldbDirectoryOnAction(ActionEvent event)
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("btnBrowsehsqldbDirectoryOnAction(): initial dir: {}", txthsqldbDirectory.getText());
		
		DirectoryChooser dumpDirCh = new DirectoryChooser();
		File initialDir = new File(txthsqldbDirectory.getText());
		if (!initialDir.isDirectory())
		{
			try {
				Files.createDirectories(initialDir.toPath());
			} catch (IOException ex) {
				LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
			}
		}
		dumpDirCh.setInitialDirectory(initialDir);
		File dumpDir = dumpDirCh.showDialog(this.getScene().getWindow());
		LOGGER.debug("got directory: {}", dumpDir);
		// null value can happen if the user just cancels
		if (dumpDir != null)
		{
			txthsqldbDirectory.setText(dumpDir.getPath());
            Preferences jp = prefs.node("SQL/hsqldb");
			if (!jp.get("directory", defaultHsqldbLocation).equalsIgnoreCase(dumpDir.getPath()))
			{// selected directory has changed
				if (!dumpDir.isDirectory())
				{
					try {
						Files.createDirectories(dumpDir.toPath());
					} catch (IOException ex) {
						LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
					}
				}
			}
		}
        
        enableHsqldbCreation(!checkHSQLDBExists(txthsqldbDirectory.getText(), txthsqldbDatabase.getText()));
        
		LOGGER.traceExit();
	}

	@FXML
	private void txthsqldbDirectoryOnAction(ActionEvent event)
	{// user has overtyped the text field
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("txthsqldbDatabaseOnAction(): text: {}", txthsqldbDirectory.getText());
		
        Preferences jp = prefs.node("SQL/hsqldb");
		if (!jp.get("directory", defaultHsqldbLocation).equalsIgnoreCase(txthsqldbDirectory.getText()))
		{// selected directory has changed, possibly by overwriting the value in the text field
			File dumpDir = new File(txthsqldbDirectory.getText());
			if (!dumpDir.isDirectory())
			{
				try {
					Files.createDirectories(dumpDir.toPath());
				} catch (IOException ex) {
					LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
				}
			}
		}
        
        enableHsqldbCreation(!checkHSQLDBExists(txthsqldbDirectory.getText(), txthsqldbDatabase.getText()));
        
		LOGGER.traceExit();
	}
    
    private boolean checkHSQLDBExists(final String folder, final String dbname)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("checkHSQLDBExists(): folder: {}, dbname: {}", folder, dbname);
        
        File initDir = new File(folder);
        if (initDir == null)
            return false;
        var list = initDir.listFiles(dd-> dd.toPath().endsWith(dbname+".script"));
        if (list == null)
            return false;
        return LOGGER.traceExit((list.length > 0));
    }

    /*
     * Enable/disable the hsqldb creation buttons
     * 
     * @param enable true if the create radio buttons should be enabled
     */    
    private void enableHsqldbCreation(boolean enable)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("enableHsqldbCreation(): enable: {}", enable);

        chkhsqlCreateEmpty.setDisable(!enable);
        chkhsqlCreateEmpty.setSelected(false);
        chkhsqlCreateInit.setDisable(!enable);
        chkhsqlCreateInit.setSelected(false);
        
    }
    
    /*
     * Create a database called dbName in directory folder.
     * Copy the .properties and .script files
     * 
     * @param folder    the folder to hold the hsqldb database files
     * @param dbname    the name of the hsqldb database
     */
    private void createHsqldb(final String folder, final String dbname)
    {
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("createHsqldb(): folder: {}, dbname: {}", folder, dbname);
        if (chkhsqlCreateEmpty.isDisabled() || chkhsqlCreateInit.isDisabled() )
        {
            LOGGER.info("createHsqldb(): create buttons are disabled");
            return;
        }
        if (checkHSQLDBExists(folder, dbname))
        {
            LOGGER.trace("createHsqldb(): database already exists");
            return;
        }
        
        File targetDir = new File(folder);
		if (!targetDir.isDirectory())
		{
			try {
				Files.createDirectories(targetDir.toPath());
			} catch (IOException ex) {
				LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
			}
		}
        
        //  find the Path to the files held in resources 
		InputStream props = this.getClass().getResourceAsStream("/hsqldb/gardennotebook.properties");
        Path targetProps = Paths.get(folder, dbname+".properties");
        try {
            Files.copy(props, targetProps);
        } catch (IOException ex) {
            LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
        }
        if (chkhsqlCreateEmpty.isSelected())
        {
			InputStream script = this.getClass().getResourceAsStream("/hsqldb/empty_gardennotebook.script");
            Path targetScript = Paths.get(folder, dbname+".script");
            try {
                Files.copy(script, targetScript);
            } catch (IOException ex) {
                LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
            }
        }
        else if (chkhsqlCreateInit.isSelected())
        {
			InputStream script = this.getClass().getResourceAsStream("/hsqldb/init_gardennotebook.script");
            Path targetScript = Paths.get(folder, dbname+".script");
            try {
                Files.copy(script, targetScript);
            } catch (IOException ex) {
                LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
            }
        }
        else
        {
            LOGGER.info("createHsqldb(): no option selected");
        }
        
    }
    
	/*
	 * appropriate actions when this is being used as a log-in screen
	 */	
	private void handleApplyForLogin()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("handleApplyForLogin(): login: {}", calledAsLogin);

		prefs.putBoolean("licenceAgreed", chkLicenceAgree.isSelected());
		prefs.putBoolean("requireLogin", chkRequireLogin.isSelected());
		
		// cache the params so that they can be used to access the DB!
        if (!mysqlTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/MySQL");
            if (chkMySQLUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "MySQL");
            }
            lp.putBoolean("cached", chkMySQLCache.isSelected());
            lp.put("host", txtMySQLHost.getText());
            lp.put("port", txtMySQLPort.getText());
            lp.put("database", txtMySQLDatabase.getText());
            lp.put("user", txtMySQLUser.getText());
            lp.put("password", txtMySQLPassword.getText());
            lp.putInt("timeout", txtMySQLTimeout.getValue());
        }
        
        if (!mariaDBTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/MariaDB");
            if (chkMariaDBUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "MariaDB");
            }
            lp.putBoolean("cached", chkMariaDBCache.isSelected());
            lp.put("host", txtMariaDBHost.getText());
            lp.put("port", txtMariaDBPort.getText());
            lp.put("database", txtMariaDBDatabase.getText());
            lp.put("user", txtMariaDBUser.getText());
            lp.put("password", txtMariaDBPassword.getText());
            lp.putInt("timeout", txtMariaDBTimeout.getValue());
        }

        if (!hsqldbTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/hsqldb");
            if (chkhsqldbUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "hsqldb");
            }
            lp.putBoolean("cached", chkhsqldbCache.isSelected());
            lp.put("directory", txthsqldbDirectory.getText());
            lp.put("database", txthsqldbDatabase.getText());
            lp.put("user", txthsqldbUser.getText());
            lp.put("password", txthsqldbPassword.getText());
            if (chkhsqldbUse.isSelected() &&                                                        //  2.4.0
                !checkHSQLDBExists(txthsqldbDirectory.getText(), txthsqldbDatabase.getText()))
            {
                createHsqldb(txthsqldbDirectory.getText(), txthsqldbDatabase.getText());
            }
        }

        if (!mssqlserverTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/MSSQLServer");
            if (chkMSSQLServerUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "MSSQLServer");
            }
            lp.putBoolean("cached", chkMSSQLServerCache.isSelected());
            lp.put("host", txtMSSQLServerHost.getText());
            lp.put("port", txtMSSQLServerPort.getText());
            lp.put("database", txtMSSQLServerDatabase.getText());
            lp.put("user", txtMSSQLServerUser.getText());
            lp.put("password", txtMSSQLServerPassword.getText());
            lp.putInt("timeout", txtMSSQLServerTimeout.getValue());
        }
        
		// check the service is OK
		String checkVal = TrugServer.getTrugServer().checkTrug(prefs);
		LOGGER.debug("handleApplyForLogin(): check: "+checkVal);
		if (!"OK".equals(checkVal))
		{
			LOGGER.debug("handleApplyForLogin(): service check failed");
			Alert checkConnect = new Alert(Alert.AlertType.ERROR, checkVal, ButtonType.OK);
			checkConnect.setTitle(resources.getString("alert.config.cannotconnect"));
			Optional<ButtonType> result = checkConnect.showAndWait();
			return;
		}

        if (!chkMySQLCache.isSelected())
		{
            Preferences lp = prefs.node("SQL/MySQL");
			lp.remove("host");
			lp.remove("port");
			lp.remove("database");
			lp.remove("user");
			lp.remove("password");
            lp.remove("timeout");
 		}

        if (!chkMariaDBCache.isSelected())
		{
            Preferences lp = prefs.node("SQL/MariaDB");
			lp.remove("host");
			lp.remove("port");
			lp.remove("database");
			lp.remove("user");
			lp.remove("password");
            lp.remove("timeout");
 		}

        if (!chkhsqldbCache.isSelected())
		{
            Preferences lp = prefs.node("SQL/hsqldb");
//			lp.remove("host");
//			lp.remove("port");
			lp.remove("directory");
			lp.remove("database");
			lp.remove("user");
			lp.remove("password");
 		}

        if (!chkMSSQLServerCache.isSelected())
		{
            Preferences lp = prefs.node("SQL/MSSQLServer");
			lp.remove("host");
			lp.remove("port");
			lp.remove("database");
			lp.remove("user");
			lp.remove("password");
            lp.remove("timeout");
 		}

		// JSON configuration
        {
            configureJSON();
        }

		prefs.putBoolean("firstTime", false);

		Parent root = new GardenSplash();
		Scene scene = new Scene(root);
		Stage stage = new Stage();
		stage.initStyle(StageStyle.UNDECORATED);
		stage.setScene(scene);
		stage.sizeToScene();
		stage.show();
	}   //  handleApplyForLogin

    //  2.8.1
    private void configureJSON()
    {
        Preferences jp = prefs.node("JSON");
        if (!jp.get("dumpDir", "JSONDumpDir").equalsIgnoreCase(txtJSONDumpDir.getText()))
        {// selected directory has changed, possibly by overwriting the value in the text field
            File dumpDir = new File(txtJSONDumpDir.getText());
            if (!dumpDir.isDirectory())
            {
                try {
                    Files.createDirectories(dumpDir.toPath());
                } catch (IOException ex) {
                    LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
                }
            }
            jp.put("dumpDir", txtJSONDumpDir.getText());
        }
        
        if (!jp.get("loadDir", "JSONLoadDir").equalsIgnoreCase(txtJSONLoadDir.getText()))
        {// selected directory has changed, possibly by overwriting the value in the text field
            File dumpDir = new File(txtJSONLoadDir.getText());
            if (!dumpDir.isDirectory())
            {
                try {
                    Files.createDirectories(dumpDir.toPath());
                } catch (IOException ex) {
                    LOGGER.catching(org.apache.logging.log4j.Level.ERROR, ex);
                }
            }
            jp.put("loadDir", txtJSONLoadDir.getText());
        }
        jp.putBoolean("dumpOnExit", chkJsonOnExit.isSelected());
        jp.putInt("retainDumps", txtJSONRetainDumps.getValue());
    }
	
	/*
	 * appropriate actions when this is being used in-app to modify the configuration
	 */	
	private void handleApplyConfig()
	{
		EntryMessage log4jEntryMsg = LOGGER.traceEntry("handleApplyConfig(): login: {}", calledAsLogin);
		
		prefs.putBoolean("requireLogin", chkRequireLogin.isSelected());
        
        if (!mysqlTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/MySQL");
            if (chkMySQLUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "MySQL");
            }
            lp.putBoolean("cached", chkMySQLCache.isSelected());
            if (chkMySQLCache.isSelected())
            {
                lp.put("host", txtMySQLHost.getText());
                lp.put("port", txtMySQLPort.getText());
                lp.put("database", txtMySQLDatabase.getText());
                lp.put("user", txtMySQLUser.getText());
                lp.put("password", txtMySQLPassword.getText());
                lp.putInt("timeout", txtMySQLTimeout.getValue());
            }
        }
        
        if (!mariaDBTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/MariaDB");
            if (chkMariaDBUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "MariaDB");
            }

            lp.putBoolean("cached", chkMariaDBCache.isSelected());
            if (chkMariaDBCache.isSelected())
            {
                lp.put("host", txtMariaDBHost.getText());
                lp.put("port", txtMariaDBPort.getText());
                lp.put("database", txtMariaDBDatabase.getText());
                lp.put("user", txtMariaDBUser.getText());
                lp.put("password", txtMariaDBPassword.getText());
                lp.putInt("timeout", txtMariaDBTimeout.getValue());
            }
        }

        if (!hsqldbTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/hsqldb");
            if (chkhsqldbUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "hsqldb");
            }

            lp.putBoolean("cached", chkhsqldbCache.isSelected());
            if (chkhsqldbCache.isSelected())
            {
                lp.put("directory", txthsqldbDirectory.getText());
                lp.put("database", txthsqldbDatabase.getText());
                lp.put("user", txthsqldbUser.getText());
                lp.put("password", txthsqldbPassword.getText());
            }
            if (!checkHSQLDBExists(txthsqldbDirectory.getText(), txthsqldbDatabase.getText()))
            {
                createHsqldb(txthsqldbDirectory.getText(), txthsqldbDatabase.getText());
            }
        }

        if (!mssqlserverTab.isDisabled())
        {
            Preferences lp = prefs.node("SQL/MSSQLServer");
            if (chkMSSQLServerUse.isSelected())
            {
                prefs.put("selectedTrug", "SQL");
                prefs.node("SQL").put("selectedServer", "MSSQLServer");
            }
            lp.putBoolean("cached", chkMSSQLServerCache.isSelected());
            if (chkMSSQLServerCache.isSelected())
            {
                lp.put("host", txtMSSQLServerHost.getText());
                lp.put("port", txtMSSQLServerPort.getText());
                lp.put("database", txtMSSQLServerDatabase.getText());
                lp.put("user", txtMSSQLServerUser.getText());
                lp.put("password", txtMSSQLServerPassword.getText());
                lp.putInt("timeout", txtMSSQLServerTimeout.getValue());
            }
        }
        
        // JSON configuration
        configureJSON();

	}   //  handleApplyConfig
	
}
