/* Copyright (C) 2004 - 2008  db4objects Inc.  http://www.db4o.com

This file is part of the db4o open source object database.

db4o is free software; you can redistribute it and/or modify it under
the terms of version 2 of the GNU General Public License as published
by the Free Software Foundation and as clarified by db4objects' GPL 
interpretation policy, available at
http://www.db4o.com/about/company/legalpolicies/gplinterpretation/
Alternatively you can write to db4objects, Inc., 1900 S Norfolk Street,
Suite 350, San Mateo, CA 94403, USA.

db4o 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.,
59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. */
package com.db4o.internal.fieldindex;

import com.db4o.foundation.*;
import com.db4o.internal.query.processor.*;

public class IndexedNodeCollector {

	private final Collection4 _nodes;
	
	private final Hashtable4 _nodeCache;

	public IndexedNodeCollector(QCandidates candidates) {
		_nodes = new Collection4();
		_nodeCache = new Hashtable4();
		collectIndexedNodes(candidates);
	}
	
	public Iterator4 getNodes() {
		return _nodes.iterator();
	}
	
	private void collectIndexedNodes(QCandidates candidates) {
		collectIndexedNodes(candidates.iterateConstraints());
		implicitlyAndJoinsOnSameField();
	}

	private void implicitlyAndJoinsOnSameField() {
		final Object[] nodes = _nodes.toArray();
		for (int i = 0; i < nodes.length; i++) {
			Object node = nodes[i];
			if (node instanceof OrIndexedLeaf) {
				OrIndexedLeaf current = (OrIndexedLeaf) node;
				OrIndexedLeaf other = findJoinOnSameFieldAtSameLevel(current);
				if (null != other) {
					nodes[Arrays4.indexOf(nodes, other)] = null;
					collectImplicitAnd(current.getConstraint(), current, other);
				}
			}
		}
	}

	private OrIndexedLeaf findJoinOnSameFieldAtSameLevel(OrIndexedLeaf join) {
		final Iterator4 i = _nodes.iterator();
		while (i.moveNext()) {
			if (i.current() == join) {
				continue;
			}
			if (i.current() instanceof OrIndexedLeaf) {
				OrIndexedLeaf current = (OrIndexedLeaf) i.current();
				if (current.getIndex() == join.getIndex()
					&& parentConstraint(current) == parentConstraint(join)) {
					return current;
				}
			}
		}
		return null;
	}

	private Object parentConstraint(OrIndexedLeaf node) {
		return node.getConstraint().parent();
	}

	private void collectIndexedNodes(final Iterator4 qcons) {
		
		while (qcons.moveNext()) {
			QCon qcon = (QCon)qcons.current();
			if (isCached(qcon)) {
				continue;
			}
			if (isLeaf(qcon)) {
				if (qcon.canLoadByIndex() && qcon.canBeIndexLeaf()) {					
					final QConObject conObject = (QConObject) qcon;
					if (conObject.hasJoins()) {
						collectJoinedNode(conObject);
					} else {
						collectStandaloneNode(conObject);
					}
				}
			} else {
				if (!qcon.hasJoins()) {
					collectIndexedNodes(qcon.iterateChildren());
				}
			}
		}		
	}
    
    private boolean isCached(QCon qcon) {
		return null != _nodeCache.get(qcon);
	}

	private void collectStandaloneNode(final QConObject conObject) {
		IndexedLeaf existing = findLeafOnSameField(conObject);
		if (existing != null) {
			collectImplicitAnd(conObject, existing, new IndexedLeaf(conObject));
		} else {
			_nodes.add(new IndexedLeaf(conObject));
		}
	}

	private void collectJoinedNode(QConObject constraintWithJoins) {
		Collection4 joins = collectTopLevelJoins(constraintWithJoins);
		if (!canJoinsBeSearchedByIndex(joins)) {
			return;
		}
		if (1 == joins.size()) {
			_nodes.add(nodeForConstraint((QCon)joins.singleElement()));
			return;
		}
		collectImplicitlyAndingJoins(joins, constraintWithJoins);
	}

	private boolean allHaveSamePath(Collection4 leaves) {
		final Iterator4 i = leaves.iterator();
		i.moveNext();
		QCon first = (QCon)i.current();
		while (i.moveNext()) {
			if (!haveSamePath(first, (QCon)i.current())) {
				return false;
			}
		}
		return true;
	}

	private boolean haveSamePath(QCon x, QCon y) {
		if (x == y) {
			return true;
		}		
		if (!x.onSameFieldAs(y)) {
			return false;
		}		
		if (!x.hasParent()) {
			return !y.hasParent();
		}
		return haveSamePath(x.parent(), y.parent());
	}

	private Collection4 collectLeaves(Collection4 joins) {
		Collection4 leaves = new Collection4();
		collectLeaves(leaves, joins);
		return leaves;
	}

	private void collectLeaves(Collection4 leaves, Collection4 joins) {
		final Iterator4 i = joins.iterator();
		while (i.moveNext()) {
			final QConJoin join = ((QConJoin)i.current());
			collectLeavesFromJoin(leaves, join);
		}
	}

	private void collectLeavesFromJoin(Collection4 leaves, QConJoin join) {
		collectLeavesFromJoinConstraint(leaves, join.i_constraint1);
		collectLeavesFromJoinConstraint(leaves, join.i_constraint2);
	}

	private void collectLeavesFromJoinConstraint(Collection4 leaves, QCon constraint) {
		if (constraint instanceof QConJoin) {
			collectLeavesFromJoin(leaves, (QConJoin) constraint);
		} else {
			if (!leaves.containsByIdentity(constraint)) {
				leaves.add(constraint);
			}
		}
	}

	private boolean canJoinsBeSearchedByIndex(Collection4 joins) {
		Collection4 leaves = collectLeaves(joins);
		return allHaveSamePath(leaves)
			&& allCanBeSearchedByIndex(leaves);
	}

	private boolean allCanBeSearchedByIndex(Collection4 leaves) {
		final Iterator4 i = leaves.iterator();
		while (i.moveNext()) {
			final QCon leaf = ((QCon)i.current());
			if (!leaf.canLoadByIndex()) {
				return false;
			}
		}
		return true;
	}
	
	private void collectImplicitlyAndingJoins(Collection4 joins, QConObject constraintWithJoins) {
		final Iterator4 i = joins.iterator();
		i.moveNext();
		IndexedNodeWithRange last = nodeForConstraint((QCon)i.current());
		while (i.moveNext()) {
			final IndexedNodeWithRange node = nodeForConstraint((QCon)i.current());
			last = new AndIndexedLeaf(constraintWithJoins, node, last);
			_nodes.add(last);
		}
	}

	private Collection4 collectTopLevelJoins(QConObject constraintWithJoins) {
		Collection4 joins = new Collection4();
		collectTopLevelJoins(joins, constraintWithJoins);
		return joins;
	}

	private void collectTopLevelJoins(Collection4 joins, QCon constraintWithJoins) {
		final Iterator4 i = constraintWithJoins.i_joins.iterator();
		while (i.moveNext()) {
			QConJoin join = (QConJoin)i.current();
			if (!join.hasJoins()) {
				if (!joins.containsByIdentity(join)) {
					joins.add(join);
				}
			} else {
				collectTopLevelJoins(joins, join);
			}
		}
	}
	
	private IndexedNodeWithRange newNodeForConstraint(QConJoin join) {
		final IndexedNodeWithRange c1 = nodeForConstraint(join.i_constraint1);
		final IndexedNodeWithRange c2 = nodeForConstraint(join.i_constraint2);
		if (join.isOr()) {
			return new OrIndexedLeaf(findLeafForJoin(join), c1, c2);
		}
		return new AndIndexedLeaf(join.i_constraint1, c1, c2);
	}

	private QCon findLeafForJoin(QConJoin join) {
		if (join.i_constraint1 instanceof QConObject) {
			return join.i_constraint1;
		}
		QCon con = join.i_constraint2;
		if (con instanceof QConObject) {
			return con;
		}
		return findLeafForJoin((QConJoin)con);
	}
	
	private IndexedNodeWithRange nodeForConstraint(QCon con) {
		IndexedNodeWithRange node = (IndexedNodeWithRange) _nodeCache.get(con);
		if (null != node || _nodeCache.containsKey(con)) {
			return node;
		}
		node = newNodeForConstraint(con);
		_nodeCache.put(con, node);
		return node;
	}

	private IndexedNodeWithRange newNodeForConstraint(QCon con) {
		if (con instanceof QConJoin) {
			return newNodeForConstraint((QConJoin)con);
		}
		return new IndexedLeaf((QConObject)con);
	}

	private void collectImplicitAnd(final QCon constraint, IndexedNodeWithRange x, final IndexedNodeWithRange y) {
		_nodes.remove(x);
		_nodes.remove(y);
		_nodes.add(new AndIndexedLeaf(constraint, x, y));
	}

	private IndexedLeaf findLeafOnSameField(QConObject conObject) {
		final Iterator4 i = _nodes.iterator();
		while (i.moveNext()) {
			if (i.current() instanceof IndexedLeaf) {
				IndexedLeaf leaf = (IndexedLeaf)i.current();
				if (conObject.onSameFieldAs(leaf.constraint())) {
					return leaf;
				}
			}
		}
		return null;
	}

	private boolean isLeaf(QCon qcon) {
		return !qcon.hasChildren();
	}
}
