/**
 * 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.renderkit.html;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.component.UIParameter;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.ActionEvent;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.event.AjaxEvent;
import org.ajax4jsf.javascript.JSFunction;
import org.ajax4jsf.javascript.JSFunctionDefinition;
import org.ajax4jsf.javascript.JSReference;
import org.ajax4jsf.javascript.ScriptUtils;
import org.ajax4jsf.renderkit.AjaxComponentRendererBase;
import org.ajax4jsf.renderkit.AjaxRendererUtils;
import org.ajax4jsf.resource.InternetResource;
import org.richfaces.component.UIToolTip;
import org.richfaces.skin.Skin;

public class ToolTipRenderer extends AjaxComponentRendererBase {
    
    private static final String AJAX_MODE = "ajax";

    private final InternetResource[] styles = { getResource("/org/richfaces/renderkit/html/css/tooltip.xcss") };
    
    private InternetResource[] stylesAll = null;
    
    protected InternetResource[] getStyles() {
        synchronized (this) {
            if (stylesAll == null) {
                InternetResource[] rsrcs = super.getStyles();
                boolean ignoreSuper = rsrcs == null || rsrcs.length == 0;
                boolean ignoreThis = styles == null || styles.length == 0;
                
                if (ignoreSuper) {
                    if (ignoreThis) {
                        stylesAll = new InternetResource[0];
                    } else {
                        stylesAll = styles;
                    }
                } else {
                    if (ignoreThis) {
                        stylesAll = rsrcs;
                    } else {
                        Set<InternetResource> rsrcsSet = new LinkedHashSet<InternetResource>();
                        
                        for (int i = 0; i < rsrcs.length; i++) {
                            rsrcsSet.add(rsrcs[i]);
                        }
                        
                        for (int i = 0; i < styles.length; i++) {
                            rsrcsSet.add(styles[i]);
                        }
                        
                        stylesAll = rsrcsSet.toArray(new InternetResource[rsrcsSet.size()]);
                    }
                }
            }
        }
        
        return stylesAll;
    }
    
    private final InternetResource[] scripts = {
            new org.ajax4jsf.javascript.PrototypeScript(),
            new org.ajax4jsf.javascript.AjaxScript(),
            getResource("/org/richfaces/renderkit/html/scripts/jquery/jquery.js"),
            getResource("/org/richfaces/renderkit/html/scripts/utils.js"),
            getResource("/org/richfaces/renderkit/html/scripts/tooltip.js") };
    
    private InternetResource[] scriptsAll = null;
    
    protected InternetResource[] getScripts() {
        synchronized (this) {
            if (scriptsAll == null) {
                InternetResource[] rsrcs = super.getScripts();
                boolean ignoreSuper = rsrcs == null || rsrcs.length == 0;
                boolean ignoreThis = scripts == null || scripts.length == 0;
                
                if (ignoreSuper) {
                    if (ignoreThis) {
                        scriptsAll = new InternetResource[0];
                    } else {
                        scriptsAll = scripts;
                    }
                } else {
                    if (ignoreThis) {
                        scriptsAll = rsrcs;
                    } else {
                        Set<InternetResource> rsrcsSet = new LinkedHashSet<InternetResource>();
                        
                        for (int i = 0; i < rsrcs.length; i++) {
                            rsrcsSet.add(rsrcs[i]);
                        }
                        
                        for (int i = 0; i < scripts.length; i++) {
                            rsrcsSet.add(scripts[i]);
                        }
                        
                        scriptsAll = rsrcsSet.toArray(new InternetResource[rsrcsSet.size()]);
                    }
                }
            }
        }
        
        return scriptsAll;
    }
    
    protected Class<? extends UIComponent> getComponentClass() {
        return org.richfaces.component.UIToolTip.class;
    }
    
    public String getBgColor(FacesContext context, UIComponent component) {
        Skin skin = getSkin(context);
        return String.valueOf(skin.getParameter(context, "headerBackgroundColor"));
    }
    
    public String getColor(FacesContext context, UIComponent component) {
        Skin skin = getSkin(context);
        return String.valueOf(skin.getParameter(context, "headerTextColor"));
    }
    
    public Map<String, Object> buildEventOptions(FacesContext context,
            UIComponent component, String targetId) {
        Map<String, Object> eventOptions = AjaxRendererUtils.buildEventOptions(
                context, component);
        String jsVarName = "_toolTip";
        
        // after element is subsituted in DOM tree, we have to re-create
        // it's JS-reference, cause old one is already invalid
        
        String clientId = component.getClientId(context);
        String oncompleteTooltip = ";" + "{" + "var " + jsVarName + " = $('"
                + clientId + "').component;" + jsVarName
                + ".toolTipContent = $('" + clientId + "content');" + jsVarName
                + ".displayDiv();" + "}";
        
        // before element will be substituted in DOM tree, we need to hide
        // toolTipe to avoid blinking
        String fireBeforeUpdateDOM = ";" + "{ var " + jsVarName + " = $('"
                + clientId + "').component;" + jsVarName
                + ".toolTip.style.display = 'none'; }";
        
        // enable ajaxSingle mode, i.e. we do not need to submit all form
        // controls to get tooltip content
        eventOptions.put("control", JSReference.THIS);
        
        if (eventOptions.containsKey("oncomplete")) {
            JSFunctionDefinition onComplete = 
                (JSFunctionDefinition) eventOptions.get("oncomplete");
            onComplete.addToBody(oncompleteTooltip);
            eventOptions.put("oncomplete", onComplete);
        } else {
            JSFunctionDefinition onComplete = new JSFunctionDefinition();
            onComplete.addParameter("request");
            onComplete.addParameter("showEvent");
            onComplete.addParameter("data");
            onComplete.addToBody(oncompleteTooltip);
            eventOptions.put("oncomplete", onComplete);
        }
        
        if (eventOptions.containsKey(AjaxRendererUtils.ONBEFOREDOMUPDATE_ATTR_NAME)) {
            JSFunctionDefinition beforeUpdate = (JSFunctionDefinition) eventOptions
                    .get(AjaxRendererUtils.ONBEFOREDOMUPDATE_ATTR_NAME);
            beforeUpdate.addToBody(fireBeforeUpdateDOM);
            eventOptions.put(AjaxRendererUtils.ONBEFOREDOMUPDATE_ATTR_NAME, beforeUpdate);
        } else {
            JSFunctionDefinition beforeUpdate = new JSFunctionDefinition();
            beforeUpdate.addParameter("request");
            beforeUpdate.addParameter("showEvent");
            beforeUpdate.addParameter("data");
            beforeUpdate.addToBody(fireBeforeUpdateDOM);
            eventOptions.put(AjaxRendererUtils.ONBEFOREDOMUPDATE_ATTR_NAME, beforeUpdate);
        }
        
        return eventOptions;
    }
    
    public void insertScript(FacesContext context, UIComponent component)
            throws IOException {
        
        StringBuffer ret = new StringBuffer();
        ret.append("<script ");
        ret.append("type=\"text/javascript\" ");
        ret.append("id =\"script").append(component.getClientId(context)).append("\">\n");
        ret.append(constructJSVariable(context, component)).append(";\n\n");
        ret.append("</script>");

        context.getResponseWriter().write(ret.toString());
    }
    
    public Map<String, Object> getParamsMap(FacesContext context, UIToolTip toolTip) {
        List<UIComponent> children = toolTip.getChildren();
        Map<String, Object> paramsMap = new HashMap<String, Object>();
        for (UIComponent child : children) {
            if (child instanceof UIParameter) {
                UIParameter param = (UIParameter) child;
                paramsMap.put(param.getName(), param.getValue());
            }
        }
        return paramsMap;
    }
    
    private String getTargetId(FacesContext context, UIComponent component) {
        UIToolTip toolTip = (UIToolTip) component;
        String forValue = toolTip.getFor();
        
        if (forValue != null && forValue.length() != 0) {
            UIComponent targetComponent = getUtils().findComponentFor(component, forValue);
            if (targetComponent != null) {
                return targetComponent.getClientId(context);
            } else {
                return forValue;
            }
        } else {
            return component.getParent().getClientId(context);
        }
    }
    
    private final static String COMMA = ",";
    private final static String QUOT = "\"";
    private final static String QUOT_COMMA = "\"" + ",";  
    
    public String constructJSVariable(FacesContext context, UIComponent component) {
        UIToolTip toolTip = (UIToolTip) component;
        String targetClientId = getTargetId(context, component);
        
        
        Map<JSReference, Object> eventsMap = new HashMap<JSReference, Object>();
        
        String eventShow = (toolTip.isAttached()) ? toolTip.getShowEvent() : "";
        if (eventShow.startsWith("on")) {
            eventShow = eventShow.substring(2);
        }
        eventsMap.put(new JSReference("showEvent"), eventShow);

        String eventHide = (toolTip.isAttached()) ? toolTip.getHideEvent() : "";
        if (eventHide.startsWith("on")) {
            eventHide = eventHide.substring(2);
        }
        eventsMap.put(new JSReference("hideEvent"), eventHide);

        eventsMap.put(new JSReference("delay"), new Integer(toolTip.getShowDelay()));
        eventsMap.put(new JSReference("hideDelay"), new Integer(toolTip.getHideDelay()));
        
        JSFunctionDefinition ajaxFunc = null;
        if (AJAX_MODE.equalsIgnoreCase(toolTip.getMode())) {
            ajaxFunc = new JSFunctionDefinition("event", "ajaxOptions");
            JSFunction function = AjaxRendererUtils.buildAjaxFunction(component, context);
            JSReference ref = new JSReference("ajaxOptions");
            function.addParameter(ref);
            ajaxFunc.addToBody(function);
        } 
        
        Map<String, Object> ajaxOptions = buildEventOptions(context, toolTip, targetClientId);
        ajaxOptions.putAll(getParamsMap(context, toolTip));
        
        Map<JSReference, Object> funcMap = new HashMap<JSReference, Object>();
        JSFunctionDefinition completeFunc = getUtils().getAsEventHandler(
                context, component, "oncomplete", "; return true;");
        funcMap.put(new JSReference("oncomplete"), completeFunc);

        JSFunctionDefinition hideFunc = getUtils().getAsEventHandler(
                context, component, "onhide", "; return true;");
        funcMap.put(new JSReference("onhide"), hideFunc);

        JSFunctionDefinition showFunc = getUtils().getAsEventHandler(
                context, component, "onshow", "; return true;");
        funcMap.put(new JSReference("onshow"), showFunc);
        
        StringBuffer ret = new StringBuffer();
        ret.append("new ToolTip(").append(ScriptUtils.toScript(eventsMap)).append(COMMA)
           .append(ScriptUtils.toScript(funcMap)).append(COMMA)
           .append(QUOT).append(toolTip.getClientId(context)).append(QUOT_COMMA)
           .append(QUOT).append(targetClientId).append(QUOT_COMMA)
           .append(QUOT).append(toolTip.getMode()).append(QUOT_COMMA)
           .append(toolTip.isDisabled()).append(COMMA)
           .append(QUOT).append(toolTip.getDirection()).append(QUOT_COMMA)
           .append(toolTip.isFollowMouse()).append(COMMA)
           .append(toolTip.getHorizontalOffset()).append(COMMA)
           .append(toolTip.getVerticalOffset()).append(COMMA)
           .append(ajaxFunc == null ? "null" : ajaxFunc.toScript()).append(COMMA)
           .append(ScriptUtils.toScript(ajaxOptions)).append(");");
        
        return ret.toString();
    }
    
    protected void doDecode(FacesContext context, UIComponent component) {
        
        UIToolTip tooltip = (UIToolTip) component;
        
        String clientId = tooltip.getClientId(context);
        
        if (context.getExternalContext().getRequestParameterMap().containsKey(clientId)) {
            if (AJAX_MODE.equals(tooltip.getMode())) {
                new AjaxEvent(component).queue();
                new ActionEvent(component).queue();
            }
        }
    }
    
    public void encodeTooltipText(FacesContext context, UIToolTip component)
            throws IOException {
        ResponseWriter responseWriter = context.getResponseWriter();
        responseWriter.startElement(component.getUsedElementType(), component);
        
        String clientId = component.getClientId(context);
        Object value = component.getValue();
        
        responseWriter.writeAttribute("id", clientId + "content", null);
        if (AJAX_MODE.equals(component.getMode())) {
            // we want to avoid rendering toolTip content during initialy page
            // displaying
            AjaxContext ajaxContext = AjaxContext.getCurrentInstance();
            if (ajaxContext != null && ajaxContext.getAjaxAreasToRender().contains(clientId + "content")) {
        
                responseWriter.write(value != null ? value.toString() : "");
                
                super.renderChildren(context, component);
            }
        } else {
            // client mode
            responseWriter.write(value != null ? value.toString() : "");
            super.renderChildren(context, component);
        }
        responseWriter.endElement(component.getUsedElementType());
    }
    
    protected void doEncodeBegin(ResponseWriter writer, FacesContext context,
            UIComponent component) throws IOException {
        
        try {
            ToolTipRenderer renderer = getRenderer((UIToolTip) component);
            renderer.doEncodeBegin(writer, context, component);
        } catch (Exception e) {
            throw new FacesException(e.getMessage(), e);
        }
    }
    
    protected void doEncodeChildren(ResponseWriter writer,
            FacesContext context, UIComponent component) throws IOException {
        
        try {
            ToolTipRenderer renderer = getRenderer((UIToolTip) component);
            renderer.doEncodeChildren(writer, context, component);
        } catch (Exception e) {
            throw new FacesException(e.getMessage(), e);
        }
        
    }
    
    protected void doEncodeEnd(ResponseWriter writer, FacesContext context,
            UIComponent component) throws IOException {
        
        try {
            ToolTipRenderer renderer = getRenderer((UIToolTip) component);
            renderer.doEncodeEnd(writer, context, component);
        } catch (Exception e) {
            throw new FacesException(e.getMessage(), e);
        }
    }
    
    /*
     * (non-Javadoc)
     * 
     * @see javax.faces.render.Renderer#getRendersChildren()
     */
    public boolean getRendersChildren() {
        return true;
    }
    
    private ToolTipRenderer getRenderer(UIToolTip toolTip) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
        Class<?> rendererClass;
        if ("block".equals(toolTip.getLayout())) {
            rendererClass = Class.forName("org.richfaces.renderkit.html.HtmlToolTipRendererBlock");
        } else {
            rendererClass = Class.forName("org.richfaces.renderkit.html.HtmlToolTipRenderer");
        }
        
        return (ToolTipRenderer) rendererClass.newInstance();
        
    }
    
}
