/*
 * Controller.fx
 */

package cookery;

import java.lang.System;
import java.io.File;


import javafx.util.Properties;
import java.io.FileInputStream;

import org.apache.commons.io.FileUtils;

import cookery_db.database.Recipe;
import cookery_db.database.RecipeIngredient;

import cookery_db.database.Category;


import java.util.Collection;

import cookery_db.TypeConverter;

import java.io.FileOutputStream;

import cookery_db.database.Ingredient;

import cookery_db.database.CookMethod;

import cookery_db.database.QuantityUnit;

import cookery_db.DerbyDBBackup;

import cookery_db.CookBookDBManager;
import java.lang.NumberFormatException;

/**
 * Handles all logic in between the view and the model. This logic includes:
 * <ul>
 *   <li>Opening and closing screens</li>
 *   <li>General actions which require interaction between the view and model</li>
 *   <li>Saving and restoring user settings</li>
 *   <li>Setting up default user settings (if they do not exist)</li>
 *   <li>Application startup and shutdown phases</li>
 * </ul>
 * @author Nick Apperley
 */
public class Controller {
    def PROGRAM_DIR = "/usr/local/Cookery";
    def USER_DIR = System.getProperty("user.home");
    def USER_SETTINGS_DIR = "{USER_DIR}/.Cookery";
    def dbManager = new CookBookDBManager();
    var stepsScreen: StepsScreen;
    var categoriesScreen: CategoriesScreen;
    var optionsScreen: OptionsScreen;
    var ingredientsScreen: IngredientsScreen;
    var recipeIngredientsScreen: RecipeIngredientsScreen;
    var mainScreen: MainScreen;

    /**
     * Creates some default user settings if they do not exist.
    */
    function setupUserSettings(): Void{
        def mainScreenProperties = new File("{USER_SETTINGS_DIR}/mainScreen.properties");
        def appProperties = new File("{USER_SETTINGS_DIR}/cookery.properties");
        def DB_DIR = "{USER_SETTINGS_DIR}/database";
        def SETTINGS_DIR = "{PROGRAM_DIR}/settings";

        if(new File(USER_SETTINGS_DIR).exists() == false){
            new File(USER_SETTINGS_DIR).mkdir();
            new File(DB_DIR).mkdir();
        }
        if(mainScreenProperties.exists() == false){
            FileUtils.copyFileToDirectory(new File("{SETTINGS_DIR}/{mainScreenProperties.getName()}"),
                new File(USER_SETTINGS_DIR));
        }
        if(appProperties.exists() == false){
            FileUtils.copyFileToDirectory(new File("{SETTINGS_DIR}/{appProperties.getName()}"),
                new File(USER_SETTINGS_DIR));
        }
    }

    /**
     * Handles the application shutdown phase.
    */
    function shutdown(): Void{
        dbManager.close();
    }

    /**
     * Generates some table headers for use in a visual table.
     @return Some table header names from a properties file.
    */
    function tableHeaders(){
        def HEADER = "Header";
        def headerNames = ["recipe", "cookMethod", "ingredients", "difficulty", "author"];
        var headers: String[];
        def properties = Properties{};
        def INPUT_FILE_PATH = "{USER_SETTINGS_DIR}/mainScreen.properties";

        properties.load(new FileInputStream(INPUT_FILE_PATH));
        for(headerName in headerNames){
            insert "{properties.get("{headerName}{HEADER}")}" into headers;
        }
        return headers;
    }

    /**
     * Loads all application settings.
     @return A properties object that contains the application settings to load from.
    */
    function loadApplicationOptions(): Properties{
        def properties = Properties{};
        def INPUT_FILE_PATH = "{USER_SETTINGS_DIR}/cookery.properties";

        properties.load(new FileInputStream(INPUT_FILE_PATH));
        return properties;
    }

    /**
     * Saves all application settings.
     @param properties The properties object where the application settings are being saved to.
    */
    function saveApplicationOptions(properties: Properties): Void{
        def OUTPUT_FILE_PATH = "{USER_SETTINGS_DIR}/cookery.properties";

        if (properties != null) {
            properties.store(new FileOutputStream(OUTPUT_FILE_PATH));
        }
    }

    /**
     * Retrieves all recipes from the database.
     @return All recipes (if there are any).
    */
    function allRecipes(): Recipe[]{
        return dbManager.allRecipes();
    }

    /**
     * Gets a specific recipe from the database.
     @param recipeName Name of the recipe to get from the database.
     @return A single recipe if it exists.
    */
    function getRecipe(recipeName: String): Recipe{
        return dbManager.getRecipe(recipeName);
    }

    /**
     * Retrieves all cook methods from the database.
     @return Some cook methods (if there are any).
    */
    function allCookMethods(): CookMethod[]{
        return dbManager.allCookMethods();
    }

    /**
     * Gets a specific cook method from the database.
     @param cookMethodName Name of the cook method to get from the database.
     @return A single cook method if it exists.
    */
    function getCookMethod(cookMethodName: String){
        return dbManager.getCookMethod(cookMethodName);
    }

    /**
     * Retrieves all ingredients from the database.
     @return Some ingredients (if there are any).
    */
    function allIngredients(): Ingredient[]{
        return dbManager.allIngredients();
    }

    /**
     * Gets a specific ingredient from the database.
     @param ingredientName Name of the ingredient to get from the database.
     @return A single ingredient if it exists.
    */
    function getIngredient(ingredientName: String){
        return dbManager.getIngredient(ingredientName);
    }

    /**
     * Retrieves all categories from the database.
     @return Some categories (if there are any).
    */
    function allCategories(): Category[]{
        return dbManager.allCategories();
    }

    /**
     * Gets a specific category from the database.
     @param catName Name of the category to get from the database.
     @return A single category if it exists.
    */
    function getCategory(catName: String): Category{
        return dbManager.getCategory(catName);
    }

    /**
     * Retrieves all quantity units from the database.
     @return Some quantity units (if there are any).
    */
    function allQuantityUnits(): QuantityUnit[]{
        return dbManager.allQuantityUnits();
    }

    /**
     * Gets a specific quantity unit from the database.
     @param unit Name of the quantity unit to get from the database.
     @return A single quantity unit if it exists.
    */
    function getQuantityUnit(unit: String): QuantityUnit{
        return dbManager.getQuantityUnit(unit);
    }

    /**
     * Creates the steps screen (if it does not exist), otherwise
     * it is brought back into focus.
     @param recipe The recipe that contains some steps.
    */
    function createStepsScreen(recipe: Recipe): Void{
        if(stepsScreen == null){
            stepsScreen = StepsScreen{
                recipe: recipe
                controller: this
            };
        }
        else{
            stepsScreen.iconified = false;
            stepsScreen.toFront();
        }
    }

    /**
     * Removes the steps screen.
    */
    function removeStepsScreen(): Void{
        stepsScreen.close();
        stepsScreen = null;
    }

    /**
     * Creates the options screen (if it does not exist), otherwise
     * it is brought back into focus.
     @param categories All of the available categories from the database.
    */
    function createOptionsScreen(categories: Category[]): Void{
        if(optionsScreen == null){
            optionsScreen = OptionsScreen{
                categories: categories
                controller: this
            };
        }
        else{
            optionsScreen.iconified = false;
            optionsScreen.toFront();
        }
    }

    /**
     * Removes the options screen.
    */
    function removeOptionsScreen(): Void{
        optionsScreen.close();
        optionsScreen = null;
    }

    /**
     * Creates the categories screen (if it does not exist), otherwise
     * it is brought back into focus.
    */
    function createCategoriesScreen(): Void{
        if(categoriesScreen == null){
            categoriesScreen = CategoriesScreen{
                controller: this
                onCategoriesChanged: function(cse){
                    if(cse.categoryAdded){
                        mainScreen.addCategory(cse.category);
                    }
                    else{
                        mainScreen.removeCategory(cse.category);
                    }
                }
            };
        }
        else{
            categoriesScreen.iconified = false;
            categoriesScreen.toFront();
        }
    }

    /**
     * Removes the categories screen.
    */
    function removeCategoriesScreen(): Void{
        categoriesScreen.close();
        categoriesScreen = null;
    }

    /**
     * Creates the ingredients screen (if it does not exist), otherwise
     * it is brought back into focus.
    */
    function createIngredientsScreen(): Void{
        if(ingredientsScreen == null){
            ingredientsScreen = IngredientsScreen{
                controller: this
                onIngredientsChange: function(ise){
                    if(ise.ingredientAdded){
                        recipeIngredientsScreen.addIngredient(ise.ingredient);
                    }
                    else{
                        recipeIngredientsScreen.removeIngredient(ise.ingredient);
                    }
                }
            };
        }
        else{
            ingredientsScreen.iconified = false;
            ingredientsScreen.toFront();
        }
    }

    /**
     * Removes the ingredients screen.
    */
    function removeIngredientsScreen(): Void{
        ingredientsScreen.close();
        ingredientsScreen = null;
    }

    /**
     * Creates the recipe ingredients screen (if it does not exist), otherwise
     * it is brought back into focus.
     @param recipe The current recipe to operate on.
    */
    function createRecipeIngredientsScreen(recipe: Recipe): Void{
        if(recipeIngredientsScreen == null){
            recipeIngredientsScreen = RecipeIngredientsScreen{
                controller: this
                recipe: recipe
                onRecipeIngredientsChanged: function(rise){
                    mainScreen.refreshRecipePanel();
                }
            };
        }
        else{
            recipeIngredientsScreen.iconified = false;
            recipeIngredientsScreen.toFront();
        }
    }

    /**
     * Removes the recipe ingredients screen.
    */
    function removeRecipeIngredientsScreen(): Void{
        recipeIngredientsScreen.close();
        recipeIngredientsScreen = null;
    }

    /**
     * Adds a new record to the database.
     @param newRecord The new record to add.
    */
    function addRecord(newRecord): Void{
        dbManager.addRecord(newRecord);
    }

    /**
     * Updates an existing record in the database.
     @param record The record to update.
    */
    function updateRecord(record): Void{
        dbManager.updateRecord(record);
    }

    /**
     * Removes a record from the database.
     @param record The record to remove.
    */
    function removeRecord(record): Void{
        dbManager.removeRecord(record);
    }

    /**
     * Converts a recipe ingredient collection to an array.
     @param recipeIngredientCol The recipe ingredient collection to convert.
     @return A recipe ingredient array.
    */
    function recipeIngredientCollectionToArray(recipeIngredientCol: Collection): RecipeIngredient[]{
        var result: RecipeIngredient[];
        
        if(recipeIngredientCol != null){
            result = TypeConverter.toRecipeIngredientArray(recipeIngredientCol);
        }
        return result;
    }

    /**
     * Opens a screen.
     @param screenName Name of the screen to open.
     @param args Some arguments to handle when opening the screen.
    */
    public function openScreen(screenName: String, args: Object[]): Void{
        if(screenName == "Ingredients"){
            createIngredientsScreen();
        }
        else if(screenName == "Steps"){
            createStepsScreen((args[0] as Recipe));
        }
        else if(screenName == "Options"){
            createOptionsScreen(allCategories());
        }
        else if(screenName == "Categories"){
            createCategoriesScreen();
        }
        else if(screenName == "Recipe Ingredients"){
            createRecipeIngredientsScreen((args[0] as Recipe));
        }
    }

    /**
     * Closes a screen.
     @param screenName Name of the screen to close.
    */
    public function closeScreen(screenName: String): Void{
        if(screenName == "Recipe Ingredients"){
            removeRecipeIngredientsScreen();
        }
        else if(screenName == "Steps"){
            removeStepsScreen();
        }
        else if(screenName == "Options"){
            removeOptionsScreen();
        }
        else if(screenName == "Categories"){
            removeCategoriesScreen();
        }
        else if(screenName == "Ingredients"){
            removeIngredientsScreen();
        }
    }

    /**
     * Executes a general action.
     @param action Name of the action to execute.
     @param args Some arguments to handle when executing the action.
     @return The result of executing the action.
    */
    public function performAction(action: String, args: Object[]): Object[]{
        var result: Object[];

        if(action == "All recipes"){
            result = allRecipes();
        }
        else if(action == "Get recipe"){
            result = getRecipe((args[0] as String));
        }
        else if(action == "All cook methods"){
            result = allCookMethods();
        }
        else if(action == "Get cook method"){
            result = getCookMethod((args[0] as String));
        }
        else if(action == "All ingredients"){
            result = allIngredients();
        }
        else if(action == "Get ingredient"){
            result = getIngredient((args[0] as String));
        }
        else if(action == "All categories"){
            result = allCategories();
        }
        else if(action == "Get category"){
            result = getCategory((args[0] as String));
        }
        else if(action == "All quantity units"){
            result = allQuantityUnits();
        }
        else if(action == "Get quantity unit"){
            result = getQuantityUnit((args[0] as String));
        }
        else if(action == "Get table headers"){
            result = tableHeaders();
        }
        else if(action == "Add record"){
            addRecord(args[0]);
        }
        else if(action == "Update record"){
            updateRecord(args[0]);
        }
        else if(action == "Remove record"){
            removeRecord(args[0]);
        }
        else if(action == "Get application options"){
            result = loadApplicationOptions();
        }
        else if(action == "Save application options"){
            saveApplicationOptions((args[0] as Properties));
        }
        else if(action == "Convert recipe ingredient collection to array"){
            result = recipeIngredientCollectionToArray((args[0] as Collection))
        }
        else if(action == "Start DB backups"){
            startDBBackups((Utility.minutesToMilliSeconds((args[0]) as Integer) as Long));
        }
        else if(action == "Stop DB backups"){
            stopDBBackups();
        }

        return result;
    }

    /**
     Sets the main screen for the application.
     @param mainScreen The main screen of the application.
    */
    public function setMainScreen(mainScreen: MainScreen): Void{
        if(this.mainScreen == null){
            this.mainScreen = mainScreen;
        }
    }

    function startDBBackups(backupInterval: Long){
        dbManager.startScheduledBackups(backupInterval);
    }

    function stopDBBackups(){
        dbManager.stopScheduledBackups();
    }

    init{
        // How often to make backups (in minutes).
        def DEFAULT_BACKUP_INTERVAL = 10;
        def BACKUP_NAME = "CookBook Backup";
        def DB_NAME = "CookBook";
        def DB_ADDR = "{USER_SETTINGS_DIR}/database";
        def BACKUP_LOCATION = "{DB_ADDR}";
        def BACKUP_DB_KEY = "backupDb";
        def BACKUP_INTERVAL_KEY = "backupInterval";
        def appSettings = loadApplicationOptions();
        def dbBackup = new DerbyDBBackup(BACKUP_NAME, BACKUP_LOCATION, DB_NAME, DB_ADDR);
        def MEM_USAGE_THRESHOLD = 0.9;
        var backupDB: Boolean = false;
        var backupInterval: Long = DEFAULT_BACKUP_INTERVAL;
        var minutes: Integer;

        if(appSettings.get(BACKUP_DB_KEY) != null){
            backupDB = Boolean.parseBoolean(appSettings.get(BACKUP_DB_KEY));
        }
        try{
            minutes = Integer.parseInt(appSettings.get(BACKUP_INTERVAL_KEY));
            backupInterval = Long.valueOf(Utility.minutesToMilliSeconds(minutes));
        }
        catch(ex: NumberFormatException){
            backupInterval = Utility.minutesToMilliSeconds(DEFAULT_BACKUP_INTERVAL);
        }

        FX.addShutdownAction(shutdown);
        MemoryWarningSystem.initialise();
        MemoryWarningSystem.addListener(LowMemoryHandler{});
        MemoryWarningSystem.setPercentageUsageThreshold(MEM_USAGE_THRESHOLD);
        setupUserSettings();
        
        dbManager.setDbBackup(dbBackup);
        if(backupDB){
            startDBBackups(backupInterval);
        }
    }
}
