/*
 * Copyright 2009-2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jdal.vaadin.data;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdal.beans.PropertyUtils;
import org.jdal.dao.Dao;
import org.jdal.dao.Page;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DataAccessException;

import com.vaadin.data.Buffered;
import com.vaadin.data.Container;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.ItemSetChangeNotifier;
import com.vaadin.data.Container.Sortable;
import com.vaadin.data.Item;
import com.vaadin.data.Item.PropertySetChangeListener;
import com.vaadin.data.Property;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.util.BeanItem;

/**
 * <p>
 * An adapter to use PageableDataSources as Vaadin Container.
 * Use a integer as itemId and load data by page from data source on
 * request.
 * </p>
 * 
 * @author Jose Luis Martin - ([email protected])
 */
public class ContainerDataSource<T> implements Container, Sortable, Indexed, 
	ItemSetChangeNotifier, PropertySetChangeListener, Buffered {

	private static final long serialVersionUID = 1L;
	private static final Log log = LogFactory.getLog(ContainerDataSource.class);

	private Page<T> page = new Page<T>();
	
	private Dao<T, ?extends Serializable> service; 
	private List<String> sortableProperties;
	private List<BeanItem<T> > items = new ArrayList<BeanItem<T>>();
	private Class<T> entityClass;
	private List<ItemSetChangeListener> listeners = new ArrayList<ItemSetChangeListener>();
	private ItemIdStrategy itemIdStrategy;
	
	private Map<Object, Item> dirtyItems = new HashMap<Object, Item>();
	private Map<Object, Item> newItems = new HashMap<Object,Item>();
	
	private boolean readThrough = false;
	private boolean writeThrough= false;
	private String sortProperty;
	
	public ContainerDataSource() {
		this(null, null);
	}
	
	public ContainerDataSource(Class<T> entityClass) {
		this(entityClass, null);
	}
	
	public ContainerDataSource(Dao<T, ?extends Serializable> service) {
		this.service = service;
		this.entityClass = service.getEntityClass();
		setItemIdStrategy(new IndexedItemIdStrategy());
	}
	
	public ContainerDataSource(Class<T> entityClass, Dao<T, ?extends Serializable> service) {
		this.service = service;
		this.entityClass = entityClass;
		setItemIdStrategy(new IndexedItemIdStrategy());
	}

	public void init() {
		page.setSortName(getSortProperty());
		loadPage();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Object nextItemId(Object itemId) {
		return isLastId(itemId) ? null : getIdByIndex(indexOfId(itemId) + 1);
	}

	/**
	 * {@inheritDoc}
	 */
	public Object prevItemId(Object itemId) {
		return isFirstId(itemId) ? null : getIdByIndex(indexOfId(itemId) - 1);
	}

	/**
	 * {@inheritDoc}
	 */
	public Object firstItemId() {
		return itemIdStrategy.firstItemId();
	}

	/**
	 * {@inheritDoc}
	 */
	public Object lastItemId() {
		return itemIdStrategy.lastItemId();
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isFirstId(Object itemId) {
		return indexOfId(itemId) == 0;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isLastId(Object itemId) {
		return indexOfId(itemId) == page.getCount() - 1;
	}


	/**
	 * {@inheritDoc}
	 */
	public Object addItemAfter(Object previousItemId)
			throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new Items after to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public Item addItemAfter(Object previousItemId, Object newItemId)
			throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new records after to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public int indexOfId(Object itemId) {
		return itemIdStrategy.indexOfId(itemId);
	}

	/**
	 * {@inheritDoc}
	 */
	public Object getIdByIndex(int index) {
		return itemIdStrategy.getIdByIndex(index);
	}

	/**
	 * {@inheritDoc}
	 */
	public Object addItemAt(int index) throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new records to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public Item addItemAt(int index, Object newItemId)
			throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new records to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public void sort(Object[] propertyId, boolean[] ascending) {
		// only use the first property :I
		page.setSortName(propertyId[0].toString());
		page.setOrder(ascending[0] ? Page.Order.ASC : Page.Order.DESC);
		loadPage();
		fireItemSetChange();
	}

	/**
	 * {@inheritDoc}
	 */
	public Collection<?> getSortableContainerPropertyIds() {
		if (sortableProperties != null)
			return sortableProperties;
		
		if (entityClass != null) {
			List<String> properties = new LinkedList<String>();
			PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(entityClass);
			for (PropertyDescriptor pd : pds)
				properties.add(pd.getName());
			
			return properties;
		}
		
		// if we have data will try introspection
		if (page.getData().size() > 0) {
			BeanItem<T> item = items.get(0);
			return item.getItemPropertyIds();
		}

		return new LinkedList<Object>();
	}

	/**
	 * {@inheritDoc}
	 */
	public Item getItem(Object itemId) {
		return itemIdStrategy.getItem(itemId);
		
	}

	public int getPageContaining(int index) {
		return index/page.getPageSize() + 1;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Collection<?> getContainerPropertyIds() {
		BeanItem<T> item = newItem();
		if (item != null) 
			return item.getItemPropertyIds();
		
		// if we have data will try introspection
		if (page.getData().size() > 0) 
			return items.get(0).getItemPropertyIds();
	
		// this is an error...
		log.error("Can't get property ids from entityClass or data");
		return new LinkedList<Object>();
	}

	/**
	 * {@inheritDoc}
	 */
	public Collection<?> getItemIds() {
		return itemIdStrategy.getItemIds();
	}

	/**
	 * {@inheritDoc}
	 */
	public Property getContainerProperty(Object itemId, Object propertyId) {
		Item item =  getItem(itemId);
		return item != null ? item.getItemProperty(propertyId) : null;
	}

	/**
	 * {@inheritDoc}
	 */
	public Class<?> getType(Object propertyId) {
		if (entityClass != null) {
			return PropertyUtils.getPropertyDescriptor(entityClass, (String)propertyId)
				.getPropertyType();
		}
		
		// if we have data will try introspection
		if (page.getData().size() > 0) {
			BeanItem<T> item = items.get(0);
			return item.getBean().getClass();
		}
		
		return Object.class;
	}

	/**
	 * {@inheritDoc}
	 */
	public int size() {
		return page.getCount();
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean containsId(Object itemId) {
		return itemIdStrategy.containsId(itemId);
	}

	/**
	 * {@inheritDoc}
	 */
	public Item addItem(Object itemId) throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new records to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public Object addItem() throws UnsupportedOperationException {
		BeanItem<T> newItem = newItem();
		
		if (newItem != null) {
			newItem.addListener(this);
			newItems.put(newItem.getBean(), newItem);
		}
		
		return newItem;
	}

	private BeanItem<T> newItem() {
		T bean = null;
		try {
			bean = BeanUtils.instantiate(entityClass);
		} catch (BeanInstantiationException be) {
			log.error(be);
			return null;
		}
		
		BeanItem<T> newItem = new BeanItem<T>(bean);
		return newItem;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean removeItem(Object itemId) {
		if (!containsId(itemId))
			return false;
		
		int index = (Integer) itemId;	
		
		if (isInPage(index)) {
			service.delete(page.getData().get(globalToPage(index)));
			loadPage();
		}
		else {
			Page<T> oneItem = new Page<T>(1, index);
			oneItem.setFilter(page.getFilter());
			service.getPage(oneItem);
			service.delete(oneItem.getData().get(0));
			page.setCount(page.getCount() - 1);
		}
		
		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean addContainerProperty(Object propertyId, Class<?> type,
			Object defaultValue) throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new properties to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean removeContainerProperty(Object propertyId)
			throws UnsupportedOperationException {
		throw new UnsupportedOperationException("ContainerDataSourceAdapter don't support adding new properties to container");
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean removeAllItems() throws UnsupportedOperationException {
		try {
			Page<T> all =  page.clone();
			all.setPageSize(Integer.MAX_VALUE);
			service.delete(all.getData());
		} catch (DataAccessException dae) {
			return false;
		}
	
		return true;
	}

	private void loadPage() {
		service.getPage(page);
		int index = 0;
		items.clear();
		for (T t : page.getData()) {
			
			BeanItem<T> item = getDirtyOrCreate(t);
			item.addListener(this);
			items.add(item);
			itemIdStrategy.itemLoaded(pageToGlobal(index++), item);
		}
	}

	/**
	 * @param t
	 * @return bean item
	 */
	@SuppressWarnings("unchecked")
	private BeanItem<T> getDirtyOrCreate(T t) {
		BeanItem<T> item = new BeanItem<T>(t);
		if (readThrough && dirtyItems.containsKey(t))
			item = (BeanItem<T>) dirtyItems.get(t);
			
		return item;
	}

	/**
	 * @param i
	 * @return
	 */
	private int pageToGlobal(int index) {
		return page.getStartIndex() + index;
	}
	

	/**
	 * Convert global index to page index.
	 * @param index global index
	 * @return the index in current page
	 */
	private int globalToPage(int index) {
		return index - page.getStartIndex();
	}

	private boolean isInPage(int index) {
		return globalToPage(index) >= 0 && globalToPage(index) < page.getPageSize();
	}
	
	public Dao<T, ?extends Serializable> getService() {
		return service;
	}

	public void setService(Dao<T, Serializable> service) {
		this.service = service;
	}
	

	public void setPage(Page<T> page) {
		this.page = page;
		loadPage();
	}

	public List<String> getSortableProperties() {
		return sortableProperties;
	}

	public void setSortableProperties(List<String> sortableProperties) {
		this.sortableProperties = sortableProperties;
	}

	public void addListener(ItemSetChangeListener listener) {
		if (!listeners.contains(listener))
			listeners.add(listener);
	}

	public void removeListener(ItemSetChangeListener listener) {
		listeners.remove(listener);
	}
	
	/**
	 * Notity listeners that the item set change
	 */
	private void fireItemSetChange() {
		ItemSetChangeEvent isce = new ItemSetChangeEvent() {

			public Container getContainer() {
				return ContainerDataSource.this;
			}
		};
		// must be first 
		itemIdStrategy.containerItemSetChange(isce);
		
		for (ItemSetChangeListener listener : listeners) {
			listener.containerItemSetChange(isce);
		}
	}
	
	/**
	 * Set the page size
	 * @param pageSize the page size to set
	 */
	public void setPageSize(int pageSize) {
		page.setPageSize(pageSize);
		loadPage();
	}
	
	/**
	 * Get the page size
	 * @return the page size
	 */
	public int getPageSize() {
		return page.getPageSize();
	}

	/**
	 * @return filter Object
	 * @see info.joseluismartin.dao.Page#getFilter()
	 */
	public Object getFilter() {
		return page.getFilter();
	}

	/**
	 * @param filter
	 * @see info.joseluismartin.dao.Page#setFilter(java.lang.Object)
	 */
	public void setFilter(Object filter) {
		page.setFilter(filter);
		loadPage();
		fireItemSetChange();
	}

	/**
	 * Get all keys from pageable datasource
	 */
	public List<Serializable> getKeys() {
		Page<T> p = new Page<T>(Integer.MAX_VALUE);
		p.setFilter(page.getFilter());
		p.setSortName(page.getSortName());
		p.setOrder(page.getOrder());
		
		return service.getKeys(p);
	}

	/**
	 * @return the itemIdStrategy
	 */
	public ItemIdStrategy getItemIdStrategy() {
		return itemIdStrategy;
	}

	/**
	 * @param itemIdStrategy the itemIdStrategy to set
	 */
	public void setItemIdStrategy(ItemIdStrategy itemIdStrategy) {
		this.itemIdStrategy = itemIdStrategy;
		itemIdStrategy.setContainerDataSource(this);
	}

	/**
	 * @param index
	 */
	public Item getItemByIndex(int index) {
		
		if (!isInPage(index)) {
			if (log.isDebugEnabled())
				log.debug("Page fault on index: " + index);
			page.setPage(getPageContaining(index));
			loadPage();
		}
		int pageIndex = globalToPage(index);
		
		return pageIndex < items.size() ? items.get(pageIndex) : null;		
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public void itemPropertySetChange(com.vaadin.data.Item.PropertySetChangeEvent event) {
		dirtyItems.put(((BeanItem<T>) event.getItem()).getBean(), event.getItem());
		if (isWriteThrough())
			commit();
	}
	
	/** 
	 * Save changes to Persistent Service
	 * @return true if all items was updated.
	 */
	@SuppressWarnings("unchecked")
	public boolean save() {
		
		// insert news 
		for (Item i : newItems.values()) {
			try {
				BeanItem<T> bi = (BeanItem<T>) i;
				service.save(bi.getBean());
				newItems.remove(bi.getBean());
			}
			catch (DataAccessException dae) {
				log.error(dae);
			}
		}
		
		// update dirties
		for (Item i : dirtyItems.values()) {
			try {
				BeanItem<T> bi = (BeanItem<T>) i;
				service.save(bi.getBean());
				dirtyItems.remove(bi.getBean());
			}
			catch (DataAccessException dae) {
				log.error(dae);
			}
		}
		
		return newItems.isEmpty() && dirtyItems.isEmpty();
	}

	/**
	 * {@inheritDoc}
	 */
	public void commit() throws SourceException, InvalidValueException {
		if (!save()) {
			// lost changes?
			discard();
			throw new SourceException(this);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void discard() throws SourceException {
		newItems.clear();
		dirtyItems.clear();
		
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isModified() {
		return dirtyItems.size() > 0 || newItems.size() > 0;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isReadThrough() {
		return readThrough;
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isWriteThrough() {
		return writeThrough;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setReadThrough(boolean readThrough) throws SourceException {
		this.readThrough = readThrough;
		
	}

	/**
	 * {@inheritDoc}
	 */
	public void setWriteThrough(boolean writeThrough) throws SourceException, InvalidValueException {
		this.writeThrough = writeThrough;
		
	}

	/**
	 * Gets the page
	 * @return the page
	 */
	public Page<T> getPage() {
		return page;
	}

	/**
	 * @return the sortProperty
	 */
	public String getSortProperty() {
		return sortProperty;
	}

	/**
	 * @param sortProperty the sortBy to set
	 */
	public void setSortProperty(String sortProperty) {
		this.sortProperty = sortProperty;
	}

	public void setBuffered(boolean buffered) {
		// TODO Auto-generated method stub
		
	}

	public boolean isBuffered() {
		// TODO Auto-generated method stub
		return false;
	}

	public void addItemSetChangeListener(ItemSetChangeListener listener) {
		// TODO Auto-generated method stub
		
	}

	public void removeItemSetChangeListener(ItemSetChangeListener listener) {
		// TODO Auto-generated method stub
		
	}

	public List<?> getItemIds(int startIndex, int numberOfItems) {
		return this.itemIdStrategy.getItemIds(startIndex, numberOfItems);
	}
}