/**
 * License Agreement.
 *
 *  JBoss RichFaces - Ajax4jsf Component Library
 *
 * 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
 */

package org.richfaces.renderkit;

import java.io.IOException;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.richfaces.component.Draggable;
import org.richfaces.component.Dropzone;
import org.richfaces.component.UIDndParam;
import org.richfaces.component.nsutils.NSUtils;
import org.richfaces.component.util.MessageUtil;
import org.richfaces.json.JSONException;
import org.richfaces.json.JSONObject;

/**
 * @author Nick Belaevski - nbelaevski@exadel.com
 * created 30.01.2007
 * 
 */
public class DnDParametersEncoder implements AttributeParametersEncoder {

	private static DnDParametersEncoder instance = null;

	private static final String DEFAULT = "default";
	private static final String DRAG = "drag";
	private static final String DROP = "drop";

	private DnDParametersEncoder() {

	}

	public static synchronized DnDParametersEncoder getInstance() {
		if (instance == null) {
			instance = new DnDParametersEncoder();
		}

		return instance;
	}

	private static final String MESSAGE_CAST = "DnD parameter [{0}] of type [{1}] applied to component [{2}] has been ignored because component doesn''t implement [{3}]!"; 
	private static final String MESSAGE_UNK_TYPE = "DnD parameter [{0}] is of unknown type [{1}]!"; 

	/* (non-Javadoc)
	 * @see org.richfaces.renderkit.AttributeParameterEncoder#doEncode(javax.faces.context.FacesContext, javax.faces.component.UIComponent)
	 */
	public void doEncode(FacesContext context, UIComponent component)
	throws IOException {

		Map params = doEncodeAsMap(context, component);
		ResponseWriter responseWriter = context.getResponseWriter();

		Set entrySet = params.entrySet();
		for (Iterator iterator = entrySet.iterator(); iterator.hasNext();) {
			Map.Entry entry = (Map.Entry) iterator.next();

			String name = (String) entry.getKey();
			JSONObject dndParams = (JSONObject) entry.getValue();

			if (dndParams != null) {
				responseWriter.writeAttribute(NSUtils.XMLNS_PREFIX + ":" + name + "dndparams", 
						dndParams.toString(), null);
			}
		}
	}


	private void renderChildren(FacesContext facesContext,
			UIComponent component) throws IOException {
		if (component.getChildCount() > 0) {
			for (Iterator it = component.getChildren().iterator(); it.hasNext();) {
				UIComponent child = (UIComponent) it.next();
				renderChild(facesContext, child);
			}
		}
	}

	private void renderChild(FacesContext facesContext, UIComponent child)
	throws IOException {
		if (!child.isRendered()) {
			return;
		}

		child.encodeBegin(facesContext);
		if (child.getRendersChildren()) {
			child.encodeChildren(facesContext);
		} else {
			renderChildren(facesContext, child);
		}
		child.encodeEnd(facesContext);
	}

	public String doEncodeAsString(FacesContext context, UIComponent component)
	throws IOException {

		JSONObject merged = new JSONObject();

		try {

			Map map = doEncodeAsMap(context, component);
			for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
				Map.Entry entry = (Map.Entry) iterator.next();

				if (entry.getValue() != null) {
					JSONObject object = (JSONObject) entry.getValue();

					for (Iterator keys = object.keys(); keys.hasNext(); ) {
						String key = (String) keys.next();

						merged.put(key, object.get(key));
					}
				}
			}
		} catch (JSONException e) {
			IOException exception = new IOException(e.getMessage());
			exception.initCause(e.getCause());
			throw exception;
		}

		return merged.toString();
	}

	protected Map doEncodeAsMap(FacesContext context, UIComponent component) throws IOException {
		Map params = new LinkedHashMap();
		params.put(DEFAULT, new JSONObject());

		boolean isDraggable = component instanceof Draggable;
		if (isDraggable) {
			params.put(DRAG, new JSONObject());
		}

		boolean isDropzone = component instanceof Dropzone;
		if (isDropzone) {
			params.put(DROP, new JSONObject());
		}

		try {
			if (component.getChildCount() != 0) {
				List children = component.getChildren();

				for (Iterator iterator = children.iterator(); iterator.hasNext();) {
					Object object = (Object) iterator.next();

					if (object instanceof UIDndParam) {
						UIDndParam dndParam = (UIDndParam) object;
						String type = dndParam.getType();
						JSONObject dndParams = null;

						if (DRAG.equals(type)) {
							if (isDraggable) {
								dndParams = (JSONObject) params.get(DRAG);
							} else {
								String messageString = MessageFormat.format(MESSAGE_CAST, new Object[] {
										MessageUtil.getLabel(context, dndParam),
										type,
										MessageUtil.getLabel(context, component),
										Draggable.class.getSimpleName()
								});

								FacesMessage message = new FacesMessage(messageString, messageString);
								context.addMessage(component.getClientId(context), message);
							}
						} else if (DROP.equals(type)) {
							if (isDropzone) {
								dndParams = (JSONObject) params.get(DROP);
							} else {
								String messageString = MessageFormat.format(MESSAGE_CAST, new Object[] {
										MessageUtil.getLabel(context, dndParam),
										type,
										MessageUtil.getLabel(context, component),
										Dropzone.class.getSimpleName()
								});

								FacesMessage message = new FacesMessage(messageString, messageString);
								context.addMessage(component.getClientId(context), message);
							}
						} else if (type == null || type.length() == 0 || DEFAULT.equals(type)) {
							dndParams = (JSONObject) params.get(DEFAULT);
						} else {
							String messageString = MessageFormat.format(MESSAGE_UNK_TYPE, new Object[] {
									MessageUtil.getLabel(context, dndParam),
									type
							});

							throw new IllegalArgumentException(messageString);
						}

						if (dndParams != null) {
							if (dndParam.isRendered()) {

								ResponseWriter responseWriter = context.getResponseWriter();
								StringWriter dumpWriter = new StringWriter();

								try {
									context.setResponseWriter(responseWriter.cloneWithWriter(dumpWriter));

									if (dndParam.getChildCount() == 0) {
										context.getResponseWriter().writeText(dndParam.getValue(), null);
									} else {
										List paramChildren = dndParam.getChildren();

										for (Iterator paramIterator = paramChildren
												.iterator(); paramIterator.hasNext();) {

											UIComponent paramChild = (UIComponent) paramIterator.next();
											renderChild(context, paramChild);

										}	
									}

									context.getResponseWriter().flush();
								} finally {
									context.setResponseWriter(responseWriter);
								}

								String childContent = dumpWriter.getBuffer().toString();
								dndParams.put(dndParam.getName(), childContent);
							}
						}
					}
				}
			}

			Set entrySet = params.entrySet();
			for (Iterator iterator = entrySet.iterator(); iterator.hasNext();) {
				Map.Entry entry = (Map.Entry) iterator.next();

				JSONObject dndParams = (JSONObject) entry.getValue();

				if (dndParams.length() == 0) {
					entry.setValue(null);
				}
			}
		} catch (JSONException e) {
			IOException exception = new IOException(e.getMessage());
			exception.initCause(e.getCause());
			throw exception;
		}

		return params;
	}

}
