/*
*
* Sample program for use with the Product
* Licensed Materials  - Property of IBM
* 5724-I66
* (c) Copyright IBM Corp.  2006, 2009
*   
*/
package com.ibm.wbit.tel.client.jsf.bean;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import com.ibm.wbit.tel.client.jsf.infrastructure.ArrayMetaInfo;
import com.ibm.wbit.tel.client.jsf.infrastructure.FacesUtils;

/**
 * ArrayInstance - the data model for representation of arrays. 
 */
public class ArrayInstance {
	
	/**
	 * RemoveInstance - to remove rows out of the array
	 */
	public class RemoveInstance {
		private boolean remove = false;
 			
		public RemoveInstance() {			
		}				
		
		public String removeRow() {
			if (isRemovePossible()) {
				remove = true; 
				doRemoveRow();			
			}
			return null;
		}
		
		public boolean isRemove() {
			return remove;
		}		
	}
	
	private static final long serialVersionUID = 602L;
	
	/**
	 * The key for the RemoveInstances.
	 */
	public static final String REMOVE_ROW_KEY = "~REMOVE~";
	
	/**
	 * Key suffix for simple type array. 
	 */
	public static final String KEY_SUFFIX_SIMPLE_TYPE = "]";
	
	/**
	 * Key suffix for complex type array. 
	 */
	public static final String KEY_SUFFIX_COMPLEX_TYPE = "]/";
	
	/**
     * The id of the message sub view. 
     */
	private String subViewID;
	
	/**
	 * In case of nested arrays the prefix of the parent array.
	 * Array of complex type ends with "/"  
	 * 		e.g. "/input1/arrayRefRef[1]/"
	 * Array of simple type ends with "]"  
	 * 		e.g. "/input1/intRef[1]"
	 * for root array.  
	 * 		null  
	 */
	private String dataTableKeyParent;
	
	/**
	 * The prefix of the array to handle.
	 * Array of complex type ends with "/"
	 * 		e.g. for root array "/input1/arrayRefRef[]/" for nested array "arrayBORef[]/".  
	 * Array of simple type ends with "]"   
	 * 		e.g. for root array "/input1/intRef[]" for nested array "nameBO[]".
	 */
	private String dataTableKey;
	
	/**
	 * The direct child arrays of this array.
	 */
	private HashSet<String> dataTableKeysChildren;
	
	/**
     * The value map from which to get initial array values.
     */
	private HashMap<String, Object> valuesInit; 

	/**
	 * Internal buffer for initial values. 
	 */
	private HashMap<String, HashMap<String, Object>> valuesInitBuffer;
	
	/**
	 * The values of the array instance.
	 */
	private ArrayList<HashMap<String, Object>> values;
	
	/**
	 * The minOccurs as of XSD definition.
	 */
	private int minOccurs; 

	/**
	 * The maxOccurs as of XSD definition.
	 */
	private int maxOccurs;
	
	/**
	 * The current number of array entries.
	 */
	private int currentOccurs;
	
	/**
	 * Flag indicating whether this ArrayInstance is for simple type or complex type.
	 */
	private boolean simpleTypeArray;

	/**
	 * The beans to handle optional nested BOs.
	 */
	private List<String> optionalBo;
	
	/**
	 * Key for the values to store the lit of optional nested BO beans.
	 */
	public static final String OPTIONAL_BO_KEY = "~OptionalNestedBO~";
		
	
	/**
	 * Constructor for root array 
	 * 
	 * @param subViewID id of the message sub view. 
	 * @param dataTableKey prefix of the root array to handle, 
	 * 			for complex type e.g. "/input1/arrayRefRef[]/" 
	 * 			for simple type e.g. "/input1/intRef[]"    
	 * @param valuesInit value map from which to get the array values.
	 */
	public ArrayInstance(String subViewID, String dataTableKey, HashMap<String, Object> valuesInit) {

		this(subViewID, null, dataTableKey, valuesInit);		
	}
		
	/**
	 * Constructor for nested array. 
	 * 
	 * @param subViewID id of the message sub view.
	 * @param dataTableKeyParent prefix of the parent array, 
	 * 			for complex type e.g. "/input1/arrayRefRef[1]/"
	 * 			for simple type not applicable since they cannot have nested data
	 * @param dataTableKey prefix of the nested array to handle, 
	 * 			for complex type e.g. "arrayBORef[]/"
	 * 			for simple type e.g. "intRef[]"   
	 * @param valuesInit value map from which to get the array values.
	 */
	private ArrayInstance(String subViewID, String dataTableKeyParent, String dataTableKey, HashMap<String, Object> valuesInit) {
		
		this.subViewID = subViewID;
		this.dataTableKeyParent = dataTableKeyParent;
		this.dataTableKey = dataTableKey;
		this.valuesInit = valuesInit;

		init();
	}
	
	public String getArrayPrefix(){
		return this.dataTableKey;
	}
	
	/**
	 * Initialize the ArrayInstance
	 */	
	private void init() {
	
		this.valuesInitBuffer = new HashMap<String, HashMap<String, Object>>();
		this.values  = new ArrayList<HashMap<String, Object>>();
		this.optionalBo = new ArrayList<String>();

		initArrayMetaInfo();
		
		if (this.valuesInit != null) {
			//make a copy
			this.valuesInit = new HashMap<String, Object>(this.valuesInit);
			initOptionalBOs();
			transformAbsoluteToRelative();	
			initValues();
		}				
	}
	
	/**
	 * Initialize the optional BOs form the values passed to the ArrayInstance.
	 */
	private void initOptionalBOs() {
		String arrayXpath = getArrayXpath();
		for(Object key: this.valuesInit.keySet()){
			if(key instanceof String){
				String xpath = FacesUtils.removeArrayIndex((String)key);
				if(xpath.startsWith(arrayXpath)){
					xpath = FacesUtils.removeLastPart(xpath);
					if(xpath != null && !xpath.endsWith("[]") && !this.optionalBo.contains(xpath) && !xpath.equals(this.dataTableKey)){
						this.optionalBo.add(xpath);
					}
				}
			}
		}
	}
	
	private String getArrayXpath(){
		if(this.dataTableKeyParent!= null){
			return  FacesUtils.removeArrayIndex(this.dataTableKeyParent)+this.dataTableKey;
		}else {
			return this.dataTableKey;
		}
	}
	

	/**
	 * Initialize the occur values and keys of direct child arrays.
	 */
	private void initArrayMetaInfo() {
		
		if ( dataTableKey.endsWith(KEY_SUFFIX_SIMPLE_TYPE) ) {
			this.simpleTypeArray = true;
		} else {
			this.simpleTypeArray = false;
		}
		
		ArrayMetaInfo ami = (ArrayMetaInfo) FacesUtils.getManagedBeanInstance("arrayMetaInfo", ArrayMetaInfo.class);
		String key = getID();
		
		this.minOccurs = ami.getMetaInfo(key).getMinOccurs();
		this.maxOccurs = ami.getMetaInfo(key).getMaxOccurs();		
		this.currentOccurs = 0;
		this.dataTableKeysChildren = ami.getMetaInfo(key).getKeysChildren();	
		this.optionalBo = ami.getMetaInfo(key).getOptionalBO();
	}
	
	/**
	 * Get ID to be used to get further meta information. 
	 * 
	 * @return String ID to be used to get further meta information
	 */
	private String getID() {
		String ret;
		
		if (dataTableKeyParent == null) {
			ret = subViewID + dataTableKey ;
		} else {
			//eliminate all array indexes
			String parentPart = dataTableKeyParent;
			String part1, part2;
			int idx, startIdx = 0;
			while ((idx = parentPart.indexOf("[", startIdx)) != -1 ) {
				
				part1 = parentPart.substring(0, idx + 1);
				part2 = parentPart.substring(parentPart.indexOf("]", startIdx + 1));
				
				parentPart = part1 + part2;
				startIdx = idx + 1 ;
			}
			ret = subViewID + parentPart + dataTableKey ;
		}
		return ret;
	}
	
	/**
	 * Initialize the ArrayList of values and ensure minOccurs.
	 */
	private void initValues() {
		
		ArrayList<String> keys = new ArrayList<String>(valuesInitBuffer.keySet());
		Collections.sort(keys);
		
		//map to proper data structure and ensure not to show more then maxOccurs
		for (int i = 0; i < keys.size() && i < maxOccurs; i++) {
			values.add( valuesInitBuffer.get(keys.get(i)));
			currentOccurs++;
		}		
		//ensure minOccurs
		if (currentOccurs < minOccurs) {
			addRows(minOccurs - currentOccurs);
		}		
	}
	
	/**
	 * Initialize the valuesInitBuffer with values from valuesInit.
	 * 
	 * Transforms mapping 
	 * 		absolute x-path -> Java value object 
	 * to mapping 	
	 * 		relative x-path -> nested data structure of ArrayInstances.
	 */
    private void transformAbsoluteToRelative() {
    	
    	String dataTableKey, arrayIndex;
    	int arrayKeyEndIndex, arrayKeyStartIndexNested;
    	
    	String prefix = getPrefix();  
    	
    	for (String hashMapKey : valuesInit.keySet() ) {
       		//do we have an array key
    		if (hashMapKey.indexOf("[") != -1 ) {
    			//only arrays with defined prefix 
    			if (hashMapKey.startsWith(prefix)) {
    				
    				//between [ and ] we have the arrayIndex
    				arrayKeyEndIndex = hashMapKey.indexOf("]", prefix.length()); 
    				arrayIndex = (hashMapKey.substring(prefix.length(), arrayKeyEndIndex)).trim();
    				    				
    				if (simpleTypeArray) {
    					dataTableKey = hashMapKey.substring(prefix.lastIndexOf("/") + 1, prefix.length() - 1);
    					//primitive type in array
    					addPrimitiveType(arrayIndex, dataTableKey, hashMapKey);
    				} else {
    					dataTableKey = hashMapKey.substring(arrayKeyEndIndex + 2);
    					//do we have a nested array?
        				arrayKeyStartIndexNested = dataTableKey.indexOf("[");
        				if (arrayKeyStartIndexNested != -1) {
        					//check if nested array is complex type or simple type
        					String suffixNestedArray = getSuffixNestedArray(dataTableKey, arrayKeyStartIndexNested);
        					dataTableKey = dataTableKey.substring(0, arrayKeyStartIndexNested + 1) + suffixNestedArray;
        					addNestedArray(arrayIndex, dataTableKey, prefix);
        				} else {					
        					//primitive type in array
        					addPrimitiveType(arrayIndex, dataTableKey, hashMapKey);
        				}    					
    				}
    			}
    		} 			
		}
    }
    
    /**
     * Get suffix to be used for nested array.  
     * 
     * @param dataTableKey
     * @param arrayKeyStartIndex
     * @return KEY_SUFFIX_SIMPLE_TYPE or KEY_SUFFIX_COMPLEX_TYPE 
     */
    private String getSuffixNestedArray(String dataTableKey, int arrayKeyStartIndex) {
    	String ret = KEY_SUFFIX_COMPLEX_TYPE; 
    	    	
    	if (dataTableKey.length() == dataTableKey.indexOf("]", arrayKeyStartIndex) + 1) {
    		return KEY_SUFFIX_SIMPLE_TYPE;
    	} 
    	return ret; 
    }
        
    /**
     * Transforms mapping 
	 * 		relative x-path -> nested data structure of ArrayInstances
	 * to mapping
	 * 		absolute x-path -> Java value object.
	 * 
	 * @return HashMap<String, Object> mapping absolute x-path to Java value object.
	 */
    @SuppressWarnings("unchecked")
	public HashMap<String, Object> transformRelativeToAbsolute() {
    	HashMap<String, Object> ret = new HashMap<String, Object>();
    	HashMap<String, Object> valuesRow;	 
    	
    	String prefix, absKey;
    	Object value; 
    	//for all rows of the array
		for (int i = 0; i < values.size() && i < currentOccurs; i++) {
			prefix = getPrefixNumbered(i + 1);
			valuesRow = values.get(i);
			
			// create filter for optional nested bos
			HashMap<String, OptionalBoInstance> optionalBoInstances 
				= (HashMap<String, OptionalBoInstance>) valuesRow.get(OPTIONAL_BO_KEY);
						
			//for all row values
			for (String key : valuesRow.keySet()) {
				value = valuesRow.get(key);
				absKey = simpleTypeArray ? prefix : prefix + key;
				//do we have a nested array?
				if (value instanceof ArrayInstance) {
					ret.putAll( ((ArrayInstance)value).transformRelativeToAbsolute());					
				} else if (!(value instanceof RemoveInstance) && !OPTIONAL_BO_KEY.equals(key)) {
					//primitive type in array
					String xpath = FacesUtils.removeArrayIndex(absKey); 
					OptionalBoInstance instance = optionalBoInstances.get(FacesUtils.removeLastPart(xpath));
					if(instance != null ){
						if(instance.isVisible()){
							//absKey = simpleTypeArray ? prefix : prefix + key;
							ret.put(absKey, value);
						}
					} else {
						//absKey = simpleTypeArray ? prefix : prefix + key;
						ret.put(absKey, value);
					}
				}
			}
		}
		
    	return ret; 
    }
    
    /**
     * Add a nested array to this array.
     * 
     * @param arrayIndex of array where to add the nested array
     * @param dataTableKey applicable for jsf value attribute
     * @param prefix generic part of the dataTableKeyParent
     */
    private void addNestedArray(String arrayIndex, String dataTableKey, String prefix) {
		// only if nested array is not already handled
		if (getValuesRow(arrayIndex).get(dataTableKey) == null) {
			prefix = prefix + arrayIndex + KEY_SUFFIX_COMPLEX_TYPE;
			ArrayInstance nestedArray = new ArrayInstance(subViewID, prefix, dataTableKey, valuesInit);
			addValue(arrayIndex, dataTableKey, nestedArray);
		} 
	}
    
    /**
     * Add primitive data type to this array. 
     * 
     * @param arrayIndex of array where to add the primitive type 
     * @param dataTableKey applicable for jsf value attribute
     * @param hashMapKey from valuesInit
     */
    private void addPrimitiveType(String arrayIndex, String dataTableKey, String hashMapKey) {
    	    	 
    	addValue(arrayIndex, dataTableKey, valuesInit.get(hashMapKey));
    }
    
    /**
     * Add passed value to proper row.
     * 
     * @param arrayIndex of array where to add the value.
     * @param dataTableKey applicable for jsf value attribute
     * @param value to be added
     */
    private void addValue(String arrayIndex, String dataTableKey, Object value) {
    	    	
    	getValuesRow(arrayIndex).put(dataTableKey, value);
    }
    
    /**
     * Get values row for passed arrayIndex.
     * 
     * @param arrayIndex of array where to add the value.
     * @return values row for passed arrayIndex
     */
    private HashMap<String, Object> getValuesRow(String arrayIndex) {
    	
    	HashMap<String, Object> ret = valuesInitBuffer.get(arrayIndex);
    	if (ret == null) {
    		ret = new HashMap<String, Object>();
    		ret.put(REMOVE_ROW_KEY, new RemoveInstance());
    		ret.put(OPTIONAL_BO_KEY, getOptionalBOMap(arrayIndex));
    		valuesInitBuffer.put(arrayIndex, ret);   
    	}  
    	return ret;   	
    }
    
    /**
     * Get the prefix to use, depending on dataTableKeyParent and dataTableKey.
     * The returned prefix will always end with "["      
     * 
     * @return the prefix to use
     */
    private String getPrefix() {
    	
    	int charToCut = simpleTypeArray ?  1 : 2; //eliminate the "]" or eliminate the "]/"
    	
    	String ret = dataTableKey.substring(0, dataTableKey.length() - charToCut);  
    	if (dataTableKeyParent != null) {
    		ret = dataTableKeyParent + ret; 
    	}
    	return ret;
    }
    
    /**
     * Get the numbered prefix to use.
     * The returned prefix will with "]" or "]/"      
     * 
     * @param arrayIndex to use for numbered prefix
     * @return the numbered prefix to use
     */
    private String getPrefixNumbered(int arrayIndex) {
    	
    	String suffix = simpleTypeArray ? KEY_SUFFIX_SIMPLE_TYPE : KEY_SUFFIX_COMPLEX_TYPE;
    	
    	return getPrefix() + arrayIndex + suffix;
    }
    
    /**
     * Get the values for this array. 
     * The returned list contains a HashMap for each array entry, for each data repetition.
     * The HashMap itself contains proper java objects according to the data type, e.g. Integer, Float, Boolean...
     * In case of a nested array it contains a further HashMap. 
     *   
     * @return the values for this array. 
     */
	public ArrayList<HashMap<String, Object>> getValues() {

		return values;
	}
	
	/**
	 * Add rows to this array instance.
	 * 
	 * @param numRowsToAdd number of rows to be added
	 */
	private void addRows(int numRowsToAdd) {
		HashMap<String, Object> row; 
		String prefix;
		ArrayInstance nestedArray;
		int arrayIndex;		
		
		for (int i = 0; i < numRowsToAdd; i++) {
			row = new HashMap<String, Object>();
			row.put(REMOVE_ROW_KEY, new RemoveInstance());
			arrayIndex = values.size() + 1;
			prefix = getPrefixNumbered(arrayIndex);
			row.put(OPTIONAL_BO_KEY, getOptionalBOMap(Integer.toString(arrayIndex)));
			//for all direct nested arrays			
			for (String dataTableKeyChild : dataTableKeysChildren) {
				nestedArray = new ArrayInstance(subViewID, prefix, dataTableKeyChild, valuesInit);
				row.put(dataTableKeyChild, nestedArray);
			}
			values.add(row);
			currentOccurs++;
		}
	}
	
	private Map<String, OptionalBoInstance> getOptionalBOMap(String prefix) {
		Map<String, OptionalBoInstance> map = new HashMap<String, OptionalBoInstance>();
		for(String xpath : this.optionalBo){
			OptionalBoInstance instance = new OptionalBoInstance();
			instance.setVisible(isVisible(xpath,prefix));
			map.put(xpath,instance);			
		}		
		return map;
	}

	private boolean isVisible(String optionalBoXpath, String number) {

		String prefix;
		if(optionalBoXpath.length() >= getPrefix().length()+1){
			String relativePath = optionalBoXpath.substring(getPrefix().length()+1);
		
			if(!relativePath.startsWith("/")){
				prefix = getPrefix()+number +"]/"+relativePath;
			}else {
				prefix = getPrefix()+number +"]"+relativePath;
			}
		}else {
			prefix = optionalBoXpath;
		}
		
		
		for(Object key: this.valuesInit.keySet()){
			if(key instanceof String){
				String xpath = (String) key;
				if(xpath.startsWith(prefix)){
					return true;
				}
			}
		}
		
		return false;
	}

	/**
	 * Add a row to the array instance
	 */
	public String addRow() {
		if (isAddPossible()) {
			addRows(1);	
		}	
		return null;
	}
	
	/**
	 * Does the remove action  
	 */
	private void doRemoveRow() {
	
		int removeIdx = 0;
		boolean rowFound = false;
		HashMap<String, Object> row;

		//get index of row to remove
		while (removeIdx < values.size() && !rowFound) {
			row = values.get(removeIdx);
			if (((RemoveInstance) row.get(REMOVE_ROW_KEY)).isRemove()) {
				rowFound = true;
			} else {
				removeIdx++;
			}
		}
		if (rowFound) {
			//remove row
			values.remove(removeIdx);	
			currentOccurs--;
			decrementDataTableKeyParent(removeIdx);
		}
	}
	
	/**
	 * Decrement array indexes of dataTableKeyParent for all direct nested arrays.
	 * This is necessary to avoid fragmentation of the array index
	 * 
	 * @param removeIdx index of removed row
	 */
	private void decrementDataTableKeyParent(int removeIdx) {
		HashMap<String, Object> row;
		int arrayIndex;
		ArrayInstance nestedArray;
		
		for (int i = removeIdx; i < values.size(); i++) {
			row = values.get(i);
			arrayIndex = i + 1;
			for (String dataTableKeyChild : dataTableKeysChildren) {
				nestedArray = (ArrayInstance) row.get(dataTableKeyChild);
				nestedArray.setIndexDataTableKeyParent(arrayIndex);
			}
		}
		
	}
	
	/**
	 * Set passed index as new index of dataTableKeyParent.
	 * The index is set to the right most array brackets.
	 * 
	 * @param arrayIndex new index of dataTableKeyParent
	 */
	private void setIndexDataTableKeyParent(int arrayIndex) {
		if (dataTableKeyParent != null){
			dataTableKeyParent = dataTableKeyParent.substring(0, dataTableKeyParent.lastIndexOf("[")+1)+arrayIndex+"]/";
		}
	}
	
	/**
	 * Is add possible according to maxOccurs and currentOccurs
	 *  
	 * @return boolean indicating if add is possible 
	 */
	public boolean isAddPossible() {
		return (currentOccurs < maxOccurs);
	}
	
	/**
	 * Is remove possible according to minOccurs and currentOccurs
	 *  
	 * @return boolean indicating if remove is possible
	 */
	public boolean isRemovePossible() {
		return (currentOccurs > minOccurs);
	}
	
	/**
	 * Update the entire ArrayInstance, which mean it is new initialized.
	 * 
	 * @param values new initial values.
	 */
	public void updateValues(HashMap<String, Object> values) {
		this.valuesInit = values;
		init();		
	}	
	

	public void initInputValuesOptionalBO(String prefix) {		
		if (optionalBo.contains(prefix)) {
			optionalBo.add(prefix);	
		} 
	}
	

    public List<String> getInputValuesOptionalBO() {
    	return optionalBo;
    }
}
