/**
 * Copyright (C) 2019 Patrik Dufresne Service Logiciel <info@patrikdufresne.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package com.planimod.ui;

import static com.planimod.test.TimeUtils.dateTime;
import static com.planimod.ui.Localized._;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.widgets.Display;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.patrikdufresne.jface.dialogs.DetailMessageDialog;
import com.patrikdufresne.managers.H2DBConfigurations;
import com.patrikdufresne.managers.ManagerException;
import com.planimod.core.Employee;
import com.planimod.core.PlanimodManagers;
import com.planimod.core.Position;
import com.planimod.core.Product;
import com.planimod.core.ProductPosition;
import com.planimod.core.Section;

/**
 * This class is the controller to select the proper database. Either to select an exiting database or to create a new
 * database with demo data.
 * 
 * @author Patrik Dufresne
 *
 */
public class DatabaseSelector {

    private static Logger logger = LoggerFactory.getLogger(DatabaseSelector.class);

    private static Employee addEmployee(PlanimodManagers managers, String refId, String firstname, String lastname, Date hireDate) throws ManagerException {
        Employee employee = new Employee();
        employee.setRefId(refId);
        employee.setFirstname(firstname);
        employee.setLastname(lastname);
        employee.setHireDate(hireDate);
        managers.getEmployeeManager().add(Arrays.asList((employee)));
        return employee;
    }

    private static Position addPosition(PlanimodManagers managers, Section section, String name, boolean classified, boolean swappable)
            throws ManagerException {
        // Add a position
        Position p = new Position();
        p.setSection(section);
        p.setName(name);
        p.setClassified(classified);
        p.setSwappable(swappable);
        managers.getPositionManager().add(Arrays.asList(p));
        return p;
    }

    private static Product addProduct(PlanimodManagers managers, String refId, String name, String familly) throws ManagerException {
        Product p = new Product();
        p.setName(name);
        if (refId != null) {
            p.setRefId(refId);
        }
        if (familly != null) {
            p.setFamily(familly);
        }
        managers.getProductManager().add(Arrays.asList(p));
        return p;
    }

    /**
     * Add multiple product position for each given position.
     * 
     * @param managers
     * @param product
     * @param positions
     * @return
     * @throws ManagerException
     */
    private static void addProductPosition(PlanimodManagers managers, Product product, Position... positions) throws ManagerException {
        Map<Position, Integer> table = new HashMap<Position, Integer>();
        for (Position pos : positions) {
            Integer count = table.get(pos);
            table.put(pos, Integer.valueOf(count != null ? count.intValue() + 1 : 1));
        }
        List<ProductPosition> list = new ArrayList<ProductPosition>();
        for (Entry<Position, Integer> e : table.entrySet()) {
            ProductPosition proPos = new ProductPosition();
            proPos.setProduct(product);
            proPos.setPosition(e.getKey());
            proPos.setNumber(e.getValue());
            list.add(proPos);
        }
        managers.getProductPositionManager().add(list);
    }

    /**
     * Add a section to the database and return the newly created object.
     * 
     * @param managers
     * @return
     * @throws ManagerException
     */
    private static Section addSection(PlanimodManagers managers, String name) throws ManagerException {
        Section section = new Section();
        section.setName(name);
        managers.getSectionManager().add(Arrays.asList((section)));
        return section;
    }

    /**
     * Return true if the given exception is raised because the database is locked by another process.
     * 
     * @param exception
     * @return
     */
    private static boolean isLockedException(Exception exception) {
        Throwable cause = exception;
        while (cause != null) {
            if (cause.getMessage().contains("Locked by another process") || cause.getMessage().contains("verrouillé")) {
                return true;
            }
            cause = cause.getCause();
        }
        return false;
    }

    public DatabaseSelector() {
        // Nothing to do.
    }

    /**
     * Create a new database.
     */
    private PlanimodManagers createDatabase(String url, boolean demo) {
        logger.info("creating database: {}", url);
        // If the user only provided a database name without absolute path, let create the database in user home.
        try {
            PlanimodManagers managers = new PlanimodManagers(H2DBConfigurations.create(url, true, false));
            if (demo) {
                populateDatabase(managers);
            }
            return managers;
        } catch (Exception e) {
            logger.error("error creating the database " + url, e);
            handleOpenDatabaseException(e);
            return null;
        }

    }

    /**
     * Called to display the right error message to the user for a given exception.
     * 
     * @param exception
     */
    private void handleOpenDatabaseException(Exception exception) {
        // To handle various exception message, we need to dig into the exception.
        Throwable cause = exception;
        while (cause != null) {
            if (cause.getMessage().contains("not found")) {
                // Display a generic message for other error.
                DetailMessageDialog
                        .openDetailWarning(
                                null,
                                Display.getAppName(),
                                _("La base de données sélectionnée n'existe pas."),
                                _("Vérifier que le nom de la base de données est correctement écrit et qu'il correspond bien à un nom de fichier existant."),
                                exception);
                return;
            }
            cause = cause.getCause();
        }
        // If nothing special is found, let display a generic message for other error.
        DetailMessageDialog
                .openDetailWarning(
                        null,
                        Display.getAppName(),
                        _("Une erreur s'est produite lors de l'ouverture de la base de données."),
                        _("Vérifiez qu'il est possible d'établir une communication avec l'ordinateur distante."),
                        exception);
    }

    /**
     * Check if the given exception is related to a failure to open the database in read-write mode. If it's the case,
     * prompt the user if we can open the database in read-only mode.
     * 
     * @param exception
     * 
     * @return True if the database should be open in read-only mode. False if the operation is cancel by user. null if the
     *         exception is not related to a readonly.
     */
    private Boolean handleReadonlyException(Exception exception) {
        // Try to determine if the exception is related to "ReadOnly" Access.
        // This occur when trying to open a readonly file with "servermode".
        if (isReadonlyExeption(exception)) {
            // Prompt user to open database in Read-only mode.
            DetailMessageDialog dlg = DetailMessageDialog
                    .openDetailYesNoQuestion(
                            null,
                            Display.getAppName(),
                            _("La base de données ne peut être ouverte en mode lecture-écriture. En ouvrir une copie ?"),
                            _(
                                    "Il se peut que vous n'ayez pas les droits suffisant pour ouvrir la base de données en mode lecture-écriture. Il vous est toutefois possible d'en ouvrir une copie. Toute modification apportée à cette copie ne peut être sauvegardée"),
                            null);
            return IDialogConstants.YES_ID == dlg.getReturnCode();
        }

        if (isLockedException(exception)) {
            // Prompt user to open database in Read-only mode.
            DetailMessageDialog dlg = DetailMessageDialog
                    .openDetailYesNoQuestion(
                            null,
                            Display.getAppName(),
                            _("La base de données est verrouillée. Voulez-vous en ouvrir une copie ?"),
                            _(
                                    "La base de données semble être verrouillée par un autre utilisateur. Il vous est toutefois possible d'ouvrir une copie. Toute modification apportée à cette copie ne peut être sauvegardée."),
                            null);
            return IDialogConstants.YES_ID == dlg.getReturnCode();
        }

        return null;
    }

    /**
     * Return true if the given exception is raised because the database is in readonly mode.
     */
    private boolean isReadonlyExeption(Exception exception) {
        // Try to determine if the exception is related to "ReadOnly" Access.
        // This occur when trying to open a readonly file with "servermode".
        Throwable cause = exception;
        while (cause != null) {
            if (cause.getMessage().contains("Feature not supported")
                    || cause.getMessage().contains("the database is read only")
                    || cause.getMessage().contains("Lock file exists")) {
                return true;
            }
            cause = cause.getCause();
        }
        return false;
    }

    /**
     * Open an existing database.
     */
    private PlanimodManagers openDatabase(String url) {
        PlanimodManagers managers;
        try {
            logger.info("Opening database: {}", url);
            managers = new PlanimodManagers(H2DBConfigurations.create(url, false, false));
            // Raise exception when in readonly mode.
            if (managers.isReadOnly()) {
                throw new RuntimeException("the database is read only");
            }
            return managers;
        } catch (Exception e) {
            logger.error("error opening the database " + url, e);

            /*
             * Check to open Readonly or Locked database.
             */
            Boolean openReadOnly = handleReadonlyException(e);
            if (Boolean.FALSE.equals(openReadOnly)) {
                // Operation cancel by user
                return null;
            } else if (Boolean.TRUE.equals(openReadOnly)) {
                try {
                    // Try to open a copy of database
                    managers = new PlanimodManagers(H2DBConfigurations.create(url, false, false, false, true));
                    managers.setIsCopy(true);
                    return managers;
                } catch (Exception e1) {
                    handleOpenDatabaseException(e1);
                    return null;
                }
            } else {
                handleOpenDatabaseException(e);
                return null;
            }
        }
    }

    /**
     * Create demo record in the database.
     * 
     * @param managers
     * @throws ManagerException
     */
    private void populateDatabase(PlanimodManagers managers) throws ManagerException {

        /*
         * Sections
         */
        Section secBoul = addSection(managers, _("Boulangerie"));
        Section secPati = addSection(managers, _("Pâtisserie"));
        Section secInsp = addSection(managers, _("Inspection/Triage"));
        Section secEmba = addSection(managers, _("Emballage"));
        Section secExpe = addSection(managers, _("Expédition"));
        Section secLivr = addSection(managers, _("Livraison"));
        Section secMain = addSection(managers, _("Maintenance"));

        /*
         * Positions
         */
        Position posBoul = addPosition(managers, secBoul, _("Boulanger"), false, false);
        Position posChef = addPosition(managers, secBoul, _("Chef Boulanger"), true, false);
        Position posEmba = addPosition(managers, secEmba, _("Emballeur"), false, false);
        Position posInsp = addPosition(managers, secInsp, _("Inspecteur contrôle-qualité"), true, false);
        Position posLivr = addPosition(managers, secExpe, _("Livreur"), true, false);
        Position posManu = addPosition(managers, secExpe, _("Manutentier"), false, false);
        Position posPrep = addPosition(managers, secMain, _("Préposé Salubrité"), false, false);
        Position posPati = addPosition(managers, secPati, _("Pâtissier"), true, false);
        Position posTrie = addPosition(managers, secInsp, _("Trieur"), false, false);

        /*
         * Products
         */
        Product prod0013611 = addProduct(managers, "0013611", _("Pain 12 grains"), _("Pain"));
        addProductPosition(managers, prod0013611, posBoul, posEmba, posManu, posTrie);

        Product prod0013447 = addProduct(managers, "0013447", _("Pain blanc ménage"), _("Pain"));
        addProductPosition(managers, prod0013447, posChef, posEmba, posManu, posTrie);

        Product prod0013449 = addProduct(managers, "0013449", _("Pain blé entier"), _("Pain"));
        addProductPosition(managers, prod0013449, posChef, posEmba, posManu, posTrie);

        Product prod0015788 = addProduct(managers, "0015569", _("Chocolatine"), _("Viennoiserie"));
        addProductPosition(managers, prod0015788, posPati, posPati, posTrie, posEmba, posEmba, posManu);

        Product prod0015569 = addProduct(managers, "0015569", _("Chaussons aux pommes"), _("Viennoiserie"));
        addProductPosition(managers, prod0015569, posPati, posTrie, posEmba, posEmba, posManu);

        Product prod0015800 = addProduct(managers, "0015800", _("Chaussons aux bleuets"), _("Viennoiserie"));
        addProductPosition(managers, prod0015800, posPati, posTrie, posEmba, posEmba, posManu);

        Product prod0014566 = addProduct(managers, "0014566", _("Chocolatine"), _("Viennoiserie"));
        addProductPosition(managers, prod0014566, posPati, posEmba, posManu, posTrie);

        /*
         * Shift
         */
        Employee empRealGosselin = addEmployee(managers, "12180", "Iven", "René", dateTime("1973-11-30 Fri 00:00"));
        Employee empLucieLacoste = addEmployee(managers, "12181", "Vaden", "Deserres", dateTime("1974-07-05 Fri 00:00"));
        Employee empJohanneLemieux = addEmployee(managers, "12182", "Germain", "Adler", dateTime("1974-08-15 Thu 00:00"));
        Employee empManonTremblay = addEmployee(managers, "12183", "Germaine", "Rivard", dateTime("1974-11-04 Mon 00:00"));
        Employee empBernardBerube = addEmployee(managers, "12184", "Mirabelle", "Duval", dateTime("1975-09-17 Wed 00:00"));
        Employee empRobertLazure = addEmployee(managers, "12185", "Norris", "Barrientos", dateTime("1976-02-11 Wed 00:00"));
        Employee empLindaBoisvert = addEmployee(managers, "12186", "Vernon", "Corbeil", dateTime("1976-02-23 Mon 00:00"));
        Employee empSergeRobidoux = addEmployee(managers, "12187", "Edmee", "Patenaude", dateTime("1976-02-24 Tue 00:00"));
        Employee empMichelDaniel = addEmployee(managers, "12188", "Jewel", "Gauvin", dateTime("1976-04-16 Fri 00:00"));
        Employee empCaroleRaymond = addEmployee(managers, "12189", "Didiane", "Simon", dateTime("1976-04-28 Wed 00:00"));

    }

    /**
     * Open a database. Returns True if a database got open. False if the operation have been cancel.
     * 
     * @return A database manager or null if user cancel the selection.
     */
    public PlanimodManagers selectDatabase() {

        // Open DB selector
        String url;
        PlanimodManagers managers = null;
        DatabaseSelectorDialog dlg = new DatabaseSelectorDialog(null);
        do {
            int returnCode = dlg.open();
            url = dlg.getDatabaseUrl();
            if (url == null || (returnCode != DatabaseSelectorDialog.OPEN && returnCode != DatabaseSelectorDialog.CREATE)) {
                // Operation cancel by user
                return null;
            }
            if (DatabaseSelectorDialog.OPEN == returnCode) {
                managers = openDatabase(url);
            } else if (DatabaseSelectorDialog.CREATE == returnCode) {
                if (!(url.contains("/") || url.contains("\\"))) {
                    url = System.getProperty("user.home") + "/" + url;
                }
                managers = createDatabase(url, dlg.getCreateDemo());
            } else {
                // Operation cancel by user
                return null;
            }

        } while (managers == null);
        // Same the url in recent list.
        DatabaseSelectorDialog.addToRecentList(url);
        return managers;
    }

}
