/*
 * 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 compid.data.tuples.Tuple;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Collection;

import net.aavalla.utils.general.StrUtils;


/**
 *
 * @author Lari Natri
 */
public class BaseDAODerby {
    
    public static final String SCHEMANAME = "COMPID";
    public static final String INTERSECT = " INTERSECT ";
    public static final String UNION = " UNION ";
    public static final String COUNTPREFIX = "SELECT COUNT(*) AS CPRE FROM (";
    public static final String COUNTPOSTFIX = ") AS CPOST";
    public static final int DATABASETIMEOUTSECS = 90;

    private static final String SQL_SETSCHEMA = "SET SCHEMA " + SCHEMANAME;
    
    protected BaseDAODerby() {
    }

    /** Returns a new connection to the database. Use this.
     * 
     * @return unique Connection object
     */
    protected static Connection getNewConnection() throws DAOException {
        Connection conn = DAOFactory.getInstance().getConnection();
        try {
            conn.prepareStatement(SQL_SETSCHEMA).execute();
        } catch (SQLException ex) {
            throw new DAOException(ex);
        }
        return conn;
    }
    
    protected static <T extends Tuple> int insertMultipleFastNoFKChecks(Connection conn,
              Collection<T> tuples) throws DAOException
    {
        return insertMultipleFastNoFKChecks(conn, tuples, false);
    }    
    
    
    protected static <T extends Tuple> int insertMultipleFastNoFKChecks(Connection conn,
                                                      Collection<T> tuples,
                                                      boolean allowDuplicates)
                         throws DAOException {
        
        if (tuples==null || tuples.size()<1) return 0;
        PreparedStatement findKeyStmt = null;
        PreparedStatement insertStmt = null;
        int newInsertions = 0;
        try {
            Tuple proto = tuples.iterator().next();
            Object[][] keysAndValues = getKeysAndValuesIncludingForeignKeys(proto);
            Object[][] kvsForExistence = getKeysAndValuesForExistenceCheck(proto);
            findKeyStmt = getFindKeyStatement(conn, proto.getPrimaryKeyLabel(),
                                              proto.getTableLabel(),
                                              kvsForExistence);
            insertStmt = getInsertStatement(conn, proto.getTableLabel(),
                                            keysAndValues);

            int pk = -1;
            Iterator<T> iter = tuples.iterator();
            while (iter.hasNext()) {
                Tuple tuple = iter.next();
                kvsForExistence = getKeysAndValuesForExistenceCheck(tuple);
                pk = tuple.getPrimaryKey();
                if (pk<0 && allowDuplicates==false) { // see if tuple is already in database:
                    pk = findPrimaryKey(findKeyStmt, kvsForExistence);
                    tuple.setPrimaryKey(pk);
                }
                if (pk<0) { // not in database, add:
                    keysAndValues = getKeysAndValuesIncludingForeignKeys(tuple);
                    pk = insertNoChecks(insertStmt, keysAndValues);
                    tuple.setPrimaryKey(pk);
                    if (pk>0) ++newInsertions;
                }
            }
            
        } catch (DAOException ex) {

            throw ex;
        } finally {
            close(findKeyStmt);
            close(insertStmt);
        }
        return newInsertions;
        
    }
    
    
    /** Inserts a tuple into database and returns the primary
     * key of the addition. If tuple already exists, the primary
     * key of the existing tuple will be returned. If the table
     * the tuple represents doesn't have any foreign columnNames, foreign
     * columnNames MUST be set to an empty HashTable. If foreignKeys==null,
     * it is assumed that columnNames are not yet set and insertion will
     * not happen: negative value is returned.
     * @param tuple Tuple to be inserted
     * @return Primary key
     */
    protected static int insertIfNotExists(Tuple tuple) throws DAOException {
        if (tuple.getPrimaryKey()>-1) return tuple.getPrimaryKey(); // already added!
        if (tuple.getForeignKeys()==null) return -1;
        
        Object[][] kvsForExistence = getKeysAndValuesForExistenceCheck(tuple);        
        Connection conn = getNewConnection();
        int pk = tuple.getPrimaryKey();
        if (pk<0) { // check if found in database
            PreparedStatement findStmt = getFindKeyStatement(conn, tuple.getPrimaryKeyLabel(),
                              tuple.getTableLabel(), kvsForExistence);
            pk = findPrimaryKey(findStmt, kvsForExistence);
            close(findStmt);
        }

        if (pk>=0) {
            close(conn);
            return pk; // found, return the primary key
        }
        
        Object[][] keysAndValues = getKeysAndValuesIncludingForeignKeys(tuple);
        String sql = "INSERT INTO "+SCHEMANAME+'.'+tuple.getTableLabel() +" ( "
                + StrUtils.catStrings(keysAndValues[0], ", ") + " )"  +" VALUES ("
                + StrUtils.catStringTimes("?", ", ",keysAndValues[0].length) + ")";

        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            stmt = prepareStatementAutokey(conn,sql);
            int idx;
            for (idx=0; idx<keysAndValues[0].length; ++idx) {
                stmt.setObject(idx+1, keysAndValues[1][idx]);
            }            

            stmt.execute();
            rs = stmt.getGeneratedKeys();
            if (rs.next()) {
                pk = rs.getInt(1);
            }
        } catch (SQLException ex) {
            pk = Tuple.PK_ERRORINSERTING;
            throw new DAOException(ex);
        } finally {
            closeAll(conn,stmt,rs);
        }
        if (pk<0) System.out.println("ERROR: tuple "+tuple+" not added");
        
        return pk;
    }
    
    private static int insertNoChecks(PreparedStatement stmt,
                                      Object[][] keysAndValues) 
            throws DAOException {
        
        ResultSet rs = null;
        int pk = -1;
        
        try {
            if (stmt==null || stmt.isClosed()
                || !(stmt.getConnection().isValid(DATABASETIMEOUTSECS))
                || stmt.getConnection().isReadOnly() || keysAndValues==null
                || keysAndValues.length!=2) { throw new DAOException();
            }            
          
            for (int i=0; i<keysAndValues[0].length; ++i) {
                stmt.setObject(i+1, keysAndValues[1][i]);
            }                  
            
            stmt.execute();
            rs = stmt.getGeneratedKeys();
            if (rs.next()) {
                pk = rs.getInt(1);
            }

        } catch (SQLException ex) {

            throw new DAOException(ex);
        } finally {
            close(rs);
        }
        return pk;
    }

    protected static <T extends Tuple> ResultSet getIntersectByPK(
            Connection conn, String sqlWithPKParams, Collection<T> tuples, int times)
            throws DAOException
    {
        PreparedStatement stmt = getIntersectStatement(conn, sqlWithPKParams, tuples, times);
        ResultSet rs = getCombinationByPK(stmt,tuples);
        return rs;
    }
    
    protected static <T extends Tuple> ResultSet getUnionByPK(
            Connection conn, String sqlWithPKParams, Collection<T> tuples, int times)
            throws DAOException
    {
        PreparedStatement stmt = getUnionStatement(conn, sqlWithPKParams, tuples, times);
        ResultSet rs = getCombinationByPK(stmt,tuples);
        return rs;
    }    

  

    protected static <T extends Tuple> ResultSet getCombinationByPK(PreparedStatement combinationStatement,
                                                                  Collection<T> tuples)
            throws DAOException
    {

        ResultSet rs = null;
        
        try {
        
            Iterator<T> tIter = tuples.iterator();
            int paramNr = 0;
            while (tIter.hasNext()) {
                combinationStatement.setInt(++paramNr, tIter.next().getPrimaryKey());
            }        

            rs = combinationStatement.executeQuery();
        } catch (SQLException ex) {
            close(rs);
            throw new DAOException(ex);
        }
        
        return rs;
    }
    
    
    protected static <T extends Tuple> PreparedStatement getIntersectStatement(Connection conn, String sqlWithPKParams,
                                                      Collection<T> tuples, int times)
                                                      throws DAOException
    {
        StringBuilder buildSQL = new StringBuilder();
        
        Iterator<T> tIter = tuples.iterator();
        int i = 0;
        while (tIter.hasNext() && times>i) {
            tIter.next();
            buildSQL.append(sqlWithPKParams);
            if (i++<times-1) buildSQL.append(INTERSECT);
        }

        return prepareStatement(conn,buildSQL.toString());
    }
    
    protected static <T extends Tuple> PreparedStatement getUnionStatement(Connection conn, String sqlWithPKParams,
                                                      Collection<T> tuples, int times)
                                                      throws DAOException
    {
        StringBuilder buildSQL = new StringBuilder();
        
        Iterator<T> tIter = tuples.iterator();
        int i = 0;
        while (tIter.hasNext() && times>i) {
            tIter.next();
            buildSQL.append(sqlWithPKParams);
            if (i++<times-1) buildSQL.append(UNION);
        }

        return prepareStatement(conn,buildSQL.toString());
    }

    protected static <T extends Tuple> int getCountOfIntersectByPK(String sqlWithPKParams, Collection<T> tuples)
            throws DAOException
    {
        return getCountOfCombinationByPK(sqlWithPKParams, tuples, true);
    }

    protected static <T extends Tuple> int getCountOfUnionByPK(String sqlWithPKParams, Collection<T> tuples)
            throws DAOException
    {
        return getCountOfCombinationByPK(sqlWithPKParams, tuples, false);
    }

    /**
     *
     * @param <T>
     * @param sqlWithPKParams
     * @param tuples
     * @param intersect true if intersect, false if union
     * @return
     * @throws compid.data.DAOException
     */
    private static <T extends Tuple> int getCountOfCombinationByPK(String sqlWithPKParams, Collection<T> tuples, boolean intersect)
            throws DAOException
    {
 
        Connection conn = getNewConnection();
        
        StringBuilder buildSQL = new StringBuilder();
        buildSQL.append(COUNTPREFIX);
        
        Iterator<T> tIter = tuples.iterator();
        int i = 0;
        while (tIter.hasNext()) {
            Tuple t = tIter.next();
            if (t.getPrimaryKey()<0) {
                t.setPrimaryKey(findPrimaryKey(conn,t));
            }
            buildSQL.append(sqlWithPKParams);
            if (i++<tuples.size()-1) {
                if (intersect) buildSQL.append(INTERSECT);
                else buildSQL.append(UNION);
            }
        }
        
        buildSQL.append(COUNTPOSTFIX);
        
        ResultSet rs = null;
        
        int count = -1;
        
        try {
        
            PreparedStatement stmt = prepareStatement(conn,buildSQL.toString());
            tIter = tuples.iterator();
            int paramNr = 0;
            while (tIter.hasNext()) {
                stmt.setInt(++paramNr, tIter.next().getPrimaryKey());
            }        

            rs = stmt.executeQuery();
            if (rs.next()) {
                count = rs.getInt(1);
            }
        } catch (DAOException ex) {
            throw ex;
        } catch (SQLException ex) {
            throw new DAOException(ex);
        } finally {
            close(conn);
        }
        
        return count;

    }    
     
    
    private static PreparedStatement getFindKeyStatement(Connection conn,
        String keyColumn, String table, Object[][] keysAndValues)
        throws DAOException
    {

        StringBuilder strB = new StringBuilder();
        for (int i=0; i<keysAndValues[0].length; ++i) {
            strB.append((String)keysAndValues[0][i] + " = ?");
            if (i<keysAndValues[0].length-1) strB.append(" AND ");
        }

        String sql = "SELECT  " + keyColumn + " FROM " + SCHEMANAME + '.'
                + table + " WHERE " + strB.toString();
 
        PreparedStatement stmt = null;
        stmt = prepareStatement(conn,sql);

        return stmt;
    }
    
    private static PreparedStatement getInsertStatement(Connection conn,
        String table, Object[][] keysAndValues) throws DAOException
    {
        PreparedStatement stmt = null;
        String sql = null;
        try {
            if (conn==null || !conn.isValid(DATABASETIMEOUTSECS)
                    || conn.isReadOnly() || table==null || table.length()<1
                    || keysAndValues==null || keysAndValues.length!=2) {
                return null;
            }

            StringBuilder strB = new StringBuilder();
            for (int i=0; i<keysAndValues[0].length; ++i) {
                strB.append(keysAndValues[0][i] + " = ?");
                if (i<keysAndValues[0].length-1) strB.append(" AND ");
            }
            sql = "INSERT INTO "+SCHEMANAME+'.'+table +" ( "
                    + StrUtils.catStrings(keysAndValues[0], ", ") + " )"  +" VALUES ("
                    + StrUtils.catStringTimes("?", ", ",keysAndValues[0].length) + ")";

            stmt = prepareStatementAutokey(conn,sql);
        } catch (SQLException ex) {
            throw new DAOException(ex);
        }

        return stmt;
    }    

    protected static int findPrimaryKey(Connection conn, Tuple tuple)
            throws DAOException {
        if (conn==null || tuple==null) {
            throw new DAOException(DAOException.ExceptionType.INVALIDPARAMETERS,
                    conn==null ? "Connection object is not set" :
                                 "Tuple object is not set");
        }

        Object[][] kvsForExistence = getKeysAndValuesForExistenceCheck(tuple);
        PreparedStatement findStmt = getFindKeyStatement(conn,
             tuple.getPrimaryKeyLabel(), tuple.getTableLabel(), kvsForExistence);
        int pk = findPrimaryKey(findStmt, kvsForExistence);
        close(findStmt);        
        return pk;
    }        
    
    
    /**
     * Searches the database and tries to find an integer key corresponding
     * to given values. This is meant to be used for finding the primary key
     * of an existing tuple.
     * @param conn Connection to use
     * @param table Table's name
     * @param keyColumn Key column's name
     * @param columnNames Column names to check
     * @param values Values corresponding to columnNames
     * @return The proto value found matching given criteria, <0 if not found
     */
    private static int findPrimaryKey(PreparedStatement stmt,
        Object[][] keysAndValues) throws DAOException
    {
        ResultSet rs = null;
        int pk = -1;
        if (stmt==null) {
            System.out.println("DEBUG: BaseDAODerby.findPrimaryKey(): statement null");
            return pk;
        }
        try {

            for(int i=0; i<keysAndValues[0].length; ++i) {
                if (keysAndValues[1]==null || keysAndValues[1][i]==null) {
                    System.out.println(keysAndValues[1]);
                    System.out.println(keysAndValues[1][i]);
                }
                stmt.setObject(i+1, keysAndValues[1][i]);
            }

            rs = stmt.executeQuery();

            if (rs.next()) {
                pk = rs.getInt(1);
                if (rs.next()) {
                    System.err.println(rs.getMetaData());
                    throw new DAOException(DAOException.ExceptionType.DUPLICATEROW, "Database inconsistent. Possible reason: \ndatabase rules don't require uniqueness where Java part does. \nPerhaps the database is created with another version of Compid.");
                }
                close(rs);
            }
        } catch (SQLException ex) {
            System.err.println(ex);
            throw new DAOException(ex);
        } finally {
            close(rs);
        }
        return pk;
    }    
    
    
    /**
     * Prepares a statement with no concurrency.
     * @param conn Connection
     * @param sql SQL
     * @return Prepared statement
     * @throws java.sql.SQLException
     */
    public static PreparedStatement prepareStatement(Connection conn, String sql)
                  throws DAOException {
        try { 
            return conn.prepareStatement(sql,ResultSet.TYPE_SCROLL_INSENSITIVE,
                                     ResultSet.CONCUR_READ_ONLY);
        } catch (SQLException ex) {
            System.err.println("DEBUG: prepareStatement() error with sql "+sql);
            throw new DAOException(ex);
        }
    }
    
    /**
     * Prepares a statement for updating rows.
     * @param conn Connection
     * @param sql SQL
     * @return Prepared statement
     * @throws java.sql.SQLException
     */
    public static PreparedStatement prepareUpdatableStatement(Connection conn, String sql)
                  throws DAOException {
        try { 
            return conn.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY,
                                     ResultSet.CONCUR_UPDATABLE);
        } catch (SQLException ex) {
            System.err.println("DEBUG: prepareUpdatableStatement(): error with SQL: " + sql);
            throw new DAOException(ex);
        }
    }    
    
    /**
     * Prepares a statement with option RETURN_GENERATED_KEYS.
     * @param conn Connection
     * @param sql SQL
     * @return Prepared statement
     * @throws java.sql.SQLException
     */
    public static PreparedStatement prepareStatementAutokey(Connection conn, String sql)
                  throws DAOException {
        try {
            return conn.prepareStatement(sql,
                    PreparedStatement.RETURN_GENERATED_KEYS);
        } catch (SQLException ex) {
            throw new DAOException(ex);
        }
    }
    
    protected static void setAutoCommit(Connection conn, boolean autoCommit)
        throws DAOException
    {
        try {
            conn.setAutoCommit(autoCommit);
        } catch (SQLException ex) {
            throw new DAOException(ex);
        }
    }
    
    protected static void commit(Connection conn) throws DAOException {
        try {
            conn.commit();
        } catch (SQLException ex) {
            throw new DAOException(ex);
        }
    }

    protected static void rollback(Connection conn) throws DAOException {
        try {
            conn.rollback();
        } catch (SQLException ex) {
            throw new DAOException(ex);
        }
    }    

    /**
     * Closes a connection
     * @param conn Connection
     */
    protected static void close(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException ex) {
            }
        }
    }

    /**
     * Closes a statement
     * @param stmt Statement
     */
    protected static void close(Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException ex) {
            }
        }
    }

    /**
     * Closes a result set
     * @param rs Result set
     */
    protected static void close(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException ex) {
            }
        }
    }

    /** Closes result set, statement and connection (in that order)
     * 
     * @param conn Connection
     * @param stmt Statement
     * @param rs Result set
     */
    protected static void closeAll(Connection conn, Statement stmt, ResultSet rs) {
        close(rs);
        close(stmt);
        close(conn);
    }
    
    private static Object[][] getKeysAndValuesForExistenceCheck(Tuple tuple) throws
            DAOException
    {
        if (tuple==null) {
            System.err.println(tuple);
            
            throw new DAOException(DAOException.ExceptionType.INVALIDPARAMETERS,
                    "getKeysAndValuesForExistenceCheck(), tuple was null");
        }

        HashMap<String,Object> map = tuple.getKeysAndValuesForExistenceCheck();

        int keyValuePairCount = map.size();

        Object[][] result = new Object[2][keyValuePairCount];
        int i = 0;
        Iterator<String> iter = map.keySet().iterator();
        while (iter.hasNext()) {
            result[0][i] = iter.next();
            result[1][i] = map.get(result[0][i]);
            ++i;
        }
        return result;        
    }
    
    private static Object[][] getKeysAndValuesIncludingForeignKeys(Tuple tuple)
        throws DAOException
    {
        if (tuple==null||tuple.getForeignKeys()==null) {
            System.err.println(tuple);
            
            throw new DAOException(DAOException.ExceptionType.INVALIDPARAMETERS,
                    "getKeysAndValuesIncludingForeignKeys(), tuple was null");
        }

        HashMap<String,Object> map = tuple.getColsAndValues();

        int keyValuePairCount = map.size() + tuple.getForeignKeys().size();

        Object[][] result = new Object[2][keyValuePairCount];
        int i = 0;
        Iterator<String> iter = tuple.getColsAndValues().keySet().iterator();
        while (iter.hasNext()) {
            result[0][i] = iter.next();
            result[1][i] = map.get(result[0][i]);
            ++i;
        }
        HashMap<String,Object> fkMap = tuple.getForeignKeys();
        iter = fkMap.keySet().iterator();
        while (iter.hasNext()) {
            result[0][i] = iter.next();
            int fk = (Integer)fkMap.get((String)result[0][i]);
            if (fk<0) {
                throw new DAOException();
            }            
            result[1][i] = fk;
            ++i;
        }
        return result;
                
    }
 

     
}
