package experimentdatabase;

import java.sql.SQLException;
import java.util.*;

/**
 * Base class for all database-related objects.
 *
 * @author Tomasz Pawlak
 */
public class DataSet implements Map<String, Object> {

	protected int id = -1;
	protected final String tableName;
	protected final DataSet parent;
	protected final Database database;
	protected final LinkedList<DataSet> children = new LinkedList<DataSet>();

	private boolean closed = false;

	protected final TreeMap<String, Object> parameters = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);

	/**
	 * @param name
	 * @param parent Null means no parent
	 */
	protected DataSet(String name, DataSet parent, Database database) {
		assert name != null;
		assert database != null;

		this.tableName = Helpers.fixDataObjectName(name);
		this.parent = parent;
		this.database = database;
	}

	/**
	 * Gets an id of the object in database. If the object is not stored in database, the id is -1.
	 *
	 * @return
	 */
	public int getId() {
		this.checkClosed();
		return this.id;
	}

	protected String getTableName() {
		this.checkClosed();
		return this.tableName;
	}

	protected DataSet getParent() {
		this.checkClosed();
		return this.parent;
	}

	public DataSet newChildDataSet(String tableName) {
		this.checkClosed();

		DataSet child = new DataSet(tableName, this, this.database);
		this.children.addLast(child);

		return child;
	}

	protected void removeChild(DataSet child) {
		this.children.remove(child);
	}

	/**
	 * Stores the object in database.
	 */
	public void save() {
		this.save(true);
	}

	protected void save(boolean withChildren) {
		this.checkClosed();

		try {
			this.database.getEngine().beginTransaction();

			// determine parent id
			if (this.parent != null) {
				int parentId = this.parent.getId();
				if (parentId == -1) {
					this.parent.save(false); // gets id from database
					parentId = this.parent.getId();
				}
				assert parentId >= 0;
				this.parameters.put("parent", parentId);
			}

			// add my id to parameters
			if (this.id >= 0)
				this.parameters.put("id", this.id);

			// save me
			int lastInsertId = this.database.writeData(this);

			// update my id
			if (lastInsertId >= 0) {
				assert this.id == -1 || this.id == lastInsertId;
				this.id = lastInsertId;
			}

			// save my children
			if (withChildren) {
				for (DataSet child : this.children) {
					child.save(true);
				}
			}

			this.database.getEngine().commit();

		} catch (SQLException ex) {
			try {
				this.database.getEngine().rollback();
			} catch (SQLException ex2) {
			}

			throw new DatabaseException("Cannot save DataSet", ex);
		}
	}

	/**
	 * Checks if the object is closed.
	 */
	protected final void checkClosed() {
		if (this.closed)
			throw new DatabaseException("The object is already closed");
	}

	/**
	 * Stores the current state of the object in database and closes it. Once the object is closed, it becomes useless,
	 * since all calls to the object's methods will throw DatabaseException.
	 *
	 * @see DatabaseException
	 */
	public void close() {
		if (!this.closed) {
			this.save();

			if (this.parent != null) {
				this.parent.removeChild(this);
			}

			this.closed = true;
		}
	}

	// **********************************
	// MAP INTERFACE
	// **********************************

	@Override
	public Object put(String name, Object value) {
		this.checkClosed();
		name = Helpers.fixDataObjectName(name);
		return this.parameters.put(name, value);
	}

	@Override
	public void putAll(Map<? extends String, ? extends Object> dictionary) {
		for (java.util.Map.Entry<? extends String, ? extends Object> entry : dictionary.entrySet()) {
			this.put(entry.getKey(), entry.getValue());
		}
	}

	@Override
	public int size() {
		this.checkClosed();
		return this.parameters.size();
	}

	@Override
	public boolean isEmpty() {
		this.checkClosed();
		return this.parameters.isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		this.checkClosed();
		return this.parameters.containsKey(key);
	}

	@Override
	public boolean containsValue(Object value) {
		this.checkClosed();
		return this.parameters.containsValue(value);
	}

	@Override
	public Object get(Object key) {
		this.checkClosed();
		return this.parameters.get(key);
	}

	@Override
	public Object remove(Object key) {
		this.checkClosed();
		return this.parameters.remove(key);
	}

	@Override
	public void clear() {
		this.checkClosed();
		this.parameters.clear();
	}

	@Override
	public Set<String> keySet() {
		this.checkClosed();
		return this.parameters.keySet();
	}

	@Override
	public Collection<Object> values() {
		this.checkClosed();
		return this.parameters.values();
	}

	@Override
	public Set<java.util.Map.Entry<String, Object>> entrySet() {
		this.checkClosed();
		return this.parameters.entrySet();
	}
}
