/**
 * 
 */
package org.richfaces.model;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;

import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.ajax4jsf.model.SerializableDataModel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Data model class for table components.<br> 
 * Usage:
 * <pre>
 * G3TableDataModel&lt;SomeDataType&gt; dataModel = new G3TableDataModel&lt;SomeDataType&gt;(new G3DataProvider &lt;SomeDataType&gt;());
 * </pre>
 * @author pawelgo
 *
 */
public class ExtendedTableDataModel<T> extends SerializableDataModel {

//	/**
//	 * Cache container for data presented in data model.
//	 * It prevents load the same data many times.
//	 * @author pawelgo
//	 *
//	 * @param <T> data type
//	 */
//	private static class DataCache<T> implements Serializable {
//		private static final long serialVersionUID = 8409239905346930920L;
//		private int startRow;
//		private int endRow;
//		//private SortOrder sortOrder;
//		private List<T> loadedData;
//
//		public DataCache(int startRow, int endRow,
//				List<T> loadedData) {
//			super();
//			this.startRow = startRow;
//			this.endRow = endRow;
//			//this.sortOrder = deepCopy(sortOrder);
//			this.loadedData = loadedData;
//		}
//
//		/**
//		 * Returns a deep copy of an object
//		 */
//		@SuppressWarnings("unused")
//		private SortOrder deepCopy(SortOrder sortOrder) {
//			if (sortOrder == null)
//				return null;
//			SortOrder order = new SortOrder();
//			if (sortOrder.getFields() != null) {
//				SortField[] fields = new SortField[sortOrder.getFields().length];
//				for (int i = 0; i < sortOrder.getFields().length; i++) {
//					SortField f = sortOrder.getFields()[i];
//					fields[i] = new SortField(f.getName(), f.getIndex(), f
//							.getAscending());
//				}//for
//				order.setFields(fields);
//			}//if
//			return order;
//		}//deepCopy
//
//		/**
//		 * Returns a deep copy of an object using serialization.
//		 */
//		static public Object deepCopy(Object oldObj) throws Exception {
//			ObjectOutputStream oos = null;
//			ObjectInputStream ois = null;
//			try {
//				ByteArrayOutputStream bos = new ByteArrayOutputStream();
//				oos = new ObjectOutputStream(bos);
//				// serialize and pass the object
//				oos.writeObject(oldObj);
//				oos.flush();
//				ByteArrayInputStream bin = new ByteArrayInputStream(bos
//						.toByteArray());
//				ois = new ObjectInputStream(bin);
//				// return the new object
//				return ois.readObject();
//			} catch (Exception e) {
//				if (log.isErrorEnabled())
//					log.error("Exception in ObjectCloner = " + e);
//				throw (e);
//			} finally {
//				oos.close();
//				ois.close();
//			}
//		}//deepCopy
//
//		public boolean match(int s, int e) {
//			return s == startRow && e == endRow;
//			//&& sortOrdersMatch(sortOrder, this.sortOrder);
//		}//match
//
//		@SuppressWarnings("unused")
//		private boolean sortOrdersMatch(SortOrder sortOrder1,
//				SortOrder sortOrder2) {
//			boolean result = sortOrder1 == sortOrder2;
//
//			if (sortOrder1 != null && sortOrder2 != null) {
//				result = sortOrder1.equals(sortOrder2);
//			}
//
//			return result;
//		}//sortOrdersMatch
//
//	}//DataCache

	private static final long serialVersionUID = 7374505108088114161L;

	private static final Log log = LogFactory.getLog(ExtendedTableDataModel.class);

	private DataProvider<T> dataProvider;
	private Object rowKey;
	//private Map<Object,T> wrappedData = new HashMap<Object,T>();
	private List<Object> wrappedKeys = null;
	private boolean detached = false;
	private Map<Object, T> wrappedData = new HashMap<Object, T>();

	//private DataCache<T> dataCache;

	public ExtendedTableDataModel(DataProvider<T> dataProvider) {
		this.dataProvider = dataProvider;
	}

	/* (non-Javadoc)
	 * @see org.ajax4jsf.model.SerializableDataModel#update()
	 */
	public void update() {
		;
	}

	/**
	 * This method never called from framework.
	 * (non-Javadoc)
	 * @see org.ajax4jsf.model.ExtendedDataModel#getRowKey()
	 */
	public Object getRowKey() {
		return rowKey;
	}

	/**
	 * This method normally called by Visitor before request Data Row.
	 * (non-Javadoc)
	 * @see org.ajax4jsf.model.ExtendedDataModel#setRowKey(java.lang.Object)
	 */
	public void setRowKey(Object key) {
		rowKey = key;
	}

	/** 
	 * This is main part of Visitor pattern. Method called by framework many times during request processing. 
	 * (non-Javadoc)
	 * @see org.ajax4jsf.model.ExtendedDataModel#walk(javax.faces.context.FacesContext, org.ajax4jsf.model.DataVisitor, org.ajax4jsf.model.Range, java.lang.Object)
	 */
	public void walk(FacesContext context, DataVisitor visitor, Range range,
			Object argument) throws IOException {
		int rowC = getRowCount();
		int firstRow = ((SequenceRange) range).getFirstRow();
		int numberOfRows = ((SequenceRange) range).getRows();
		if (numberOfRows <= 0) {
			numberOfRows = rowC;
		}
		if (detached) { // Is this serialized model
			// Here we just ignore current Rage and use whatever data was saved in serialized model. 
			// Such approach uses much more getByPk() operations, instead of just one request by range.
			// Concrete case may be different from that, so you can just load data from data provider by range.
			// We are using wrappedKeys list only to preserve actual order of items.
			for (Object key : wrappedKeys) {
				setRowKey(key);
				visitor.process(context, key, argument);
			}
		} else { // if not serialized, than we request data from data provider
			wrappedKeys = new ArrayList<Object>();
			int endRow = firstRow + numberOfRows;
			if (endRow > rowC){
				endRow = rowC; 
			}
			for (T item : loadData(firstRow, endRow)) {
				Object key = getKey(item);
				wrappedKeys.add(key);
				wrappedData.put(key, item);
				visitor.process(context, key, argument);
			}
		}
	}//walk

	/**
	 * Load range of data items from the source.
	 * Starting from startRow, and up to but excluding endRow
	 * @param startRow
	 * @param endRow
	 * @return list of ordered data
	 */
	protected List<T> loadData(int startRow, int endRow) {
		if (log.isDebugEnabled())
			log.debug("load data from range: " + startRow + " - " + endRow);
		if (startRow < 0){
			startRow = 0;
			throw new IllegalArgumentException("Illegal start index value: " + startRow);
		}
		int rowCount = getRowCount();
		if (endRow > rowCount){
			endRow = rowCount;
			throw new IllegalArgumentException("Illegal end index value: " + endRow);
		}
		/*
		if (dataCache == null || !dataCache.match(startRow, endRow)) {
			if (log.isDebugEnabled())
				log.debug("load and store in cache");
			List<T> data = dataProvider.getItemsByRange(startRow, endRow);
			dataCache = new DataCache<T>(startRow, endRow, data);
		}
		return dataCache.loadedData;
		 */
		//load all from provider and get sublist
		return dataProvider.getItemsByRange(0, rowCount).subList(startRow, endRow);
	}//loadData

	/**
	 * This method must return actual data rows count from the Data Provider. It is used by pagination control
	 * to determine total number of data items.
	 * (non-Javadoc)
	 * @see javax.faces.model.DataModel#getRowCount()
	 */
	private Integer rowCount; // better to buffer row count locally

	public int getRowCount() {
		if (rowCount == null) {
			rowCount = new Integer(dataProvider.getRowCount());
		} else {
			return rowCount.intValue();
		}
		return rowCount.intValue();
		//return dataProvider.getRowCount();
	}

	/**
	 *  This is main way to obtain data row. It is intensively used by framework. 
	 * We strongly recommend use of local cache in that method. 
	 * (non-Javadoc)
	 * @see javax.faces.model.DataModel#getRowData()
	 */
	public T getRowData() {
		if (rowKey == null) {
			return null;
		} else {
			return  getObjectByKey(rowKey);
		}
	}

	@SuppressWarnings("unchecked")
	public Object getKey(T o) {
		return dataProvider.getKey(o);
	}

	public T getObjectByKey(Object key) {
		T t = wrappedData.get(key);
		if (t == null){
			t = dataProvider.getItemByKey(key);
			wrappedData.put(key, t);
		}
		return t;
	}

	private Integer rowIndex;

	/**
	 * Unused rudiment from old JSF staff. (non-Javadoc)
	 * 
	 * @see javax.faces.model.DataModel#getRowIndex()
	 */
	public int getRowIndex() {
		//throw new UnsupportedOperationException();
		return rowIndex.intValue();
	}

	/**
	 * Unused rudiment from old JSF staff.
	 * (non-Javadoc)
	 * @see javax.faces.model.DataModel#setRowIndex(int)
	 */
	public void setRowIndex(int rowIndex) {
		//throw new UnsupportedOperationException();
		this.rowIndex = rowIndex;
	}

	/**
	 * Unused rudiment from old JSF staff.
	 * (non-Javadoc)
	 * @see javax.faces.model.DataModel#getWrappedData()
	 */
	public Object getWrappedData() {
		throw new UnsupportedOperationException();
	}

	/**
	 * Unused rudiment from old JSF staff.
	 * (non-Javadoc)
	 * @see javax.faces.model.DataModel#setWrappedData(java.lang.Object)
	 */
	public void setWrappedData(Object data) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Never called by framework.
	 * (non-Javadoc)
	 * @see javax.faces.model.DataModel#isRowAvailable()
	 */
	public boolean isRowAvailable() {
		return getRowData() != null;
	}

	/**
	 * This method suppose to produce SerializableDataModel that will be
	 * serialized into View State and used on a post-back. In current
	 * implementation we just mark current model as serialized. In more
	 * complicated cases we may need to transform data to actually serialized
	 * form.
	 */
	public SerializableDataModel getSerializableModel(Range range) {
		if (wrappedKeys != null) {
			detached = true;
			// Some activity to detach persistent data from wrappedData map may be taken here.
			// In that specific case we are doing nothing.
			return this;
		} else {
			return null;
		}
	}

	public DataProvider<T> getDataProvider() {
		return dataProvider;
	}

	public void setDataProvider(DataProvider<T> dataProvider) {
		this.dataProvider = dataProvider;
	}

}
