/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.jdbc.kernel;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.Discriminator;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.Strategy;
import org.apache.openjpa.jdbc.meta.Version;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.jdbc.sql.RowImpl;
import org.apache.openjpa.jdbc.sql.RowManager;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.PCState;
import org.apache.openjpa.kernel.StateManagerImpl;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.OptimisticException;

/**
 * Base update manager with common functionality.
 *
 * @author Abe White
 */
public abstract class AbstractUpdateManager
    implements UpdateManager, Configurable {

    protected JDBCConfiguration conf = null;
    protected DBDictionary dict = null;

    @Override
    public void setConfiguration(Configuration conf) {
        this.conf = (JDBCConfiguration) conf;
        dict = this.conf.getDBDictionaryInstance();
    }

    @Override
    public void startConfiguration() {
    }

    @Override
    public void endConfiguration() {
    }

    @Override
    public Collection flush(Collection states, JDBCStore store) {
        Connection conn = store.getConnection();
        try {
            PreparedStatementManager psMgr = newPreparedStatementManager(store,
                conn);
            return flush(states, store, psMgr);
        } finally {
            try { conn.close(); } catch (SQLException se) {}
        }
    }

    protected Collection flush(Collection states, JDBCStore store,
        PreparedStatementManager psMgr) {
        // run through all the states and update them as necessary
        RowManager rowMgr = newRowManager();
        Collection customs = new LinkedList();
        Collection exceps = psMgr.getExceptions();
        Collection mappedByIdStates = new ArrayList();
        for (Object state : states) {
            OpenJPAStateManager obj = (OpenJPAStateManager) state;
            if (obj instanceof StateManagerImpl) {
                StateManagerImpl sm = (StateManagerImpl) obj;
                if (sm.getMappedByIdFields() != null)
                    mappedByIdStates.add(sm);
                else exceps = populateRowManager(sm, rowMgr, store, exceps,
                        customs);
            }
            else
                exceps = populateRowManager(obj, rowMgr, store, exceps,
                        customs);
        }

        // flush rows
        exceps = flush(rowMgr, psMgr, exceps);

        if (mappedByIdStates.size() != 0) {
            for (Object mappedByIdState : mappedByIdStates) {
                StateManagerImpl sm = (StateManagerImpl) mappedByIdState;
                exceps = populateRowManager(sm, rowMgr, store, exceps, customs);
            }
            // flush rows
            exceps = flush(rowMgr, psMgr, exceps);
        }

        // now do any custom mappings
        for (Iterator itr = customs.iterator(); itr.hasNext();) {
            try {
                ((CustomMapping) itr.next()).execute(store);
            } catch (SQLException se) {
                exceps = addException(exceps, SQLExceptions.getStore(se, dict));
            } catch (OpenJPAException ke) {
                exceps = addException(exceps, ke);
            }
        }

        // return all exceptions
        return exceps;
    }

    /**
     * Return a new {@link RowManager}.
     */
    protected abstract RowManager newRowManager();

    /**
     * Return a new {@link PreparedStatementManager}.
     */
    protected abstract PreparedStatementManager newPreparedStatementManager(
        JDBCStore store, Connection conn);

    /**
     * Flush all rows of the given row manager. Add exceptions to
     * <code>exceps</code> (which may start as null) using
     * {@link #addException}. Return <code>exceps</code>.
     */
    protected abstract Collection flush(RowManager rowMgr,
        PreparedStatementManager psMgr, Collection exceps);

    /**
     * Populate the row manager with rows to be flushed for the given state.
     *
     * @param exceps exceptions encountered when flushing will be added to
     * this list and returned; the list may be null initially
     * @param customs buffer custom mappings
     * @return the exceptions list
     */
    protected Collection populateRowManager(OpenJPAStateManager sm,
        RowManager rowMgr, JDBCStore store, Collection exceps,
        Collection customs) {
    	int action = Row.ACTION_UPDATE;
        try {
            BitSet dirty;
            if (sm.getPCState() == PCState.PNEW && !sm.isFlushed()) {
            	action = Row.ACTION_INSERT;
            	insert(sm, (ClassMapping) sm.getMetaData(), rowMgr, store,
                    customs);
            } else if (sm.getPCState() == PCState.PNEWFLUSHEDDELETED
                || sm.getPCState() == PCState.PDELETED) {
            	action = Row.ACTION_DELETE;
            	delete(sm, (ClassMapping) sm.getMetaData(), rowMgr, store,
                    customs);
            } else if ((dirty = ImplHelper.getUpdateFields(sm)) != null) {
                update(sm, dirty, (ClassMapping) sm.getMetaData(), rowMgr,
                    store, customs, false);
            } else if (sm.isVersionUpdateRequired()) {
                updateIndicators(sm, (ClassMapping) sm.getMetaData(), rowMgr,
                    store, customs, true);
            } else if (sm.isVersionCheckRequired()) {
                if (!((ClassMapping) sm.getMetaData()).getVersion().
                    checkVersion(sm, store, false))
                    exceps = addException(exceps, new OptimisticException(sm.
                        getManagedInstance()));
            }
        } catch (SQLException se) {
            exceps = addException(exceps, SQLExceptions.getStore(se, dict));
        } catch (OpenJPAException ke) {
        	RowImpl row = (RowImpl) rowMgr.getRow(((ClassMapping) sm.getMetaData()).getTable(), action, sm, false);
            if (row != null) {
                row.setFlushed(true);
            }
            exceps = addException(exceps, ke);
        }
        return exceps;
    }

    /**
     * Add the given exception to the given list, which may start out as null.
     */
    protected Collection addException(Collection exceps, Exception err) {
        if (exceps == null)
            exceps = new LinkedList();
        exceps.add(err);
        return exceps;
    }

    /**
     * Recursive method to insert the given instance, base class first.
     */
    protected void insert(OpenJPAStateManager sm, ClassMapping mapping,
        RowManager rowMgr, JDBCStore store, Collection customs)
        throws SQLException {
        Boolean custom = mapping.isCustomInsert(sm, store);
        if (!Boolean.FALSE.equals(custom))
            mapping.customInsert(sm, store);
        if (Boolean.TRUE.equals(custom))
            return;

        ClassMapping sup = mapping.getJoinablePCSuperclassMapping();
        if (sup != null)
            insert(sm, sup, rowMgr, store, customs);

        mapping.insert(sm, store, rowMgr);
        FieldMapping[] fields = mapping.getDefinedFieldMappings();
        if (((StateManagerImpl)sm).getMappedByIdFields() != null) {
            // when there is mappedByIdFields, the id field is not
            // fully populated. We need to insert other fields first
            // so that in the process of inserting other fields,
            // the values of mappedById fields can be set into
            // the id fields. Once the id fields are fully populated,
            // we will then insert the id fields.
            fields = reorderFields(fields);
        }

        BitSet dirty = sm.getDirty();
        for (FieldMapping field : fields) {
            if (dirty.get(field.getIndex())
                    && !bufferCustomInsert(field, sm, store, customs)) {
                field.insert(sm, store, rowMgr);
            }
        }
        if (sup == null) {
            Version vers = mapping.getVersion();
            if (!bufferCustomInsert(vers, sm, store, customs))
                vers.insert(sm, store, rowMgr);
            Discriminator dsc = mapping.getDiscriminator();
            if (!bufferCustomInsert(dsc, sm, store, customs))
                dsc.insert(sm, store, rowMgr);
        }
    }

    private FieldMapping[] reorderFields(FieldMapping[] fields) {
        List<FieldMapping> pkFmds = new ArrayList<>();
        FieldMapping[] ret = new FieldMapping[fields.length];
        int j = 0;
        for (FieldMapping field : fields) {
            if (!field.isPrimaryKey())
                ret[j++] = field;
            else
                pkFmds.add(field);
        }
        for (FieldMapping pkFmd : pkFmds) {
            ret[j++] = pkFmd;
        }
        return ret;
    }

    /**
     * If the given mapping uses a custom insert, places a
     * {@link CustomMapping} struct for it in the given collection and
     * returns true, else returns false.
     */
    private boolean bufferCustomInsert(Strategy strat, OpenJPAStateManager sm,
        JDBCStore store, Collection customs) {
        Boolean custom = strat.isCustomInsert(sm, store);
        if (!Boolean.FALSE.equals(custom))
            customs.add(new CustomMapping(CustomMapping.INSERT, sm, strat));
        return Boolean.TRUE.equals(custom);
    }

    /**
     * Recursive method to delete the given instance, base class last.
     */
    protected void delete(OpenJPAStateManager sm, ClassMapping mapping,
        RowManager rowMgr, JDBCStore store, Collection customs)
        throws SQLException {
        Boolean custom = mapping.isCustomDelete(sm, store);
        if (!Boolean.FALSE.equals(custom))
            mapping.customDelete(sm, store);
        if (Boolean.TRUE.equals(custom))
            return;

        FieldMapping[] fields = mapping.getDefinedFieldMappings();
        for (FieldMapping field : fields)
            if (!bufferCustomDelete(field, sm, store, customs))
                field.delete(sm, store, rowMgr);

        ClassMapping sup = mapping.getJoinablePCSuperclassMapping();
        if (sup == null) {
            Version vers = mapping.getVersion();
            if (!bufferCustomDelete(vers, sm, store, customs))
                vers.delete(sm, store, rowMgr);
            Discriminator dsc = mapping.getDiscriminator();
            if (!bufferCustomDelete(dsc, sm, store, customs))
                dsc.delete(sm, store, rowMgr);
        }
        mapping.delete(sm, store, rowMgr);

        if (sup != null)
            delete(sm, sup, rowMgr, store, customs);
    }

    /**
     * @see #bufferCustomInsert
     */
    private boolean bufferCustomDelete(Strategy strat, OpenJPAStateManager sm,
        JDBCStore store, Collection customs) {
        Boolean custom = strat.isCustomDelete(sm, store);
        if (!Boolean.FALSE.equals(custom))
            customs.add(new CustomMapping(CustomMapping.DELETE, sm, strat));
        return Boolean.TRUE.equals(custom);
    }

    /**
     * Recursive method to update the given instance.
     */
    protected void update(OpenJPAStateManager sm, BitSet dirty,
        ClassMapping mapping, RowManager rowMgr, JDBCStore store,
        Collection customs, boolean updateIndicators) throws SQLException {
        Boolean custom = mapping.isCustomUpdate(sm, store);
        if (!Boolean.FALSE.equals(custom))
            mapping.customUpdate(sm, store);
        if (Boolean.TRUE.equals(custom))
            return;

        // update all fields before all mappings so that the mappings can
        // detect whether any fields in their rows have been modified
        FieldMapping[] fields = mapping.getDefinedFieldMappings();
        for (FieldMapping field : fields) {
            if (dirty.get(field.getIndex())
                    && !bufferCustomUpdate(field, sm, store, customs)) {
                field.update(sm, store, rowMgr);
                if (!updateIndicators) {
                    FieldMapping[] inverseFieldMappings =
                            field.getInverseMappings();
                    if (inverseFieldMappings.length == 0) {
                        updateIndicators = true;
                    }
                    else {
                        for (FieldMapping inverseFieldMapping :
                                inverseFieldMappings) {
                            if (inverseFieldMapping.getMappedBy() != null) {
                                updateIndicators = true;
                                break;
                            }
                        }
                    }
                }
            }
        }

        ClassMapping sup = mapping.getJoinablePCSuperclassMapping();
        if (sup == null) {
            if (updateIndicators) {
                updateIndicators(sm, mapping, rowMgr, store, customs, false);
            }
        }
        else
            update(sm, dirty, sup, rowMgr, store, customs, updateIndicators);

        mapping.update(sm, store, rowMgr);
    }

    /**
     * Update version and discriminator indicators.
     */
    protected void updateIndicators(OpenJPAStateManager sm,
            ClassMapping mapping, RowManager rowMgr, JDBCStore store,
            Collection customs,
        boolean versionUpdateOnly) throws SQLException {
        while (mapping.getJoinablePCSuperclassMapping() != null)
            mapping = mapping.getJoinablePCSuperclassMapping();

        Version vers = mapping.getVersion();
        if (!bufferCustomUpdate(vers, sm, store, customs))
            vers.update(sm, store, rowMgr);

        if (versionUpdateOnly) {
            // if we are only updating the version column, we need to add
            // in the primary key select
            mapping.update(sm, store, rowMgr);
        } else {
            // otherwise we need to make sure we update the discriminator too
            Discriminator dsc = mapping.getDiscriminator();
            if (!bufferCustomUpdate(dsc, sm, store, customs))
                dsc.update(sm, store, rowMgr);
        }
    }

    /**
     * @see #bufferCustomInsert
     */
    private boolean bufferCustomUpdate(Strategy strat, OpenJPAStateManager sm,
        JDBCStore store, Collection customs) {
        Boolean custom = strat.isCustomUpdate(sm, store);
        if (!Boolean.FALSE.equals(custom))
            customs.add(new CustomMapping(CustomMapping.UPDATE, sm, strat));
        return Boolean.TRUE.equals(custom);
    }

    /**
     * Executes customized mapping updates.
     */
    protected static class CustomMapping {

        public static final int INSERT = 0;
        public static final int UPDATE = 1;
        public static final int DELETE = 3;

        private final int _action;
        private final OpenJPAStateManager _sm;
        private final Strategy _strat;

        public CustomMapping(int action, OpenJPAStateManager sm, Strategy strat)
        {
            _action = action;
            _sm = sm;
            _strat = strat;
        }

        public void execute(JDBCStore store) throws SQLException {
            switch (_action) {
            case INSERT:
                _strat.customInsert(_sm, store);
                break;
            case UPDATE:
                _strat.customUpdate(_sm, store);
                break;
            case DELETE:
                _strat.customDelete(_sm, store);
                break;
            }
        }
    }
}
