/**
* License Agreement.
*
* JBoss RichFaces - Ajax4jsf Component Library
*
* Copyright (C) 2008 CompuGROUP Holding AG
* 
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

*/
package org.richfaces.component;

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

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

import org.richfaces.json.JSONArray;
import org.richfaces.json.JSONCollection;
import org.richfaces.json.JSONException;
import org.richfaces.json.JSONMap;
import org.richfaces.json.JSONObject;
import org.richfaces.json.JSONStringer;
import org.richfaces.json.JSONWriter;
import org.richfaces.model.Ordering;

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

	private static final long serialVersionUID = -3103664821855261335L;

	public static final String TABLE_STATE_ATTR_NAME = "tableState";
	
	public static final String NONE_COLUMN_ID = "none";
	
	//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 attribute
		String value = (String)extendedDataTable.getAttributes().get(TABLE_STATE_ATTR_NAME);
		JSONMap stateMap = null;
		if ((value != null) && (value.length() > 0)){
			try {
				stateMap = new JSONMap(value);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
		//initialize columns order part
		try{
			columnsOrder = ColumnsOrder.getColumnsOrder(extendedDataTable, (stateMap == null ? null : (JSONCollection)stateMap.get("columnsOrder")));
		}
		catch(Exception e){
			columnsOrder = ColumnsOrder.getColumnsOrder(extendedDataTable, (JSONCollection)null);
		}
		//initialize columns visibility part
		try{
			columnsVisibility = ColumnsVisibility.getColumnsVisibility(extendedDataTable, (stateMap == null ? null : (JSONMap)stateMap.get("columnsVisibility")));
		}
		catch(Exception e){
			columnsVisibility = ColumnsVisibility.getColumnsVisibility(extendedDataTable, (JSONMap)null);
		}
		//initialize columns size part
		try{
			columnsSizeState = ColumnsSizeState.getColumnsSize(extendedDataTable, (stateMap == null ? null : (JSONMap)stateMap.get("columnsSizeState")));
		}
		catch(Exception e){
			columnsSizeState = ColumnsSizeState.getColumnsSize(extendedDataTable, (JSONMap)null);
		}
		//initialize column grouping part
		try{
			columnGroupingState = ColumnGroupingState.getColumnGropingState(extendedDataTable, (stateMap == null ? null : (JSONMap)stateMap.get("columnGroupingState")));
		}
		catch(Exception e){
			columnGroupingState = ColumnGroupingState.getColumnGropingState(extendedDataTable, (JSONMap)null);
		}
	}//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 in JSON format.
	 */
	public String toString(){
		return toJSON().toString();
	}//toString
	
	public JSONObject toJSON(){
		JSONObject result = new JSONObject();
		try {
			result.put("columnsOrder", columnsOrder.toJSON());
			result.put("columnsVisibility", columnsVisibility.toJSON());
			result.put("columnsSizeState", columnsSizeState.toJSON());
			result.put("columnGroupingState", columnGroupingState.toJSON());
		} catch (JSONException e) {
			e.printStackTrace();
		}
		return result;
	}

	/*
	 * (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#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 DEFAULT_WIDTH = "100";
	
	private JSONMap value;

	private ColumnsSizeState() {
		super();
	}
	
	static ColumnsSizeState getColumnsSize(UIExtendedDataTable extendedDataTable, JSONMap map){
		ColumnsSizeState columnsSize = new ColumnsSizeState();
		columnsSize.init(extendedDataTable, map);
		return columnsSize;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, JSONMap map){
		value = null;
		if ((map != null) && (map.size()>0)){
//			//try to restore state from string
			value = map;
//			try {
//				value = new JSONMap(val);
//			} catch (JSONException e) {
//				e.printStackTrace();
//			}
		}
		
		if (value == null){
			createDefaultColumnsSizeState(extendedDataTable);
		}
	}
	
	/**
	 * Converts its state to String representation in JSON format.
	 */
	public String toString(){
		if (value == null){
			return "";
		}
		return value.toString();
	}
	
	/**
	 * Get state in JSON format.
	 * @return JSON object contains state
	 */
	public JSONMap toJSON(){
		return value;
	}
	
	/**
	 * Create default column order based on component children.
	 */
	private void createDefaultColumnsSizeState(UIExtendedDataTable extendedDataTable){
		try {
			JSONWriter writer = new JSONStringer().object();
			for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
				UIColumn col = iter.next();
				writer.key(col.getId()).value(getDefaultColumnSize(col));
			}
			value = new JSONMap(writer.endObject().toString());
		} catch (JSONException e) {
			e.printStackTrace();
		}
	}
	
	private String getDefaultColumnSize(UIComponent column){
		String widthStr = (String) column.getAttributes().get("width");
		return (widthStr == null) ? DEFAULT_WIDTH : widthStr;
	}
	
	public String getColumnSize(UIComponent column){
		if (value == null)
			return getDefaultColumnSize(column);
		String res = (String)value.get(column.getId());
		if (res == null){
			res = getDefaultColumnSize(column);
		}
		return res;
	}
	
//	private String formatWidth(String value){
//		return String.valueOf(HtmlDimensions.decode(value).intValue()); 
//	}
	
	public void changeColumnSize(UIExtendedDataTable extendedDataTable, String newValue){
		if (value == null)
			return;
		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();
				value.put(colId, newWidths[index++]);
			}//if
		}//for
	}//changeColumnSize
	
}//ColumnsSizeState

class ColumnsOrder implements Serializable{

	private static final long serialVersionUID = 907700564445889954L;

	private JSONArray value;

	private ColumnsOrder() {
		super();
	}
	
	static ColumnsOrder getColumnsOrder(UIExtendedDataTable extendedDataTable, JSONCollection collection){
		ColumnsOrder columnsOrder = new ColumnsOrder();
		columnsOrder.init(extendedDataTable, collection);
		return columnsOrder;
	}
	
	static ColumnsOrder getColumnsOrder(UIExtendedDataTable extendedDataTable, String val) throws JSONException{
		ColumnsOrder columnsOrder = new ColumnsOrder();
		columnsOrder.init(extendedDataTable, new JSONCollection(val));
		return columnsOrder;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, JSONCollection collection){
		value = null;
		if ((collection != null) && (collection.size()>0)){
			//try to restore state from string 
			value = new JSONArray(collection);
//			try {
//				value = new JSONArray(val);
//			} catch (JSONException e) {
//				e.printStackTrace();
//			}
		}
		
		if (value == null){
			createDefaultColumnsOrder(extendedDataTable);
		}
	}
	
	
	
	/**
	 * Converts its state to String representation in JSON format.
	 */
	public String toString(){
		if (value == null){
			return "";
		}
		return value.toString();
	}
	
	/**
	 * Get state in JSON format.
	 * @return JSON object contains state
	 */
	public JSONArray toJSON(){
		return value;
	}
	
	/**
	 * Create default column order based on component children.
	 */
	private void createDefaultColumnsOrder(UIExtendedDataTable extendedDataTable){
		value = new JSONArray();
		for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
			UIColumn col = iter.next();
			value.put(col.getId());
		}
	}
	
	/**
	 * Get column index in order. 
	 * @param columnId column id to be found
	 * @return column index or null if not found 
	 */
	private int getColumnIndex(String columnId){
		if (value == null)
			return -1;
		for (int i=0;i<value.length(); i++){
			Object val = value.opt(i);
			if (columnId.equals(val)){
				return i;
			}
		}
		return -1;
	}//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>(value.length());
		for (int i=0;i<value.length(); i++){
			list.add(value.optString(i));
		}
		//get index of source column
		int sourceIndex = list.indexOf(sourceColumnId);
		//remove from order if exist
		if (sourceIndex != -1)
			list.remove(sourceIndex);
		//get index of target column
		int targetIndex = list.indexOf(targetColumnId);
		//add source column after or before target column
		if (targetIndex == -1)//add to end
			list.add(sourceColumnId);
		else{
			//add at proper position
			list.add((targetIndex + (dropBefore ? 0 : 1)), sourceColumnId);
		}
		//convert from list to JSON
		value = new JSONArray(list);
	}
	
	/**
	 * 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 == -1) {
					return ((index2 == -1) ? 0 : 1);
				}
				return ((index2 == -1) ? -1 : index1.compareTo(index2));
			}
		});
		return childs;
	}

}//ColumnsOrder

class ColumnsVisibility 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 JSONMap value;

	private ColumnsVisibility() {
		super();
	}
	
	static ColumnsVisibility getColumnsVisibility(UIExtendedDataTable extendedDataTable, String val) throws JSONException{
		ColumnsVisibility columnsVisibility = new ColumnsVisibility();
		columnsVisibility.init(extendedDataTable, new JSONMap(val));
		return columnsVisibility;
	}
	
	static ColumnsVisibility getColumnsVisibility(UIExtendedDataTable extendedDataTable, JSONMap map){
		ColumnsVisibility columnsVisibility = new ColumnsVisibility();
		columnsVisibility.init(extendedDataTable, map);
		return columnsVisibility;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, JSONMap map){
		value = null;
		if ((map != null) && (map.size()>0)){
			value = map;
		}
		
		if (value == null){
			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 in JSON format.
	 */
	public String toString(){
		if (value == null){
			return "";
		}
		return value.toString();
	}
	
	/**
	 * Get state in JSON format.
	 * @return JSON object contains state
	 */
	public JSONMap toJSON(){
		return value;
	}
	
	/**
	 * Create default column visibility based on component children.
	 */
	private void createDefaultColumnsVisibility(UIExtendedDataTable extendedDataTable){
		try {
			JSONWriter writer = new JSONStringer().object();
			for (Iterator<UIColumn> iter = extendedDataTable.getChildColumns(); iter.hasNext();) {
				UIColumn col = iter.next();
				writer.key(col.getId()).value(TRUE);
			}
			value = new JSONMap(writer.endObject().toString());
		} catch (JSONException e) {
			e.printStackTrace();
		}
	}//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;
		return TRUE.equals(value.get(columnId));
	}//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);
		value.put(columnId, (visible ? TRUE : FALSE));
	}//changeVisibility

}//ColumnsVisibility

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

	//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 ColumnGroupingState() {
		super();
	}
	
	static ColumnGroupingState getColumnGropingState(UIExtendedDataTable extendedDataTable, JSONMap map){
		ColumnGroupingState groupingState = new ColumnGroupingState();
		groupingState.init(extendedDataTable, map);
		return groupingState;
	}
	
	/**
	 * Converts its state from String representation or create default state if it is not set.
	 */
	private void init(UIExtendedDataTable extendedDataTable, JSONMap map){
		columnId = null;
		ordering = Ordering.UNSORTED;
		groupExpanded = new ArrayList<Boolean>();
		if ((map != null) && (map.size() > 0)){
			//try to restore state from string 
//			try {
				columnId = (String)map.get("columnId");
				ordering = Ordering.valueOf((String)map.get("order"));
//			} catch (JSONException e) {
//				e.printStackTrace();
//			}
		}
		
		if (columnId != null){
			//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 in JSON format.
	 */
	public String toString(){
		return toJSON().toString();
	}
	
	/**
	 * Get state in JSON format.
	 * @return JSON object contains state
	 */
	public JSONObject toJSON(){
		JSONObject result = new JSONObject();
		if (columnId != null){
			try {
				result.put("columnId", columnId);
				result.put("order", ordering);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
		return result;
	}
	
	/**
	 * Gets grouped column id.
	 * @return grouped column id if grouping is on, otherwise false
	 */
	String getGroupingColumnId(){
		return columnId;
	}
	
	/**
	 * 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 = ExtendedDataTableState.NONE_COLUMN_ID;
		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
