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

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.util.InternalException;

/**
 * Manages SQL rows during an insert/update/delete process.
 *
 * @author Abe White
 */
public class RowManagerImpl
    implements RowManager {

    private Map<Key, PrimaryRow> _inserts = null;
    private Map<Key, PrimaryRow> _updates = null;
    private Map<Key, PrimaryRow> _deletes = null;
    private Collection<SecondaryRow> _secondaryUpdates = null;
    private Collection<SecondaryRow> _secondaryDeletes = null;
    private Collection<Row> _allRowUpdates = null;
    private Collection<Row> _allRowDeletes = null;

    // we maintain a list of the order of all primary rows if the user
    // wants to be able to fetch them in order
    private final List<PrimaryRow> _primaryOrder;

    // track whether we're dealing with any auto-inc columns
    private boolean _auto = false;

    // cache the last key and primary row; when looping over
    // all the field mappings of a class each one will probably ask for the
    // same key, so avoid the key creation and row lookup when possible
    private Key _key = null;
    private PrimaryRow _row = null;

    /**
     * Constructor.
     *
     * @param order whether to keep track of the order in which rows are added
     */
    public RowManagerImpl(boolean order) {
        _primaryOrder = (order) ? new ArrayList<>() : null;
    }

    /**
     * Whether any primary rows have auto-assign constraints.
     */
    public boolean hasAutoAssignConstraints() {
        return _auto;
    }

    /**
     * Return the ordered primary rows. Only available if ordering requested
     * on construction.
     */
    public List<PrimaryRow> getOrdered() {
        if(_primaryOrder == null ) {
            return Collections.emptyList();
        }
        else {
            return _primaryOrder;
        }
    }

    /**
     * Return all inserted primary rows.
     */
    public Collection<PrimaryRow> getInserts() {
        if(_inserts == null ) {
            return Collections.emptyList();
        }
        else {
            return _inserts.values();
        }
    }

    /**
     * Return all updated primary rows.
     */
    public Collection<PrimaryRow> getUpdates() {
        if(_updates == null ){
            return Collections.emptyList();
        }
        else {
            return _updates.values();
        }
    }

    /**
     * Return all deleted primary rows.
     */
    public Collection<PrimaryRow> getDeletes() {
        if(_deletes == null) {
            return Collections.emptyList();
        }
        else {
            return _deletes.values();
        }
    }

    /**
     * Return all inserted and updated secondary rows.
     */
    public Collection<SecondaryRow> getSecondaryUpdates() {
        if(_secondaryUpdates == null) {
            return Collections.emptyList();
        }
        else {
            return _secondaryUpdates;
        }
    }

    /**
     * Return all deleted secondary rows.
     */
    public Collection<SecondaryRow> getSecondaryDeletes() {
        if(_secondaryDeletes == null) {
            return Collections.emptyList();
        }
        else {
            return _secondaryDeletes;
        }
    }

    /**
     * Return any 'all row' updates.
     */
    public Collection<Row> getAllRowUpdates() {
        if(_allRowUpdates == null) {
            return Collections.emptyList();
        }
        else {
            return _allRowUpdates;
        }
    }

    /**
     * Return any 'all row' deletes.
     */
    public Collection<Row> getAllRowDeletes() {
        if(_allRowDeletes == null) {
            return Collections.emptyList();
        }
        else {
            return _allRowDeletes;
        }

    }

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

    @Override
    public void flushSecondaryRow(Row row)
        throws SQLException {
        if (!row.isValid())
            return;

        SecondaryRow srow = (SecondaryRow) row;
        if (srow.getAction() == Row.ACTION_DELETE) {
            if (_secondaryDeletes == null)
                _secondaryDeletes = new ArrayList<>();
            _secondaryDeletes.add((SecondaryRow) srow.clone());
        } else {
            if (_secondaryUpdates == null)
                _secondaryUpdates = new ArrayList<>();
            _secondaryUpdates.add((SecondaryRow) srow.clone());
        }
    }

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

    @Override
    public void flushAllRows(Row row) {
        if (!row.isValid())
            return;

        switch (row.getAction()) {
            case Row.ACTION_UPDATE:
                if (_allRowUpdates == null)
                    _allRowUpdates = new ArrayList<>();
                _allRowUpdates.add(row);
                break;
            case Row.ACTION_DELETE:
                if (_allRowDeletes == null)
                    _allRowDeletes = new ArrayList<>();
                _allRowDeletes.add(row);
                break;
            default:
                throw new InternalException("action = " + row.getAction());
        }
    }

    @Override
    public Row getRow(Table table, int action, OpenJPAStateManager sm,
        boolean create) {
        if (sm == null)
            return null;

        // check if request matches cached version
        if (_key != null && _key.table == table && _key.sm == sm
            && _row != null && _row.getAction() == action)
            return _row;

        Map<Key, PrimaryRow> map;
        if (action == Row.ACTION_DELETE) {
            if (_deletes == null && create)
                _deletes = new LinkedHashMap<>();
            map = _deletes;
        } else if (action == Row.ACTION_INSERT) {
            if (_inserts == null && create)
                _inserts = new LinkedHashMap<>();
            map = _inserts;
        } else {
            if (_updates == null && create)
                _updates = new LinkedHashMap<>();
            map = _updates;
        }
        if (map == null)
            return null;

        _key = new Key(table, sm);
        _row = map.get(_key);

        if (_row == null && create) {
            _row = new PrimaryRow(table, action, sm);
            map.put(_key, _row);
            if (_primaryOrder != null) {
                _row.setIndex(_primaryOrder.size());
                _primaryOrder.add(_row);
            }

            if (!_auto && action == Row.ACTION_INSERT)
                _auto = table.getAutoAssignedColumns().length > 0;
        }

        if (_row != null)
            _row.setFailedObject(sm.getManagedInstance());
        return _row;
    }

    /**
     * Key for hashing virtual rows.
     */
    private static class Key {

        public final Table table;
        public final OpenJPAStateManager sm;

        public Key(Table table, OpenJPAStateManager sm) {
            this.table = table;
            this.sm = sm;
        }

        @Override
        public int hashCode() {
            return ((table == null) ? 0  : table.hashCode()) + ((sm == null) ? 0  : sm.hashCode()) % Integer.MAX_VALUE;
        }

        @Override
        public boolean equals(Object other) {
            if (other == null)
                return false;
            if (other == this)
                return true;

            Key key = (Key) other;
            return table == key.table && sm == key.sm;
        }
    }
}
