/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.shared_tomahawk.renderkit.html;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import javax.faces.component.UINamingContainer;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.apache.myfaces.shared_tomahawk.config.MyfacesConfig;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils.ScriptContext;
import org.apache.myfaces.shared_tomahawk.renderkit.html.util.JavascriptUtils;
import org.apache.myfaces.shared_tomahawk.renderkit.html.util.ResourceUtils;

public final class HtmlJavaScriptUtils
{
    private static final Logger log = Logger.getLogger(HtmlJavaScriptUtils.class
            .getName());

    private static final String AUTO_SCROLL_PARAM = "autoScroll";
    private static final String AUTO_SCROLL_FUNCTION = "getScrolling";

    private static final String SET_HIDDEN_INPUT_FN_NAME = "oamSetHiddenInput";
    private static final String SET_HIDDEN_INPUT_FN_NAME_JSF2 = "myfaces.oam.setHiddenInput";

    private static final String FIRST_SUBMIT_SCRIPT_ON_PAGE = "org.apache.MyFaces.FIRST_SUBMIT_SCRIPT_ON_PAGE";
    private static final String CLEAR_HIDDEN_INPUT_FN_NAME = "oamClearHiddenInput";
    

    @SuppressWarnings("unchecked")
    public static void renderFormSubmitScript(FacesContext facesContext)
            throws IOException
    {

        Map map = facesContext.getExternalContext().getRequestMap();
        Boolean firstScript = (Boolean) map.get(FIRST_SUBMIT_SCRIPT_ON_PAGE);

        if (firstScript == null || firstScript.equals(Boolean.TRUE))
        {
            map.put(FIRST_SUBMIT_SCRIPT_ON_PAGE, Boolean.FALSE);
            renderFormSubmitScriptIfNecessary(facesContext);

            //we have to render the config just in case
            renderConfigOptionsIfNecessary(facesContext);
        }
    }
    
    /**
     * @param facesContext
     * @throws IOException
     */
    private static void renderFormSubmitScriptIfNecessary(
            FacesContext facesContext) throws IOException
    {
        final ExternalContext externalContext = facesContext
                .getExternalContext();
        final MyfacesConfig currentInstance = MyfacesConfig
                .getCurrentInstance(externalContext);
        ResponseWriter writer = facesContext.getResponseWriter();

        if (currentInstance.isRenderFormSubmitScriptInline())
        {
            writer.startElement(HTML.SCRIPT_ELEM, null);
            writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", null);

            boolean autoScroll = currentInstance.isAutoScroll();

            ScriptContext context = new ScriptContext(
                    currentInstance.isPrettyHtml());
            context.prettyLine();
            context.increaseIndent();

            prepareScript(facesContext, context, autoScroll);

            writer.writeText(context.toString(), null);

            writer.endElement(HTML.SCRIPT_ELEM);
        }
        else
        {
            ResourceUtils
                    .renderMyfacesJSInlineIfNecessary(facesContext, writer);
        }
    }
    
    private static void renderConfigOptionsIfNecessary(FacesContext facesContext)
            throws IOException
    {
        ResponseWriter writer = facesContext.getResponseWriter();
        MyfacesConfig config = MyfacesConfig.getCurrentInstance(facesContext
                .getExternalContext());
        ScriptContext script = new ScriptContext(config.isPrettyHtml());
        boolean autoScroll = config.isAutoScroll();
        boolean autoSave = JavascriptUtils.isSaveFormSubmitLinkIE(facesContext
                .getExternalContext());

        if (autoScroll || autoSave)
        {
            script.prettyLine();
            script.increaseIndent();
            script.append("(!window.myfaces) ? window.myfaces = {} : null;");
            script.append("(!myfaces.core) ? myfaces.core = {} : null;");
            script.append("(!myfaces.core.config) ? myfaces.core.config = {} : null;");
        }

        if (autoScroll)
        {
            script.append("myfaces.core.config.autoScroll = true;");
        }
        if (autoSave)
        {
            script.append("myfaces.core.config.ieAutoSave = true;");
        }
        if (autoScroll || autoSave)
        {
            writer.startElement(HTML.SCRIPT_ELEM, null);
            writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", null);
            writer.writeText(script.toString(), null);
            writer.endElement(HTML.SCRIPT_ELEM);
        }
    }
    /**
     * @param facesContext
     * @param context
     * @param autoScroll
     */
    private static void prepareScript(FacesContext facesContext,
            ScriptContext context, boolean autoScroll)
    {

        final char separatorChar = UINamingContainer
                .getSeparatorChar(facesContext);
        context.prettyLine();

        //render a function to create a hidden input, if it doesn't exist
        context.append("function ");
        context.append(SET_HIDDEN_INPUT_FN_NAME).append(
                "(formname, name, value)");
        context.append("{");
        context.append("var form = document.forms[formname];");
        context.prettyLine();
        context.append("if (typeof form == 'undefined')");
        context.append("{");
        context.append("form = document.getElementById(formname);");
        context.append("}");
        context.prettyLine();
        context.append("if(typeof form.elements[name]!='undefined' && "+
                "(form.elements[name].nodeName=='INPUT' || form.elements[name].nodeName=='input'))");
        context.append("{");
        context.append("form.elements[name].value=value;");
        context.append("}");
        context.append("else");
        context.append("{");
        context.append("var newInput = document.createElement('input');");
        context.prettyLine();
        context.append("newInput.setAttribute('type','hidden');");
        context.prettyLine();
        context.append("newInput.setAttribute('id',name);"); // IE hack; See MYFACES-1805
        context.prettyLine();
        context.append("newInput.setAttribute('name',name);");
        context.prettyLine();
        context.append("newInput.setAttribute('value',value);");
        context.prettyLine();
        context.append("form.appendChild(newInput);");
        context.append("}");

        context.append("}");

        context.prettyLine();

        context.prettyLine();

        //render a function to clear a hidden input, if it exists        
        context.append("function ");
        context.append(CLEAR_HIDDEN_INPUT_FN_NAME).append(
                "(formname, name, value)");
        context.append("{");
        context.append("var form = document.forms[formname];");
        context.prettyLine();
        context.append("if (typeof form == 'undefined')");
        context.append("{");
        context.append("form = document.getElementById(formname);");
        context.append("}");
        context.prettyLine();
        context.append("var hInput = form.elements[name];");
        context.prettyLine();
        context.append("if(typeof hInput !='undefined')");
        context.append("{");
        //context.append("form.elements[name].value=null;");
        context.append("form.removeChild(hInput);");
        context.append("}");

        context.append("}");

        context.prettyLine();

        context.append("function ");
        context.append(HtmlRendererUtils.SUBMIT_FORM_FN_NAME).append(
                "(formName, linkId, target, params)");
        context.append("{");

        //call the script to clear the form (clearFormHiddenParams_<formName>) method - 
        //optionally, only necessary for IE5.5.
        //todo: if IE5.5. is ever desupported, we can get rid of this and instead rely on 
        //the last part of this script to
        //clear the parameters
        appendClearHiddenCommandFormParamsFunctionCall(
                context, null);

        if (autoScroll)
        {
            appendAutoScrollAssignment(facesContext, context, null);
        }

        context.prettyLine();

        context.append("var form = document.forms[formName];");
        context.prettyLine();
        context.append("if (typeof form == 'undefined')");
        context.append("{");
        context.append("form = document.getElementById(formName);");
        context.append("}");
        context.prettyLine();

        if (JavascriptUtils.isSaveFormSubmitLinkIE(FacesContext
                .getCurrentInstance().getExternalContext()))
        {
            context.append("var agentString = navigator.userAgent.toLowerCase();");
            context.prettyLine();
            //context.append("var isIE = false;");
            context.prettyLine();
            context.append("if (agentString.indexOf('msie') != -1)");

            context.append("{");
            context.append("if (!(agentString.indexOf('ppc') != -1 &&"+
                    " agentString.indexOf('windows ce') != -1 && version >= 4.0))");
            context.append("{");
            context.append("window.external.AutoCompleteSaveForm(form);");
            //        context.append("isIE = false;");
            context.append("}");
            //        context.append("else");
            //        context.append("{");
            //        context.append("isIE = true;");
            //        context.prettyLine();
            //        context.append("}");

            context.append("}");

            context.prettyLine();
        }
        //set the target (and save it). This should be done always, 
        //and the default value of target is always valid.
        context.append("var oldTarget = form.target;");
        context.prettyLine();
        context.append("if(target != null)");
        context.append("{");
        context.prettyLine();
        context.append("form.target=target;");
        context.append("}");

        //set the submit parameters

        context.append("if((typeof params!='undefined') && params != null)");
        context.append("{");
        context.prettyLine();
        context.append("for(var i=0, param; (param = params[i]); i++)");
        context.append("{");
        context.append(SET_HIDDEN_INPUT_FN_NAME).append(
                "(formName,param[0], param[1]);");
        context.append("}");
        context.append("}");

        context.prettyLine();

        context.append(SET_HIDDEN_INPUT_FN_NAME);
        context.append("(formName,formName +'" + separatorChar + "'+'"
                + HtmlRendererUtils.HIDDEN_COMMANDLINK_FIELD_NAME
                + "',linkId);");

        context.prettyLine();
        context.prettyLine();

        //do the actual submit calls

        context.append("if(form.onsubmit)");
        context.append("{");
        context.append("var result=form.onsubmit();");
        context.prettyLine();
        context.append("if((typeof result=='undefined')||result)");
        context.append("{");
        context.append("try");
        context.append("{");
        context.append("form.submit();");
        context.append("}");
        context.append("catch(e){}");
        context.append("}");
        context.append("}");
        context.append("else ");
        context.append("{");
        context.append("try");
        context.append("{");
        context.append("form.submit();");
        context.append("}");
        context.append("catch(e){}");
        context.append("}");

        //reset the target
        context.prettyLine();
        //Restore the old target, no more questions asked
        context.append("form.target=oldTarget;");
        context.prettyLine();

        //clear the individual parameters - to make sure that even if the clear-function isn't called,
        // the back button/resubmit functionality will still work in all browsers except IE 5.5.

        context.append("if((typeof params!='undefined') && params != null)");
        context.append("{");
        context.prettyLine();
        context.append("for(var i=0, param; (param = params[i]); i++)");
        context.append("{");
        context.append(CLEAR_HIDDEN_INPUT_FN_NAME).append(
                "(formName,param[0], param[1]);");
        context.append("}");
        context.append("}");

        context.prettyLine();

        context.append(CLEAR_HIDDEN_INPUT_FN_NAME);
        context.append("(formName,formName +'" + separatorChar + "'+'"
                + HtmlRendererUtils.HIDDEN_COMMANDLINK_FIELD_NAME
                + "',linkId);");

        //return false, so that browser does not handle the click
        context.append("return false;");
        context.append("}");

        context.prettyLineDecreaseIndent();
    }
    
    public static void appendAutoScrollAssignment(StringBuilder onClickValue,
            String formName)
    {
        appendAutoScrollAssignment(FacesContext.getCurrentInstance(),
                new ScriptContext(onClickValue, false), formName);
    }

    /**
     * Adds the hidden form input value assignment that is necessary for the autoscroll
     * feature to an html link or button onclick attribute.
     */
    public static void appendAutoScrollAssignment(FacesContext context,
            StringBuilder onClickValue, String formName)
    {
        appendAutoScrollAssignment(context, new ScriptContext(onClickValue,
                false), formName);
    }
    
    private static void appendAutoScrollAssignment(FacesContext context,
            ScriptContext scriptContext, String formName)
    {
        String formNameStr = formName == null ? "formName" : (new StringBuilder(
                "'").append(formName).append("'").toString());
        String paramName = new StringBuilder().append("'")
                .append(AUTO_SCROLL_PARAM).append("'").toString();
        String value = new StringBuilder().append(AUTO_SCROLL_FUNCTION)
                .append("()").toString();

        scriptContext.prettyLine();
        scriptContext.append("if(typeof window." + AUTO_SCROLL_FUNCTION
                + "!='undefined')");
        scriptContext.append("{");
        if (MyfacesConfig.getCurrentInstance(context.getExternalContext())
                .isRenderFormSubmitScriptInline())
        {
            scriptContext.append(SET_HIDDEN_INPUT_FN_NAME);
        }
        else
        {
            scriptContext.append(SET_HIDDEN_INPUT_FN_NAME_JSF2);
        }
        scriptContext.append("(").append(formNameStr).append(",")
                .append(paramName).append(",").append(value).append(");");
        scriptContext.append("}");

    }
    
    public static String getAutoScrollFunction(FacesContext facesContext)
    {
        ScriptContext script = new ScriptContext(MyfacesConfig
                .getCurrentInstance(facesContext.getExternalContext())
                .isPrettyHtml());

        script.prettyLineIncreaseIndent();

        script.append("function ");
        script.append(AUTO_SCROLL_FUNCTION);
        script.append("()");
        script.append("{");
        script.append("var x = 0; var y = 0;");
        script.append("if (self.pageXOffset || self.pageYOffset)");
        script.append("{");
        script.append("x = self.pageXOffset;");
        script.prettyLine();
        script.append("y = self.pageYOffset;");
        script.append("}");
        script.append(" else if ((document.documentElement && document.documentElement.scrollLeft)||"+
                "(document.documentElement && document.documentElement.scrollTop))");
        script.append("{");
        script.append("x = document.documentElement.scrollLeft;");
        script.prettyLine();
        script.append("y = document.documentElement.scrollTop;");
        script.append("}");
        script.append(" else if (document.body) ");
        script.append("{");
        script.append("x = document.body.scrollLeft;");
        script.prettyLine();
        script.append("y = document.body.scrollTop;");
        script.append("}");
        script.append("return x + \",\" + y;");
        script.append("}");

        ExternalContext externalContext = facesContext.getExternalContext();
        String oldViewId = JavascriptUtils.getOldViewId(externalContext);
        if (oldViewId != null
                && oldViewId.equals(facesContext.getViewRoot().getViewId()))
        {
            //ok, we stayed on the same page, so let's scroll it to the former place
            String scrolling = (String) externalContext
                    .getRequestParameterMap().get(AUTO_SCROLL_PARAM);
            if (scrolling != null && scrolling.length() > 0)
            {
                int x = 0;
                int y = 0;
                int comma = scrolling.indexOf(',');
                if (comma == -1)
                {
                    log.warning("Illegal autoscroll request parameter: "
                            + scrolling);
                }
                else
                {
                    try
                    {
                        //we convert to int against XSS vulnerability
                        x = Integer.parseInt(scrolling.substring(0, comma));
                    }
                    catch (NumberFormatException e)
                    {
                        log.warning("Error getting x offset for autoscroll feature. Bad param value: "
                                + scrolling);
                        x = 0; //ignore false numbers
                    }

                    try
                    {
                        //we convert to int against XSS vulnerability
                        y = Integer.parseInt(scrolling.substring(comma + 1));
                    }
                    catch (NumberFormatException e)
                    {
                        log.warning("Error getting y offset for autoscroll feature. Bad param value: "
                                + scrolling);
                        y = 0; //ignore false numbers
                    }
                }
                script.append("window.scrollTo(").append(x).append(",")
                        .append(y).append(");\n");
            }
        }

        return script.toString();
    }
    
    /**
     * Renders the hidden form input that is necessary for the autoscroll feature.
     */
    public static void renderAutoScrollHiddenInput(FacesContext facesContext,
            ResponseWriter writer) throws IOException
    {
        HtmlRendererUtils.writePrettyLineSeparator(facesContext);
        writer.startElement(HTML.INPUT_ELEM, null);
        writer.writeAttribute(HTML.TYPE_ATTR, "hidden", null);
        writer.writeAttribute(HTML.NAME_ATTR, AUTO_SCROLL_PARAM, null);
        writer.endElement(HTML.INPUT_ELEM);
        HtmlRendererUtils.writePrettyLineSeparator(facesContext);
    }

    /**
     * Renders the autoscroll javascript function.
     */
    public static void renderAutoScrollFunction(FacesContext facesContext,
            ResponseWriter writer) throws IOException
    {
        HtmlRendererUtils.writePrettyLineSeparator(facesContext);
        writer.startElement(HTML.SCRIPT_ELEM, null);
        writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR,
                HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
        writer.writeText(getAutoScrollFunction(facesContext), null);
        writer.endElement(HTML.SCRIPT_ELEM);
        HtmlRendererUtils.writePrettyLineSeparator(facesContext);
    }
    
    public static void appendClearHiddenCommandFormParamsFunctionCall(
            StringBuilder buf, String formName)
    {
        appendClearHiddenCommandFormParamsFunctionCall(new ScriptContext(buf,
                false), formName);
    }
    
    private static void appendClearHiddenCommandFormParamsFunctionCall(
            ScriptContext context, String formName)
    {
        String functionName = HtmlRendererUtils
                .getClearHiddenCommandFormParamsFunctionName(formName);
        if (formName == null)
        {
            context.prettyLine();
            context.append("var clearFn = ");
            context.append(functionName);
            context.append(";");
            context.prettyLine();
            context.append("if(typeof window[clearFn] =='function')");
            context.append("{");
            context.append("window[clearFn](formName);");
            context.append("}");
        }
        else
        {
            context.prettyLine();
            context.append("if(typeof window.");
            context.append(functionName);
            context.append("=='function')");
            context.append("{");
            context.append(functionName).append("('").append(formName)
                    .append("');");
            context.append("}");
        }
    }
    
    /**
     * Prefixes the given String with "clear_" and removes special characters
     *
     * @param formName
     * @return String
     */
    public static String getClearHiddenCommandFormParamsFunctionName(
            String formName)
    {
        final char separatorChar = UINamingContainer
                .getSeparatorChar(FacesContext.getCurrentInstance());
        if (formName == null)
        {
            return "'" + HtmlRendererUtils.CLEAR_HIDDEN_FIELD_FN_NAME
                    + "_'+formName.replace(/-/g, '\\$" + separatorChar
                    + "').replace(/" + separatorChar + "/g,'_')";
        }

        return JavascriptUtils
                .getValidJavascriptNameAsInRI(HtmlRendererUtils.CLEAR_HIDDEN_FIELD_FN_NAME + "_"
                        + formName.replace(separatorChar, '_'));
    }

    public static String getClearHiddenCommandFormParamsFunctionNameMyfacesLegacy(
            String formName)
    {
        return "clear_"
                + JavascriptUtils.getValidJavascriptName(formName, false);
    }
    
    /**
     * Render the javascript function that is called on a click on a commandLink
     * to clear the hidden inputs. This is necessary because on a browser back,
     * each hidden input still has it's old value (browser cache!) and therefore
     * a new submit would cause the according action once more!
     *
     * @param writer
     * @param formName
     * @param dummyFormParams
     * @param formTarget
     * @throws IOException
     */
    public static void renderClearHiddenCommandFormParamsFunction(
            ResponseWriter writer, String formName, Set dummyFormParams,
            String formTarget) throws IOException
    {
        //render the clear hidden inputs javascript function
        String functionName = getClearHiddenCommandFormParamsFunctionName(formName);
        writer.startElement(HTML.SCRIPT_ELEM, null);
        writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", null);

        // Using writeComment instead of write with <!-- tag
        StringBuilder script = new StringBuilder();
        script.append("function ");
        script.append(functionName);
        script.append("() {");
        if (dummyFormParams != null)
        {
            script.append("\n  var f = document.forms['");
            script.append(formName);
            script.append("'];");
            int i = 0;
            for (Iterator it = dummyFormParams.iterator(); it.hasNext();)
            {
                String elemVarName = "elem" + i;
                script.append("\n  var ").append(elemVarName).append(" = ");
                script.append("f.elements['").append((String) it.next())
                        .append("'];");
                script.append("\n  if(typeof ").append(elemVarName)
                        .append(" !='undefined' && ");
                script.append(elemVarName).append(".nodeName=='INPUT'){");
                script.append("\n   if (").append(elemVarName)
                        .append(".value != '') {");
                script.append("\n    " + elemVarName + ".value='';");
                script.append("\n   }");
                script.append("\n  }");
                i++;
            }
        }
        // clear form target
        script.append("\n  f.target=");
        if (formTarget == null || formTarget.length() == 0)
        {
            //Normally one would think that setting target to null has the
            //desired effect, but once again IE is different...
            //Setting target to null causes IE to open a new window!
            script.append("'';");
        }
        else
        {
            script.append("'");
            script.append(formTarget);
            script.append("';");
        }
        script.append("\n}");

        //Just to be sure we call this clear method on each load.
        //Otherwise in the case, that someone submits a form by pressing Enter
        //within a text input, the hidden inputs won't be cleared!
        script.append("\n");
        script.append(functionName);
        script.append("();");

        writer.writeText(script.toString(), null);
        writer.endElement(HTML.SCRIPT_ELEM);
    }
    
    /**
     * This function correctly escapes the given JavaScript code
     * for the use in the jsf.util.chain() JavaScript function.
     * It also handles double-escaping correclty.
     *
     * @param javaScript
     * @return
     */
    public static String escapeJavaScriptForChain(String javaScript)
    {
        // first replace \' with \\'
        //String escaped = StringUtils.replace(javaScript, "\\'", "\\\\'");

        // then replace ' with \'
        // (this will replace every \' in the original to \\\')
        //escaped = StringUtils.replace(escaped, '\'', "\\'");

        //return escaped;

        StringBuilder out = null;
        for (int pos = 0; pos < javaScript.length(); pos++)
        {
            char c = javaScript.charAt(pos);

            if (c == '\\' || c == '\'')
            {
                if (out == null)
                {
                    out = new StringBuilder(javaScript.length() + 8);
                    if (pos > 0)
                    {
                        out.append(javaScript, 0, pos);
                    }
                }
                out.append('\\');
            }
            if (out != null)
            {
                out.append(c);
            }
        }

        if (out == null)
        {
            return javaScript;
        }
        else
        {
            return out.toString();
        }
    }
    
    public static void renderViewStateJavascript(FacesContext facesContext,
            String hiddenId, String serializedState) throws IOException
    {
        ResponseWriter writer = facesContext.getResponseWriter();

        writer.startElement(HTML.SCRIPT_ELEM, null);
        writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", null);

        final ExternalContext externalContext = facesContext
                .getExternalContext();
        final MyfacesConfig currentInstance = MyfacesConfig
                .getCurrentInstance(externalContext);

        ScriptContext context = new ScriptContext(
                currentInstance.isPrettyHtml());
        context.prettyLine();
        context.increaseIndent();

        context.append("function setViewState() {\n");
        context.append("\tvar state = '");
        context.append(serializedState);
        context.append("';\n");
        context.append("\tfor (var i = 0; i < document.forms.length; i++) {\n");
        context.append("\t\tdocument.forms[i]['" + hiddenId
                + "'].value = state;\n");
        context.append("\t}\n");
        context.append("}\n");
        context.append("setViewState();\n");

        context.decreaseIndent();

        writer.writeText(context.toString(), null);

        writer.endElement(HTML.SCRIPT_ELEM);
    }
}
