/*
 * Copyright 2009 Lari Natri <lari.natri@gmail.com>
 *
 *  This program 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/>.
 */

package compid.data.db_derby;

import compid.data.*;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement; // should use PreparedStatement?
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;


/**
 * This class is DAO factory for default embedded Derby
 * database
 * 
 * @author Lari Natri
 */
public class DAOFactoryDerby extends DAOFactory {
    
    public static final String PKLABELPOSTFIX = "_ID";
    private static final String URLPREFIX = "jdbc:derby:";

    
    private DatasetInfoDAO datasetInfoDAO = null;
    private PeptideDAO peptideDAO = null;
    private PeptideHitDAO peptideHitDAO = null;
    private ProteinDAO proteinDAO = null;
    private ProteinHitDAO proteinHitDAO = null;
    private QueryDAO queryDAO = null;
    private ProtHitPeptHitJoinDAO protHitPeptHitJoinDAO = null;
    private ProtDbInfoDAO protDbInfoDAO = null;
    private ProtSeqDAO protSeqDAO = null;
    private SettingDAO settingDAO = null;
    private QueryHandler queryHandler = null;
    private UpdateHandler updateHandler = null;

    /** Holds database parameters as properties */
    private final static Properties connectionProperties;

    static {
        connectionProperties = new Properties();
        connectionProperties.setProperty("derby.driver","org.apache.derby.jdbc.EmbeddedDriver");
        Properties sysProps = System.getProperties();
        Properties derbyProps = loadDerbyProperties();
        for (Object key : derbyProps.keySet()) {
            sysProps.setProperty((String)key,derbyProps.getProperty((String)key));
        }
    }
    
    /** Constructor */
    public DAOFactoryDerby() throws DAOException {
        super();
        loadDriver();
    }

    @Override
    public String getDatabaseUrl() {
        synchronized(connectionProperties) {
            return connectionProperties.getProperty("dburl");
        }
    }

    /**
     * Returns a connection object for this database.
     * @return Connection object
     */
    @Override
    public Connection getConnection() throws DAOException {
        try {
            synchronized(connectionProperties) {
                return DriverManager.getConnection(connectionProperties.getProperty("dburl"),
                                                   connectionProperties);
            }

        } catch (SQLException ex) {
            boolean anotherInstanceRunning = false;
            if (ex.getMessage().startsWith("Another instance")) {
                anotherInstanceRunning = true;
            }
            SQLException nextEx = ex.getNextException();
            while (nextEx!=null) {
                if (nextEx.getMessage().startsWith("Another instance")) {
                    anotherInstanceRunning = true;
                    break;
                }
                nextEx = nextEx.getNextException();
            }
            if (anotherInstanceRunning) {
                throw new DAOException(DAOException.ExceptionType.ANOTHERINSTANCEHASBOOTED);
            } else {
                throw new DAOException(ex);
            }
        }
    }


    @Override
    protected boolean connect(String url, String username, String password, boolean createIfNotExists) throws DAOException {
        synchronized(connectionProperties) {
            connectionProperties.put("user", username);
            if (url.startsWith(URLPREFIX)==false) {
                url = URLPREFIX+url;
            }
            connectionProperties.put("dburl", url);
            connectionProperties.put("password", password);
        }
        if (createIfNotExists && DAOFactory.dbExists(DBType.DERBYEMBEDDED, url, username, password)==false) {
            createDatabase();
            return true;
        } else {
            return false;
        }

    }

    @Override
    protected boolean isConnected() {
        try {
            this.getConnection().close();
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    /** Shuts the database down. It will be restarted automatically
     *  if needed by using methods in this class. */
    @Override
    protected void disconnect() throws DAOException {
        connectionProperties.put("shutdown", "true");
        try {
            DriverManager.getConnection(connectionProperties.getProperty("db.url"),
                                        connectionProperties).close();
        } catch (SQLException ex) { // ignore shutdown exception

        } finally {
            connectionProperties.remove("shutdown");
        }
    }

    /**
     * Checks whether database exists. Currently only checks for the
     * directory, it doesn't check tables yet. TODO!
     * @return true if database exists, false otherwise
     */
    //@Override
    //protected boolean dbExists(String url) throws DAOException {
    //    boolean bExists = false;
    //    String dbLocation = getDatabaseLocation();
    //    File dbFileDir = new File(dbLocation);
    //    if (dbFileDir.exists()) {
    //        bExists = true;
    //    }
    //    return bExists;
    //}


    @Override
    public DatasetInfoDAO getDataSetInfoDAO() {
        if (datasetInfoDAO==null) datasetInfoDAO = new DatasetInfoDAODerby(this);
        return datasetInfoDAO;
    }        
    
    @Override
    public PeptideDAO getPeptideDAO() {
        if (peptideDAO==null) peptideDAO = new PeptideDAODerby(this);
        return peptideDAO;
    }
    
    @Override
    public PeptideHitDAO getPeptideHitDAO() {
        if (peptideHitDAO==null) peptideHitDAO = new PeptideHitDAODerby(this);
        return peptideHitDAO;
    }
    
    @Override
    public ProteinDAO getProteinDAO() {
        if (proteinDAO==null) proteinDAO = new ProteinDAODerby(this);
        return proteinDAO;
    }

    
    @Override
    public ProteinHitDAO getProteinHitDAO() {
        if (proteinHitDAO==null) proteinHitDAO = new ProteinHitDAODerby(this);
        return proteinHitDAO;
    }

    @Override
    public QueryDAO getQueryDAO() {
        if (queryDAO==null) queryDAO = new QueryDAODerby(this);
        return queryDAO;
    }

    @Override
    public ProtHitPeptHitJoinDAO getProtHitPeptHitJoinDAO() {
        if (protHitPeptHitJoinDAO==null) protHitPeptHitJoinDAO = new ProtHitPeptHitJoinDAODerby(this);
        return protHitPeptHitJoinDAO;
    }


    @Override
    public ProtDbInfoDAO getProtDbInfoDAO() {
        if (protDbInfoDAO==null) protDbInfoDAO = new ProtDbInfoDAODerby(this);
        return protDbInfoDAO;
    }

    @Override
    public ProtSeqDAO getProtSeqDAO() {
        if (protSeqDAO==null) protSeqDAO = new ProtSeqDAODerby(this);
        return protSeqDAO;
    }

    @Override
    public SettingDAO getSettingDAO() {
        if (settingDAO==null) settingDAO = new SettingDAODerby(this);
        return settingDAO;
    }

    @Override
    public QueryHandler getQueryHandler() {
        if (queryHandler==null) queryHandler = new QueryHandlerDerby(this);
        return queryHandler;
    }

    @Override
    public UpdateHandler getUpdateHandler() {
        if (updateHandler==null) updateHandler = new UpdateHandlerDerby(this);
        return updateHandler;
    }
    
    
    /**
     * This initializer loads the database driver and creates
     * the database if it doesn't exist.
     */
    private void loadDriver() throws DAOException {
        boolean driverIsLoaded = false;
        String driverName = connectionProperties.getProperty("derby.driver");

        for (Enumeration<Driver> e = DriverManager.getDrivers();
             e.hasMoreElements(); ) {
            if (e.nextElement().getClass().getName().equals(driverName))
                driverIsLoaded = true;
        }
        if (!driverIsLoaded)
            // load Derby driver
            try {
                Class.forName(driverName);
            } catch (ClassNotFoundException ex) {
                throw new DAOException(DAOException.ExceptionType.CANTLOADDRIVER,
                        ex.getMessage());
            }
    }

    public static String getPkLabelPostfix() {
        return PKLABELPOSTFIX;
    }

    
    /**
     * Returns the full directory path to the current database.
     *
     * @return path to database
     */
    //public static String getDatabaseLocation() throws DAOException {
    //    String dbLocation = System.getProperty("derby.system.home") + "/" +
    //            getDerbyProperties().getProperty("databaseName","compid");
    //    return dbLocation;
    //}
    

    /**
     * Loads database properties from included settings file. TODO:
     * allow using of other properties files (in users home dir for
     * example).
     * @return database properties
     * @throws DAOException
     */
    private static Properties loadDerbyProperties() throws DAOException {
        InputStream dbPropInputStream = null;
        dbPropInputStream = DAOFactoryDerby.class.
                getResourceAsStream("EmbeddedDerby.properties");
        Properties dbProp = new Properties();
        try {
            dbProp.load(dbPropInputStream);
        } catch (IOException ex) {
            throw new DAOException(ex);
        } finally {
            try {
                dbPropInputStream.close();
            } catch (Exception ex) {}
        }
        return dbProp;
    }

    /**
     * Sets the Derby embedded database system directory. Directories will
     * be tried in following order: from properties file derby.system.home
     * property, system's <user.home>/.compid, and finally ./.compid .
     */
//    private static void setDBSystemDir() throws DAOException {
//        String systemDir = getDerbyProperties().getProperty("derby.system.home");
//        if (systemDir==null) {
//            systemDir = System.getProperty("user.home", ".") + "/.compid";
//            //systemDir = System.getProperty("user.home", ".") + "/COMPID_REAL/DB";
//        }
//        System.setProperty("derby.system.home", systemDir);
//
//        // Create db system dir if it doesn't exist
//        File fileSystemDir = new File(systemDir);
//        boolean error = false;
//        try {
//            fileSystemDir.mkdir();
//        } catch (Exception ex) { error = true; }
//        if (!fileSystemDir.exists()||error) {
//            System.err.println("Can't find nor create database system dir: "
//                       +fileSystemDir.getAbsolutePath());
//            System.exit(1);
//        }
//    }
//
    
    /**
     * Creates the database tables. Resource file containing
     * necessary SQL commands is loaded and executed.
     * 
     * @param dbConnection Connection object to use, must have
     *                     table creation rights
     * @return true if tables were created ok
     */
    private static boolean createTables(Connection dbConnection)
        throws DAOException 
    {
        boolean bCreatedTables = false;
        Statement statement = null;
        try {
            InputStream is = DAOFactoryDerby.class.
                getResourceAsStream("CreateDBSchema_Derby.sql"); // NOI18N
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            Vector<String> sqls = new Vector<String>();
            String command = "";
            String line;
            while((line=br.readLine())!=null) {
                if (!line.trim().startsWith("--")) {
                    command+=" "+line;
                    if (line.contains(";")) {
                        String newStr = line.substring(line.indexOf(";")+1);
                        command = command.substring(0,command.indexOf(";"));
                        sqls.add(command);
                        command = newStr;
                    }
                }
            }
            br.close(); isr.close(); is.close();
            
            statement = dbConnection.createStatement();
            // execute gotten SQL statements
            for(String cmd : sqls) {
                statement.execute(cmd);
            }
            bCreatedTables = true;
            
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
            throw new DAOException(ex);
        } catch (SQLException ex) {
            System.out.println(ex.getMessage());
            throw new DAOException(ex);
        }
        
        return bCreatedTables;
    }
    
    /**
     * Creates the database directory and calls assistant function
     * to create the tables.
     * @return true if database creation was ok
     */
    private boolean createDatabase() throws DAOException {
        boolean bCreated = false;
        try {
            connectionProperties.put("create", "true");
            bCreated = createTables(getConnection());
        } catch (DAOException ex) {
            throw ex;
        } finally {
            connectionProperties.remove("create");
        }
        return bCreated;
    }
    


    
    

}
