/*
 * Copyright (c) 2007-2009, James Leigh All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution. 
 * - Neither the name of the openrdf.org nor the names of its contributors may
 *   be used to endorse or promote products derived from this software without
 *   specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */
package org.openrdf.repository.object.behaviours;

import static org.openrdf.query.QueryLanguage.SPARQL;
import info.aduna.iteration.CloseableIteration;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.openrdf.annotations.Precedes;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.query.BindingSet;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.TupleQueryResult;
import org.openrdf.repository.Repository;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.object.ObjectConnection;
import org.openrdf.repository.object.RDFObject;
import org.openrdf.repository.object.exceptions.ObjectPersistException;
import org.openrdf.repository.object.exceptions.ObjectStoreException;
import org.openrdf.repository.object.traits.Mergeable;
import org.openrdf.repository.object.traits.Refreshable;

/**
 * This behaviour provides a java.util.List interface for RDF containers.
 * 
 * @author James Leigh
 */
@Precedes(RDFObjectImpl.class)
public abstract class RDFSContainer extends AbstractList<Object> implements
		Refreshable, Mergeable, RDFObject {

	private static final int UNKNOWN = -1;

	private static final int BSIZE = 64;

	private volatile int _size = UNKNOWN;

	private List<Object[]> blocks = new ArrayList<Object[]>();

	public void refresh() {
		_size = UNKNOWN;
	}

	@Override
	public Object get(int index) {
		try {
			int b = index / BSIZE;
			Object[] block = getBlock(b);
			if (block != null)
				return block[index % BSIZE];
			Object[] list = loadBlock(b);
			assignBlock(b, list);
			return list[index % BSIZE];
		} catch (RepositoryException e) {
			throw new ObjectStoreException(e);
		} catch (QueryEvaluationException e) {
			throw new ObjectStoreException(e);
		}
	}

	@Override
	public void add(int index, Object obj) {
		ObjectConnection conn = getObjectConnection();
		try {
			boolean autoCommit = conn.isAutoCommit();
			if (autoCommit)
				conn.setAutoCommit(false);
			try {
				for (int i = size() - 1; i >= index; i--) {
					replace(i + 1, get(i));
				}
				replace(index, obj);
				if (_size > UNKNOWN)
					_size++;
				if (autoCommit)
					conn.setAutoCommit(true);
			} finally {
				if (autoCommit && !conn.isAutoCommit()) {
					conn.rollback();
					conn.setAutoCommit(true);
				}
			}
		} catch (RepositoryException e) {
			throw new ObjectPersistException(e);
		}
	}

	@Override
	public Object set(int index, Object obj) {
		ObjectConnection conn = getObjectConnection();
		try {
			boolean autoCommit = conn.isAutoCommit();
			if (autoCommit)
				conn.setAutoCommit(false);
			try {
				Object old = getAndSet(index, obj);
				if (autoCommit)
					conn.setAutoCommit(true);
				return old;
			} finally {
				if (autoCommit && !conn.isAutoCommit()) {
					conn.rollback();
					conn.setAutoCommit(true);
				}
			}
		} catch (RepositoryException e) {
			throw new ObjectPersistException(e);
		}
	}

	public void merge(Object source) {
		if (source instanceof java.util.List) {
			ObjectConnection conn = getObjectConnection();
			try {
				boolean autoCommit = conn.isAutoCommit();
				if (autoCommit)
					conn.setAutoCommit(false);
				try {
					java.util.List list = (java.util.List) source;
					int size = list.size();
					for (int i = 0, n = size; i < n; i++) {
						Object value = list.get(i);
						if (value != null) {
							assign(i, value);
						}
					}
					if (_size > UNKNOWN && _size < size)
						_size = size;
					if (autoCommit)
						conn.setAutoCommit(true);
				} finally {
					if (autoCommit && !conn.isAutoCommit()) {
						conn.rollback();
						conn.setAutoCommit(true);
					}
				}
			} catch (RepositoryException e) {
				throw new ObjectPersistException(e);
			}
		}
	}

	@Override
	public Object remove(int index) {
		ObjectConnection conn = getObjectConnection();
		try {
			boolean autoCommit = conn.isAutoCommit();
			if (autoCommit) {
				conn.setAutoCommit(false);
			}
			Object obj = get(index);
			int size = size();
			for (int i = index; i < size - 1; i++) {
				replace(i, get(i + 1));
			}
			URI pred = getMemberPredicate(size - 1);
			conn.remove(getResource(), pred, null);
			Object[] block = getBlock((size - 1) / BSIZE);
			if (block != null) {
				block[(size - 1) % BSIZE] = null;
			}
			if (_size > UNKNOWN)
				_size--;
			if (autoCommit) {
				conn.setAutoCommit(true);
			}
			return obj;
		} catch (RepositoryException e) {
			throw new ObjectPersistException(e);
		}
	}

	@Override
	public void clear() {
		try {
			ObjectConnection conn = getObjectConnection();
			Resource resource = getResource();
			int size = _size;
			if (size < 0) {
				size = (int) findSize();
			}
			for (int i = 0; i < size; i++) {
				URI pred = getMemberPredicate(i);
				conn.remove(resource, pred, null);
			}
		} catch (RepositoryException e) {
			throw new ObjectPersistException(e);
		}
	}

	@Override
	public int size() {
		try {
			if (_size < 0) {
				synchronized (this) {
					if (_size < 0) {
						int index = findSize();
						_size = index;
					}
				}
			}
			return _size;
		} catch (RepositoryException e) {
			throw new ObjectStoreException(e);
		}
	}

	@Override
	public String toString() {
		return super.toString();
	}

	private URI getMemberPredicate(int index) {
		ObjectConnection conn = getObjectConnection();
		Repository repository;
		repository = conn.getRepository();
		String uri = RDF.NAMESPACE + '_' + (index + 1);
		return repository.getValueFactory().createURI(uri);
	}

	private int getIndex(URI pred) {
		assert pred.stringValue().startsWith(RDF.NAMESPACE + '_');
		return Integer.parseInt(pred.getLocalName().substring(1)) - 1;
	}

	private Object getAndSet(int index, Object o) throws RepositoryException {
		if (o == null)
			throw new NullPointerException();
		URI pred = getMemberPredicate(index);
		Object old = get(index);
		ObjectConnection conn = getObjectConnection();
		if (old != null) {
			conn.remove(getResource(), pred, null);
		}
		conn.add(getResource(), pred, conn.addObject(o));
		Object[] block = getBlock(index / BSIZE);
		if (block != null) {
			block[index % BSIZE] = o;
		}
		return old;
	}

	private void assign(int index, Object o) throws RepositoryException {
		if (o == null)
			throw new NullPointerException();
		URI pred = getMemberPredicate(index);
		Value newValue = getObjectConnection().addObject(o);
		ObjectConnection conn = getObjectConnection();
		conn.add(getResource(), pred, newValue);
		clearBlock(index / BSIZE);
	}

	private void replace(int index, Object o) throws RepositoryException {
		if (o == null)
			throw new NullPointerException();
		URI pred = getMemberPredicate(index);
		ObjectConnection conn = getObjectConnection();
		Value newValue = getObjectConnection().addObject(o);
		boolean autoCommit = conn.isAutoCommit();
		if (autoCommit)
			conn.setAutoCommit(false);
		try {
			conn.remove(getResource(), pred, null);
			conn.add(getResource(), pred, newValue);
			if (autoCommit)
				conn.setAutoCommit(true);
		} finally {
			if (autoCommit && !conn.isAutoCommit()) {
				conn.rollback();
				conn.setAutoCommit(true);
			}
		}
		Object[] block = getBlock(index / BSIZE);
		if (block != null) {
			block[index % BSIZE] = o;
		}
	}

	private int findSize() throws RepositoryException {
		CloseableIteration<? extends Statement, RepositoryException> iter;
		HashSet<URI> set = new HashSet<URI>();
		ObjectConnection conn = getObjectConnection();
		iter = conn.getStatements(getResource(), null, null);
		try {
			while (iter.hasNext()) {
				set.add(iter.next().getPredicate());
			}
		} finally {
			iter.close();
		}
		int index = 0;
		while (set.contains(getMemberPredicate(index)))
			index++;
		return index;
	}

	private synchronized Object[] getBlock(int b) {
		if (blocks.size() > b)
			return blocks.get(b);
		return null;
	}

	private synchronized void assignBlock(int b, Object[] list) {
		while (blocks.size() <= b) {
			blocks.add(null);
		}
		blocks.set(b, list);
	}

	private synchronized void clearBlock(int b) {
		if (blocks.size() > b) {
			blocks.set(b, null);
		}
	}

	private Object[] loadBlock(int b) throws RepositoryException, QueryEvaluationException {
		TupleQuery query = createBlockQuery(b);
		TupleQueryResult result = query.evaluate();
		BindingSet bindings = result.next();
		ObjectConnection con = getObjectConnection();
		Object[] list = new Object[BSIZE];
		while (bindings != null) {
			URI pred = (URI) bindings.getValue("pred");
			int idx = getIndex(pred);
			Value value = bindings.getValue("value");
			Set<URI> types = new HashSet<URI>(4);
			do {
				Value c = bindings.getValue("value_class");
				if (c instanceof URI) {
					types.add((URI) c);
				}
				bindings = result.hasNext() ? result.next() : null;
			} while (bindings != null && pred.equals(bindings.getValue("pred")));
			int i = idx % BSIZE;
			if (value instanceof Literal) {
				list[i] = con.getObject((Literal) value);
			} else {
				list[i] = con.getObject(types, (Resource) value);
			}
		}
		return list;
	}

	private TupleQuery createBlockQuery(int b) throws RepositoryException {
		StringBuilder sb = new StringBuilder();
		sb.append("SELECT ?pred ?value ?value_class\n");
		sb.append("WHERE { $self ?pred ?value\n");
		sb.append("OPTIONAL { ?value a ?value_class }\n");
		sb.append("FILTER (");
		for (int i = b * BSIZE, n = b * BSIZE + BSIZE; i < n; i++) {
			sb.append("?pred = <");
			sb.append(RDF.NAMESPACE);
			sb.append("_");
			sb.append((i + 1));
			sb.append(">");
			if (i + 1 < n) {
				sb.append(" || ");
			}
		}
		sb.append(")}\n");
		ObjectConnection con = getObjectConnection();
		try {
			TupleQuery query = con.prepareTupleQuery(SPARQL, sb.toString());
			query.setBinding("self", getResource());
			return query;
		} catch (MalformedQueryException e) {
			throw new RepositoryException(e);
		}
	}
}