/*
 * 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 info.aduna.iteration.CloseableIteration;
import info.aduna.iteration.ConvertingIteration;

import java.util.AbstractSequentialList;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.NoSuchElementException;

import org.openrdf.annotations.Precedes;
import org.openrdf.model.BNode;
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.ValueFactory;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.RepositoryResult;
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;

/**
 * Java instance for rdf:List as a familiar interface to manipulate this List.
 * This implemention can only be modified when in autoCommit (autoFlush), or
 * when read uncommitted is supported.
 * 
 * @author James Leigh
 */
@Precedes(RDFObjectImpl.class)
public abstract class RDFList extends AbstractSequentialList<Object> implements
		Refreshable, Mergeable, RDFObject {

	private int _size = -1;

	private RDFList parent;

	public void refresh() {
		_size = -1;
		if (parent != null)
			parent.refresh();
	}

	public void merge(Object source) {
		if (source instanceof java.util.List) {
			clear();
			addAll((java.util.List) source);
		}
	}

	ValueFactory getValueFactory() {
		RepositoryConnection conn = getObjectConnection();
		return conn.getValueFactory();
	}

	private CloseableIteration<Value, RepositoryException> getValues(Resource subj, URI pred, Value obj) {
		try {
			RepositoryResult<Statement> stmts;
			ObjectConnection conn = getObjectConnection();
			stmts = conn.getStatements(subj, pred, obj);
			return new ConvertingIteration<Statement, Value, RepositoryException>(stmts) {
				@Override
				protected Value convert(Statement stmt) throws RepositoryException {
					return stmt.getObject();
				}
			};
		} catch (RepositoryException e) {
			throw new ObjectStoreException(e);
		}
	}

	void addStatement(Resource subj, URI pred, Value obj) {
		if (obj == null)
			return;
		try {
			ObjectConnection conn = getObjectConnection();
			conn.add(subj, pred, obj);
		} catch (RepositoryException e) {
			throw new ObjectPersistException(e);
		}
	}

	void removeStatements(Resource subj, URI pred, Value obj) {
		try {
			ObjectConnection conn = getObjectConnection();
			conn.remove(subj, pred, obj);
		} catch (RepositoryException e) {
			throw new ObjectPersistException(e);
		}
	}

	@Override
	public int size() {
		if (_size < 0) {
			synchronized (this) {
				if (_size < 0) {
					Resource list = getResource();
					int size;
					for (size = 0; list != null && !list.equals(RDF.NIL); size++) {
						Resource nlist = getRest(list);
						if (nlist == null && getFirst(list) == null)
							break;
						list = nlist;
					}
					_size = size;
				}
			}
		}
		return _size;
	}

	@Override
	public ListIterator<Object> listIterator(final int index) {
		return new ListIterator<Object>() {
			private ArrayList<Resource> prevLists = new ArrayList<Resource>();

			private boolean removed;

			Resource list;
			{
				for (int i = 0; i < index; i++) {
					next();
				}
			}

			public void add(Object o) {
				ObjectConnection conn = getObjectConnection();
				try {
					boolean autoCommit = conn.isAutoCommit();
					if (autoCommit)
						conn.setAutoCommit(false);
					try {
						if (getResource().equals(RDF.NIL)) {
							// size == 0
							throw new ObjectPersistException(
									"cannot add a value to the nil list");
							/*
							 * list = _id = getValueFactory().createBNode();
							 * addStatement(list, RDF.FIRST,
							 * SesameProperty.createValue(List.this, o));
							 * addStatement(list, RDF.REST, RDF.NIL);
							 */
						}
						Value value = o == null ? null : getObjectConnection()
								.addObject(o);
						if (getFirst(getResource()) == null) {
							// size == 0
							list = getResource();
							addStatement(list, RDF.FIRST, value);
							addStatement(list, RDF.REST, RDF.NIL);
						} else if (list == null) {
							// index = 0
							Value first = getFirst(getResource());
							Resource rest = getRest(getResource());
							BNode newList = getValueFactory().createBNode();
							addStatement(newList, RDF.FIRST, first);
							addStatement(newList, RDF.REST, rest);
							removeStatements(getResource(), RDF.FIRST, first);
							removeStatements(getResource(), RDF.REST, rest);
							addStatement(getResource(), RDF.FIRST, value);
							addStatement(getResource(), RDF.REST, newList);
						} else if (!list.equals(RDF.NIL)) {
							Resource rest = getRest(list);
							BNode newList = getValueFactory().createBNode();
							removeStatements(list, RDF.REST, rest);
							addStatement(list, RDF.REST, newList);
							addStatement(newList, RDF.FIRST, value);
							addStatement(newList, RDF.REST, rest);
						} else {
							// index == size
							throw new NoSuchElementException();
						}
						if (autoCommit)
							conn.setAutoCommit(true);
						refresh();
					} finally {
						if (autoCommit && !conn.isAutoCommit()) {
							conn.rollback();
							conn.setAutoCommit(true);
						}
					}
				} catch (RepositoryException e) {
					throw new ObjectPersistException(e);
				}
			}

			public void set(Object o) {
				ObjectConnection conn = getObjectConnection();
				try {
					boolean autoCommit = conn.isAutoCommit();
					if (autoCommit)
						conn.setAutoCommit(false);
					try {
						if (getResource().equals(RDF.NIL)) {
							// size == 0
							throw new NoSuchElementException();
						} else if (list.equals(RDF.NIL)) {
							// index = size
							throw new NoSuchElementException();
						} else {
							Value first = getFirst(list);
							removeStatements(list, RDF.FIRST, first);
							if (o != null) {
								Value obj = getObjectConnection().addObject(o);
								addStatement(list, RDF.FIRST, obj);
							}
						}
						if (autoCommit)
							conn.setAutoCommit(true);
					} finally {
						if (autoCommit && !conn.isAutoCommit()) {
							conn.rollback();
							conn.setAutoCommit(true);
						}
					}
					refresh();
				} catch (RepositoryException e) {
					throw new ObjectPersistException(e);
				}
			}

			public void remove() {
				ObjectConnection conn = getObjectConnection();
				try {
					boolean autoCommit = conn.isAutoCommit();
					if (autoCommit)
						conn.setAutoCommit(false);
					try {
						if (prevLists.size() < 1) {
							// remove index == 0
							Value first = getFirst(list);
							removeStatements(list, RDF.FIRST, first);
							Resource next = getRest(list);
							first = getFirst(next);
							Resource rest = getRest(next);
							removeStatements(list, RDF.REST, next);
							if (first != null) {
								removeStatements(next, RDF.FIRST, first);
								addStatement(list, RDF.FIRST, first);
							}
							if (rest != null) {
								removeStatements(next, RDF.REST, rest);
								addStatement(list, RDF.REST, rest);
							}
						} else {
							// remove index > 0
							Resource removedList = list;
							list = prevLists.remove(prevLists.size() - 1);
							Value first = getFirst(removedList);
							Resource rest = getRest(removedList);
							removeStatements(removedList, RDF.FIRST, first);
							removeStatements(removedList, RDF.REST, rest);
							removeStatements(list, RDF.REST, removedList);
							addStatement(list, RDF.REST, rest);
						}
						if (autoCommit)
							conn.setAutoCommit(true);
						removed = true;
						refresh();
					} finally {
						if (autoCommit && !conn.isAutoCommit()) {
							conn.rollback();
							conn.setAutoCommit(true);
						}
					}
				} catch (RepositoryException e) {
					throw new ObjectStoreException(e);
				}
			}

			public boolean hasNext() {
				Resource next;
				if (list == null) {
					next = getResource();
				} else {
					next = getRest(list);
				}
				return getFirst(next) != null;
			}

			public Object next() {
				if (list == null) {
					list = getResource();
				} else if (!removed) {
					prevLists.add(list);
					list = getRest(list);
				} else {
					removed = false;
				}
				Value first = getFirst(list);
				if (first == null)
					throw new NoSuchElementException();
				return createInstance(first);
			}

			public int nextIndex() {
				if (list == null)
					return 0;
				return prevLists.size() + 1;
			}

			public int previousIndex() {
				return prevLists.size() - 1;
			}

			public boolean hasPrevious() {
				return prevLists.size() > 0;
			}

			public Object previous() {
				list = prevLists.remove(prevLists.size() - 1);
				removed = false;
				Value first = getFirst(list);
				if (first == null)
					throw new NoSuchElementException();
				return createInstance(first);
			}

			private Object createInstance(Value first) {
				try {
					if (first instanceof Resource)
						return getObjectConnection()
								.getObject((Resource) first);
					return getObjectConnection().getObjectFactory()
							.createObject(((Literal) first));
				} catch (RepositoryException e) {
					throw new ObjectStoreException(e);
				}
			}
		};
	}

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

	Value getFirst(Resource list) {
		if (list == null)
			return null;
		try {
			CloseableIteration<Value, RepositoryException> stmts;
			stmts = getValues(list, RDF.FIRST, null);
			try {
				if (stmts.hasNext())
					return stmts.next();
				return null;
			} finally {
				stmts.close();
			}
		} catch (RepositoryException e) {
			throw new ObjectStoreException(e);
		}
	}

	Resource getRest(Resource list) {
		if (list == null)
			return null;
		try {
			CloseableIteration<Value, RepositoryException> stmts;
			stmts = getValues(list, RDF.REST, null);
			try {
				if (stmts.hasNext())
					return (Resource) stmts.next();
				return null;
			} finally {
				stmts.close();
			}
		} catch (RepositoryException e) {
			throw new ObjectStoreException(e);
		}
	}
}