/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.cmp.jdbc;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.ejb.CreateException;
import org.jboss.as.cmp.context.CmpEntityBeanContext;
import org.jboss.as.cmp.jdbc.bridge.JDBCCMPFieldBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCCMRFieldBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCEntityBridge;
import org.jboss.as.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.as.cmp.jdbc.metadata.JDBCEntityCommandMetaData;
import org.jboss.logging.Logger;
import org.jboss.security.AuthenticationManager;

/**
 * Base class for create commands that drives the operation sequence.
 *
 * @author <a href="mailto:jeremy@boynes.com">Jeremy Boynes</a>
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 */
public abstract class JDBCAbstractCreateCommand implements JDBCCreateCommand {
    protected Logger log;
    protected boolean debug;
    protected boolean trace;
    protected JDBCEntityBridge entity;
    protected AuthenticationManager securityManager;
    protected boolean createAllowed;
    protected SQLExceptionProcessor exceptionProcessor;
    protected String insertSQL;
    protected JDBCFieldBridge[] insertFields;
    protected boolean insertAfterEjbPostCreate;

    // Generated fields
    private JDBCCMPFieldBridge createdPrincipal;
    private JDBCCMPFieldBridge createdTime;
    private JDBCCMPFieldBridge updatedPrincipal;
    private JDBCCMPFieldBridge updatedTime;

    public void init(JDBCStoreManager manager) {
        log = Logger.getLogger(getClass().getName() + '.' + manager.getMetaData().getName());
        debug = log.isDebugEnabled();
        trace = log.isTraceEnabled();

        entity = (JDBCEntityBridge) manager.getEntityBridge();
        insertAfterEjbPostCreate = manager.getCmpConfig().isInsertAfterEjbPostCreate();

        // set create allowed
        createAllowed = true;
        JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
        for (int i = 0; i < pkFields.length; i++) {
            if (pkFields[i].isReadOnly()) {
                createAllowed = false;
                log.debug("Create will not be allowed because pk field "
                        + pkFields[i].getFieldName() + "is read only.");
                break;
            }
        }

        initGeneratedFields();

        JDBCEntityCommandMetaData entityCommand = manager.getMetaData().getEntityCommand();
        if (entityCommand == null) {
            throw new RuntimeException("entity-command is null");
        }
        initEntityCommand(entityCommand);

        initInsertFields();
        initInsertSQL();
    }

    protected void initEntityCommand(JDBCEntityCommandMetaData entityCommand) {
        exceptionProcessor = null;
    }

    public Object execute(Method m, Object[] args, CmpEntityBeanContext ctx) throws CreateException {
        // TODO: implement this logic nicer
        if (insertAfterEjbPostCreate) {
            if (!JDBCEntityBridge.isEjbCreateDone(ctx)) {
                checkCreateAllowed();
                generateFields(ctx);
                JDBCEntityBridge.setEjbCreateDone(ctx);
            } else {
                beforeInsert(ctx);
                performInsert(ctx);
                afterInsert(ctx);
                JDBCEntityBridge.setCreated(ctx);
            }
        } else {
            checkCreateAllowed();
            generateFields(ctx);
            beforeInsert(ctx);
            performInsert(ctx);
            afterInsert(ctx);
            JDBCEntityBridge.setCreated(ctx);
        }
        return getPrimaryKey(ctx);
    }

    protected void checkCreateAllowed() throws CreateException {
        if (!createAllowed) {
            throw new CreateException("Creation is not allowed because a primary key field is read only.");
        }
    }

    protected JDBCCMPFieldBridge getGeneratedPKField() {
        // extract the pk field to be generated
        JDBCCMPFieldBridge pkField = null;
        JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
        for (int i = 0; i < pkFields.length; ++i) {
            if (pkField != null)
                throw new RuntimeException("Generation only supported with single PK field");
            pkField = (JDBCCMPFieldBridge) pkFields[i];
        }
        return pkField;
    }

    protected void initGeneratedFields() {
        createdPrincipal = entity.getCreatedPrincipalField();
        if (securityManager == null && createdPrincipal != null) {
            throw new RuntimeException("No security-domain configured but created-by specified");
        }
        updatedPrincipal = entity.getUpdatedPrincipalField();
        if (securityManager == null && updatedPrincipal != null) {
            throw new RuntimeException("No security-domain configured but updated-by specified");
        }
        createdTime = entity.getCreatedTimeField();
        updatedTime = entity.getUpdatedTimeField();
    }

    protected void generateFields(CmpEntityBeanContext ctx) throws CreateException {
        // Audit principal fields
        if (securityManager != null) {
            String principalName = ctx.getCallerPrincipal().getName();
            if (createdPrincipal != null && createdPrincipal.getInstanceValue(ctx) == null) {
                createdPrincipal.setInstanceValue(ctx, principalName);
            }
            /*
            if(updatedPrincipal != null && updatedPrincipal.getInstanceValue(ctx) == null)
            {
               updatedPrincipal.setInstanceValue(ctx, principalName);
            }
            */
        }

        // Audit time fields
        Date date = null;
        if (createdTime != null && createdTime.getInstanceValue(ctx) == null) {
            date = new Date();
            createdTime.setInstanceValue(ctx, date);
        }
        /*
        if(updatedTime != null && updatedTime.getInstanceValue(ctx) == null)
        {
           if(date == null)
              date = new Date();
           updatedTime.setInstanceValue(ctx, date);
        }
        */
    }

    protected void initInsertFields() {
        JDBCFieldBridge[] fields = entity.getTableFields();
        List insertFieldsList = new ArrayList(fields.length);
        for (int i = 0; i < fields.length; i++) {
            JDBCFieldBridge field = fields[i];
            if (isInsertField(field))
                insertFieldsList.add(field);
        }

        insertFields = (JDBCFieldBridge[]) insertFieldsList.toArray(new JDBCFieldBridge[insertFieldsList.size()]);
    }

    protected boolean isInsertField(JDBCFieldBridge field) {
        boolean result =
                !(field instanceof JDBCCMRFieldBridge)
                        && field.getJDBCType() != null
                        && !field.isReadOnly();
        if (field instanceof JDBCCMPFieldBridge)
            result = result && !((JDBCCMPFieldBridge) field).isRelationTableField();
        return result;
    }

    protected void initInsertSQL() {
        StringBuffer sql = new StringBuffer(250);
        sql.append(SQLUtil.INSERT_INTO)
                .append(entity.getQualifiedTableName())
                .append(" (");

        SQLUtil.getColumnNamesClause(insertFields, sql);

        sql.append(')')
                .append(SQLUtil.VALUES).append('(');
        SQLUtil.getValuesClause(insertFields, sql)
                .append(')');
        insertSQL = sql.toString();

        if (debug)
            log.debug("Insert Entity SQL: " + insertSQL);
    }

    protected void beforeInsert(CmpEntityBeanContext ctx) throws CreateException {
    }

    protected void performInsert(CmpEntityBeanContext ctx) throws CreateException {
        Connection c = null;
        PreparedStatement ps = null;
        boolean throwRuntimeExceptions = entity.getMetaData().getThrowRuntimeExceptions();

        // if metadata is true, the getconnection is done inside
        // its own try catch block to throw a runtime exception (EJBException)
        if (throwRuntimeExceptions) {
            try {
                c = entity.getDataSource().getConnection();
            } catch (SQLException sqle) {
                javax.ejb.EJBException ejbe = new javax.ejb.EJBException("Could not get a connection; " + sqle);
                ejbe.initCause(sqle);
                throw ejbe;
            }
        }

        try {
            if (log.isDebugEnabled()) {
                log.debug("Executing SQL: " + insertSQL);
            }


            // if metadata is false, the getconnection is done inside this try catch block
            if (!throwRuntimeExceptions) {
                c = entity.getDataSource().getConnection();
            }
            ps = prepareStatement(c, insertSQL, ctx);

            // set the parameters
            int index = 1;
            for (int fieldInd = 0; fieldInd < insertFields.length; ++fieldInd) {
                index = insertFields[fieldInd].setInstanceParameters(ps, index, ctx);
            }

            // execute statement
            int rowsAffected = executeInsert(index, ps, ctx);
            if (rowsAffected != 1) {
                throw new CreateException("Expected one affected row but update returned" + rowsAffected +
                        " for id=" + ctx.getPrimaryKey());
            }
        } catch (SQLException e) {
            if (exceptionProcessor != null && exceptionProcessor.isDuplicateKey(e)) {
                log.error("Failed to create instance.", e);
                throw new CreateException(
                        "Integrity constraint violation. Possibly unique key violation or invalid foreign key value."
                );
            } else {
                log.error("Could not create entity", e);
                CreateException ce = new CreateException("Could not create entity: " + e);
                ce.initCause(e);
                throw ce;
            }
        } finally {
            JDBCUtil.safeClose(ps);
            JDBCUtil.safeClose(c);
        }

        // Mark the inserted fields as clean.
        for (int fieldInd = 0; fieldInd < insertFields.length; ++fieldInd) {
            insertFields[fieldInd].setClean(ctx);
        }
    }

    protected PreparedStatement prepareStatement(Connection c, String sql, CmpEntityBeanContext ctx) throws SQLException {
        return c.prepareStatement(sql);
    }

    protected int executeInsert(int paramIndex, PreparedStatement ps, CmpEntityBeanContext ctx) throws SQLException {
        return ps.executeUpdate();
    }

    protected void afterInsert(CmpEntityBeanContext ctx) throws CreateException {
    }

    protected Object getPrimaryKey(CmpEntityBeanContext ctx) {
        return entity.extractPrimaryKeyFromInstance(ctx);
    }
}
