/*
 * This file is part of the DistSim distributed simulation framework (client)
 * Copyright (C) 2007 Ulf Hermann
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package brn.distsim.client.data;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * This class represents all changeable data that can be loaded from a database.
 * It's counterpart is Saveable which represents all data objects that can be
 * saved to the database. A list of dependent data objects, which must be
 * saveable is kept and loaded and updated to the users wishes. Changes since
 * the last synchronization with the database are tracked in three sets
 * "inserted", "updated" and "deleted". These changes can be synchronized with
 * the database by calling commit(). loadFromDb(), on the other hand, undoes all
 * changes and resets the object's state to the state of the database.
 * 
 * @author alve
 * 
 * @param <D>
 */
public abstract class DbBackedData<D extends Saveable> implements Iterable<D> {

	protected Connection definitions;

	protected Statement statement;

	protected Set<D> updated;

	protected Set<D> inserted;

	protected Set<D> deleted;

	protected ArrayList<D> dependentData;

	protected int id;
	
	/**
	 * Prepares a DbBackedData object
	 * 
	 * @param definitions
	 *            connection to the database
	 */
	protected DbBackedData(Connection definitions) {
		this.definitions = definitions;
		try {
			statement = definitions.createStatement();
			dependentData = new ArrayList<D>();
			updated = new HashSet<D>();
			deleted = new HashSet<D>();
			inserted = new HashSet<D>();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * determines if this object has been changed. As DbBackedData doesn't carry
	 * any own data, only the sets reflecting changes in the dependent data
	 * objects need to be searched.
	 * 
	 * @return true if something has been changed in the dependent data objects,
	 *         false otherwise
	 */
	public boolean isChanged() {
		return (!(inserted.isEmpty() && updated.isEmpty() && deleted.isEmpty()));
	}

	/**
	 * write all changes in dependent data objects to the database. Examine the
	 * "delete", "update" and "insert" sets and perform the respective changes
	 * in the database. Clear the sets if successful. This method starts a
	 * transaction if none is running and rolls back its transaction if an error
	 * occures. If a transaction is already running and an error occures the
	 * exception is thrown without rolling back the transaction. The dependent
	 * data objects may call their own commit methods when
	 * performDb{Updata|Insert|Delete} is called, so this method can be called
	 * recursively.
	 * 
	 * @throws SQLException
	 *             if some query fails
	 */
	public void commit() throws SQLException {
		boolean commit = definitions.getAutoCommit();
		try {
			if (commit)
				definitions.setAutoCommit(false);

			for (D i : updated) {
				i.performDbUpdate();
			}
			updated.clear();

			for (D i : deleted) {
				i.performDbDelete();
			}
			deleted.clear();

			for (D i : inserted) {
				i.performDbInsert(id);
			}
			inserted.clear();
			if (commit) {
				definitions.commit();
				definitions.setAutoCommit(true);
			}
		} catch (SQLException e) {
			if (commit) {
				definitions.rollback();
				definitions.setAutoCommit(true);
			}
			throw e;
		}
	}

	/**
	 * removes the dependent data object at position pos. removes the data
	 * object from the "insert" and "update" sets, and adds it to the "delete"
	 * set.
	 * 
	 * @param pos
	 *            the position of the object to be removed
	 * @return the object which was removed
	 */
	public D remove(int pos) {
		D data = dependentData.remove(pos);
		removeInChangeTrackers(data);
		return data;
	}

	protected void removeInChangeTrackers(D data) {
		if (inserted.contains(data)) {
			inserted.remove(data);
		} else {
			updated.remove(data);
			deleted.add(data);
		}
	}

	/**
	 * add a dependent data object to the end of the list mark the item as "to
	 * be inserted".
	 * 
	 * @param item
	 *            the dependet data object to be added
	 */
	public void add(D item) {
		dependentData.add(item);
		inserted.add(item);
	}

	public void add(int pos, D item) {
		dependentData.add(pos, item);
		inserted.add(item);
	}

	/**
	 * Replace the dependent data object at position pos. throw an exception if
	 * pos is beyond the last object in the list. Mark the new item as "to be
	 * updated" or "to be inserted"
	 * 
	 * @param pos
	 *            the position to be replaced
	 * @param item
	 *            the item to be put at pos
	 * @return the item which previously was at pos
	 */
	public D set(int pos, D item) {
		D old = dependentData.get(pos);
		dependentData.set(pos, item);
		if (inserted.contains(old)) {
			inserted.remove(old);
			inserted.add(item);
		} else {
			updated.remove(old);
			updated.add(item);
		}
		return old;
	}

	/**
	 * retrieve the dependent data object at position pos
	 * 
	 * @param pos
	 *            position of the item to be retrieved
	 * @return the retrieved item
	 */
	public D get(int pos) {
		return dependentData.get(pos);
	}

	/**
	 * clear the dependent data list and mark all dependent data as "to be
	 * deleted"
	 * 
	 */
	public void deleteAll() {
		deleted.addAll(dependentData);
		deleted.removeAll(inserted);
		dependentData.clear();
		inserted.clear();
		updated.clear();
	}

	/**
	 * return the number of dependent data objects currently held
	 * 
	 * @return the number of dependent data objects
	 */
	public int size() {
		return dependentData.size();
	}

	protected void loadFromDb(ResultSet data) throws SQLException {
		dependentData.clear();
		updated.clear();
		deleted.clear();
		inserted.clear();
		while (data.next()) {
			D entry = getDependent(data);
			dependentData.add(entry);
		}
	}

	/**
	 * tells if the dependent data object at position pos was inserted since the
	 * last commit. This is useful if you don't want to change data belonging to
	 * already running or completed simulations.
	 * 
	 * @param pos
	 *            the position of the dependent data object to be examined
	 * @return true if the object was inserted in this session, false if it
	 *         already was there before.
	 */
	public boolean isInserted(int pos) {
		return inserted.contains(dependentData.get(pos));
	}

	/**
	 * Loads this objects data and the dependent data of the first "step" from
	 * the database. If a StudyTree is loaded, the Studies are loaded, too, but
	 * no groups. If a Study is loaded, it's groups are loaded, too, but no
	 * simulations ...
	 * 
	 * @throws SQLException
	 */
	public abstract void loadFromDb() throws SQLException;

	/**
	 * create an empty dependent data object, as we can't easily call a
	 * generic's constructor ... why, oh why ...
	 * 
	 * @return the empty dependent object
	 */
	protected abstract D getEmptyDependent();

	/**
	 * create a dependent data object from a database record
	 * 
	 * @param data
	 *            the source data
	 * @return a dependent object created from data
	 * @throws SQLException
	 *             if data somehow can't be accessed properly
	 */
	protected abstract D getDependent(ResultSet data) throws SQLException;

	/**
	 * clone a dependent data object
	 * 
	 * @param other
	 *            the object to be cloned
	 * @return the new object
	 */
	protected abstract D getDependent(D other);

	public Iterator<D> iterator() {
		return dependentData.iterator();
	}
	
	public int getId() {
		return id;
	}
	
	/**
	 * reassign the id and commit the entries to the database.
	 * 
	 * @param newId
	 * @throws SQLException
	 */
	protected void commit(int newId) throws SQLException {
		id = newId;
		commit();
	}
}
