/**
 * Copyright 2005-2016 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.krad.uif.util;

import org.apache.commons.lang.StringUtils;
import org.kuali.rice.krad.datadictionary.state.StateMapping;
import org.kuali.rice.krad.datadictionary.validation.constraint.BaseConstraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.CaseConstraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.Constraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.MustOccurConstraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.PrerequisiteConstraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.SimpleConstraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.ValidCharactersConstraint;
import org.kuali.rice.krad.datadictionary.validation.constraint.WhenConstraint;
import org.kuali.rice.krad.messages.MessageService;
import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
import org.kuali.rice.krad.uif.UifConstants;
import org.kuali.rice.krad.uif.control.TextControl;
import org.kuali.rice.krad.uif.field.Field;
import org.kuali.rice.krad.uif.field.InputField;
import org.kuali.rice.krad.uif.view.FormView;
import org.kuali.rice.krad.uif.view.View;
import org.kuali.rice.krad.uif.widget.DatePicker;
import org.kuali.rice.krad.util.KRADUtils;
import org.kuali.rice.krad.web.form.UifFormBase;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Contains all the methods necessary for generating the js required to perform validation client
 * side. The processAndApplyConstraints(InputField field, View view) is the key method of this class
 * used by InputField to setup its client side validation mechanisms.
 * 
 * Methods now take into account state based validation and states on constraints.
 * 
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public class ClientValidationUtils {
    // used to give validation methods unique signatures
    private static int methodKey = 0;

    // list used to temporarily store mustOccurs field names for the error
    // message
    private static List<List<String>> mustOccursPathNames;

    public static final String LABEL_KEY_SPLIT_PATTERN = ",";

    public static final String PREREQ_MSG_KEY = "prerequisite";
    public static final String POSTREQ_MSG_KEY = "postrequisite";
    public static final String MUSTOCCURS_MSG_KEY = "mustOccurs";
    public static final String MUSTOCCURS_MSG_EQUAL_KEY = "mustOccursEqualMinMax";
    public static final String GENERIC_FIELD_MSG_KEY = "general.genericFieldName";

    public static final String ALL_MSG_KEY = "general.all";
    public static final String ATMOST_MSG_KEY = "general.atMost";
    public static final String AND_MSG_KEY = "general.and";
    public static final String OR_MSG_KEY = "general.or";

    // enum representing names of rules provided by the jQuery plugin
    public static enum ValidationMessageKeys {
        REQUIRED("required"),
        MIN_EXCLUSIVE("minExclusive"),
        MAX_INCLUSIVE("maxInclusive"),
        MIN_LENGTH("minLengthConditional"),
        MAX_LENGTH("maxLengthConditional");

        private ValidationMessageKeys(String name) {
            this.name = name;
        }

        private final String name;

        @Override
        public String toString() {
            return name;
        }

        public static boolean contains(String name) {
            for (ValidationMessageKeys element : EnumSet.allOf(ValidationMessageKeys.class)) {
                if (element.toString().equalsIgnoreCase(name)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Returns formatted message text for the given message namespace, component, and key
     * 
     * @param namespace namespace code the message is associated with, if null the default namespace
     *        will be used
     * @param componentCode component code the message is associated with, if null default component
     *        code is used
     * @param messageKey key for the message to retrieve
     * @param params list of parameters for the message text
     * @return formatted message text
     */
    public static String generateMessageText(String namespace, String componentCode, String messageKey,
            List<String> params) {
        String message = "NO MESSAGE";
        if (StringUtils.isNotEmpty(messageKey)) {
            message = KRADServiceLocatorWeb.getMessageService().getMessageText(namespace, componentCode, messageKey);
            if (params != null && !params.isEmpty() && StringUtils.isNotEmpty(message)) {
                message = MessageFormat.format(message, params.toArray());
                message = MessageStructureUtils.translateStringMessage(message);
            }
        }

        if (StringUtils.isEmpty(message)) {
            message = messageKey;
        }

        //replace characters that might cause issues with their equivalent html codes
        message = KRADUtils.convertToHTMLAttributeSafeString(message);

        return message;
    }

    /**
     * Generates the js object used to override all default messages for validator jquery plugin
     * with custom messages retrieved from the message service
     * 
     * @return script for message override
     */
    public static String generateValidatorMessagesOption() {
        MessageService messageService = KRADServiceLocatorWeb.getMessageService();

        String mOption = "";
        String keyValuePairs = "";
        for (ValidationMessageKeys element : EnumSet.allOf(ValidationMessageKeys.class)) {
            String key = element.toString();
            String message = messageService.getMessageText(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + key);

            if (StringUtils.isNotEmpty(message)) {
                message = MessageStructureUtils.translateStringMessage(message);
                keyValuePairs = keyValuePairs + "\n" + key + ": '" + message + "',";
            }
        }

        keyValuePairs = StringUtils.removeEnd(keyValuePairs, ",");
        if (StringUtils.isNotEmpty(keyValuePairs)) {
            mOption = "{" + keyValuePairs + "}";
        }

        return mOption;
    }

    /**
     * Returns the add method jquery validator call for the regular expression stored in
     * validCharactersConstraint.
     * @param field input field
     * @param validCharactersConstraint constraint providing the regex
     * @return js validator.addMethod script
     */
    public static String getRegexMethod(InputField field, ValidCharactersConstraint validCharactersConstraint) {
        return getRegexMethod(field, validCharactersConstraint, true);
    }

    /**
     * Returns the add method jquery validator call for the regular expression stored in
     * validCharactersConstraint.
     *
     * @param field input field
     * @param validCharactersConstraint constraint providing the regex
     * @param escape whether to escape key or not
     * @return js validator.addMethod script
     */
    public static String getRegexMethod(InputField field, ValidCharactersConstraint validCharactersConstraint,
            boolean escape) {
        String message = generateMessageText(validCharactersConstraint.getMessageNamespaceCode(),
                validCharactersConstraint.getMessageComponentCode(), validCharactersConstraint.getMessageKey(),
                validCharactersConstraint.getValidationMessageParams());
        String key = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;

        // replace characters known to cause issues if not escaped
        String regex = validCharactersConstraint.getValue();
        if (regex.contains("\\\\")) {
            regex = regex.replaceAll("\\\\", "\\\\\\\\");
        }
        if (regex.contains("/")) {
            regex = regex.replace("/", "\\/");
        }

        return "\njQuery.validator.addMethod(\"" + (escape ? ScriptUtils.escapeName(key) : key)
                + "\", function(value, element) {\n "
                + "return (this.optional(element) !== false) || /"
                + regex
                + "/.test(value);"
                + "}, \""
                + message
                + "\");";
    }

    /**
     * Returns the add method jquery validator call for the regular expression stored in
     * validCharactersConstraint that explicitly checks a boolean. Needed because one method accepts
     * params and the other doesn't.
     * @param field input field
     * @param validCharactersConstraint constraint providing the regex
     * 
     * @return js validator.addMethod script
     */
    public static String getRegexMethodWithBooleanCheck(InputField field,
            ValidCharactersConstraint validCharactersConstraint) {
        String message = generateMessageText(validCharactersConstraint.getMessageNamespaceCode(),
                validCharactersConstraint.getMessageComponentCode(), validCharactersConstraint.getMessageKey(),
                validCharactersConstraint.getValidationMessageParams());
        String key = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;

        // replace characters known to cause issues if not escaped
        String regex = validCharactersConstraint.getValue();
        if (regex.contains("\\\\")) {
            regex = regex.replaceAll("\\\\", "\\\\\\\\");
        }
        if (regex.contains("/")) {
            regex = regex.replace("/", "\\/");
        }

        return "\njQuery.validator.addMethod(\""
                + ScriptUtils.escapeName(key)
                + "\", function(value, element, doCheck) {\n if(doCheck === false){return true;}else{"
                + "return (this.optional(element) !== false) || /"
                + regex
                + "/.test(value);}"
                + "}, \""
                + message
                + "\");";
    }

    /**
     * This method processes a single CaseConstraint. Internally it makes calls to
     * processWhenConstraint for each WhenConstraint that exists in this constraint. It adds a
     * "dependsOn" css class to this field for the field which the CaseConstraint references.
     * 
     * @param field input field
     * @param view active view
     * @param constraint case constraint providing the field reference
     * @param andedCase the boolean logic to be anded when determining if this case is satisfied
     *        (used for nested CaseConstraints)
     * @param validationState validation state
     * @param stateMapping state mapping
     */
    public static void processCaseConstraint(InputField field, View view, CaseConstraint constraint, String andedCase,
            String validationState, StateMapping stateMapping) {
        if (constraint.getOperator() == null) {
            constraint.setOperator("equals");
        }

        String operator = "==";
        if (constraint.getOperator().equalsIgnoreCase("not_equals") || constraint.getOperator().equalsIgnoreCase(
                "not_equal")) {
            operator = "!=";
        } else if (constraint.getOperator().equalsIgnoreCase("greater_than_equal")) {
            operator = ">=";
        } else if (constraint.getOperator().equalsIgnoreCase("less_than_equal")) {
            operator = "<=";
        } else if (constraint.getOperator().equalsIgnoreCase("greater_than")) {
            operator = ">";
        } else if (constraint.getOperator().equalsIgnoreCase("less_than")) {
            operator = "<";
        } else if (constraint.getOperator().equalsIgnoreCase("has_value")) {
            operator = "";
        }
        // add more operator types here if more are supported later

        field.getControl().addStyleClass("dependsOn-" + ScriptUtils.escapeName(constraint.getPropertyName()));

        if (constraint.getWhenConstraint() != null && !constraint.getWhenConstraint().isEmpty()) {
            //String fieldPath = field.getBindingInfo().getBindingObjectPath() + "." + constraint.getPropertyName();
            String fieldPath = constraint.getPropertyName();
            for (WhenConstraint wc : constraint.getWhenConstraint()) {
                wc = ConstraintStateUtils.getApplicableConstraint(wc, validationState, stateMapping);
                if (wc != null) {
                    processWhenConstraint(field, view, constraint, wc, ScriptUtils.escapeName(fieldPath), operator,
                            andedCase, validationState, stateMapping);
                }
            }
        }
    }

    /**
     * This method processes the WhenConstraint passed in. The when constraint is used to create a
     * boolean statement to determine if the constraint will be applied. The necessary rules/methods
     * for applying this constraint are created in the createRule call. Note the use of the use of
     * coerceValue js function call.
     * 
     * @param view
     * @param wc
     * @param fieldPath
     * @param operator
     * @param andedCase
     */
    private static void processWhenConstraint(InputField field, View view, CaseConstraint caseConstraint,
            WhenConstraint wc, String fieldPath, String operator, String andedCase, String validationState,
            StateMapping stateMapping) {
        String ruleString = "";
        // prerequisite constraint

        String booleanStatement = "";
        if (wc.getValues() != null) {

            String caseStr = "";
            if (!caseConstraint.isCaseSensitive()) {
                caseStr = ".toUpperCase()";
            }
            for (int i = 0; i < wc.getValues().size(); i++) {
                if (operator.isEmpty()) {
                    // has_value case
                    if (wc.getValues().get(i) instanceof String && ((String) wc.getValues().get(i)).equalsIgnoreCase(
                            "false")) {
                        booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "') == '')";
                    } else {
                        booleanStatement = booleanStatement + "(coerceValue('" + fieldPath + "') != '')";
                    }
                } else {
                    // everything else
                    booleanStatement = booleanStatement
                            + "(coerceValue('"
                            + fieldPath
                            + "')"
                            + caseStr
                            + " "
                            + operator
                            + " \""
                            + wc.getValues().get(i)
                            + "\""
                            + caseStr
                            + ")";
                }
                if ((i + 1) != wc.getValues().size()) {
                    booleanStatement = booleanStatement + " || ";
                }
            }

        }

        if (andedCase != null) {
            booleanStatement = "(" + booleanStatement + ") && (" + andedCase + ")";
        }

        if (wc.getConstraint() != null && StringUtils.isNotEmpty(booleanStatement)) {
            Constraint constraint = ConstraintStateUtils.getApplicableConstraint(wc.getConstraint(), validationState,
                    stateMapping);
            if (constraint != null) {
                ruleString = createRule(field, constraint, booleanStatement, view, validationState, stateMapping);
            }
        }

        if (StringUtils.isNotEmpty(ruleString)) {
            addScriptToPage(view, field, ruleString);
        }
    }

    /**
     * Adds the script to the view to execute on a jQuery document ready event.
     * 
     * @param view active view
     * @param field input field
     * @param script script to run on the document ready event
     */
    public static void addScriptToPage(View view, InputField field, String script) {
        String prefixScript = "";

        if (field.getOnDocumentReadyScript() != null) {
            prefixScript = field.getOnDocumentReadyScript();
        }
        field.setOnDocumentReadyScript(prefixScript + "\n" + "runValidationScript(function(){" + script + "});");
    }

    /**
     * Determines which fields are being evaluated in a boolean statement, so handlers can be
     * attached to them if needed, returns these names in a list.
     * 
     * @param statement statement to parse
     * @return list of field names
     */
    private static List<String> parseOutFields(String statement) {
        List<String> fieldNames = new ArrayList<String>();
        String[] splits = StringUtils.splitByWholeSeparator(statement, "coerceValue(");
        for (String s : splits) {
            //must be a coerceValue param and not preceding content from the split, always starts with "'"
            if (!s.startsWith("'")) {
                continue;
            }

            s = s.substring(1);
            String fieldName = StringUtils.substringBefore(s, "'");
            //Only add field name once for this condition check
            if (!fieldNames.contains(fieldName)) {
                fieldNames.add(fieldName);
            }

        }
        return fieldNames;
    }

    /**
     * This method takes in a constraint to apply only when the passed in booleanStatement is valid.
     * The method will create the necessary addMethod and addRule jquery validator calls for the
     * rule to be applied to the field when the statement passed in evaluates to true during runtime
     * and this field is being validated. Note the use of custom methods for min/max length/value.
     * 
     * @param field the field to apply the generated methods and rules to
     * @param constraint the constraint to be applied when the booleanStatement evaluates to true
     *        during validation
     * @param booleanStatement the booleanstatement in js - should return true when the validation
     *        rule should be applied
     * @param view
     * @return rule based on the constraint
     */
    @SuppressWarnings("boxing")
    private static String createRule(InputField field, Constraint constraint, String booleanStatement, View view,
            String validationState, StateMapping stateMapping) {
        String rule = "";
        int constraintCount = 0;
        if (constraint instanceof BaseConstraint && ((BaseConstraint) constraint).getApplyClientSide()) {
            if (constraint instanceof SimpleConstraint) {
                if (((SimpleConstraint) constraint).getRequired() != null && ((SimpleConstraint) constraint)
                        .getRequired()) {
                    rule = rule + "required: function(element){\nreturn (" + booleanStatement + ");}";

                    //special requiredness indicator handling
                    String showIndicatorScript = "";
                    boolean hasConditionalReqCheck = false;
                    for (String checkedField : parseOutFields(booleanStatement)) {
                        showIndicatorScript = showIndicatorScript +
                                "setupShowReqIndicatorCheck('" + checkedField + "', '" + field.getBindingInfo()
                                        .getBindingPath() + "', " + "function(){\nreturn (" + booleanStatement
                                + ");});\n";
                        hasConditionalReqCheck = true;
                    }

                    addScriptToPage(view, field, showIndicatorScript);

                    constraintCount++;
                }

                if (((SimpleConstraint) constraint).getMinLength() != null) {
                    if (constraintCount > 0) {
                        rule = rule + ",\n";
                    }
                    rule = rule
                            + "minLengthConditional: ["
                            + ((SimpleConstraint) constraint).getMinLength()
                            + ", function(){return "
                            + booleanStatement
                            + ";}]";
                    constraintCount++;
                }

                if (((SimpleConstraint) constraint).getMaxLength() != null) {
                    if (constraintCount > 0) {
                        rule = rule + ",\n";
                    }
                    rule = rule
                            + "maxLengthConditional: ["
                            + ((SimpleConstraint) constraint).getMaxLength()
                            + ", function(){return "
                            + booleanStatement
                            + ";}]";
                    constraintCount++;
                }

                if (((SimpleConstraint) constraint).getExclusiveMin() != null) {
                    if (constraintCount > 0) {
                        rule = rule + ",\n";
                    }
                    rule = rule
                            + "minExclusive: ["
                            + ((SimpleConstraint) constraint).getExclusiveMin()
                            + ", function(){return "
                            + booleanStatement
                            + ";}]";
                    constraintCount++;
                }

                if (((SimpleConstraint) constraint).getInclusiveMax() != null) {
                    if (constraintCount > 0) {
                        rule = rule + ",\n";
                    }
                    rule = rule
                            + "maxInclusive: ["
                            + ((SimpleConstraint) constraint).getInclusiveMax()
                            + ", function(){return "
                            + booleanStatement
                            + ";}]";
                    constraintCount++;
                }

                rule = "jQuery('[name=\""
                        + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                        + "\"]').rules(\"add\", {"
                        + rule
                        + "\n});";
            } else if (constraint instanceof ValidCharactersConstraint) {
                String regexMethod = "";
                String methodName = "";
                if (StringUtils.isNotEmpty(((ValidCharactersConstraint) constraint).getValue())) {
                    regexMethod = ClientValidationUtils.getRegexMethodWithBooleanCheck(field,
                            (ValidCharactersConstraint) constraint) + "\n";
                    methodName = "validChar-" + field.getBindingInfo().getBindingPath() + methodKey;
                    methodKey++;
                } else {
                    if (StringUtils.isNotEmpty(((ValidCharactersConstraint) constraint).getMessageKey())) {
                        methodName = ((ValidCharactersConstraint) constraint).getMessageKey();
                    }
                }

                if (StringUtils.isNotEmpty(methodName)) {
                    rule = regexMethod
                            + "jQuery('[name=\""
                            + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                            + "\"]').rules(\"add\", {\n\""
                            + methodName
                            + "\" : function(element){return ("
                            + booleanStatement
                            + ");}\n});";
                }
            } else if (constraint instanceof PrerequisiteConstraint) {
                processPrerequisiteConstraint(field, (PrerequisiteConstraint) constraint, view, booleanStatement);
            } else if (constraint instanceof CaseConstraint) {
                processCaseConstraint(field, view, (CaseConstraint) constraint, booleanStatement, validationState,
                        stateMapping);
            } else if (constraint instanceof MustOccurConstraint) {
                processMustOccurConstraint(field, view, (MustOccurConstraint) constraint, booleanStatement);
            }
        }

        return rule;
    }

    /**
     * Simpler version of processPrerequisiteConstraint
     * 
     * @param field input field
     * @param constraint prerequisite constraint to process
     * @param view active view
     * @see ClientValidationUtils#processPrerequisiteConstraint(org.kuali.rice.krad.uif.field.InputField,
     *      PrerequisiteConstraint, View, String)
     */
    public static void processPrerequisiteConstraint(InputField field, PrerequisiteConstraint constraint, View view) {
        processPrerequisiteConstraint(field, constraint, view, "true");
    }

    /**
     * Processes a Prerequisite constraint that should be applied when the booleanStatement passed
     * in evaluates to true.
     * 
     * @param field input field
     * @param constraint prerequisite constraint to process
     * @param view active view
     * @param booleanStatement the booleanstatement in js - should return true when the validation
     *        rule should be applied
     */
    public static void processPrerequisiteConstraint(InputField field, PrerequisiteConstraint constraint, View view,
            String booleanStatement) {
        if (constraint != null && constraint.getApplyClientSide().booleanValue()) {
            String dependsClass = "dependsOn-" + ScriptUtils.escapeName(constraint.getPropertyName());
            String addClass = "jQuery('[name=\""
                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                    + "\"]').addClass('"
                    + dependsClass
                    + "');"
                    +
                    "jQuery('[name=\""
                    + ScriptUtils.escapeName(constraint.getPropertyName())
                    + "\"]').addClass('"
                    + "dependsOn-"
                    + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                    + "');";

            addScriptToPage(view, field, addClass
                    + getPrerequisiteStatement(field, view, constraint, booleanStatement)
                    + getPostrequisiteStatement(field, constraint, booleanStatement));

            //special requiredness indicator handling
            String showIndicatorScript = "setupShowReqIndicatorCheck('" + ScriptUtils.escapeName(
                    field.getBindingInfo().getBindingPath()) + "', '" + ScriptUtils.escapeName(
                    constraint.getPropertyName()) + "', " + "function(){\nreturn (coerceValue('" + ScriptUtils
                    .escapeName(field.getBindingInfo().getBindingPath()) + "') && " + booleanStatement + ");});\n";

            addScriptToPage(view, field, showIndicatorScript);
        }
    }

    /**
     * Creates the script necessary for executing a prerequisite rule in which this field occurs
     * after the field specified in the prerequisite rule - since it requires a specific set of UI
     * logic. Builds an if statement containing an addMethod jquery validator call. Adds a
     * "dependsOn" css class to this field for the field specified.
     * 
     * @param constraint prerequisiteConstraint
     * @param booleanStatement the booleanstatement in js - should return true when the validation
     *        rule should be applied
     * @return statement derived from the constraint
     */
    private static String getPrerequisiteStatement(InputField field, View view, PrerequisiteConstraint constraint,
            String booleanStatement) {
        methodKey++;

        MessageService messageService = KRADServiceLocatorWeb.getMessageService();

        String message = "";
        if (StringUtils.isEmpty(constraint.getMessageKey())) {
            message = messageService.getMessageText(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + "prerequisite");
            message = MessageStructureUtils.translateStringMessage(message);
        } else {
            message = generateMessageText(constraint.getMessageNamespaceCode(),
                    constraint.getMessageComponentCode(), constraint.getMessageKey(),
                    constraint.getValidationMessageParams());
        }

        if (StringUtils.isEmpty(message)) {
            message = "prerequisite - No message";
        } else {
            Field requiredField = (Field) view.getViewIndex().getDataFieldByPath(constraint.getPropertyName());
            if (requiredField != null && StringUtils.isNotEmpty(requiredField.getLabel())) {
                message = MessageFormat.format(message, requiredField.getLabel());
            } else {
                String genericFieldLabel = messageService.getMessageText(GENERIC_FIELD_MSG_KEY);
                message = MessageFormat.format(message, genericFieldLabel);
            }
        }

        // field occurs before case
        String methodName = "prConstraint-"
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                + methodKey;

        String addClass = "jQuery('[name=\""
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                + "\"]').addClass('"
                + methodName
                + "');\n";

        String method = "\njQuery.validator.addMethod(\"" + methodName + "\", function(value, element) {\n" +
                " if(" + booleanStatement + "){ return ((this.optional(element) !== false) || (coerceValue('" + ScriptUtils
                        .escapeName(constraint.getPropertyName()) + "')));}else{return true;} " +
                "}, \"" + message + "\");";

        String ifStatement = "if(occursBefore('"
                + ScriptUtils.escapeName(constraint.getPropertyName())
                + "','"
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                +
                "')){"
                + addClass
                + method
                + "}";

        return ifStatement;
    }

    /**
     * This method creates the script necessary for executing a prerequisite rule in which this
     * field occurs before the field specified in the prerequisite rule - since it requires a
     * specific set of UI logic. Builds an if statement containing an addMethod jquery validator
     * call.
     * 
     * @param constraint prerequisiteConstraint
     * @param booleanStatement the booleanstatement in js - should return true when the validation
     *        rule should be applied
     * @return statement derived from the constraint
     */
    private static String getPostrequisiteStatement(InputField field, PrerequisiteConstraint constraint,
            String booleanStatement) {
        MessageService messageService = KRADServiceLocatorWeb.getMessageService();

        // field occurs after case
        String message = "";
        if (StringUtils.isEmpty(constraint.getMessageKey())) {
            message = messageService.getMessageText(UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + "postrequisite");
            message = MessageStructureUtils.translateStringMessage(message);
        } else {
            message = generateMessageText(constraint.getMessageNamespaceCode(), constraint.getMessageComponentCode(),
                    constraint.getMessageKey(), constraint.getValidationMessageParams());
        }

        if (StringUtils.isEmpty(constraint.getMessageKey())) {
            if (StringUtils.isNotEmpty(field.getLabel())) {
                message = MessageFormat.format(message, field.getLabel());
            } else {
                String genericFieldLabel = messageService.getMessageText(GENERIC_FIELD_MSG_KEY);
                message = MessageFormat.format(message, genericFieldLabel);
            }
        }

        String function = "function(element){\n" +
                "return (coerceValue('"
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                + "') && "
                + booleanStatement
                + ");}";
        String postStatement = "\nelse if(occursBefore('"
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                + "','"
                + ScriptUtils.escapeName(constraint.getPropertyName())
                +
                "')){\njQuery('[name=\""
                + ScriptUtils.escapeName(constraint.getPropertyName())
                +
                "\"]').rules(\"add\", { required: \n"
                + function
                + ", \nmessages: {\nrequired: \""
                + message
                + "\"}});}\n";

        return postStatement;

    }

    /**
     * This method processes the MustOccurConstraint. The constraint is only applied when the
     * booleanStatement evaluates to true during validation. This method creates the addMethod and
     * add rule calls for the jquery validation plugin necessary for applying this constraint to
     * this field.
     * 
     * @param field input field
     * @param view active view
     * @param mc must occur constraint to process
     * @param booleanStatement the booleanstatement in js - should return true when the validation
     *        rule should be applied
     */
    public static void processMustOccurConstraint(InputField field, View view, MustOccurConstraint mc,
            String booleanStatement) {
        methodKey++;
        mustOccursPathNames = new ArrayList<List<String>>();
        // TODO make this show the fields its requiring
        String methodName = "moConstraint-"
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                + methodKey;
        String method = "\njQuery.validator.addMethod(\"" + methodName + "\", function(value, element) {\n" +
                " if("
                + booleanStatement
                + "){return ((this.optional(element) !== false) || ("
                + getMustOccurStatement(field, mc)
                + "));}else{return true;}"
                +
                "}, \""
                + getMustOccursMessage(view, mc)
                + "\");";
        String rule = method
                + "jQuery('[name=\""
                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                + "\"]').rules(\"add\", {\n\""
                + methodName
                + "\": function(element){return ("
                + booleanStatement
                + ");}\n});";
        addScriptToPage(view, field, rule);
    }

    /**
     * This method takes in a MustOccurConstraint and returns the statement used in determining if
     * the must occurs constraint has been satisfied when this field is validated. Note the use of
     * the mustOccurCheck method. Nested mustOccurConstraints are ored against the result of the
     * mustOccurCheck by calling this method recursively.
     * 
     * @param constraint must occur constraint
     * @return statement derived from the constraint
     */
    @SuppressWarnings("boxing")
    private static String getMustOccurStatement(InputField field, MustOccurConstraint constraint) {
        String statement = "";
        List<String> attributePaths = new ArrayList<String>();
        if (constraint != null && constraint.getApplyClientSide()) {
            String array = "[";
            if (constraint.getPrerequisiteConstraints() != null) {
                for (int i = 0; i < constraint.getPrerequisiteConstraints().size(); i++) {
                    field.getControl().addStyleClass("dependsOn-" + constraint.getPrerequisiteConstraints().get(i)
                            .getPropertyName());
                    array = array + "'" + ScriptUtils.escapeName(constraint.getPrerequisiteConstraints().get(i)
                            .getPropertyName()) + "'";
                    attributePaths.add(constraint.getPrerequisiteConstraints().get(i).getPropertyName());

                    if (i + 1 != constraint.getPrerequisiteConstraints().size()) {
                        array = array + ",";
                    }
                }
            }
            array = array + "]";
            statement = "mustOccurTotal(" + array + ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
            //add min to string list
            if (constraint.getMin() != null) {
                attributePaths.add(constraint.getMin().toString());
            } else {
                attributePaths.add(null);
            }
            //add max to string list
            if (constraint.getMax() != null) {
                attributePaths.add(constraint.getMax().toString());
            } else {
                attributePaths.add(null);
            }

            mustOccursPathNames.add(attributePaths);
            if (StringUtils.isEmpty(statement)) {
                statement = "0";
            }
            if (constraint.getMustOccurConstraints() != null) {
                for (MustOccurConstraint mc : constraint.getMustOccurConstraints()) {
                    statement = "mustOccurCheck(" + statement + " + " + getMustOccurStatement(field, mc) +
                            ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
                }
            } else {
                statement = "mustOccurCheck(" + statement +
                        ", " + constraint.getMin() + ", " + constraint.getMax() + ")";
            }
        }
        return statement;
    }

    /**
     * Generates a message for the must occur constraint (if no label key is specified). This
     * message is most accurate when must occurs is a single or double level constraint. Beyond
     * that, the message will still be accurate but may be confusing for the user - this
     * auto-generated message however will work in MOST use cases.
     * 
     * @param view active view
     * @param constraint must occur constraint
     * @return message generated from for the must occur contraint
     */
    private static String getMustOccursMessage(View view, MustOccurConstraint constraint) {
        MessageService messageService = KRADServiceLocatorWeb.getMessageService();

        String message = "";
        if (StringUtils.isNotEmpty(constraint.getMessageKey())) {
            message = generateMessageText(constraint.getMessageNamespaceCode(), constraint.getMessageComponentCode(),
                    constraint.getMessageKey(), constraint.getValidationMessageParams());
        } else {
            String and = messageService.getMessageText(AND_MSG_KEY);
            String or = messageService.getMessageText(OR_MSG_KEY);
            String mustOccursMsgEqualMinMax = messageService.getMessageText(
                    UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + MUSTOCCURS_MSG_EQUAL_KEY);
            String atMost = messageService.getMessageText(ATMOST_MSG_KEY);
            String genericLabel = messageService.getMessageText(GENERIC_FIELD_MSG_KEY);
            String mustOccursMsg = messageService.getMessageText(
                    UifConstants.Messages.VALIDATION_MSG_KEY_PREFIX + MUSTOCCURS_MSG_KEY);

            String statement = "";
            for (int i = 0; i < mustOccursPathNames.size(); i++) {
                String andedString = "";

                List<String> paths = mustOccursPathNames.get(i);
                if (!paths.isEmpty()) {
                    //note that the last 2 strings are min and max and rest are attribute paths
                    String min = paths.get(paths.size() - 2);
                    String max = paths.get(paths.size() - 1);
                    for (int j = 0; j < paths.size() - 2; j++) {
                        InputField field = (InputField) view.getViewIndex().getDataFieldByPath(paths.get(j).trim());
                        String label = genericLabel;
                        if (field != null && StringUtils.isNotEmpty(field.getLabel())) {
                            label = field.getLabel();
                        }
                        if (min.equals(max)) {
                            if (j == 0) {
                                andedString = label;
                            } else if (j == paths.size() - 3) {
                                andedString = andedString + " " + and + " " + label;
                            } else {
                                andedString = andedString + ", " + label;
                            }
                        } else {
                            andedString = andedString + "(" + label + ")";
                        }
                    }
                    if (min.equals(max)) {
                        andedString = "(" + andedString + ")";
                    }

                    if (StringUtils.isNotBlank(andedString) && !andedString.equals("()")) {
                        if (StringUtils.isNotEmpty(min) && StringUtils.isNotEmpty(max) && !min.equals(max)) {
                            andedString = MessageFormat.format(mustOccursMsg, min + "-" + max) + " " + andedString;
                        } else if (StringUtils.isNotEmpty(min)
                                && StringUtils.isNotEmpty(max)
                                && min.equals(max)
                                && i == 0) {
                            andedString = mustOccursMsgEqualMinMax + " " + andedString;
                        } else if (StringUtils.isNotEmpty(min)
                                && StringUtils.isNotEmpty(max)
                                && min.equals(max)
                                && i != 0) {
                            //leave andedString as is
                        } else if (StringUtils.isNotEmpty(min)) {
                            andedString = MessageFormat.format(mustOccursMsg, min) + " " + andedString;
                        } else if (StringUtils.isNotEmpty(max)) {
                            andedString = MessageFormat.format(mustOccursMsg, atMost + " " + max) + " " + andedString;
                        }
                    }
                }
                if (StringUtils.isNotEmpty(andedString)) {
                    if (StringUtils.isNotBlank(statement)) {
                        statement = statement + " " + or.toUpperCase() + " " + andedString;
                    } else {
                        statement = andedString;
                    }
                }
            }
            if (StringUtils.isNotEmpty(statement)) {
                message = statement;
                message = message.replace(")(", " " + or + " ");
            }
        }

        return message;
    }

    /**
     * This method processes all the constraints on the InputField passed in and adds all the
     * necessary jQuery and js required (validator's rules, methods, and messages) to the View's
     * onDocumentReady call. The result is js that will validate all the constraints contained on an
     * InputField during user interaction with the field using the jQuery validation plugin and
     * custom code.
     * 
     * @param field input field
     * @param view active view
     * @param model active model
     */
    @SuppressWarnings("boxing")
    public static void processAndApplyConstraints(InputField field, View view, Object model) {
        methodKey = 0;
        String validationState = ConstraintStateUtils.getClientViewValidationState(model, view);
        StateMapping stateMapping = view.getStateMapping();

        if (view instanceof FormView && ((FormView) view).isValidateClientSide()) {
            SimpleConstraint simpleConstraint = ConstraintStateUtils.getApplicableConstraint(
                    field.getSimpleConstraint(), validationState, stateMapping);
            if (simpleConstraint != null && simpleConstraint.getApplyClientSide()) {

                if ((simpleConstraint.getRequired() != null) && (simpleConstraint.getRequired().booleanValue())) {
                    field.getControl().addStyleClass("required");
                }

                if (simpleConstraint.getExclusiveMin() != null) {
                    if (field.getControl() instanceof TextControl
                            && ((TextControl) field.getControl()).getDatePicker() != null) {
                        DatePicker datePicker = ((TextControl) field.getControl()).getDatePicker();
                        Map<String, String> dpTemplateOptions = datePicker.getTemplateOptions();

                        if (dpTemplateOptions == null) {
                            datePicker.setTemplateOptions(dpTemplateOptions = new HashMap<String, String>());
                        }

                        dpTemplateOptions.put("minDate",
                                simpleConstraint.getExclusiveMin());
                    } else {
                        String rule = "jQuery('[name=\""
                                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                                + "\"]').rules(\"add\", {\n minExclusive: ["
                                + simpleConstraint.getExclusiveMin()
                                + "]});";
                        addScriptToPage(view, field, rule);
                    }
                }

                if (simpleConstraint.getInclusiveMax() != null) {
                    if (field.getControl() instanceof TextControl
                            && ((TextControl) field.getControl()).getDatePicker() != null) {
                        ((TextControl) field.getControl()).getDatePicker().getTemplateOptions().put("maxDate",
                                simpleConstraint.getInclusiveMax());
                    } else {
                        String rule = "jQuery('[name=\""
                                + ScriptUtils.escapeName(field.getBindingInfo().getBindingPath())
                                + "\"]').rules(\"add\", {\n maxInclusive: ["
                                + simpleConstraint.getInclusiveMax()
                                + "]});";
                        addScriptToPage(view, field, rule);
                    }
                }
            }

            ValidCharactersConstraint validCharactersConstraint = ConstraintStateUtils.getApplicableConstraint(
                    field.getValidCharactersConstraint(), validationState, stateMapping);

            if (validCharactersConstraint != null && validCharactersConstraint.getApplyClientSide()) {
                if (StringUtils.isNotEmpty(validCharactersConstraint.getValue())) {
                    // set regex value takes precedence
                    String script = ClientValidationUtils.getRegexMethod(field, validCharactersConstraint, false);
                    if (((UifFormBase) model).getRequestJsonTemplate() == UifConstants.TableToolsValues.JSON_TEMPLATE) {
                        script = script.replaceAll("\\.", "\\\\u002e");
                    }
                    addScriptToPage(view, field, script);
                    field.getControl().addStyleClass(
                            "validChar-" + field.getBindingInfo().getBindingPath() + methodKey);
                    methodKey++;
                } else {
                    //blindly assume that if there is no regex value defined that there must be a method by this name
                    if (StringUtils.isNotEmpty(validCharactersConstraint.getMessageKey())) {
                        field.getControl().addStyleClass(validCharactersConstraint.getMessageKey());
                    }
                }
            }

            CaseConstraint caseConstraint = ConstraintStateUtils.getApplicableConstraint(field.getCaseConstraint(),
                    validationState, stateMapping);
            if (caseConstraint != null && caseConstraint.getApplyClientSide()) {
                processCaseConstraint(field, view, caseConstraint, null, validationState, stateMapping);
            }

            if (field.getDependencyConstraints() != null) {
                for (PrerequisiteConstraint prc : field.getDependencyConstraints()) {
                    prc = ConstraintStateUtils.getApplicableConstraint(prc, validationState, stateMapping);
                    if (prc != null) {
                        processPrerequisiteConstraint(field, prc, view);
                    }
                }
            }

            if (field.getMustOccurConstraints() != null) {
                for (MustOccurConstraint mc : field.getMustOccurConstraints()) {
                    mc = ConstraintStateUtils.getApplicableConstraint(mc, validationState, stateMapping);
                    if (mc != null) {
                        processMustOccurConstraint(field, view, mc, "true");
                    }
                }
            }

        }
    }

}
