/**
 * 
 */
package org.richfaces.component;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import org.ajax4jsf.util.HtmlDimensions;
import org.richfaces.model.Ordering;

/**
 * @author pawelgo
 *
 */
public class ExtendedDataTableState implements Serializable {

	private static final long serialVersionUID = -3103664821855261335L;

	public static final String TABLE_STATE_ATTR_NAME = "tableState";
	
	protected static final String SEP = ":";
	
	protected ColumnsOrder columnsOrder;
	protected ColumnsVisibility columnsVisibility;
	protected ColumnsSizeState columnsSizeState;
	protected ColumnGroupingState columnGroupingState;
	
	public static ExtendedDataTableState getExtendedDataTableState(UIExtendedDataTable extendedDataTable){
		ExtendedDataTableState state = new ExtendedDataTableState();
		state.init(extendedDataTable);
		return state;
	}//init
	
	/**
	 * Converts its state based on table attribute value or create default state if it is not set.
	 */
	protected void init(UIExtendedDataTable extendedDataTable){
		//get state value from components attributes
		String value = (String)extendedDataTable.getAttributes().get(TABLE_STATE_ATTR_NAME);
		//split state value into parts
		String[] values = fromString(value);
		//initialize columns order part
		String val = (values != null && values.length>0) ? values[0] : null;
		columnsOrder = ColumnsOrder.getColumnsOrder(extendedDataTable, val);
		//initialize columns visibility part
		val = (values != null && values.length>1) ? values[1] : null;
		columnsVisibility = ColumnsVisibility.getColumnsVisibility(extendedDataTable, val);
		//initialize columns size part
		val = (values != null && values.length>2) ? values[2] : null;
		columnsSizeState = ColumnsSizeState.getColumnsSize(extendedDataTable, val);
		//initialize column grouping part
		val = (values != null && values.length>3) ? values[3] : null;
		columnGroupingState = ColumnGroupingState.getColumnGropingState(extendedDataTable, val);
	}//init
	
	/**
	 * Puts own state into component state. 
	 */
	public void publishChanges(FacesContext context, UIExtendedDataTable extendedDataTable){
		ValueExpression ve = extendedDataTable.getValueExpression(TABLE_STATE_ATTR_NAME);
		if ((null != ve) && (!ve.isReadOnly(context.getELContext()))) {
			ve.setValue(context.getELContext(), toString());
		}
	}//publishChanges
	
	/**
	 * Converts its state to String representation.
	 */
	public String toString(){
		String[] values = new String[4];
		values[0] = columnsOrder.toString();
		values[1] = columnsVisibility.toString();
		values[2] = columnsSizeState.toString();
		values[3] = columnGroupingState.toString();
		StringBuilder builder = new StringBuilder();
		for (String str : values){
			builder.append(str).append(SEP);
		}//for
		return builder.toString();
	}//toString
	
	public String[] fromString(String value){
		return (value == null) ? null : value.split(SEP);
	}//fromString

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnsOrder#changeOrder(String, String)
	 */
	public void changeColumnsOrder(String sourceColumnId, String targetColumnId, boolean dropBefore) {
		columnsOrder.changeOrder(sourceColumnId, targetColumnId, dropBefore);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnsOrder#sortColumns(FacesContext, List)
	 */
	public List<UIComponent> sortColumns(FacesContext context,
			List<UIComponent> children) {
		return columnsOrder.sortColumns(context, children);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnsVisibility#isVisible(String)
	 */
	public boolean isColumnVisible(String columnId) {
		return columnsVisibility.isVisible(columnId);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnsVisibility#toggleVisibility(UIExtendedDataTable, String)
	 */
	public void toggleColumnVisibility(UIExtendedDataTable extendedDataTable,
			String columnId) {
		columnsVisibility.toggleVisibility(extendedDataTable, columnId);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnsSizeState#changeColumnSize(UIExtendedDataTable, String)
	 */
	public void changeColumnSize(UIExtendedDataTable extendedDataTable,
			String newValue) {
		columnsSizeState.changeColumnSize(extendedDataTable, newValue);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnsSizeState#getColumnSize(UIComponent)
	 */
	public String getColumnSize(UIComponent column) {
		return columnsSizeState.getColumnSize(column);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#isGroupingOn()
	 */
	public boolean isGroupingOn(){
		return columnGroupingState.isGroupingOn();
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#getGroupingColumnId()
	 */
	public String getGroupingColumnId(){
		return columnGroupingState.getGroupingColumnId();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#groupBy(String, Ordering)
	 */
	public void groupBy(String colId, Ordering ordering) {
		columnGroupingState.groupBy(colId, ordering);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#resetGroupVisibilityState()
	 */
	public void resetGroupVisibilityState(){
		columnGroupingState.resetGroupVisibilityState();
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#disableGrouping()
	 */
	public void disableGrouping(){
		columnGroupingState.disableGrouping();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#groupIsExpanded(int)
	 */
	public boolean groupIsExpanded(int index) {
		return columnGroupingState.groupIsExpanded(index);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ColumnGroupingState#toggleGroup(int)
	 */
	public void toggleGroup(int index) {
		columnGroupingState.toggleGroup(index);
	}
	
	
}

class ColumnsSizeState implements Serializable{

	private static final long serialVersionUID = 8724163192351491340L;

	private static final String SEP = ";";
	
	private static final String DEFAULT_WIDTH = "100";
	
	private String value;

	private ColumnsSizeState() {
		super();
	}
	
	static ColumnsSizeState getColumnsSize(UIExtendedDataTable extendedDataTable, String val){
		ColumnsSizeState columnsSize = new ColumnsSizeState();
		columnsSize.init(extendedDataTable, val);
		return columnsSize;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, String val){
		value = val;
		if ((value == null) || (value.length() == 0))
			createDefaultColumnsSizeState(extendedDataTable);
	}
	
	/**
	 * Converts its state to String representation.
	 */
	public String toString(){
		return value;
	}
	
	/**
	 * Create default column order based on component children.
	 */
	private void createDefaultColumnsSizeState(UIExtendedDataTable extendedDataTable){
		StringBuilder builder = new StringBuilder();
		
		for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
			UIColumn col = iter.next();
			builder.append(col.getId().toUpperCase()).append("-").append(getDefaultColumnSize(col)).append(SEP);
		}
		value = builder.toString();
	}
	
	private String getDefaultColumnSize(UIComponent column){
		String widthStr = (String) column.getAttributes().get("width");
		return formatWidth((widthStr == null || widthStr.length() == 0) ? DEFAULT_WIDTH : widthStr);
	}
	
	public String getColumnSize(UIComponent column){
		if (value == null)
			return getDefaultColumnSize(column);
		String[] widths = value.split(SEP);
		if (widths != null){
			String colId = column.getId().toUpperCase();
			for (String val : widths){
				if (val.toUpperCase().startsWith(colId+"-")){
					return  formatWidth(val.split("-")[1]);
				}
			}//for
		}
		return getDefaultColumnSize(column);		
	}
	
	private String formatWidth(String value){
		return String.valueOf(HtmlDimensions.decode(value).intValue()); 
	}
	
	public void changeColumnSize(UIExtendedDataTable extendedDataTable, String newValue){
		if (value == null)
			return;
		Set<String> widths = new HashSet<String>(Arrays.asList(value.toUpperCase().split(SEP)));
		String[] newWidths = newValue.split(";");
		int index = 0;
		for (Iterator<UIColumn> iter = extendedDataTable.getSortedColumns(); iter.hasNext();) {
			UIComponent col = (UIComponent) iter.next();
			if (col.isRendered()){
				String colId = col.getId().toUpperCase();
				//remove existing item
				Set<String> toDel = new HashSet<String>();
				for (String val : widths){
					if (val.toUpperCase().startsWith(colId+"-")){
						toDel.add(val);
					}
				}//for
				widths.removeAll(toDel);
				//create new item
				String newWidth = newWidths[index++];
				String item = colId + "-" + newWidth;;
				widths.add(item);
			}//if
		}//for
		//build new value
		StringBuilder builder = new StringBuilder();
		for (String val : widths){
			builder.append(val).append(SEP);
		}
		value = builder.toString();
	}//changeColumnSize
	
}//ColumnsSizeState

class ColumnsOrder implements Serializable{

	private static final long serialVersionUID = 907700564445889954L;

	private static final String SEP = ";";

	private String value;

	private ColumnsOrder() {
		super();
	}
	
	static ColumnsOrder getColumnsOrder(UIExtendedDataTable extendedDataTable, String val){
		ColumnsOrder columnsOrder = new ColumnsOrder();
		columnsOrder.init(extendedDataTable, val);
		return columnsOrder;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, String val){
		value = val;
		if ((value == null) || (value.length() == 0))
			createDefaultColumnsOrder(extendedDataTable);
	}
	
	/**
	 * Converts its state to String representation.
	 */
	public String toString(){
		return value;
	}
	
	/**
	 * Create default column order based on component children.
	 */
	private void createDefaultColumnsOrder(UIExtendedDataTable extendedDataTable){
		StringBuilder builder = new StringBuilder();
		for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
			UIColumn child = iter.next();
			builder.append(child.getId().toUpperCase()).append(SEP);
		}
		value = builder.toString();
	}
	
	/**
	 * Get column index in order. 
	 * @param columnId column id to be found
	 * @return column index or null if not found 
	 */
	private Integer getColumnIndex(String columnId){
		if (value == null)
			return null;
		List<String> list = Arrays.asList(value.toUpperCase().split(SEP));
		if (list.contains(columnId.toUpperCase()))
			return list.indexOf(columnId.toUpperCase());
		return null;
	}//getColumnIndex
	
	/**
	 * Changes column order. Moves source column to be next to target column.
	 * @param sourceColumnId source column id to be moved
	 * @param targetColumnId target column id
	 * @param dropBefore
	 */
	void changeOrder(String sourceColumnId, String targetColumnId, boolean dropBefore){
		if (value == null)
			return;
		if (sourceColumnId.equals(targetColumnId))
			return;
		List<String> list = new ArrayList<String>(Arrays.asList(value.toUpperCase().split(SEP)));
		//get index of source column
		int sourceIndex = list.indexOf(sourceColumnId.toUpperCase());
		//remove from order if exist
		if (sourceIndex != -1)
			list.remove(sourceIndex);
		//get index of target column
		int targetIndex = list.indexOf(targetColumnId.toUpperCase());
		//add source column after or before target column
		if (targetIndex == -1)//add to end
			list.add(sourceColumnId.toUpperCase());
		else{
			//add at proper position
			list.add((targetIndex + (dropBefore ? 0 : 1)), sourceColumnId.toUpperCase());
		}
		//convert from List to String
		StringBuilder builder = new StringBuilder();
		for (String str : list)
			builder.append(str).append(SEP);			
		value = builder.toString();
	}
	
	/**
	 * Sort column by given order.
	 * @param context faces context
	 * @param children list of unsorted columns
	 * @return list of sorted columns
	 */
	List<UIComponent> sortColumns(final FacesContext context, List<UIComponent> children){
		List<UIComponent> childs = new ArrayList<UIComponent>(children);
		Collections.sort(childs, new Comparator<UIComponent>() {
			public int compare(UIComponent o1, UIComponent o2) {
				Integer index1 = getColumnIndex(o1.getId());
				Integer index2 = getColumnIndex(o2.getId());
				if (index1 == null) {
					return ((index2 == null) ? 0 : 1);
				}
				return ((index2 == null) ? -1 : index1.compareTo(index2));
			}
		});
		return childs;
	}

}//ColumnsOrder

class ColumnsVisibility implements Serializable{
	
	private static final long serialVersionUID = -3923409650272094713L;

	private static final String SEP = ";";

	private String value;

	private ColumnsVisibility() {
		super();
	}
	
	static ColumnsVisibility getColumnsVisibility(UIExtendedDataTable extendedDataTable, String val){
		ColumnsVisibility columnsVisibility = new ColumnsVisibility();
		columnsVisibility.init(extendedDataTable, val);
		return columnsVisibility;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, String val){
		value = val;
		if ((value == null) || (value.length() == 0))
			createDefaultColumnsVisibility(extendedDataTable);
		//set visibility flag for all columns
		for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
			UIColumn child = iter.next();
			if (child instanceof UIColumn) {
				UIColumn dataColumn = (UIColumn) child;
				dataColumn.setVisible(isVisible(dataColumn.getId()));
			}//if
		}//for
	}//init
	
	/**
	 * Converts its state to String representation.
	 */
	public String toString(){
		return value;
	}
	
	/**
	 * Create default column visibility based on component children.
	 */
	private void createDefaultColumnsVisibility(UIExtendedDataTable extendedDataTable){
		StringBuilder builder = new StringBuilder();
		for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
			UIColumn kid = iter.next();
			builder.append(kid.getId().toUpperCase()).append(SEP);
		}
		value = builder.toString();
	}//createDefaultColumnsVisibility
	
	/**
	 * Get column visibility. 
	 * @param columnId column id to be found
	 * @return true if column is visible, otherwise false
	 */
	boolean isVisible(String columnId){
		if (value == null)
			return true;
		Set<String> visibleIds = new HashSet<String>(Arrays.asList(value.toUpperCase().split(SEP)));
		return visibleIds.contains(columnId.toUpperCase());
	}//isVisible
	
	/**
	 * Toggle column visibility.
	 * @param extendedDataTable table component
	 * @param columnId column id
	 */
	void toggleVisibility(UIExtendedDataTable extendedDataTable, String columnId){
		if (value == null)
			return;
		UIColumn column = null;
		//find column by id
		for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
			UIColumn col = iter.next();
			if (col.getId().equalsIgnoreCase(columnId)){
				if (col instanceof UIColumn){
					column = (UIColumn) col;
				}
				break;
			}//if
		}//for
		if (column == null)
			return;
		boolean visible = column.isVisible();
		//toggle visibility
		visible = !visible;
		//set visibility flag for column
		column.setVisible(visible);
		Set<String> visibleIds = new HashSet<String>(Arrays.asList(value.toUpperCase().split(SEP)));
		if (visible){
			//add id to set
			visibleIds.add(columnId.toUpperCase());
		}
		else{
			//remove id from list
			visibleIds.remove(columnId.toUpperCase());
		}
		//convert from Set to String
		StringBuilder builder = new StringBuilder();
		for (String str : visibleIds)
			builder.append(str).append(SEP);			
		value = builder.toString();
	}//changeVisibility

}//ColumnsVisibility

class ColumnGroupingState implements Serializable{
	
	private static final long serialVersionUID = -3923409650272094713L;

	private static final String SEP = ";";
	//private static final String TRUE = "1";
	//private static final String FALSE = "0";
	private static final Boolean DEF = Boolean.TRUE;//expanded

	private String columnId;
	private List<Boolean> groupExpanded;
	private Ordering ordering; 
	//private String value;

	private ColumnGroupingState() {
		super();
	}
	
	static ColumnGroupingState getColumnGropingState(UIExtendedDataTable extendedDataTable, String val){
		ColumnGroupingState groupingState = new ColumnGroupingState();
		groupingState.init(extendedDataTable, val);
		return groupingState;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, String val){
		columnId = null;
		ordering = Ordering.UNSORTED;
		groupExpanded = new ArrayList<Boolean>();
		if ((val == null) || (val.length() == 0))
			return;
		List<String> tmp = Arrays.asList(val.split(SEP));
		if (!tmp.isEmpty()){
			columnId = tmp.get(0);						//column id
			ordering = Ordering.valueOf(tmp.get(1));	//sort order
			if (ordering == null)
				ordering = Ordering.UNSORTED;
//			tmp = tmp.subList(2, tmp.size());//remove fist and second item
//			for (String s : tmp){
//				groupExpanded.add(Boolean.valueOf(s.equals(TRUE)));
//			}//for
		}
		//get column by id and set sort order
		for (Iterator<UIColumn> columns = extendedDataTable.getChildColumns(); columns.hasNext(); ){
			UIColumn child = columns.next();
			if (columnId.equalsIgnoreCase(child.getId())) {
				child.setSortOrder(ordering);
				break;
			}
		}
	}//init
	
	/**
	 * Converts its state to String representation.
	 */
	public String toString(){
		if (columnId == null)
			return "";
		StringBuilder builder = new StringBuilder();
		builder.append(columnId).append(SEP);	//add column id
		builder.append(ordering).append(SEP);	//add sort order
//		for (Boolean b : groupExpanded){
//			builder.append(b ? TRUE : FALSE).append(SEP);
//		}
		return builder.toString();
	}
	
	/**
	 * Gets grouped column id.
	 * @return grouped column id if grouping is on, otherwise false
	 */
	String getGroupingColumnId(){
		return columnId;
	}
	
	/**
	 * Checks if grouping is on. 
	 * @return true if grouping is on, otherwise false
	 */
	boolean isGroupingOn(){
		return (columnId != null);
	}
	
	/**
	 * Turn on grouping for column. 
	 * @param colId id of column to be grouped
	 * @param ordering sort order
	 */
	void groupBy(String colId, Ordering ordering){
		columnId = colId;
		this.ordering = ordering;
		resetGroupVisibilityState();
	}
	
	/**
	 * Resets information about group visibility state.
	 * All group will be mark as expanded.
	 */
	void resetGroupVisibilityState(){
		groupExpanded.clear();
	}
	
	/**
	 * Turn off grouping. 
	 */
	void disableGrouping(){
		columnId = null;
		ordering = Ordering.UNSORTED;
		resetGroupVisibilityState();
	}
	
	/**
	 * Toggle group. It means that group will be expanded if is collapsed
	 * and group will be collapsed if is expanded.
	 * @param index index of group to be toggled 
	 */
	void toggleGroup(int index){
		if (index < 0)
			throw new IllegalArgumentException("Illegal index value :"+index);
		if (index >= groupExpanded.size()){
			//add default values for lower indexes
			int count = index - groupExpanded.size() + 1;
			for (int i = 0; i < count; i++){
				groupExpanded.add(DEF);
			}///for
		}
		groupExpanded.add(index,!groupExpanded.remove(index));
	}
	
	/**
	 * Checks if group is expanded. 
	 * @param index index of group to be tested 
	 * @return true if group is expanded, otherwise false
	 */
	boolean groupIsExpanded(int index){
		if (index < 0)
			throw new IllegalArgumentException("Illegal index value :"+index);
		if (index >= groupExpanded.size()){
			return DEF;
		}
		return groupExpanded.get(index).booleanValue();
	}

}//ColumnGroupingState