package org.richfaces.renderkit;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.el.ValueExpression;
import javax.faces.FactoryFinder;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.ajax4jsf.Filter;
import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.context.AjaxContextImpl;
import org.ajax4jsf.event.AjaxEvent;
import org.ajax4jsf.exception.FileUploadException;
import org.ajax4jsf.javascript.JSFunction;
import org.ajax4jsf.javascript.JSFunctionDefinition;
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;
import org.ajax4jsf.request.MultipartRequest;
import org.ajax4jsf.resource.CountingOutputWriter;
import org.richfaces.component.UIFileUpload;
import org.richfaces.component.UIProgressBar;
import org.richfaces.component.util.HtmlUtil;
import org.richfaces.event.UploadEvent;
import org.richfaces.model.UploadItem;

/**
 * Class provides base renderer for upload file component
 * 
 * @author "Andrey Markavtsov"
 * 
 */
public abstract class FileUploadRendererBase extends
		TemplateEncoderRendererBase {

	/** Attribute name where collection of files uploaded will be stored */
	private static final String _FILES_UPLOADED_ATTRIBUTE_NAME = "uploadData";

	/** Multipart request class name */
	private static final String _MULTIPART_REQUEST_CLASS_NAME = MultipartRequest.class
			.getName();

	/** File upload bundle name */
	private static final String FILE_UPLOAD_BUNDLE = "org.richfaces.renderkit.fileUpload";

	/** Bundle prefix */
	private static final String bundlePrefix = "RICH_FILE_UPLOAD_";

	/** Bundle postfix */
	private static final String bundlePostfix = "_LABEL";

	/** Set of bundles that can be defined */
	private static final String[] bundlesLables = { "add", "upload", "stop",
			"clear_all", "entry_cancel", "entry_clear", "entry_stop", "done",
			"size_error", "transfer_error","progress" };

	/** Default labels values */
	private static final String[] defaultLables = { "Add...", "<b>Upload</b>",
			"<b>Stop</b>", "Clear All", "Cancel", "Clear", "Stop",
			"<b>Done</b>", "File size restricted", "Transfer error occuried","uploading"};

	/** Set of attributes that can define label's value */
	private static final String[] labelAttribues = { "addControlLabel",
			"uploadControlLabel", "stopControlLabel", "clearAllControlLabel",
			"cancelEntryControlLabel", "clearControlLabel",
			"stopEntryControlLabel", null, null, null, null };

	/**
	 * Overrides standard JSF component method.
	 * 
	 * @param context
	 *            faces context
	 * @param component
	 *            file upload component
	 */
	@Override
	@SuppressWarnings("unchecked")
	protected void doDecode(FacesContext context, UIComponent component) {
		UIFileUpload fileUpload = (UIFileUpload) component;
		ServletRequest request = (ServletRequest) context.getExternalContext()
				.getRequest();

		String clientId = component.getClientId(context);

		new AjaxEvent(component).queue();

		Class requestClazz = request.getClass();

		if (_MULTIPART_REQUEST_CLASS_NAME.equals(requestClazz.getName())
				|| "org.jboss.seam.web.MultipartRequest".equals(requestClazz
						.getName())) {

			HttpSession session = (HttpSession) context.getExternalContext()
					.getSession(false);
			String uid = request.getParameter(Filter.UPLOAD_FILES_ID);
			String id = request.getParameter("id");
			decreaseFileCounter(session, id);
			Map<String, MultipartRequest> map = (Map<String, MultipartRequest>) session
					.getAttribute(Filter.REQUESTS_SESSIONS_BEAN_NAME);

			MultipartRequest multipartRequest = map.get(uid);

			clientId = clientId + ":file";
			String fileName = multipartRequest.getFileName(clientId);
			Object file = multipartRequest.getFile(clientId);
			fileUpload.setLocalFileName(fileName);
			onUploadComplete(context, file, fileUpload);
		}

	}

	/**
	 * Put max file count into session scope
	 * 
	 * @param context
	 * @param component
	 * @param clientId
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public Integer initMaxFilesCount(FacesContext context,
			UIComponent component, String clientId) {
		Integer max = (Integer) component.getAttributes().get(
				"maxFilesQuantity");
		HttpSession session = (HttpSession) context.getExternalContext()
				.getSession(false);
		synchronized (session) {
			Map<String, Integer> map = (Map<String, Integer>) session
					.getAttribute(Filter.UPLOADED_COUNTER);
			if (map == null) {
				map = Collections
						.synchronizedMap(new HashMap<String, Integer>());
			}
			map.put(clientId, max);
		}
		return max;
	}

	/**
	 * Gets form id
	 * 
	 * @param context -
	 *            faces context
	 * @param component -
	 *            component
	 * @return String form id
	 */
	public String getFormId(FacesContext context, UIComponent component) {
		UIComponent form = AjaxRendererUtils.getNestingForm(component);
		if (form != null) {
			return form.getClientId(context);
		}
		return "";
	}

	/**
	 * Gets container id
	 * 
	 * @param context -
	 *            faces context
	 * @param component -
	 *            component
	 * @return String container id
	 */
	public String getContainerId(FacesContext context, UIComponent component) {
		UIComponent container = (UIComponent) AjaxRendererUtils
				.findAjaxContainer(context, component);
		if (container != null) {
			return container.getClientId(context);
		}
		return null;
	}

	/**
	 * Generates map with internalized labels to be put into JS
	 * 
	 * @param o
	 *            map of labels
	 * @return javascript hash map
	 */
	public Object _getLabels(Object o) {
		return ScriptUtils.toScript(o);
	}

	/**
	 * Gets internalized labels. At the first system is looking for them in
	 * appropriate attributes if they are defined. Next search place is
	 * application and file upload bundles. If no result - default label value
	 * will be set up.
	 * 
	 * @param context
	 *            facesContext instance
	 * @param component
	 *            UIComponent
	 * @return map of labels
	 */
	public Map<String, String> getLabels(FacesContext context,
			UIComponent component) {
		Map<String, String> labelsMap = new HashMap<String, String>();
		ResourceBundle bundle1 = null;
		ResourceBundle bundle2 = null;
		UIFileUpload fileUpload = (UIFileUpload) component;
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		String messageBundle = context.getApplication().getMessageBundle();
		Object locale = fileUpload.getLocale();
		if (locale == null) {
			HttpServletRequest request = (HttpServletRequest) context
					.getExternalContext().getRequest();
			locale = request.getLocale();
		}
		if (null != messageBundle) {
			bundle1 = ResourceBundle.getBundle(messageBundle, fileUpload
					.getAsLocale(locale), loader);
		}
		try {
			bundle2 = ResourceBundle.getBundle(FILE_UPLOAD_BUNDLE, fileUpload
					.getAsLocale(locale), loader);

		} catch (MissingResourceException e) {
			// No external bundle was found, ignore this exception.
		}
		initLabels(labelsMap, bundle1, bundle2, fileUpload);
		return labelsMap;
	}

	/**
	 * Sets values for labels.
	 * 
	 * @param map
	 *            map of labels
	 * @param bundle1
	 *            application bundle
	 * @param bundle2
	 *            file upload bundle
	 * @param fileUpload
	 */
	private void initLabels(Map<String, String> map, ResourceBundle bundle1,
			ResourceBundle bundle2, UIFileUpload fileUpload) {
		int i = 0;
		for (String name : bundlesLables) {
			boolean found = false;
			if (labelAttribues[i] != null) {
				String attributeName = labelAttribues[i];
				if (fileUpload.getAttributes().get(attributeName) != null) {
					map.put(name, (String) fileUpload.getAttributes().get(
							attributeName));
					found = true;
				}
			}
			if (!found && (bundle1 != null || bundle2 != null)) {
				String label = getFromBundle(name, bundle1, bundle2);
				if (label != null) {
					map.put(name, getFromBundle(name, bundle1, bundle2));
					found = true;
				}
			}
			if (!found) {
				map.put(name, defaultLables[i]);
			}
			i++;
		}
	}

	/**
	 * Looking for labels name in bundles provided.
	 * 
	 * @param bundle1
	 *            application bundle
	 * @param bundle2
	 *            file upload bundle
	 * @return String label value
	 */
	private String getFromBundle(String name, ResourceBundle bundle1,
			ResourceBundle bundle2) {
		name = bundlePrefix + name.toUpperCase() + bundlePostfix;
		String v = null;
		if (bundle1 != null) {
			try {
				v = bundle1.getString(name);
			} catch (Exception e) {
				if (bundle2 != null) {
					try {
						v = bundle2.getString(name);
					} catch (Exception ex) {
						// no value has been found
					}
				}

			}

		} else if (bundle2 != null) {
			try {
				v = bundle2.getString(name);
				ByteArrayOutputStream b = new ByteArrayOutputStream();
				b.write(v.getBytes());
				b.toString("UTF-8");
			} catch (Exception e) {
				// no value has been found
			}
		}
		return v;
	}

	/**
	 * Method decrease file counter
	 * 
	 * @param session
	 * @param id
	 */
	@SuppressWarnings("unchecked")
	private void decreaseFileCounter(HttpSession session, String id) {
		Map<String, Integer> map = (Map<String, Integer>) session
				.getAttribute(Filter.UPLOADED_COUNTER);
		if (map != null) {
			Integer i = map.get(id);
			if (i == null) {
				i = 0;
			} else {
				i--;
			}
			map.put(id, i);
		}
	}

	/**
	 * Put uploaded file into data attribute defined
	 * 
	 * @param context
	 * @param fileUpload
	 * @param file
	 */
	@SuppressWarnings("unchecked")
	private void storeData(FacesContext context, UIFileUpload fileUpload,
			Object file) {
		ValueExpression data = fileUpload
				.getValueExpression(_FILES_UPLOADED_ATTRIBUTE_NAME);
		UploadItem item = new UploadItem(fileUpload.getLocalFileName(),file);
		if (data != null) {
			if (data.getValue(context.getELContext()) instanceof Collection) {
				Collection collection = (Collection) data.getValue(context
						.getELContext());
				collection.add(item);
			}
		}
		new UploadEvent(fileUpload, item).queue();
	}

	/**
	 * Call with method after uploading completed
	 * 
	 * @param context
	 * @param file
	 * @param fileUpload
	 */
	private void onUploadComplete(FacesContext context, Object file,
			UIFileUpload fileUpload) {
		storeData(context, fileUpload, file);
		try {
			AjaxContext ajaxContext = AjaxContextImpl
					.getCurrentInstance(context);
			ajaxContext.setAjaxRequest(true);
		} catch (Exception e) {
			e.getMessage();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.renderkit.RendererBase#getComponentClass()
	 */
	@Override
	protected Class<? extends UIComponent> getComponentClass() {
		return UIFileUpload.class;
	}

	/**
	 * Generates common JS script by action value
	 * 
	 * @param context
	 * @param component
	 * @param action
	 * @param oncomplete
	 * @return
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	private String getActionScript(FacesContext context, UIComponent component,
			String action, Object oncomplete) throws IOException {
		ComponentVariables variables = ComponentsVariableResolver.getVariables(
				this, component);
		String clientId = component.getClientId(context);
		String containerId = (String) variables.getVariable("containerId");
		JSFunction ajaxFunction = new JSFunction(
				AjaxRendererUtils.AJAX_FUNCTION_NAME);
		ajaxFunction.addParameter(containerId);
		ajaxFunction.addParameter(new JSReference("formId"));
		ajaxFunction.addParameter(new JSReference("event"));
		// AjaxRendererUtils.buildAjaxFunction(
		// component, context);

		Map options = AjaxRendererUtils.buildEventOptions(context, component);
		Map parameters = (Map) options.get("parameters");
		parameters.put("action", action);
		parameters.put(Filter.UPLOAD_FILES_ID, new JSReference("uid"));
		parameters.put(clientId, clientId);
		if (oncomplete != null) {
			options.put("onbeforedomupdate", oncomplete);
		}
		ajaxFunction.addParameter(options);

		JSFunctionDefinition function = new JSFunctionDefinition("uid");
		function.addParameter("formId");
		function.addParameter("event");
		function.addToBody(ajaxFunction.toScript());

		return function.toScript();
	}

	/**
	 * Return accepted types map
	 * 
	 * @param context
	 * @param component
	 * @return
	 */
	public Object getAcceptedTypes(FacesContext context, UIComponent component) {
		String acceptedTypes = (String) component.getAttributes().get(
				"acceptedTypes");
		if (acceptedTypes != null && acceptedTypes.length() > 0) {
			Map<String, Boolean> accepted = new HashMap<String, Boolean>();
			acceptedTypes = acceptedTypes.replaceAll("[\\s]", "");
			String[] types = acceptedTypes.split("[,;|]");
			if (types != null) {
				for (String type : types) {
					accepted.put(type.toLowerCase(), true);
				}
				return ScriptUtils.toScript(accepted);
			}
		}
		return JSReference.NULL;
	}

	/**
	 * Generates JS script for stopping uploading process
	 * 
	 * @param context
	 * @param component
	 * @return
	 * @throws IOException
	 */
	public String getStopScript(FacesContext context, UIComponent component)
			throws IOException {
		return getActionScript(context, component, "stop", null);
	}

	/**
	 * Generates JS script for getting file size from server
	 * 
	 * @param context
	 * @param component
	 * @return
	 * @throws IOException
	 */
	public String getFileSizeScript(FacesContext context, UIComponent component)
			throws IOException {
		JSFunctionDefinition oncomplete = new JSFunctionDefinition();
		oncomplete.addParameter("request");
		oncomplete.addParameter("event");
		oncomplete.addParameter("data");
		StringBuffer body = new StringBuffer("$('");
		body.append(component.getClientId(context));
		body.append("').component.getFileSize(data);");
		oncomplete.addToBody(body);
		return getActionScript(context, component, "progress", oncomplete);

	}

	/**
	 * Generates progress label markup
	 * 
	 * @param context
	 * @param component
	 * @return
	 * @throws IOException
	 */
	public Object getLabelMarkup(FacesContext context, UIComponent component)
			throws IOException {
		CountingOutputWriter customWriter = new CountingOutputWriter();
		StringBuffer result = null;
		UIComponent label = component.getFacet("label");
		try {
			if (label != null) {

				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, label, false);
				if (writer != null) {
					context.setResponseWriter(writer);
				}
				result = customWriter.getContent();
			}

		} catch (Exception e) {
			e.getMessage();
		}
		return (result != null) ? new JSLiteral(result.toString())
				: JSReference.NULL;
	}

	/**
	 * Gets a string representing css specific height of downloaded file list
	 * panel.
	 * 
	 * @param component
	 *            file upload component
	 * @return a string representing css specific height of downloaded file list
	 *         panel
	 */
	public String getFileListHeight(UIFileUpload component) {
		return HtmlUtil.qualifySize(component.getListHeight());
	}

	/**
	 * Gets a string representing css specific width of downloaded file list
	 * panel.
	 * 
	 * @param component
	 *            file upload component
	 * @return a string representing css specific width of downloaded file list
	 *         panel
	 */
	public String getFileListWidth(UIFileUpload component) {
		return HtmlUtil.qualifySize(component.getListWidth());
	}

	/**
	 * Generate component custom events functions
	 * 
	 * @param context
	 * @param component
	 * @param attributeName
	 * @return
	 */
	public String getAsEventHandler(FacesContext context,
			UIComponent component, String attributeName) {
		Object eventHandler = RendererUtils.getInstance().getAsEventHandler(
				context, component, attributeName, "");
		if (eventHandler != null) {
			return eventHandler.toString();
		}
		return JSReference.NULL.toScript();
	}

	/**
	 * Gets progress bar Id
	 * 
	 * @param context
	 * @param component
	 * @return
	 * @throws IOException
	 */
	public String getProgressBarId(FacesContext context, UIComponent component)
			throws IOException {
		return getProgressBar(context, component).getClientId(context);
	}

	/**
	 * Renders progress bar
	 * 
	 * @param context
	 * @param component
	 * @throws IOException
	 */
	public void renderProgress(FacesContext context, UIComponent component)
			throws IOException {
		UIComponent progressBar = getProgressBar(context, component);
		renderChild(context, progressBar);
	}

	/**
	 * Creates progress bar component
	 * 
	 * @param context
	 * @param fileUpload
	 * @return
	 */
	private UIComponent createProgressBar(FacesContext context,
			UIComponent fileUpload) {
		UIComponent progressBar = fileUpload.getFacet("progress");
		if (null == progressBar) {
			progressBar = context.getApplication().createComponent(
					UIProgressBar.COMPONENT_TYPE);
		}
		fileUpload.getFacets().put("progress", progressBar);
		return progressBar;
	}

	/**
	 * Returns progress bar
	 * 
	 * @param context
	 * @param component
	 * @return
	 */
	public UIComponent getProgressBar(FacesContext context,
			UIComponent component) {
		UIComponent progressBar = component.getFacet("progress");
		if (null == progressBar) {
			progressBar = createProgressBar(context, component);
		}
		progressBar.getAttributes().put("minValue", -1);
		progressBar.getAttributes().put("enabled", false);
		progressBar.setTransient(false);
		return progressBar;
	}

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

}
