/*
 * 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.meta.strats;

import java.sql.SQLException;
import java.util.BitSet;
import java.util.Collection;

import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.Table;
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.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StoreManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.ArrayStateImage;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;

/**
 * Uses a state image to determine whether concurrency violations take place.
 *
 * @author Abe White
 */
public class StateComparisonVersionStrategy
    extends AbstractVersionStrategy {

    
    private static final long serialVersionUID = 1L;

    public static final String ALIAS = "state-comparison";

    private static final Localizer _loc = Localizer.forPackage
        (StateComparisonVersionStrategy.class);

    @Override
    public String getAlias() {
        return ALIAS;
    }

    @Override
    public void map(boolean adapt) {
        ClassMapping cls = vers.getClassMapping();
        if (cls.getJoinablePCSuperclassMapping() != null
            || cls.getEmbeddingMetaData() != null)
            throw new MetaDataException(_loc.get("not-base-vers", cls));

        vers.getMappingInfo().assertNoSchemaComponents(vers, true);
    }

    @Override
    public void insert(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
        Object[] state = ArrayStateImage.newImage(fields.length);
        BitSet loaded = ArrayStateImage.getLoaded(state);

        // take a snapshot of all versionable field values
        for (int i = 0; i < fields.length; i++) {
            if (!fields[i].isPrimaryKey() && fields[i].isVersionable()) {
                loaded.set(i);
                state[i] = sm.fetch(fields[i].getIndex());
            }
        }
        sm.setNextVersion(state);
    }

    /**
     * This method is for class mappings that take over the insert
     * process, but still want to use this indicator for optimistic locking.
     */
    @Override
    public void customInsert(OpenJPAStateManager sm, JDBCStore store)
        throws SQLException {
        insert(sm, store, null);
    }

    @Override
    public void update(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        // if there is no recorded state (for example, modification made to
        // hollow instance and no fields every loaded), can't do anything
        Object[] state = (Object[]) sm.getVersion();
        if (state == null)
            return;

        BitSet loaded = ArrayStateImage.getLoaded(state);
        Object[] nextState = ArrayStateImage.clone(state);

        // loop through fields and update changing values for the next state
        // image, plus add WHERE conditions on updates to make sure that
        // db values match our previous image
        FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
        Row row;
        if (sm.isVersionCheckRequired()) {
            for (int i = 0, max = loaded.length(); i < max; i++) {
                if (!loaded.get(i))
                    continue;

                // update our next state image with the new field value
                if (sm.getDirty().get(i) && !sm.getFlushed().get(i))
                    nextState[i] = sm.fetch(fields[i].getIndex());

                // fetch the row for this field; if no row exists, then we can't
                // add one because we have no updates to perform; that means we
                // won't detect OL exceptions when another transaction changes
                // fields that aren't in any of the same tables as fields that
                // this transaction changed
                row = rm.getRow(fields[i].getTable(), Row.ACTION_UPDATE,
                    sm, false);
                if (row == null)
                    continue;

                // set WHERE criteria matching the previous state image so the
                // update will fail for any changes made by another transaction
                fields[i].where(sm, store, rm, state[i]);
                row.setFailedObject(sm.getManagedInstance());
            }
        }
        sm.setNextVersion(nextState);
    }

    /**
     * This method is for class mappings that take over the update
     * process, but still want to use this indicator for optimistic locking.
     *
     * @param sm the instance to test
     * @param store store manager context
     * @param table only state image values in this table will be tested;
     * if the custom mapping uses different updates for
     * different tables, this method can be called multiple
     * times for the multiple tables
     * @param record set this parameter to true the last time you call
     * this method, so the indicator can setup the next
     * version of the given state manager
     * @return a {@link CustomUpdate} whose getSQL method yields a
     * boolean SQL expression that tests whether the current
     * record is equal to our recorded state image, and whose
     * setParameters method parameterizes the given prepared
     * statement with the values used in the above boolean expression
     */
    public CustomUpdate customUpdate(OpenJPAStateManager sm, JDBCStore store,
        Table table, boolean record)
        throws SQLException {
        CustomUpdate custom = new CustomUpdate(table);
        Object[] state = (Object[]) sm.getVersion();
        if (state == null)
            return custom;

        BitSet loaded = ArrayStateImage.getLoaded(state);
        Object[] nextState = null;
        if (record)
            nextState = ArrayStateImage.clone(state);

        FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
        for (int i = 0, max = loaded.length(); i < max; i++) {
            if (!loaded.get(i))
                continue;

            if (record && sm.getDirty().get(i) && !sm.getFlushed().get(i))
                nextState[i] = sm.fetch(fields[i].getIndex());
            if (fields[i].getTable() == table)
                fields[i].where(sm, store, custom, state[i]);
        }
        if (record)
            sm.setNextVersion(nextState);

        return custom;
    }

    @Override
    public void afterLoad(OpenJPAStateManager sm, JDBCStore store) {
        FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();

        Object[] state = (Object[]) sm.getVersion();
        if (state == null)
            state = ArrayStateImage.newImage(fields.length);
        BitSet loaded = ArrayStateImage.getLoaded(state);

        // take a snapshot of all versionable field values that were loaded
        for (int i = 0; i < fields.length; i++) {
            if (!fields[i].isPrimaryKey()
                && fields[i].isVersionable()
                && sm.getLoaded().get(fields[i].getIndex())
                && !loaded.get(i)
                && !sm.getDirty().get(fields[i].getIndex())) {
                loaded.set(i);
                state[i] = sm.fetch(fields[i].getIndex());
            }
        }
        sm.setVersion(state);
    }

    @Override
    public boolean checkVersion(OpenJPAStateManager sm, JDBCStore store,
        boolean updateVersion)
        throws SQLException {
        if (updateVersion)
            sm.setVersion(null);
        return !updateVersion;
    }

    @Override
    public int compareVersion(Object v1, Object v2) {
        return (ArrayStateImage.sameVersion((Object[]) v1, (Object[]) v2))
            ? StoreManager.VERSION_SAME : StoreManager.VERSION_DIFFERENT;
    }

    /**
     * Row implementation we use to pass to versionable mappings so they
     * can set up the where conditions we need to add to update statements.
     *
     * @author Abe White
     */
    public static class CustomUpdate
        extends RowImpl
        implements RowManager {

        private CustomUpdate(Table table) {
            this(table.getColumns());
        }

        private CustomUpdate(Column[] cols) {
            super(cols, Row.ACTION_UPDATE);
        }

        /**
         * Return a boolean SQL expression that should be added to the
         * WHERE clause of an UPDATE to test whether the current database
         * record matches our stored version.
         */
        @Override
        public String getSQL(DBDictionary dict) {
            Column[] cols = getTable().getColumns();
            StringBuilder buf = new StringBuilder();
            boolean hasWhere = false;
            Object val;
            for (Column col : cols) {
                val = getWhere(col);
                if (val == null)
                    continue;

                if (hasWhere)
                    buf.append(" AND ");
                if (val == NULL)
                    buf.append(dict.getColumnDBName(col) + " IS NULL");
                else
                    buf.append(dict.getColumnDBName(col) + " = ?");
                hasWhere = true;
            }
            return buf.toString();
        }

        @Override
        protected RowImpl newInstance(Column[] cols, int action) {
            return new CustomUpdate(cols);
        }

        /////////////////////////////
        // RowManager implementation
        /////////////////////////////

        public boolean hasAutoAssignConstraints() {
            return false;
        }

        public Collection getInserts() {
            throw new InternalException();
        }

        public Collection getUpdates() {
            throw new InternalException();
        }

        public Collection getDeletes() {
            throw new InternalException();
        }

        public Collection getSecondaryUpdates() {
            throw new InternalException();
        }

        public Collection getSecondaryDeletes() {
            throw new InternalException();
        }

        public Collection getAllRowUpdates() {
            throw new InternalException();
        }

        public Collection getAllRowDeletes() {
            throw new InternalException();
        }

        @Override
        public Row getRow(Table table, int action, OpenJPAStateManager sm,
            boolean create) {
            // verionable mappings will never want to create rows, so we
            // can always safely return null
            if (table != getTable())
                return null;
            return this;
        }

        @Override
        public Row getSecondaryRow(Table table, int action) {
            throw new InternalException();
        }

        @Override
        public void flushSecondaryRow(Row row) {
        }

        @Override
        public Row getAllRows(Table table, int action) {
            throw new InternalException();
        }

        @Override
        public void flushAllRows(Row row) {
        }

        @Override
        public void setObject(Column col, Object val)
            throws SQLException {
            throw new InternalException();
        }
    }
}
