/**
 * 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
 */

/*
 * AbstractProgressBarRenderer.java		Date created: 20.12.2007
 * Last modified by: $Author$
 * $Revision$	$Date$
 */

package org.richfaces.renderkit;

import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.faces.FactoryFinder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.ActionEvent;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.servlet.http.HttpServletResponse;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.event.AjaxEvent;
import org.ajax4jsf.javascript.JSFunction;
import org.ajax4jsf.javascript.JSLiteral;
import org.ajax4jsf.javascript.JSReference;
import org.ajax4jsf.javascript.ScriptUtils;
import org.ajax4jsf.renderkit.AjaxRendererUtils;
import org.ajax4jsf.renderkit.ComponentVariables;
import org.ajax4jsf.renderkit.ComponentsVariableResolver;
import org.ajax4jsf.renderkit.RendererUtils.HTML;
import org.ajax4jsf.resource.CountingOutputWriter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.component.UIProgressBar;

/**
 * Abstract progress bar renderer
 * 
 * @author "Andrey Markavtsov"
 * 
 */
public class AbstractProgressBarRenderer extends TemplateEncoderRendererBase {
	
	private static final Log log = LogFactory.getLog(AbstractProgressBarRenderer.class);

	/** Ajax function performing polling */
	private static final String AJAX_POLL_FUNCTION = "A4J.AJAX.Poll";

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.renderkit.AjaxCommandRendererBase#doDecode(javax.faces.context.FacesContext,
	 *      javax.faces.component.UIComponent)
	 */
	@Override
	protected void doDecode(FacesContext facesContext, UIComponent uiComponent) {
		new AjaxEvent(uiComponent).queue();
		//uiComponent.queueEvent(new ActionEvent(uiComponent));
	}

	/**
	 * Render progress state forced from javascript
	 * 
	 * @param state
	 * @param context
	 * @param component
	 * @throws IOException
	 */
	public void renderForcedState(String state, FacesContext context,
			UIComponent component) throws IOException {
		if ("initialState".equals(state)) {
			encodeInitialState(context, component, true);
		} else if ("progressState".equals(state)) {
			encodeProgressState(context, component, true);
		} else if ("completeState".equals(state)) {
			encodeCompletedState(context, component, true);
		}
		component.getAttributes().remove(UIProgressBar.FORCE_PERCENT_PARAM);
	}

	/**
	 * Gets state forced from javascript
	 * 
	 * @param component
	 * @return
	 */
	public String getForcedState(FacesContext context, UIComponent component) {
		String forcedState = null;
		Map<String, String> params = context.getExternalContext()
				.getRequestParameterMap();
		if (params.containsKey(UIProgressBar.FORCE_PERCENT_PARAM)) {
			forcedState = params.get(UIProgressBar.FORCE_PERCENT_PARAM);
		}
		return forcedState;
	}

	/**
	 * Renderes label markup
	 * 
	 * @param context
	 * @param component
	 * @return
	 */
	public StringBuffer getMarkup(FacesContext context, UIComponent component) {
		StringBuffer result = new StringBuffer();
		CountingOutputWriter customWriter = new CountingOutputWriter();
		try {
			if (hasChildren(component)) {
				
				ResponseWriter writer = context.getResponseWriter();

				String defaultRenderKitId = context.getApplication()
						.getDefaultRenderKitId();
				if (null == defaultRenderKitId) {
					defaultRenderKitId = RenderKitFactory.HTML_BASIC_RENDER_KIT;
				}
				RenderKitFactory renderKitFactory = (RenderKitFactory) FactoryFinder
						.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
				RenderKit renderKit = renderKitFactory.getRenderKit(context,
						defaultRenderKitId);

				ResponseWriter responseWriter = renderKit.createResponseWriter(
						customWriter, null, "UTF-8");
				context.setResponseWriter(responseWriter);
				writeScriptBody(context, component, true);
				result = customWriter.getContent();
				
				if (writer != null) {
					context.setResponseWriter(writer);
				}else {
					HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
					PrintWriter wr = response.getWriter();
					if (wr != null) {
						context.setResponseWriter(renderKit.createResponseWriter(wr, null, "UTF-8"));
					}
				}
			} else {
				writeScriptBody(customWriter, (String) component
						.getAttributes().get("label"));
				result = customWriter.getContent();
			}
		} catch (Exception e) {
			log.error("Error occurred during rendering of progress bar label. It switched to empty string", e);
		}

		return result;

	}

	/**
	 * Methods encodes AJAX script for polling
	 * 
	 * @param context -
	 *            faces context
	 * @param component -
	 *            component instance
	 * @throws IOException -
	 *             IOException
	 */
	public void encodePollScript(FacesContext context, UIComponent component)
			throws IOException {

		ResponseWriter writer = context.getResponseWriter();
		UIProgressBar progressBar = (UIProgressBar) component;
		String clientId = component.getClientId(context);
		StringBuffer pollScript = new StringBuffer("\n");
		StringBuffer script = new StringBuffer("\n");
		if (isAjaxMode(component) && progressBar.isEnabled()) {
			JSFunction function = AjaxRendererUtils.buildAjaxFunction(
					component, context, AJAX_POLL_FUNCTION);

			function.addParameter(new JSReference("$('" + clientId
					+ "').component.options"));
			function.appendScript(script);

			pollScript.append(script);
		} else {
			pollScript.append(getStopPollScript(clientId));
		}
		pollScript.append(";\n");
		writer.writeText(pollScript.toString(), null);
	}

	/**
	 * Encodes script for state rendering in client mode
	 * 
	 * @param context
	 * @param component
	 * @param state
	 * @throws IOException
	 */
	public void encodeRenderStateScript(FacesContext context,
			UIComponent component, String state) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		StringBuffer script = new StringBuffer("\n");
		script.append(
				"$('" + component.getClientId(context)
						+ "').component.renderState('").append(state).append(
				"');");
		writer.write(script.toString());
	}

	/**
	 * Encodes script for label rendering in client
	 * 
	 * @param context
	 * @param component
	 * @throws IOException
	 */
	public void encodeLabelScript(FacesContext context, UIComponent component)
			throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		StringBuffer script = new StringBuffer("\n");
		script.append(
				"$('" + component.getClientId(context)
						+ "').component.renderLabel(").append(
				getMarkup(context, component)).append(",null);\n");
		writer.append(script.toString());

	}

	/**
	 * Encode initial javascript
	 * 
	 * @param context
	 * @param component
	 * @throws IOException
	 */
	public void encodeInitialScript(FacesContext context,
			UIComponent component, String state) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		UIProgressBar progressBar = (UIProgressBar) component;
		ComponentVariables variables = ComponentsVariableResolver.getVariables(
				this, component);
		StringBuffer script = new StringBuffer();
		String clientId = component.getClientId(context);
		String containerId = ((UIComponent) AjaxRendererUtils
				.findAjaxContainer(context, component)).getClientId(context);
		String mode = (String) component.getAttributes().get("mode");
		UIComponent form = AjaxRendererUtils.getNestingForm(component);
		String formId = "";
		if (form != null) {
			formId = form.getClientId(context);
		} else if ("ajax".equals(mode)) {
			// Ignore form absent. It can be rendered by forcing from any
			// component
			// throw new FaceletException("Progress bar component in ajax mode
			// should be placed inside the form");
		}
		Number minValue = getNumber(component.getAttributes().get("minValue"));
		Number maxValue = getNumber(component.getAttributes().get("maxValue"));
		Number value = (Number) variables.getVariable("value");
		StringBuffer markup = getMarkup(context, component);

		script.append("new ProgressBar('").append(clientId).append("','") // id
				.append(containerId).append("','") // containerId
				.append(formId).append("','") // formId
				.append(mode).append("',") // mode
				.append(minValue).append(",") // min value
				.append(maxValue).append(","); // max value
		script.append(getContext(component)); // context
		script.append(",");
		script.append(markup != null ? new JSLiteral(markup.toString())
				: JSReference.NULL); // markup
		script.append(",");
		script.append(ScriptUtils.toScript(buildAjaxOptions(clientId, // options
				progressBar, context)));
		String progressVar = (String) component.getAttributes().get(
				"progressVar");
		if (progressVar != null) {
			script.append(",'");
			script.append(progressVar); // progress var
			script.append("','");
		} else {
			script.append(",null,'");
		}
		script.append(state);
		script.append("',");
		script.append(value.toString());
		script.append(")\n;");
		writer.write(script.toString());
	}

	/**
	 * Creates options map for AJAX requests
	 * 
	 * @param clientId
	 * @param progressBar
	 * @param context
	 * @return
	 */
	private Map buildAjaxOptions(String clientId, UIProgressBar progressBar,
			FacesContext context) {
		Map options = AjaxRendererUtils.buildEventOptions(context, progressBar);
		Integer interval = new Integer(progressBar.getInterval());
		options.put("pollId", clientId);
		options.put("pollinterval", interval);
		if (progressBar.getAttributes().containsKey("ignoreDupResponses")) {
			options.put("ignoreDupResponses", progressBar.getAttributes().get(
					"ignoreDupResponses"));
		}
		Map parameters = (Map) options.get("parameters");
		parameters.put("percent", "percent");
		parameters.put(clientId, clientId);
		parameters.put("ajaxSingle", clientId);
		// options.put("onbeforedomupdate", getOnComplete(clientId, progressBar,
		// context));
		return options;
	}

	/**
	 * Check if component mode is AJAX
	 * 
	 * @param component
	 * @return
	 */
	private boolean isAjaxMode(UIComponent component) {
		String mode = (String) component.getAttributes().get("mode");
		return "ajax".equalsIgnoreCase(mode);
	}

	/**
	 * Encodes client mode
	 * 
	 * @param context
	 * @param component
	 * @param value
	 * @param min
	 * @param max
	 * @throws IOException
	 */
	public void encodeClientMode(FacesContext context, UIComponent component,
			Number value, Number min, Number max) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		String clientId = component.getClientId(context);
		String state = (value.doubleValue() <= min.doubleValue()) ? "initialState"
				: ((value.doubleValue() >= max.doubleValue()) ? "completeState"
						: "progressState");
		writer.startElement(HTML.DIV_ELEM, component);
		writer.writeAttribute(HTML.id_ATTRIBUTE, clientId, null);
		encodeInitialState(context, component, false);
		encodeProgressState(context, component, false);
		encodeCompletedState(context, component, false);
		startScript(writer, component);
		encodeInitialScript(context, component, state);
		encodeLabelScript(context, component);
		encodeRenderStateScript(context, component, state);
		endScript(writer, component);
		writer.endElement(HTML.DIV_ELEM);
	}

	/**
	 * Methods encodes start facet of progress bar component
	 * 
	 * @param context -
	 *            faces context
	 * @param component -
	 *            component instance
	 * @throws IOException -
	 *             IOException
	 */
	public void encodeInitialState(FacesContext context, UIComponent component,
			boolean isAjaxMode) throws IOException {
		String clientId = component.getClientId(context);
		String initialClass = (String) component.getAttributes().get(
				"initialClass");
		String style = (String) component.getAttributes().get("style");
		ResponseWriter writer = context.getResponseWriter();
		writer.startElement(HTML.DIV_ELEM, component);
		getUtils().writeAttribute(writer, HTML.class_ATTRIBUTE, initialClass);
		getUtils().writeAttribute(writer, "style",
				style + (!isAjaxMode ? "display: none" : ""));
		writer.writeAttribute(HTML.id_ATTRIBUTE, (isAjaxMode) ? clientId
				: clientId + ":initialState", null);
		UIComponent initial = component.getFacet("initial");
		if (initial != null) {
			renderChild(context, initial);
		}
		if (isAjaxMode) {
			startScript(writer, component);
			encodeInitialScript(context, component, "initialState");
			encodePollScript(context, component);
			endScript(writer, component);
		}
		writer.endElement(HTML.DIV_ELEM);

	}

	/**
	 * Encodes progress state of the component
	 * 
	 * @param context
	 * @param component
	 * @throws IOException
	 */
	public void encodeProgressState(FacesContext context,
			UIComponent component, boolean isAjaxMode) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		ComponentVariables variables = ComponentsVariableResolver.getVariables(
				this, component);
		String clientId = component.getClientId(context);
		String styleClass = (String) variables.getVariable("styleClass");
		boolean isSimple = isSimpleMarkup(component);
		String shellClass = (!isSimple) ? "rich-progress-bar-shell-dig "
				: "rich-progress-bar-shell ";
		writer.startElement("div", component);
		getUtils().writeAttribute(
				writer,
				"class",
				"rich-progress-bar-block rich-progress-bar-width " + shellClass
						+ (styleClass != null ? styleClass : ""));
		getUtils().writeAttribute(writer, HTML.id_ATTRIBUTE,
				(isAjaxMode) ? clientId : clientId + ":progressState");
		getUtils().writeAttribute(
				writer,
				"style",
				variables.getVariable("style")
						+ (!isAjaxMode ? "display: none" : ""));
		getUtils().encodePassThru(context, component);
		encodeProgressBar(context, component, isSimple);
		if (isAjaxMode) {
			startScript(writer, component);
			encodeInitialScript(context, component, "progressState");
			encodeLabelScript(context, component);
			encodePollScript(context, component);
			endScript(writer, component);
		}
		writer.endElement("div");
	}

	/**
	 * Encodes html of component
	 * 
	 * @param context -
	 *            faces context
	 * @param component -
	 *            component instance
	 * @throws IOException
	 */
	public void encodeProgressBar(FacesContext context, UIComponent component,
			boolean isSimple) throws IOException {
		ResponseWriter writer = context.getResponseWriter();
		String clientId = component.getClientId(context);
		ComponentVariables variables = ComponentsVariableResolver.getVariables(
				this, component);

		Number value = (Number) variables.getVariable("percent");

		String width = String.valueOf(value.intValue());
		String style = (String) component.getAttributes().get("style");

		String completeClass = (String) component.getAttributes().get(
				"completeClass");

		String remainClass = (String) component.getAttributes().get(
				"remainClass");

		if (!isSimple) {
			// <div class='remainClass' id='clientId:remain'
			// style='style'></div>
			writer.startElement("div", component);
			getUtils().writeAttribute(
					writer,
					"class",
					"rich-progress-bar-width rich-progress-bar-remained rich-progress-bar-padding "
							+ remainClass);
			getUtils().writeAttribute(writer, "id", clientId + ":remain");
			getUtils().writeAttribute(writer, "style", style);
			writer.endElement("div");

			// <div class='rich-progress-bar-uploaded-dig' id='clientId:upload'
			// style='style'>
			writer.startElement("div", component);
			getUtils().writeAttribute(writer, "class",
					"rich-progress-bar-uploaded-dig");
			getUtils().writeAttribute(writer, "id", clientId + ":upload");
			getUtils().writeAttribute(
					writer,
					"style",
					(style != null ? style + ";" : "") + " width: " + width
							+ "%;");

			// <div class='completeClass' id='clientId:complete'
			// style='style'></div>
			writer.startElement("div", component);
			getUtils()
					.writeAttribute(
							writer,
							"class",
							"rich-progress-bar-height-dig rich-progress-bar-width rich-progress-bar-completed rich-progress-bar-padding "
									+ completeClass);
			getUtils().writeAttribute(writer, "id", clientId + ":complete");
			getUtils().writeAttribute(writer, "style", style);
			writer.endElement("div");

			// </div>
			writer.endElement("div");
		} else {
			writer.startElement("div", component);
			getUtils().writeAttribute(
					writer,
					"class",
					"rich-progress-bar-height rich-progress-bar-uploaded "
							+ completeClass);
			getUtils().writeAttribute(writer, "id", clientId + ":upload");
			getUtils().writeAttribute(
					writer,
					"style",
					(style != null ? style + ";" : "") + " width: " + width
							+ "%;");
			writer.write(" ");
			writer.endElement("div");
		}

	}

	/**
	 * Returns parameters attr
	 * 
	 * @param component
	 * @param renderer
	 * @param percent
	 * @return
	 */
	public String getParameters(UIComponent component) {
		String parameters = (String) component.getAttributes()
				.get("parameters");
		return parameters;
	}

	/**
	 * Writes start script element
	 * 
	 * @param writer
	 * @param component
	 * @throws IOException
	 */
	private void startScript(ResponseWriter writer, UIComponent component)
			throws IOException {
		writer.startElement(HTML.SPAN_ELEM, component);
		writer.writeAttribute(HTML.style_ATTRIBUTE, "display: none;", null);
		writer.startElement(HTML.SCRIPT_ELEM, component);
		writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", null);
	}

	/**
	 * Writes end script element
	 * 
	 * @param writer
	 * @param component
	 * @throws IOException
	 */
	private void endScript(ResponseWriter writer, UIComponent component)
			throws IOException {
		writer.endElement(HTML.SCRIPT_ELEM);
		writer.endElement(HTML.SPAN_ELEM);
	}

	/**
	 * Returns context for macrosubstitution
	 * 
	 * @param component
	 * @return
	 */
	private JSLiteral getContext(UIComponent component) {
		StringBuffer buffer = new StringBuffer();
		String parameters = getParameters(component);
		JSLiteral literal = null;
		if (parameters != null) {
			buffer.append("{").append(parameters).append("}");
			literal = new JSLiteral(buffer.toString());
		} else {
			literal = new JSLiteral("null");
		}
		return literal;
	}

	/**
	 * Return true if component has children components
	 * 
	 * @param component
	 * @return
	 */
	private boolean hasChildren(UIComponent component) {
		return (component.getChildCount() != 0);
	}

	/**
	 * Returns true if markup should rendered as simple 2 divs
	 * 
	 * @param component
	 * @return
	 */
	public boolean isSimpleMarkup(UIComponent component) {
		if (hasChildren(component)) {
			return false;
		} else {
			if (component.getAttributes().get("label") != null) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Methods encodes finish facet of progress bar component
	 * 
	 * @param context -
	 *            faces context
	 * @param component -
	 *            component instance
	 * @throws IOException -
	 *             IOException
	 */
	public void encodeCompletedState(FacesContext context,
			UIComponent component, boolean isAjaxMode) throws IOException {
		String clientId = component.getClientId(context);
		String finishClass = (String) component.getAttributes().get(
				"finishClass");
		String style = (String) component.getAttributes().get("style");
		ResponseWriter writer = context.getResponseWriter();
		writer.startElement(HTML.DIV_ELEM, component);
		writer.writeAttribute(HTML.id_ATTRIBUTE, (isAjaxMode) ? clientId
				: clientId + ":completeState", null);
		getUtils().writeAttribute(writer, HTML.class_ATTRIBUTE, finishClass);
		getUtils().writeAttribute(writer, "style",
				style + (!isAjaxMode ? "display: none" : ""));

		UIComponent completed = component.getFacet("complete");
		if (completed != null) {
			renderChild(context, completed);
		}
		if (isAjaxMode) {
			startScript(writer, component);
			encodeInitialScript(context, component, "completeState");
			writer.write(getStopPollScript(clientId).toString());
			endScript(writer, component);
		}
		writer.endElement(HTML.DIV_ELEM);

	}

	/**
	 * Converts value attr to number value
	 * 
	 * @param v -
	 *            value attr
	 * @return result
	 */
	public Number getNumber(Object v) {
		Number result = null;
		if (v != null) {
			try {
				if (v instanceof String) {
					result = Double.parseDouble((String) v);
				} else {
					Number n = (Number) v;
					if (n instanceof BigDecimal || n instanceof Double
							|| n instanceof Float) {
						result = n.floatValue();
					} else if (n instanceof Integer || n instanceof Long) {
						result = n.longValue();
					}
				}
			} catch (Exception e) {
				// no action
			}
			return result;
		}
		return new Integer(0);
	}

	/**
	 * Calculates percent value according to min & max value
	 * 
	 * @param value
	 * @param minValue
	 * @param maxValue
	 * @return
	 */
	public Number calculatePercent(Number value, Number minValue,
			Number maxValue) {
		if (minValue.doubleValue() < value.doubleValue()
				&& value.doubleValue() < maxValue.doubleValue()) {
			return (Number) ((value.doubleValue() - minValue.doubleValue()) * 100.0 / (maxValue
					.doubleValue() - minValue.doubleValue()));
		} else if (value.doubleValue() <= minValue.doubleValue()) {
			return 0;
		} else if (value.doubleValue() >= maxValue.doubleValue()) {
			return 100;
		} 
		return 0;
	}

	/**
	 * Returns JS script to stop polling
	 * 
	 * @param clientId
	 * @return
	 */
	private StringBuffer getStopPollScript(String clientId) {
		StringBuffer buffer = new StringBuffer();
		buffer.append("A4J.AJAX.StopPoll('").append(clientId).append("');\n");
		return buffer;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.renderkit.RendererBase#getComponentClass()
	 */
	protected Class getComponentClass() {
		// only poll component is allowed.
		return UIProgressBar.class;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.richfaces.renderkit.TemplateEncoderRendererBase#encodeChildren(javax.faces.context.FacesContext,
	 *      javax.faces.component.UIComponent)
	 */
	@Override
	public void encodeChildren(FacesContext context, UIComponent component)
			throws IOException {
		; // We should not render children
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.renderkit.RendererBase#doEncodeChildren(javax.faces.context.ResponseWriter,
	 *      javax.faces.context.FacesContext, javax.faces.component.UIComponent)
	 */
	@Override
	public void doEncodeChildren(ResponseWriter writer, FacesContext context,
			UIComponent component) throws IOException {
		; // We should not render children
	}

}
