package cookery_db;

import cookery_db.database.Category;
import cookery_db.database.CookMethod;
import cookery_db.database.Ingredient;
import cookery_db.database.QuantityUnit;
import cookery_db.database.Recipe;
import java.io.File;
import java.io.IOException;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;
import javax.persistence.Query;
import oracle.toplink.essentials.config.TopLinkProperties;

/**
 * Manages all database related operations for the CookBook database.
 * @author Nick Apperley
 */
public class CookBookDBManager
{
    private FileHandler logFile = null;
    private EntityManagerFactory emf = null;
    private Timer dbBackupScheduler = null;
    private DBBackup dbBackup = null;

    /**
     * Creates a new CookBookDBManager object.
     */
    public CookBookDBManager()
    {
        setupLogFile();
        open();
    }

    /**
     * Opens a connection to the CookBook database.
     */
    private void open()
    {
        HashMap properties = new HashMap();
        final String JDBC_URL = "jdbc:derby:" + System.getProperty("user.home") +
                File.separator + ".Cookery" + File.separator + "database" +
                File.separator + "CookBook";

        try
        {
            properties.put(TopLinkProperties.JDBC_URL, JDBC_URL);
            emf = Persistence.createEntityManagerFactory(
                    "Cookery_DBPU", properties);
        } catch (Exception ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.SEVERE, "Cannot open CookBook database", ex);
            logFile.publish(
                    new LogRecord(Level.SEVERE, "Unable to establish a " +
                    "connection to the CookBook database (does the database " +
                    "exist?)"));
        }
    }

    /**
     * Initialises the log file.
     */
    private void setupLogFile()
    {
        final boolean NEW_LOG_FILE = false;
        final String LOG_FILE_PATTERN = "Cookery Log";

        try
        {
            logFile = new FileHandler(LOG_FILE_PATTERN, NEW_LOG_FILE);
        } catch (IOException ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.WARNING, "Unable to create Cookery log file", ex);
        } catch (SecurityException ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.WARNING, "Unable to create Cookery log file due " +
                    "to security restrictions", ex);
        }
    }

    /**
     * Sets up the database backup.
     * @param dbBackup The database backup to use.
     */
    public void setDbBackup(DBBackup dbBackup)
    {
        // Stop all scheduled backups if no database backup has been set up.
        if (dbBackup == null)
        {
            stopScheduledBackups();
        }
        this.dbBackup = dbBackup;
    }

    /**
     * Begins the scheduling of backups for the database.
     * @param backupInterval How often to backup the DB (in milli seconds)
     */
    public void startScheduledBackups(Long backupInterval)
    {
        if (dbBackupScheduler == null && dbBackup != null)
        {
            dbBackupScheduler = new Timer(
                    dbBackup.getDatabaseName() + "DB Backup");
            dbBackupScheduler.scheduleAtFixedRate(
                    (TimerTask) dbBackup, backupInterval, backupInterval);
        }
    }

    /**
     * Ends the scheduling of backups for the database.
     */
    public void stopScheduledBackups()
    {
        if (dbBackupScheduler != null)
        {
            dbBackupScheduler.cancel();
            dbBackupScheduler = null;
        }
    }

    /**
     * Adds a new record to the database.
     * @param newRecord The new record to add.
     */
    public void addRecord(Object newRecord)
    {
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        try
        {
            em.persist(newRecord);
            em.getTransaction().commit();
        } catch (Exception ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.SEVERE, "Cannot add record", ex);
            logFile.publish(
                    new LogRecord(Level.SEVERE, "Cannot add record"));
            em.getTransaction().rollback();
        } finally
        {
            em.close();
        }
    }

    /**
     * Removes an existing record from the database.
     * @param record The record to remove.
     */
    public void removeRecord(Object record)
    {
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        try
        {
            record = em.merge(record);
            em.remove(record);
            em.getTransaction().commit();
        } catch (Exception ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.SEVERE, "Cannot remove record", ex);
            logFile.publish(
                    new LogRecord(Level.SEVERE, "Cannot remove record"));
            em.getTransaction().rollback();
        } finally
        {
            em.close();
        }
    }

    /**
     * Updates an existing record in the database.
     * @param record The updated record.
     */
    public void updateRecord(Object record)
    {
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();
        try
        {
            em.merge(record);
            em.getTransaction().commit();
        } catch (Exception ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.SEVERE, "Cannot update record", ex);
            logFile.publish(
                    new LogRecord(Level.SEVERE, "Cannot update record"));
            em.getTransaction().rollback();
        } finally
        {
            em.close();
        }
    }

    /**
     * Gets all the categories from the database.
     * @return Null if there are no categories.
     */
    public Category[] allCategories()
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "Category.findAll";
        List queryResult = null;
        Category[] categories = null;

        Query query = em.createNamedQuery(QUERY);
        queryResult = query.getResultList();
        if (queryResult.isEmpty() != true)
        {
            categories = new Category[queryResult.size()];
            categories = (Category[]) queryResult.toArray(categories);
        }

        em.close();
        return categories;
    }

    /**
     * Gets all the cook methods from the database.
     * @return Null if there are no cook methods.
     */
    public CookMethod[] allCookMethods()
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "CookMethod.findAll";
        List queryResult = null;
        CookMethod[] cookMethods = null;

        Query query = em.createNamedQuery(QUERY);
        queryResult = query.getResultList();
        if (queryResult.isEmpty() != true)
        {
            cookMethods = new CookMethod[queryResult.size()];
            cookMethods = (CookMethod[]) queryResult.toArray(cookMethods);
        }

        em.close();
        return cookMethods;
    }

    /**
     * Gets a single cook method from the database.
     * @param cookMethodName The name of the cook method to retrieve.
     * @return Null if the cook method does not exist.
     */
    public CookMethod getCookMethod(String cookMethodName)
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "CookMethod.findByName";
        CookMethod queryResult = null;
        Query query = em.createNamedQuery(QUERY);

        query.setParameter("name", cookMethodName);
        try
        {
            queryResult = (CookMethod) query.getSingleResult();
        } catch (NoResultException ex)
        {
            // Ignoring the exception because returning a null indicates
            // nothing has been found.
        }

        em.close();
        return queryResult;
    }

    /**
     * Gets all the recipes from the database.
     * @return Null if there are no recipes.
     */
    public Recipe[] allRecipes()
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "Recipe.findAll";
        List queryResult = null;
        Recipe[] recipes = null;

        Query query = em.createNamedQuery(QUERY);
        queryResult = query.getResultList();
        if (queryResult.isEmpty() != true)
        {
            recipes = new Recipe[queryResult.size()];
            recipes = (Recipe[]) queryResult.toArray(recipes);
        }

        em.close();
        return recipes;
    }

    /**
     * Gets a single recipe from the database.
     * @param recipeName The name of the recipe to retrieve.
     * @return Null if the recipe does not exist.
     */
    public Recipe getRecipe(String recipeName)
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "Recipe.findByName";
        Recipe queryResult = null;
        Query query = em.createNamedQuery(QUERY);

        query.setParameter("name", recipeName);

        try
        {
            queryResult = (Recipe) query.getSingleResult();
        } catch (NoResultException ex)
        {
            // Ignoring the exception because returning a null indicates
            // nothing has been found.
        }

        em.close();
        return queryResult;
    }

    /**
     * Gets all the ingredients from the database.
     * @return Null if there are no ingredients.
     */
    public Ingredient[] allIngredients()
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "Ingredient.findAll";
        List queryResult = null;
        Ingredient[] ingredients = null;

        Query query = em.createNamedQuery(QUERY);
        queryResult = query.getResultList();
        if (queryResult.isEmpty() != true)
        {
            ingredients = new Ingredient[queryResult.size()];
            ingredients = (Ingredient[]) queryResult.toArray(ingredients);
        }

        em.close();
        return ingredients;
    }

    /**
     * Gets a single ingredient from the database.
     * @param ingredientName The name of the ingredient to retrieve.
     * @return Null if the ingredient does not exist.
     */
    public Ingredient getIngredient(String ingredientName)
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "Ingredient.findByName";
        Ingredient queryResult = null;
        Query query = em.createNamedQuery(QUERY);

        query.setParameter("name", ingredientName);

        try
        {
            queryResult = (Ingredient) query.getSingleResult();
        } catch (NoResultException ex)
        {
            // Ignoring the exception because returning a null indicates
            // nothing has been found.
        }

        em.close();
        return queryResult;
    }

    /**
     * Gets a single category from the database.
     * @param catName The name of the category to retrieve.
     * @return Null if the category does not exist.
     */
    public Category getCategory(String catName)
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "Category.findByName";
        Category queryResult = null;
        Query query = em.createNamedQuery(QUERY);

        query.setParameter("name", catName);

        try
        {
            queryResult = (Category) query.getSingleResult();
        } catch (NoResultException ex)
        {
            // Ignoring the exception because returning a null indicates
            // nothing has been found.
        }

        em.close();
        return queryResult;
    }

    /**
     * Gets all the quantity units from the database.
     * @return Null if there are no quantity units.
     */
    public QuantityUnit[] allQuantityUnits()
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "QuantityUnit.findAll";
        List queryResult = null;
        QuantityUnit[] quantityUnits = null;

        Query query = em.createNamedQuery(QUERY);
        queryResult = query.getResultList();
        if (queryResult.isEmpty() != true)
        {
            quantityUnits = new QuantityUnit[queryResult.size()];
            quantityUnits = (QuantityUnit[]) queryResult.toArray(quantityUnits);
        }

        em.close();
        return quantityUnits;
    }

    /**
     * Gets a single quantity unit from the database.
     * @param unit The name of the unit to retrieve.
     * @return Null if the quantity unit does not exist.
     */
    public QuantityUnit getQuantityUnit(String unit)
    {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        String QUERY = "QuantityUnit.findByUnit";
        QuantityUnit queryResult = null;
        Query query = em.createNamedQuery(QUERY);

        query.setParameter("unit", unit);

        try
        {
            queryResult = (QuantityUnit) query.getSingleResult();
        } catch (NoResultException ex)
        {
            // Ignoring the exception because returning a null indicates
            // nothing has been found.
        }

        em.close();
        return queryResult;
    }

    /**
     * Closes the connection to the Cookery database. This means that a new
     * instance of CookBookDBManager will need to be created in order to access
     * the database again.
     */
    public void close()
    {
        String DB_URL = "jdbc:derby:" + System.getProperty("user.home") +
                File.separator + ".Cookery" + File.separator +
                "database" + File.separator + "CookBook;" + "shutdown=true";

        stopScheduledBackups();
        emf.close();
        try
        {
            DriverManager.getConnection(DB_URL);
        } catch (SQLNonTransientConnectionException ex)
        {
            // Ignore this exception since it is normal for Derby to
            // throw this exception when the DB is being shutdown.
        } catch (SQLException ex)
        {
            Logger.getLogger(CookBookDBManager.class.getName()).log(
                    Level.SEVERE, null, ex);
        }
    }
}
