/**
 * 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
 */
package org.richfaces.component;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.component.UIComponent;
import javax.faces.component.UIMessages;
import javax.faces.context.FacesContext;

import org.ajax4jsf.component.AjaxOutput;
import org.ajax4jsf.renderkit.RendererUtils;

/**
 * @author Anton Belevich
 * 
 */
public abstract class UIRichMessages extends UIMessages implements AjaxOutput {

	public static final String COMPONENT_TYPE = "org.richfaces.component.RichMessages";

	private String forVal = null;
	private boolean updated = false;

	private List<FacesMessageWithId> renderedMessages;

	/**
	 * <p>
	 * Return the client identifier of the component for which this component
	 * represents associated message(s) (if any).
	 * </p>
	 */
	public String getFor() {

		if (this.forVal != null) {
			return (this.forVal);
		}
		ValueExpression ve = getValueExpression("for");
		if (ve != null) {
			try {
				return ((String) ve.getValue(getFacesContext().getELContext()));
			} catch (ELException e) {
				throw new FacesException(e);
			}
		} else {
			return (null);
		}

	}

	/**
	 * <p>
	 * Set the client identifier of the component for which this component
	 * represents associated message(s) (if any). This property must be set
	 * before the message is displayed.
	 * </p>
	 * 
	 * @param newFor
	 *            The new client id
	 */
	public void setFor(String newFor) {

		forVal = newFor;

	}

	public boolean isUpdated() {
		return updated;
	}

	public boolean isAjaxRendered() {
		return true;
	}

	public Iterator<FacesMessage> getMessages(FacesContext context) {
		String forInputId = getFor();
		if (null == this.renderedMessages || null != forInputId) {
			List<String> severenities = getSeverenities();
			renderedMessages = new ArrayList<FacesMessageWithId>();
			if (isGlobalOnly()) {
				addMessagesForId(context, null, severenities);
			} else if (null != forInputId) {
				UIComponent componentFor = RendererUtils.getInstance()
						.findComponentFor(context, this, forInputId);
				if (null != componentFor) {
					addMessagesForId(context,
							componentFor.getClientId(context), severenities);
				}
			} else {
				Iterator<String> clientIdsWithMessages = context
						.getClientIdsWithMessages();
				while (clientIdsWithMessages.hasNext()) {
					addMessagesForId(context, clientIdsWithMessages.next(),
							severenities);
				}
			}
		}
		;
		final Iterator<FacesMessageWithId> iterator = this.renderedMessages
				.iterator();
		return new Iterator<FacesMessage>() {

			public boolean hasNext() {
				return iterator.hasNext();
			}

			public FacesMessage next() {
				return iterator.next().getMessage();
			}

			public void remove() {
			}
		};
	}

	public void updateMessages(FacesContext context, String forId) {
		updated = true;
		if (null == renderedMessages) {
			renderedMessages = new ArrayList<FacesMessageWithId>();
		} else {
			Iterator<FacesMessageWithId> renderedMessageIterator = renderedMessages
					.iterator();
			while (renderedMessageIterator.hasNext()) {
				String clientId = renderedMessageIterator.next().getClientId();
				if ((null == forId && null == clientId)
						|| (null != forId && forId.equals(clientId))) {
					renderedMessageIterator.remove();
				}
			}
		}
		addMessagesForId(context, forId, getSeverenities());
	}

	/**
	 * @param context
	 * @param forId
	 * @param severenities
	 *            TODO
	 */
	protected void addMessagesForId(FacesContext context, String forId,
			List<String> severenities) {
		Iterator<FacesMessage> messages = context.getMessages(forId);
		while (messages.hasNext()) {
			FacesMessage message = messages.next();
			if (severenities.size() == 0 || severenities.contains("ALL")
					|| isApplicableMessage(severenities, message)) {
				renderedMessages.add(new FacesMessageWithId(forId, message));
			}
		}
	}

	/**
	 * @param severenities
	 * @param message
	 * @return
	 */
	protected boolean isApplicableMessage(List<String> severenities,
			FacesMessage message) {
		if (severenities.size() == 0 || severenities.contains("ALL")) {
			return true;
		}
		Severity severity = message.getSeverity();
		for (Object key : FacesMessage.VALUES_MAP.keySet()) {
			Severity sev = (Severity) FacesMessage.VALUES_MAP.get(key);
			if (0 == sev.compareTo(severity)) {
				return severenities.contains(key);
			}
		}
		return false;
	}

	public void setAjaxRendered(boolean ajaxRendered) {
		if (!ajaxRendered) {
			new IllegalArgumentException();
		}
	}

	@Override
	public void encodeBegin(FacesContext context) throws IOException {
		if (!isUpdated()) {
			setRenderedMessages(null);
		}
		super.encodeBegin(context);
	}

	public abstract String getLevel();

	public abstract void setLevel(String level);

	public List<String> getSeverenities() {
		String level = getLevel();
		List<String> severenities;
		if (null != level) {
			String[] levels = level.split(",");
			severenities = new ArrayList<String>(levels.length);
			for (int i = 0; i < levels.length; i++) {
				String levelName = levels[i].toUpperCase().trim();
				severenities.add(levelName);
			}
		} else {
			severenities = Collections.emptyList();
		}
		return severenities;
	}

	private Object[] values;

	public Object saveState(FacesContext context) {

		if (values == null) {
			values = new Object[3];
		}

		values[0] = super.saveState(context);
		values[1] = this.forVal;
		values[2] = saveAttachedState(context, getRenderedMessages());
		return (values);

	}

	@SuppressWarnings("unchecked")
	public void restoreState(FacesContext context, Object state) {

		values = (Object[]) state;
		super.restoreState(context, values[0]);
		forVal = (String) values[1];
		setRenderedMessages((List<FacesMessageWithId>) restoreAttachedState(
				context, values[2]));
	}

	/**
	 * @author asmirnov
	 * 
	 */
	@SuppressWarnings("serial")
	public static class FacesMessageWithId implements Serializable {
		private FacesMessage message;

		private String clientId;

		/**
		 * @param clientId
		 * @param message
		 */
		public FacesMessageWithId(String clientId, FacesMessage message) {
			this.message = message;
			this.clientId = clientId;
		}

		/**
		 * @return the message
		 */
		public FacesMessage getMessage() {
			return message;
		}

		/**
		 * @return the clientId
		 */
		public String getClientId() {
			return clientId;
		}

		/**
		 * <p>
		 * Persist {@link FacesMessageWithId} artifacts,
		 * including the non serializable <code>Severity</code>.
		 * </p>
		 */
		private void writeObject(ObjectOutputStream out) throws IOException {
			if (clientId != null) {
				out.writeBoolean(true);
				out.writeUTF(clientId);
			} else {
				out.writeBoolean(false);
			}

			out.writeInt(message.getSeverity().getOrdinal());
			
			String summary = message.getSummary();
			if (summary != null) {
				out.writeBoolean(true);
				out.writeUTF(summary);
			} else {
				out.writeBoolean(false);
			}
			
			String detail = message.getDetail();
			if (detail != null) {
				out.writeBoolean(true);
				out.writeUTF(detail);
			} else {
				out.writeBoolean(false);
			}
		}

		/**
		 * <p>
		 * Reconstruct {@link FacesMessageWithId} from
		 * serialized artifacts.
		 * </p>
		 */
		private void readObject(ObjectInputStream in) throws IOException,
				ClassNotFoundException {
			
			if (in.readBoolean()) {
				clientId = in.readUTF();
			}

			Severity severity = FacesMessage.SEVERITY_INFO;
			int ordinal = in.readInt();
			if (ordinal >= 0 && ordinal < FacesMessage.VALUES.size()) {
				severity = (Severity) FacesMessage.VALUES.get(ordinal);
			}
			
			String summary = null;
			if (in.readBoolean()) {
				summary = in.readUTF();
			}
			
			String detail = null;
			if (in.readBoolean()) {
				detail = in.readUTF();
			}
			
			message = new FacesMessage(severity, summary, detail);
		}

	}

	/**
	 * @return the renderedMessages
	 */
	protected List<FacesMessageWithId> getRenderedMessages() {
		return renderedMessages;
	}

	/**
	 * @param renderedMessages
	 *            the renderedMessages to set
	 */
	protected void setRenderedMessages(List<FacesMessageWithId> renderedMessages) {
		this.renderedMessages = renderedMessages;
	}

}
