/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * UpdateQueryPlan.java
 *
 * Created on October 3, 2001
 *
 */

package com.sun.jdo.spi.persistence.support.sqlstore.sql.generator;

import org.netbeans.modules.dbschema.ColumnElement;
import org.netbeans.modules.dbschema.TableElement;
import com.sun.jdo.api.persistence.support.JDOFatalInternalException;
import com.sun.jdo.spi.persistence.support.sqlstore.ActionDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.SQLStoreManager;
import com.sun.jdo.spi.persistence.support.sqlstore.Transaction;
import com.sun.jdo.spi.persistence.support.sqlstore.model.*;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.UpdateJoinTableDesc;
import com.sun.jdo.spi.persistence.support.sqlstore.sql.UpdateObjectDescImpl;
import com.sun.jdo.spi.persistence.utility.I18NHelper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;


/**
 * This class is used to generated update/insert/delete statements.
 */
public class UpdateQueryPlan extends QueryPlan {

    /** Flag indicating whether we use batch update. */
    private boolean batch = false;

    private UpdateObjectDescImpl updateDesc;

    public UpdateQueryPlan(ActionDesc desc, SQLStoreManager store) {
        super(desc, store);
        this.updateDesc = (UpdateObjectDescImpl) desc;
        this.action = getAction(updateDesc.getUpdateAction());
    }

    private static int getAction(int updateAction) {
        if (updateAction == ActionDesc.LOG_CREATE) {
            return ACT_INSERT;
        } else if (updateAction == ActionDesc.LOG_DESTROY) {
            return ACT_DELETE;
        } else if (updateAction == ActionDesc.LOG_UPDATE) {
            return ACT_UPDATE;
        } else {
            return ACT_NOOP;
        }
    }

    /**
     * Specifies an field the data for which needs to be updated,
     * and the mapped columns for which therefor need to be updated.
     * For update queries the column will be put in the set lists,
     * and for insert queries the column will be put into the
     * insert values lists.
     *
     * @param fieldDesc Updated field corresponding to a column in the database.
     * @param value New value.
     */
    private void addColumn(LocalFieldDesc fieldDesc, Object value) {

        // Ignore secondary tracked fields.
        if ((fieldDesc.sqlProperties & FieldDesc.PROP_SECONDARY_TRACKED_FIELD) > 0) {
            return;
        }

        for (Iterator iter = fieldDesc.getColumnElements(); iter.hasNext(); ) {
            ColumnElement columnElement = (ColumnElement) iter.next();
            TableElement tableElement = columnElement.getDeclaringTable();

            if (tableElement == null) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                        "core.configuration.fieldnotable", // NOI18N
                        fieldDesc.getName()));
            }

            QueryTable t = findQueryTable(tableElement);
            UpdateStatement s = null;

            if (t == null) {
                t = addQueryTable(tableElement, null);
                s = (UpdateStatement) addStatement(t);
            } else {
                s = (UpdateStatement) getStatement(t);
            }

            if (fieldDesc.isVersion() && action == ACT_UPDATE) {
                // For update, version columns will be flagged specially.
                s.addVersionColumn(columnElement);
            } else {
                s.addColumn(columnElement, value);
            }

        }
    }

    public void build() {
        build(false);
    }

    /**
     * Builds a UpdateQueryPlan for an object based update
     * type (i.e. insert, update, delete) ActionDesc.
     *
     * @param batch Flag indicating whether we use batch update.
     */
    public void build(boolean batch) {

        if ((status & ST_BUILT) > 0) return;

        this.batch = batch;

        generateStatements();

        addColumns();

        if (statements.size() > 0) {
            processStatements();
        }

        processJoinTables();

        status = status | ST_BUILT;
    }

    private void generateStatements() {
        // For insert and delete we build a statement for each table
        if ((action == ACT_DELETE) || (action == ACT_INSERT)) {
            Iterator iter = config.getTables();
            while (iter.hasNext()) {
                TableDesc t = (TableDesc) iter.next();

                // Skip join tables
                if (!t.isJoinTable()) {
                    if (findQueryTable(t.getTableElement()) == null)
                        addStatement(addQueryTable(t));
                }
            }

            // For insert statements, we need to reverse the order.
            if (action == ACT_INSERT) {
                int i = 0;
                int j = statements.size() - 1;

                while (i < j) {
                    Statement s = (Statement) statements.get(i);
                    statements.set(i, statements.get(j));
                    statements.set(j, s);
                    i++;
                    j--;
                }
            }
        }
    }

    /**
     * For inserts and updates we figure out values (they might be hidden)
     * and add the columns necessary. <code>addColumn</code> will figure out
     * which tables need to be included.
     */
    private void addColumns() {
        if ((action == ACT_UPDATE) || (action == ACT_INSERT)) {
            List updatedFields = updateDesc.getUpdatedFields();
            int size = (updatedFields != null) ? updatedFields.size() : 0;
            for (int i = 0; i < size; i++) {
                LocalFieldDesc f = (LocalFieldDesc) updatedFields.get(i);
                Object value = batch ? f : updateDesc.getAfterValue(f);
                addColumn(f, value);
            }
        }
    }

    private void addConstraints(UpdateStatement statement,
                                ArrayList localFields,
                                ArrayList foreignFields,
                                ArrayList columns) {

        boolean isBeforeImageRequired = updateDesc.isBeforeImageRequired();
        for (int i = 0; i < localFields.size(); i++) {
            LocalFieldDesc lf = (LocalFieldDesc) localFields.get(i);
            LocalFieldDesc ff = (LocalFieldDesc) foreignFields.get(i);
            ColumnElement ce = (ColumnElement) columns.get(i);

            addConstraint(statement, lf, ff, ce, isBeforeImageRequired);
        }

        // Add the constraint on the version field if needed.
        if (getConfig().hasVersionConsistency() && action != ACT_INSERT) {
            QueryTable table = (QueryTable) statement.getQueryTables().get(0);
            LocalFieldDesc versionField = table.getTableDesc().getVersionField();
            ColumnElement ce = (ColumnElement) versionField.getColumnElements().next();

            addConstraint(statement, versionField, versionField, ce, false);
        }

        statement.markConstraintAdded();
    }

    private void addConstraint(UpdateStatement statement,
                               LocalFieldDesc lf,
                               LocalFieldDesc ff,
                               ColumnElement ce,
                               boolean isBeforeImageRequired) {

        if (action != ACT_INSERT) {
            if (batch) {
                statement.addConstraint(ce, lf, ff);
            }
            else {
                Object value = isBeforeImageRequired ?
                    updateDesc.getBeforeValue(ff) : updateDesc.getAfterValue(ff);
                statement.addConstraint(lf, value);
            }
        } else {
            Object value = batch ? ff : updateDesc.getAfterValue(ff);
            statement.addColumn(ce, value);
        }
    }

    private void addSecondaryTableConstraint(UpdateStatement statement) {
        QueryTable table = (QueryTable) statement.getQueryTables().get(0);
        ReferenceKeyDesc key = table.getTableDesc().getPrimaryTableKey();

        ArrayList localFields = key.getReferencingKey().getFields();
        ArrayList foreignFields = key.getReferencedKey().getFields();
        ArrayList columns = key.getReferencingKey().getColumns();

        addConstraints(statement, localFields, foreignFields, columns);
    }

    private void addBasetableConstraint(UpdateStatement statement) {
        QueryTable table = (QueryTable) statement.getQueryTables().get(0);
        KeyDesc key = table.getTableDesc().getKey();

        ArrayList localFields = key.getFields();
        ArrayList columns = key.getColumns();

        addConstraints(statement, localFields, localFields, columns);
    }

    private void processRelatedStatements(UpdateStatement statement) {
        ArrayList secondaryTableStatements = statement.getSecondaryTableStatements();

        if (secondaryTableStatements != null) {
            for (int i = 0; i < secondaryTableStatements.size(); i++) {
                UpdateStatement secondaryTableStatement = (UpdateStatement) secondaryTableStatements.get(i);

                if (!secondaryTableStatement.isConstraintAdded()) {
                    processRelatedStatements(secondaryTableStatement);

                    addSecondaryTableConstraint(secondaryTableStatement);
                }
            }
        }
    }

    protected void processStatements() {
        int size = statements.size();

        if (size > 1) {
            super.processStatements();

            for (int i = 0; i < size; i++) {
                UpdateStatement statement = (UpdateStatement) statements.get(i);

                if (!statement.isConstraintAdded())
                    processRelatedStatements(statement);
            }
        }

        UpdateStatement masterStatement = null;

        if (size == 1)
            masterStatement = (UpdateStatement) statements.get(0);
        else {
            // Look for the master statement. It should be the one
            // with no constraints added.
            for (int i = 0; i < size; i++) {
                masterStatement = (UpdateStatement) statements.get(i);

                if (!masterStatement.isConstraintAdded()) break;
            }
        }

        if (action != ACT_INSERT)
            addBasetableConstraint(masterStatement);

        if ((action != ACT_INSERT) && (updateDesc.getConcurrency() != null))
            updateDesc.getConcurrency().update(this);
    }

    private void processJoinTables() {
        Collection fields = updateDesc.getUpdatedJoinTableFields();

        if (fields == null) return;

        Iterator fieldIter = fields.iterator();

        ArrayList deleteStatements = new ArrayList();
        ArrayList insertStatements = new ArrayList();

        while (fieldIter.hasNext()) {
            ForeignFieldDesc f = (ForeignFieldDesc) fieldIter.next();
            Collection descs = updateDesc.getUpdateJoinTableDescs(f);
            Iterator descIter = descs.iterator();

            ColumnElement c = (ColumnElement) f.assocLocalColumns.get(0);
            QueryTable t = addQueryTable(config.findTableDesc(c.getDeclaringTable()));

            while (descIter.hasNext()) {
                UpdateJoinTableDesc desc = (UpdateJoinTableDesc) descIter.next();
                int action = getAction(desc.getAction());

                UpdateStatement s = (UpdateStatement) createStatement(t);
                s.setAction(action);

                if (action == ACT_INSERT) {
                    insertStatements.add(s);
                } else if (action == ACT_DELETE) {

                    // RESOLVE: There are redundant deletes from join tables that causes
                    // update to fail with no rows affected. To work around this problem
                    // for now, we set the minAffectedRows to 0.
                    // We need to figure out why there are redundant deletes.

                    s.minAffectedRows = 0;
                    deleteStatements.add(s);
                }

                s.addLocalConstraints(action, f, desc.getParentStateManager());
                s.addForeignConstraints(action, f, desc.getForeignStateManager());
            }
        }

        // All join table delete statements have to go first and all
        // join table insert statements have to go last.

        ArrayList oldStatements = statements;
        statements = deleteStatements;
        statements.addAll(oldStatements);
        statements.addAll(insertStatements);
    }

    protected Statement newStatement() {
        return new UpdateStatement(store.getVendorType(), this, batch);
    }

    /**
     * Determines if the amount of batched operations exceeded a threshold.
     * @param tran the transaction
     * @return true if the amount of batched operations exceeded a threshold
     */
    public boolean checkBatchThreshold(Transaction tran)
    {
        for (int i = 0, size = statements.size(); i < size; i++) {
            UpdateStatement updateStatement = (UpdateStatement) statements.get(i);
            if (updateStatement.exceedsBatchThreshold(tran))
                return true;
        }
        return false;
    }

}
