/*
 * 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.Collection;
import java.util.Iterator;

import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.sql.PrimaryRow;
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.RowManagerImpl;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.kernel.OpenJPAStateManager;

/**
 * Update manager that writes SQL in object-level operation order.
 *
 * @author Abe White
 */
public class OperationOrderUpdateManager
    extends AbstractUpdateManager {

    @Override
    public boolean orderDirty() {
        return true;
    }

    @Override
    protected RowManager newRowManager() {
        return new RowManagerImpl(true);
    }

    @Override
    protected PreparedStatementManager newPreparedStatementManager
        (JDBCStore store, Connection conn) {
        return new PreparedStatementManagerImpl(store, conn);
    }

    @Override
    protected Collection flush(RowManager rowMgr,
        PreparedStatementManager psMgr, Collection exceps) {
        RowManagerImpl rmimpl = (RowManagerImpl) rowMgr;

        // first take care of all secondary table deletes and 'all row' deletes
        // (which are probably secondary table deletes), since no foreign
        // keys ever rely on secondary table pks
        flush(rmimpl.getAllRowDeletes(), psMgr);
        flush(rmimpl.getSecondaryDeletes(), psMgr);

        // now do any 'all row' updates, which typically null keys
        flush(rmimpl.getAllRowUpdates(), psMgr);

        // gather any updates we need to avoid fk constraints on deletes
        Collection constraintUpdates = null;
        for (Iterator itr = rmimpl.getDeletes().iterator(); itr.hasNext();) {
            try {
                constraintUpdates = analyzeDeleteConstraints(rmimpl,
                    (PrimaryRow) itr.next(), constraintUpdates);
            } catch (SQLException se) {
                exceps = addException(exceps, SQLExceptions.getStore
                    (se, dict));
            }
        }
        if (constraintUpdates != null) {
            flush(constraintUpdates, psMgr);
            constraintUpdates.clear();
        }

        // flush primary rows in order
        for (Iterator itr = rmimpl.getOrdered().iterator(); itr.hasNext();) {
            try {
                constraintUpdates = flushPrimaryRow(rmimpl, (PrimaryRow)
                    itr.next(), psMgr, constraintUpdates);
            } catch (SQLException se) {
                exceps = addException(exceps, SQLExceptions.getStore
                    (se, dict));
            }
        }
        if (constraintUpdates != null)
            flush(constraintUpdates, psMgr);

        // take care of all secondary table inserts and updates last, since
        // they may rely on previous inserts or updates, but nothing relies
        // on them
        flush(rmimpl.getSecondaryUpdates(), psMgr);

        // flush any left over prepared statements
        psMgr.flush();
        return exceps;
    }

    /**
     * Analyze the delete constraints on the given row, gathering necessary
     * updates to null fks before deleting.
     */
    private Collection analyzeDeleteConstraints(RowManagerImpl rowMgr,
        PrimaryRow row, Collection updates)
        throws SQLException {
        if (!row.isValid())
            return updates;

        ForeignKey[] fks = row.getTable().getForeignKeys();
        OpenJPAStateManager sm;
        PrimaryRow rel;
        RowImpl update;
        for (ForeignKey fk : fks) {
            // when deleting ref fks we set the where value instead
            sm = row.getForeignKeySet(fk);
            if (sm == null)
                sm = row.getForeignKeyWhere(fk);
            if (sm == null)
                continue;

            // only need an update if we have an fk to a row that's being
            // deleted before we are
            rel = (PrimaryRow) rowMgr.getRow(fk.getPrimaryKeyTable(),
                    Row.ACTION_DELETE, sm, false);
            if (rel == null || !rel.isValid()
                    || rel.getIndex() >= row.getIndex())
                continue;

            // create an update to null the offending fk before deleting.  use
            // a primary row to be sure to copy delayed-flush pks/fks
            update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
            row.copyInto(update, true);
            update.setForeignKey(fk, row.getForeignKeyIO(fk), null);
            if (updates == null)
                updates = new ArrayList();
            updates.add(update);
        }
        return updates;
    }

    /**
     * Flush the given row, creating deferred updates for dependencies.
     */
    private Collection flushPrimaryRow(RowManagerImpl rowMgr, PrimaryRow row,
        PreparedStatementManager psMgr, Collection updates)
        throws SQLException {
        if (!row.isValid())
            return updates;

        // already analyzed deletes
        if (row.getAction() == Row.ACTION_DELETE) {
            psMgr.flush(row);
            return updates;
        }

        ForeignKey[] fks = row.getTable().getForeignKeys();
        OpenJPAStateManager sm;
        PrimaryRow rel;
        PrimaryRow update;
        for (ForeignKey fk : fks) {
            sm = row.getForeignKeySet(fk);
            if (sm == null)
                continue;

            // only need an update if we have an fk to a row that's being
            // inserted after we are; if row is dependent on itself and no
            // fk, must be an auto-inc because otherwise we wouldn't have
            // recorded it
            rel = (PrimaryRow) rowMgr.getRow(fk.getPrimaryKeyTable(),
                    Row.ACTION_INSERT, sm, false);
            if (rel == null || !rel.isValid()
                    || rel.getIndex() < row.getIndex()
                    || (rel == row && !fk.isDeferred() && !fk.isLogical()))
                continue;

            // don't insert or update with the given fk; create a deferred
            // update for after the rel row has been inserted; use a primary row
            // to prevent setting values until after flush to get auto-inc
            update = new PrimaryRow(row.getTable(), Row.ACTION_UPDATE, null);
            if (row.getAction() == Row.ACTION_INSERT)
                update.wherePrimaryKey(row.getPrimaryKey());
            else
                row.copyInto(update, true);
            update.setForeignKey(fk, row.getForeignKeyIO(fk), sm);
            row.clearForeignKey(fk);

            if (updates == null)
                updates = new ArrayList();
            updates.add(update);
        }

        if (row.isValid()) // if update, maybe no longer needed
            psMgr.flush(row);
        return updates;
    }

    /**
     * Flush the given collection of secondary rows.
     */
    protected void flush(Collection rows, PreparedStatementManager psMgr) {
        if (rows.isEmpty())
            return;

        RowImpl row;
        for (Object o : rows) {
            row = (RowImpl) o;
            if (row.isValid())
                psMgr.flush(row);
        }
    }
}
