/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * 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
 */

/*
 * ColumnsHandler.java		Date created: 07.12.2007
 * Last modified by: $Author$
 * $Revision$	$Date$
 */

package org.richfaces.taglib;

import java.io.IOException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.el.ELException;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.servlet.jsp.JspTagException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.el.ELBuilder;
import org.richfaces.iterator.ForEachIterator;
import org.richfaces.iterator.SimpleForEachIterator;

import com.sun.facelets.FaceletContext;
import com.sun.facelets.FaceletHandler;
import com.sun.facelets.tag.MetaRule;
import com.sun.facelets.tag.MetaRuleset;
import com.sun.facelets.tag.MetaTagHandler;
import com.sun.facelets.tag.Metadata;
import com.sun.facelets.tag.MetadataTarget;
import com.sun.facelets.tag.Tag;
import com.sun.facelets.tag.TagAttribute;
import com.sun.facelets.tag.TagAttributes;
import com.sun.facelets.tag.jsf.ComponentConfig;



/**
 * TODO Class description goes here.
 * 
 * @author "Andrey Markavtsov"
 * 
 */
public class ColumnsHandler extends MetaTagHandler {

	private static final Log log = LogFactory.getLog(ColumnsHandler.class);
	
	com.sun.facelets.tag.jsf.ComponentHandler handler;

	private static final String ITERATION_INDEX_VARIABLE = "__richfaces_iteration_index_variable";
	
	private static final String ITERATION_INDEX_EXPRESSION = "#{" + ITERATION_INDEX_VARIABLE + "}";

	private static final String F_GENERATION_SERIES_ATTRIBUTE = "org.richfaces.F_COLUMNS_GENERATION_SERIES";

	/** value attribute */
	private TagAttribute value;

	/** end attribute */
	private TagAttribute columns;

	/** begin attribute */
	private TagAttribute begin;

	/** var attribute */
	private TagAttribute var;

	/** index attribute */
	private TagAttribute index;

	/** end attribute */
	private TagAttribute end;

	class IterationContext {

		/** Iterator for columns's tag value attribute */
		public ForEachIterator items; // our 'digested' items

		/** Value attribute value */
		public Object rawItems; // our 'raw' items

		/** Var attr - defines page variable for current item */
		public String _indexId;

		/** Integer value begin attr */
		public Integer _begin;

		/** Integer value end attr */
		public Integer _end;

		/** Integer value of end attr. */
		public Integer _columns;

		/** String value of var attr */
		public String _itemId = null;

		/** Current column counter */
		public Integer _index = 0;

		/** Expression for var item */
		public IteratedExpression iteratedExpression;

		public String valueExpr;

		public String getVarReplacement() {
			if (valueExpr == null) {
				return String.valueOf(index);
			}else if (items.getVarReplacement() != null) {
				return items.getVarReplacement();
			}
			return valueExpr + "[" + _index + "]";
		}

		public String getIndexReplacement() {
			return String.valueOf(_index); 
		}
	};

	ThreadLocal<IterationContext> iterationContextLocal = new ThreadLocal<IterationContext>();

	public IterationContext getIterationContext() {
		return iterationContextLocal.get();
	}

	/**
	 * TODO Description goes here.
	 * 
	 * @param config
	 */
	public ColumnsHandler(final ComponentConfig config) {
		super(config);
		
		final ComponentConfig columnConfig;
		
		TagAttribute idAttribute = config.getTag().getAttributes().get("id");
		if (idAttribute != null && idAttribute.isLiteral()) {
			columnConfig = new ComponentConfig() {

				private Tag tag;

				{
					Tag initialTag = config.getTag();
					TagAttribute[] allInitialAttributes = initialTag.getAttributes().getAll();
					TagAttribute[] attributes = new TagAttribute[allInitialAttributes.length];
					for (int i = 0; i < allInitialAttributes.length; i++) {
						TagAttribute initialAttribute = allInitialAttributes[i];
						String localName = initialAttribute.getLocalName();
						String attributeValue = initialAttribute.getValue();
						
						if ("id".equals(localName)) {
							attributeValue += ITERATION_INDEX_EXPRESSION;
						}
						
						attributes[i] = new TagAttribute(initialAttribute.getLocation(),
								initialAttribute.getNamespace(), 
								localName, 
								initialAttribute.getQName(), 
								attributeValue);
					}
					
					TagAttributes tagAttributes = new TagAttributes(attributes);
					this.tag = new Tag(initialTag, tagAttributes);
				}
				
				public String getComponentType() {
					return config.getComponentType();
				}

				public String getRendererType() {
					return config.getRendererType();
				}

				public FaceletHandler getNextHandler() {
					return config.getNextHandler();
				}

				public Tag getTag() {
					return tag;
				}

				public String getTagId() {
					return config.getTagId();
				}
				
			};
		} else {
			columnConfig = config;
		}
		
		handler = new ColumnTagHandler(columnConfig) {
			
			@Override
			protected MetaRuleset createMetaRuleset(Class type) {
				MetaRuleset ruleset = super.createMetaRuleset(type);
				ruleset.addRule(new MetaRule() {

					@Override
					public Metadata applyRule(final String name,
							final TagAttribute attribute, MetadataTarget meta) {
						if (ColumnsAttributes.FILTER_ATTRIBUTES.indexOf(name) != -1 || 
							ColumnsAttributes.SORT_ATTRIBUTES.indexOf(name) != -1) {
							
							return new Metadata() {

								@Override
								public void applyMetadata(FaceletContext ctx,
										Object instance) {
									if (!attribute.isLiteral()) {
										String expr = attribute.getValue();
										IterationContext itContext = iterationContextLocal.get();

										ValueExpression ve = ELBuilder.createValueExpression(expr, Object.class, ctx.getExpressionFactory(), ctx, 
												itContext._itemId, itContext._indexId, 
												itContext.getVarReplacement(), itContext.getIndexReplacement());
										((UIComponent)instance).setValueExpression(name, ve);
									}else {
										((UIComponent)instance).getAttributes().put(name, attribute.getValue());
									}
								}

							};
						}
						return null;
					}

				});
				return ruleset;
			}

			@Override
			protected void applyNextHandler(FaceletContext ctx, UIComponent c)
			throws IOException, FacesException, ELException {
				c.getAttributes().put(F_GENERATION_SERIES_ATTRIBUTE, RequestUniqueIdGenerator.generateId(ctx.getFacesContext()));
				super.applyNextHandler(ctx, c);
			}
		};
	}


	/**
	 * Extracts tags attributes values
	 */
	private void initVariables(FaceletContext ctx) {
		initColumnsCount(ctx);
		initIndex(ctx);
		initVar(ctx);
		initBegin(ctx);
		initEnd(ctx);
	}

	/**
	 * Method prepares all we need for starting of tag rendering
	 * 
	 * @throws JspTagException
	 */
	private void prepare(FaceletContext ctx) {

		initVariables(ctx);

		IterationContext itContext = getIterationContext();

		try {

			this.value = getAttribute("value");

			// produce the right sort of ForEachIterator
			if (this.value != null) {
				itContext.valueExpr = ELBuilder.getVarReplacement(this.value.getValue());

				// If this is a deferred expression, make a note and get
				// the 'items' instance.

				itContext.rawItems = this.value.getObject(ctx);

				// extract an iterator over the 'items' we've got
				itContext.items = SimpleForEachIterator
				.supportedTypeForEachIterator(itContext.rawItems);
			} else {
				// no 'items', so use 'begin' and 'end'
				itContext.items = SimpleForEachIterator.beginEndForEachIterator(itContext._columns - 1);
			}
		} catch (Exception e) {
			log.error(e.getLocalizedMessage(), e);
			// TODO: handle exception
		}

		correctFirst(ctx);
	}

	/**
	 * Inits first iteration item
	 */
	private void correctFirst(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		if (itContext.items != null) {
			if (itContext._begin > 0 && (itContext._index < itContext._begin)) {
				while ((itContext._index < itContext._begin && hasNext())) {
					next(ctx);
				}
				if (!hasNext()) {
					itContext._index = 0;
				}
			}
		}
	}

	/**
	 * Return true if we didn't complete column's count
	 * 
	 * @return
	 * @throws JspTagException
	 */
	private boolean hasNext() {
		IterationContext itContext = getIterationContext();
		try {
			if (itContext._end != 0) {
				return (itContext._index < itContext._end) ? itContext.items.hasNext() : false;
			} else {
				return itContext.items.hasNext();
			}
		} catch (Exception e) {
			return false;
		}

	}

	/**
	 * Iterate to next column
	 * 
	 * @return
	 * @throws JspTagException
	 */
	private Object next(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		try {
			Object o = itContext.items.next();
			itContext._index++;
			return o;
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Extracts integer value from end attr
	 */
	private void initColumnsCount(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		this.columns = getAttribute("columns");
		if (columns != null) {
			try {
				itContext._columns = Integer.parseInt((String) columns.getObject(ctx));
				if (itContext._columns < 0) {
					itContext._columns = 0; // If end is negative set up zero
				}
			} catch (Exception e) {
				itContext._columns = 0;
			}
		} else {
			itContext._columns = 0;
		}
	}

	/**
	 * Extracts integer value from begin attr
	 */
	private void initBegin(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		this.begin = getAttribute("begin");
		if (begin != null) {
			try {
				Object o = begin.getObject(ctx);
				if (o instanceof Number) {
					itContext._begin = ((Number)o).intValue();
				}else if (o instanceof String) {
					itContext._begin = Integer.parseInt((String) o);
				}
				itContext._begin--;
				if (itContext._begin < 0) {
					itContext._begin = 0; // If end is negative set up zero
				}
			} catch (Exception e) {
				itContext._begin = 0;
			}
		} else {
			itContext._begin = 0;
		}
	}

	/**
	 * Extracts integer value from end attr
	 */
	private void initEnd(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		this.end = getAttribute("end");
		if (end != null) {
			try {
				Object o = end.getObject(ctx);
				if (o instanceof Number) {
					itContext._end = ((Number)o).intValue();
				}else if ( o instanceof String) {
					itContext._end = Integer.parseInt((String) o);
				}
				if (itContext._end < 0) {
					itContext._end = 0; // If end is negative set up zero
				}
			} catch (Exception e) {
				itContext._end = 0;
			}
		} else {
			itContext._end = 0;
		}
	}

	/**
	 * Extracts string value from var attr
	 */
	private void initVar(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		this.var = getAttribute("var");
		if (var != null) {
			try {
				itContext._itemId = (String) var.getObject(ctx);
			} catch (ClassCastException e) {
				itContext._itemId = null;
			}

		}
	}

	/**
	 * Extracts string value from index attr
	 */
	private void initIndex(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		this.index = getAttribute("index");
		if (index != null) {
			try {
				itContext._indexId = (String) index.getObject(ctx);
			} catch (ClassCastException e) {
				itContext._indexId = null;
			}

		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.richfaces.taglib.ComponentHandler#apply(com.sun.facelets.FaceletContext,
	 *      javax.faces.component.UIComponent)
	 */
	//@Override
	public void apply(FaceletContext ctx, UIComponent parent)
	throws IOException, FacesException, ELException {

		IterationContext iterationContext = new IterationContext();
		iterationContextLocal.set(iterationContext);

		clearOldColumns(ctx.getFacesContext(), parent);
		prepare(ctx);  // prepare data 

		try {
			while (hasNext()) {  // for each
				exposeVariables(ctx);
				handler.apply(ctx, parent);
				next(ctx);
			}
		} catch (Exception e) {
			log.error(e.getLocalizedMessage(), e);
			// TODO: handle exception
		} finally {
			release();
			unExposeVariables(ctx);
		}

	}


	private void clearOldColumns(FacesContext context, UIComponent parent) {
		if (parent.getChildCount() > 0) {
			Integer generatedId = RequestUniqueIdGenerator.generateId(context);
			
			Iterator<UIComponent> childrenIt = parent.getChildren().iterator();
			while (childrenIt.hasNext()) {
				UIComponent c = childrenIt.next();

				Object generationSeries = c.getAttributes().get(F_GENERATION_SERIES_ATTRIBUTE);
				if (generationSeries != null && !generationSeries.equals(generatedId)) {
					childrenIt.remove();
				}
			}
		}
	}

	/**
	 * Sets page request variables
	 * 
	 * @throws JspTagException
	 */
	private void exposeVariables(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		VariableMapper vm = ctx.getVariableMapper();
		int k = itContext._index;

		if (itContext._itemId != null) {
			if (vm != null) {
				if (value != null) {
					ValueExpression srcVE = value.getValueExpression(ctx,
							Object.class);
					ValueExpression ve = getVarExpression(ctx, srcVE);
					vm.setVariable(itContext._itemId, ve);
				}
			}

		}

		// Set up index variable

		if (itContext._indexId != null) {
			if (vm != null) {
				ValueExpression ve = new IteratedIndexExpression(k);
				vm.setVariable(itContext._indexId, ve);
			}

		}

		int componentsCount = itContext._index - itContext._begin;
		if (componentsCount != 0) {
			ValueExpression ve = ctx.getExpressionFactory().createValueExpression(UIViewRoot.UNIQUE_ID_PREFIX + componentsCount, String.class);
			vm.setVariable(ITERATION_INDEX_VARIABLE, ve);
		}
	}

	/**
	 * Removes page attributes that we have exposed and, if applicable, restores
	 * them to their prior values (and scopes).
	 */
	private void unExposeVariables(FaceletContext ctx) {
		IterationContext itContext = getIterationContext();
		VariableMapper vm = ctx.getVariableMapper();
		// "nested" variables are now simply removed
		if (itContext._itemId != null) {
			if (vm != null)
				vm.setVariable(itContext._itemId, null);
		}
		if (itContext._indexId != null) {
			if (vm != null)
				vm.setVariable(itContext._indexId, null);
		}
		
		vm.setVariable(ITERATION_INDEX_VARIABLE, null);
	}

	/**
	 * Return expression for page variables
	 * 
	 * @param expr
	 * @return
	 */
	private ValueExpression getVarExpression(FaceletContext ctx,
			ValueExpression expr/*, IterationContext itContext*/) {
		IterationContext itContext = getIterationContext();
		Object o = expr.getValue(ctx.getFacesContext().getELContext());
		int k = itContext._index;
		if (o.getClass().isArray() || o instanceof List) {
			return new IndexedValueExpression(expr, k);
		}

		if (o instanceof Collection || o instanceof Iterator
				|| o instanceof Enumeration || o instanceof Map
				|| o instanceof String) {

			if (itContext.iteratedExpression == null) {
				itContext.iteratedExpression = new IteratedExpression(expr, ",");
			}
			return new IteratedValueExpression(itContext.iteratedExpression, k);
		}

		throw new ELException("FOREACH_BAD_ITEMS");
	}

	/**
	 * Release iteration variables
	 */
	private void release() {
		IterationContext itContext = getIterationContext();
		itContext.items = null;
		itContext._index = 0;
	}

}
